package de.avatic.lcc.controller.configuration;

import com.fasterxml.jackson.databind.ObjectMapper;
import de.avatic.lcc.dto.configuration.matrixrates.MatrixRateDTO;
import de.avatic.lcc.dto.configuration.rates.ContainerRateDTO;
import de.avatic.lcc.dto.generic.ValidityPeriodDTO;
import de.avatic.lcc.model.db.rates.ValidityPeriodState;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.jdbc.Sql;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;

import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.hamcrest.Matchers.*;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@SpringBootTest
@AutoConfigureMockMvc
@Transactional
class RateControllerIntegrationTest {

    private static final String BASE_URL = "/api/rates";
    @Autowired
    private MockMvc mockMvc;
    @Autowired
    private ObjectMapper objectMapper;

    // Container Rate Tests
    @Nested
    @DisplayName("/api/rates/container - List container rates")
    class ListContainerRatesTest {

        @Test
        @Sql(scripts = {"classpath:master_data/reduced_rate_setup.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
        @Sql(scripts = {"classpath:master_data/reduced_rate_setup-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
        void testListContainerRates_WithoutFilter_ShouldReturnAllRates() throws Exception {
            mockMvc.perform(get(BASE_URL + "/container")
                            .contentType(MediaType.APPLICATION_JSON))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$", hasSize(greaterThan(0))))
                    .andExpect(header().exists("X-Total-Count"))
                    .andExpect(header().exists("X-Page-Count"))
                    .andExpect(header().string("X-Current-Page", "1"))
                    .andExpect(jsonPath("$[0].id").exists())
                    .andExpect(jsonPath("$[0].origin").exists())
                    .andExpect(jsonPath("$[0].destination").exists())
                    .andExpect(jsonPath("$[0].type").exists())
                    .andExpect(jsonPath("$[0].rates").exists())
                    .andExpect(jsonPath("$[0].lead_time").exists())
                    .andExpect(jsonPath("$[0].validity_period").exists());
        }

        @Test
        @Sql(scripts = {"classpath:master_data/reduced_rate_setup.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
        @Sql(scripts = {"classpath:master_data/reduced_rate_setup-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
        void testListContainerRates_WithPagination_ShouldReturnPagedResults() throws Exception {
            mockMvc.perform(get(BASE_URL + "/container")
                            .param("limit", "5")
                            .param("page", "1")
                            .contentType(MediaType.APPLICATION_JSON))
                    .andDo(print())
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$", hasSize(equalTo(5))))
                    .andExpect(header().string("X-Current-Page", "1"));
        }

        @Test
        @Sql(scripts = {"classpath:master_data/reduced_rate_setup.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
        @Sql(scripts = {"classpath:master_data/reduced_rate_setup-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
        void testListContainerRates_WithValidityPeriodFilter_ShouldReturnFilteredRates() throws Exception {
            // Assuming validity period ID 1 exists in test data
            mockMvc.perform(get(BASE_URL + "/container")
                            .param("valid", "1")
                            .contentType(MediaType.APPLICATION_JSON))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$[*].validity_period.id", everyItem(equalTo(1))));
        }

        @Test
        @Sql(scripts = {"classpath:master_data/reduced_rate_setup.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
        @Sql(scripts = {"classpath:master_data/reduced_rate_setup-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
        void testListContainerRates_WithValidAtFilter_ShouldReturnValidRates() throws Exception {
            LocalDateTime validAt = LocalDateTime.now();
            String formattedDate = validAt.format(DateTimeFormatter.ISO_DATE_TIME);

            mockMvc.perform(get(BASE_URL + "/container")
                            .param("validAt", formattedDate)
                            .contentType(MediaType.APPLICATION_JSON))
                    .andExpect(status().isOk());
        }
    }


    // Container Rate Tests
    @Nested
    @DisplayName("/api/rates/container/id - get container rate detail")
    class GetContainerRatesTest {

        @Test
        @Sql(scripts = {"classpath:master_data/reduced_rate_setup.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
        @Sql(scripts = {"classpath:master_data/reduced_rate_setup-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
        void testGetContainerRate_WithValidId_ShouldReturnRate() throws Exception {
            // First, get a valid ID from the list
            MvcResult listResult = mockMvc.perform(get(BASE_URL + "/container")
                            .param("limit", "1"))
                    .andExpect(status().isOk())
                    .andReturn();

            List<ContainerRateDTO> rates = objectMapper.readValue(
                    listResult.getResponse().getContentAsString(),
                    objectMapper.getTypeFactory().constructCollectionType(List.class, ContainerRateDTO.class)
            );

            if (!rates.isEmpty()) {
                Integer rateId = rates.getFirst().getId();

                mockMvc.perform(get(BASE_URL + "/container/{id}", rateId)
                                .contentType(MediaType.APPLICATION_JSON))
                        .andExpect(status().isOk())
                        .andExpect(jsonPath("$.id").value(rateId))
                        .andExpect(jsonPath("$.origin").exists())
                        .andExpect(jsonPath("$.destination").exists())
                        .andExpect(jsonPath("$.type").exists())
                        .andExpect(jsonPath("$.rates.FEU").exists())
                        .andExpect(jsonPath("$.rates.TEU").exists())
                        .andExpect(jsonPath("$.rates.HQ").exists());
            }
        }

        @Test
        void testGetContainerRate_WithInvalidId_ShouldReturn400() throws Exception {
            mockMvc.perform(get(BASE_URL + "/container/{id}", 99999)
                            .contentType(MediaType.APPLICATION_JSON))
                    .andExpect(status().isBadRequest());
        }

    }


    // Matrix Rate Tests
    @Nested
    @DisplayName("/api/rates/matrix/ - list matrix rates")
    class ListMatrixRatesTest {
        @Test
        @Sql(scripts = {"classpath:master_data/reduced_rate_setup.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
        @Sql(scripts = {"classpath:master_data/reduced_rate_setup-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
        void testListMatrixRates_WithoutFilter_ShouldReturnAllRates() throws Exception {
            mockMvc.perform(get(BASE_URL + "/matrix")
                            .contentType(MediaType.APPLICATION_JSON))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$", hasSize(greaterThanOrEqualTo(0))))
                    .andExpect(header().exists("X-Total-Count"))
                    .andExpect(header().exists("X-Page-Count"))
                    .andExpect(header().string("X-Current-Page", "1"));
        }

        @Test
        @Sql(scripts = {"classpath:master_data/reduced_rate_setup.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
        @Sql(scripts = {"classpath:master_data/reduced_rate_setup-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
        void testListMatrixRates_WithValidFilter_ShouldReturnFilteredRates() throws Exception {
            mockMvc.perform(get(BASE_URL + "/matrix")
                            .param("valid", "1")
                            .contentType(MediaType.APPLICATION_JSON))
                    .andExpect(status().isOk());
        }
    }

    @Nested
    @DisplayName("/api/rates/matrix/id - get matrix rate detail")
    class GetMatrixRatesTest {
        @Test
        @Sql(scripts = {"classpath:master_data/reduced_rate_setup.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
        @Sql(scripts = {"classpath:master_data/reduced_rate_setup-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
        void testGetMatrixRate_WithValidId_ShouldReturnRate() throws Exception {
            // First, get a valid ID from the list
            MvcResult listResult = mockMvc.perform(get(BASE_URL + "/matrix")
                            .param("limit", "1"))
                    .andExpect(status().isOk())
                    .andReturn();

            List<MatrixRateDTO> rates = objectMapper.readValue(
                    listResult.getResponse().getContentAsString(),
                    objectMapper.getTypeFactory().constructCollectionType(List.class, MatrixRateDTO.class)
            );

            if (!rates.isEmpty()) {
                Integer rateId = rates.getFirst().getId();

                mockMvc.perform(get(BASE_URL + "/matrix/{id}", rateId)
                                .contentType(MediaType.APPLICATION_JSON))
                        .andExpect(status().isOk())
                        .andExpect(jsonPath("$.id").value(rateId))
                        .andExpect(jsonPath("$.origin").exists())
                        .andExpect(jsonPath("$.destination").exists())
                        .andExpect(jsonPath("$.rate").exists());
            }
        }
    }


    // Validity Period Tests
    @Nested
    @DisplayName("/api/rates/periods - List validity periods")
    class ListPeriodsTest {
        @Test
        @Sql(scripts = {"classpath:master_data/reduced_rate_setup.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
        @Sql(scripts = {"classpath:master_data/reduced_rate_setup-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
        void testListPeriods_ShouldReturnAllPeriods() throws Exception {
            mockMvc.perform(get(BASE_URL + "/periods")
                            .contentType(MediaType.APPLICATION_JSON))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$", hasSize(greaterThanOrEqualTo(0))));
        }
    }


    @Nested
    @DisplayName("/api/rates/periods/id - Invalidate validity periods")
    class GetPeriodsTest {
        @Test
        @Sql(scripts = {"classpath:master_data/reduced_rate_setup.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
        @Sql(scripts = {"classpath:master_data/reduced_rate_setup-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
        void testInvalidatePeriod_WithValidId_ShouldReturnOk() throws Exception {
            // First get a period to invalidate
            MvcResult listResult = mockMvc.perform(get(BASE_URL + "/periods"))
                    .andExpect(status().isOk())
                    .andReturn();

            List<ValidityPeriodDTO> periods = objectMapper.readValue(
                    listResult.getResponse().getContentAsString(),
                    objectMapper.getTypeFactory().constructCollectionType(List.class, ValidityPeriodDTO.class)
            );

            assertThat(periods).asInstanceOf(InstanceOfAssertFactories.LIST).isNotEmpty();
            Integer periodId = periods.stream().filter(p -> p.getState().equals(ValidityPeriodState.EXPIRED)).findFirst().orElseThrow(() -> new AssertionError("No expired periods found")).getId();

            mockMvc.perform(delete(BASE_URL + "/periods/{id}", periodId)
                            .contentType(MediaType.APPLICATION_JSON))
                    .andExpect(status().isOk());

            listResult = mockMvc.perform(get(BASE_URL + "/periods"))
                    .andExpect(status().isOk())
                    .andReturn();

            periods = objectMapper.readValue(
                    listResult.getResponse().getContentAsString(),
                    objectMapper.getTypeFactory().constructCollectionType(List.class, ValidityPeriodDTO.class)
            );

            assertThat(periods).asInstanceOf(InstanceOfAssertFactories.LIST).isNotEmpty();
            assertThat(periods.stream().filter(p -> p.getId().equals(periodId)).findFirst().orElseThrow(() -> new AssertionError("Periods with id " + periodId + " not found"))).extracting(ValidityPeriodDTO::getState).isEqualTo(ValidityPeriodState.INVALID);

        }

        @Test
        @Sql(scripts = {"classpath:master_data/reduced_rate_setup.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
        @Sql(scripts = {"classpath:master_data/reduced_rate_setup-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
        void testInvalidatePeriod_WithInvalidId_ShouldReturn404() throws Exception {
            mockMvc.perform(delete(BASE_URL + "/periods/{id}", 99999)
                            .contentType(MediaType.APPLICATION_JSON))
                    .andExpect(status().isBadRequest());
        }
    }


    // Staged Changes Tests
    @Nested
    @DisplayName("/api/rates/staged_changes - staged changes")
    class StagedChangesTest {
        @Test
        @Sql(scripts = {"classpath:master_data/reduced_rate_setup.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
        @Sql(scripts = {"classpath:master_data/reduced_rate_setup-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
        void testCheckRateDrafts_ShouldReturnBoolean() throws Exception {
            mockMvc.perform(get(BASE_URL + "/staged_changes")
                            .contentType(MediaType.APPLICATION_JSON))
                    .andExpect(status().isOk())
                    .andDo(print())
                    .andExpect(jsonPath("$").isBoolean())
                    .andExpect(jsonPath("$").value(true));

        }

        @Test
        @Sql(scripts = {"classpath:master_data/reduced_rate_setup.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
        @Sql(scripts = {"classpath:master_data/reduced_rate_setup-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
        void testApproveRateDrafts_ShouldReturnOk() throws Exception {
            mockMvc.perform(put(BASE_URL + "/staged_changes")
                            .contentType(MediaType.APPLICATION_JSON))
                    .andExpect(status().isOk());
        }
    }


    // Edge Cases and Error Handling Tests
    @Nested
    @DisplayName("/api/rates/ - Edge cases")
    class EdgeCasesTest {
        @Test
        @Sql(scripts = {"classpath:master_data/reduced_rate_setup.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
        @Sql(scripts = {"classpath:master_data/reduced_rate_setup-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
        void testListContainerRates_WithInvalidDateFormat_ShouldReturn400() throws Exception {
            mockMvc.perform(get(BASE_URL + "/container")
                            .param("validAt", "invalid-date")
                            .contentType(MediaType.APPLICATION_JSON))
                    .andExpect(status().isBadRequest());
        }

        @Test
        @Sql(scripts = {"classpath:master_data/reduced_rate_setup.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
        @Sql(scripts = {"classpath:master_data/reduced_rate_setup-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
        void testListContainerRates_WithNegativeLimit_ShouldReturn400() throws Exception {
            mockMvc.perform(get(BASE_URL + "/container")
                            .param("limit", "-1")
                            .contentType(MediaType.APPLICATION_JSON))
                    .andExpect(status().isBadRequest());
        }

        @Test
        @Sql(scripts = {"classpath:master_data/reduced_rate_setup.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
        @Sql(scripts = {"classpath:master_data/reduced_rate_setup-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
        void testListMatrixRates_WithNegativePage_ShouldReturn400() throws Exception {
            mockMvc.perform(get(BASE_URL + "/matrix")
                            .param("page", "-1")
                            .contentType(MediaType.APPLICATION_JSON))
                    .andExpect(status().isBadRequest());
        }
    }


    // Performance Tests
    @Nested
    @DisplayName("/api/rates/ - Performance tests")
    class PerformanceTests {
        @Test
        @Sql(scripts = {"classpath:master_data/reduced_rate_setup.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
        @Sql(scripts = {"classpath:master_data/reduced_rate_setup-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
        void testListContainerRates_LargeLimit_ShouldCompleteInReasonableTime() throws Exception {
            long startTime = System.currentTimeMillis();

            mockMvc.perform(get(BASE_URL + "/container")
                            .param("limit", "1000")
                            .contentType(MediaType.APPLICATION_JSON))
                    .andExpect(status().isOk());

            long endTime = System.currentTimeMillis();
            long duration = endTime - startTime;

            // Assert that the request completes within 5 seconds
            assertTrue(duration < 5000, "Request took too long: " + duration + "ms");
        }
    }


}