package de.avatic.lcc.controller.configuration;

import com.fasterxml.jackson.databind.ObjectMapper;
import de.avatic.lcc.dto.generic.MaterialDTO;
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 static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@SpringBootTest
@AutoConfigureMockMvc
@Transactional
@DisplayName("MaterialController Integration Tests")
class MaterialControllerIntegrationTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @Nested
    @DisplayName("GET /api/materials/ - List Materials")
    @Sql(scripts = {"classpath:master_data/countries_properties.sql", "classpath:master_data/nodes.sql", "classpath:master_data/material_packaging.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
    @Sql(scripts = {"classpath:master_data/material_packaging-cleanup.sql", "classpath:master_data/nodes-cleanup.sql", "classpath:master_data/countries_properties-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
    class ListMaterialsTests {

        @Test
        @DisplayName("Should return all materials with default pagination")
        void shouldReturnAllMaterialsWithDefaultPagination() throws Exception {
            mockMvc.perform(get("/api/materials/")
                            .contentType(MediaType.APPLICATION_JSON))
                    .andDo(print())
                    .andExpect(status().isOk())
                    .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                    .andExpect(jsonPath("$", hasSize(18))) // Based on the test data, we have 18 materials
                    .andExpect(header().string("X-Total-Count", "18"))
                    .andExpect(header().string("X-Page-Count", "1"))
                    .andExpect(header().string("X-Current-Page", "1"))
                    .andExpect(jsonPath("$", hasSize(18)))
                    .andExpect(jsonPath("$[0].part_number", notNullValue()))
                    .andExpect(jsonPath("$[0].name", notNullValue()))
                    .andExpect(jsonPath("$[0].hs_code", notNullValue()));
        }

        @Test
        @DisplayName("Should return materials with custom pagination")
        void shouldReturnMaterialsWithCustomPagination() throws Exception {
            mockMvc.perform(get("/api/materials/")
                            .param("limit", "5")
                            .param("page", "1")
                            .contentType(MediaType.APPLICATION_JSON))
                    .andDo(print())
                    .andExpect(status().isOk())
                    .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                    .andExpect(jsonPath("$", hasSize(5)))
                    .andExpect(header().string("X-Total-Count", "18"))
                    .andExpect(header().string("X-Page-Count", "4")) // 18 materials / 5 per page = 4 pages
                    .andExpect(header().string("X-Current-Page", "1"));
        }

        @Test
        @DisplayName("Should return second page of materials")
        void shouldReturnSecondPageOfMaterials() throws Exception {
            mockMvc.perform(get("/api/materials/")
                            .param("limit", "10")
                            .param("page", "2")
                            .contentType(MediaType.APPLICATION_JSON))
                    .andDo(print())
                    .andExpect(status().isOk())
                    .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                    .andExpect(jsonPath("$", hasSize(8))) // Second page should have 8 materials (18 - 10)
                    .andExpect(header().string("X-Total-Count", "18"))
                    .andExpect(header().string("X-Page-Count", "2"))
                    .andExpect(header().string("X-Current-Page", "2"));
        }

        @Test
        @DisplayName("Should filter materials by part number")
        void shouldFilterMaterialsByPartNumber() throws Exception {
            mockMvc.perform(get("/api/materials/")
                            .param("filter", "28152640129")
                            .contentType(MediaType.APPLICATION_JSON))
                    .andDo(print())
                    .andExpect(status().isOk())
                    .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                    .andExpect(jsonPath("$", hasSize(1)))
                    .andExpect(jsonPath("$[0].part_number", is("28152640129")))
                    .andExpect(jsonPath("$[0].name", containsString("gearbox housing blank")))
                    .andExpect(header().string("X-Total-Count", "1"))
                    .andExpect(header().string("X-Page-Count", "1"))
                    .andExpect(header().string("X-Current-Page", "1"));
        }

        @Test
        @DisplayName("Should filter materials by name")
        void shouldFilterMaterialsByName() throws Exception {
            mockMvc.perform(get("/api/materials/")
                            .param("filter", "wheel")
                            .contentType(MediaType.APPLICATION_JSON))
                    .andDo(print())
                    .andExpect(status().isOk())
                    .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                    .andExpect(jsonPath("$", hasSize(greaterThan(0))))
                    .andExpect(jsonPath("$[*].name", everyItem(containsStringIgnoringCase("wheel"))));
        }

        @Test
        @DisplayName("Should return empty list when filter matches no materials")
        void shouldReturnEmptyListWhenFilterMatchesNoMaterials() throws Exception {
            mockMvc.perform(get("/api/materials/")
                            .param("filter", "nonexistent")
                            .contentType(MediaType.APPLICATION_JSON))
                    .andDo(print())
                    .andExpect(status().isOk())
                    .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                    .andExpect(jsonPath("$", hasSize(0)))
                    .andExpect(header().string("X-Total-Count", "0"))
                    .andExpect(header().string("X-Page-Count", "0"))
                    .andExpect(header().string("X-Current-Page", "1"));
        }

        @Test
        @DisplayName("Should handle empty filter parameter")
        void shouldHandleEmptyFilterParameter() throws Exception {
            mockMvc.perform(get("/api/materials/")
                            .param("filter", "")
                            .contentType(MediaType.APPLICATION_JSON))
                    .andDo(print())
                    .andExpect(status().isOk())
                    .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                    .andExpect(jsonPath("$", hasSize(18))) // Should return all materials
                    .andExpect(header().string("X-Total-Count", "18"));
        }

        @Test
        @DisplayName("Should handle out of bounds page number")
        void shouldHandleOutOfBoundsPageNumber() throws Exception {
            mockMvc.perform(get("/api/materials/")
                            .param("page", "999")
                            .param("limit", "10")
                            .contentType(MediaType.APPLICATION_JSON))
                    .andDo(print())
                    .andExpect(status().isOk())
                    .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                    .andExpect(jsonPath("$", hasSize(0))) // Should return empty list
                    .andExpect(header().string("X-Total-Count", "18"))
                    .andExpect(header().string("X-Current-Page", "999"));
        }

        @Test
        @DisplayName("Should handle negative page number")
        void shouldHandleNegativePageNumber() throws Exception {
            mockMvc.perform(get("/api/materials/")
                            .param("page", "-1")
                            .param("limit", "10")
                            .contentType(MediaType.APPLICATION_JSON))
                    .andDo(print())
                    .andExpect(status().isBadRequest());
        }

        @Test
        @DisplayName("Should handle large limit parameter")
        void shouldHandleLargeLimitParameter() throws Exception {
            mockMvc.perform(get("/api/materials/")
                            .param("limit", "1000")
                            .contentType(MediaType.APPLICATION_JSON))
                    .andDo(print())
                    .andExpect(status().isOk())
                    .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                    .andExpect(jsonPath("$", hasSize(18))) // Should return all available materials
                    .andExpect(header().string("X-Total-Count", "18"))
                    .andExpect(header().string("X-Page-Count", "1"));
        }
    }

    @Nested
    @DisplayName("GET /api/materials/{id} - Get Material Details")
    @Sql(scripts = {"classpath:master_data/countries_properties.sql", "classpath:master_data/nodes.sql", "classpath:master_data/material_packaging.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
    @Sql(scripts = {"classpath:master_data/material_packaging-cleanup.sql", "classpath:master_data/nodes-cleanup.sql", "classpath:master_data/countries_properties-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
    class GetMaterialDetailsTests {

        @Test
        @DisplayName("Should return material details for existing material")
        void shouldReturnMaterialDetailsForExistingMaterial() throws Exception {
            // First, get the list of materials to find a valid ID
            MvcResult listResult = mockMvc.perform(get("/api/materials/")
                            .param("limit", "1"))
                    .andExpect(status().isOk())
                    .andReturn();

            var content = objectMapper.readValue(listResult.getResponse().getContentAsString(), MaterialDTO[].class);
            assertThat(content.length).isEqualTo(1);

            mockMvc.perform(get("/api/materials/" + content[0].getId())
                            .contentType(MediaType.APPLICATION_JSON))
                    .andDo(print())
                    .andExpect(status().isOk())
                    .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                    .andExpect(jsonPath("$.id", notNullValue()))
                    .andExpect(jsonPath("$.part_number", notNullValue()))
                    .andExpect(jsonPath("$.name", notNullValue()))
                    .andExpect(jsonPath("$.hs_code", notNullValue()))
                    .andExpect(jsonPath("$.normalized_part_number", notNullValue()))
                    .andExpect(jsonPath("$.is_deprecated", notNullValue()));
        }

        @Test
        @DisplayName("Should return material details with correct data structure")
        void shouldReturnMaterialDetailsWithCorrectDataStructure() throws Exception {

            MvcResult listResult = mockMvc.perform(get("/api/materials/")
                            .param("limit", "1")
                            .param("page", "3"))
                    .andExpect(status().isOk())
                    .andReturn();

            var content = objectMapper.readValue(listResult.getResponse().getContentAsString(), MaterialDTO[].class);
            assertThat(content.length).isEqualTo(1);

            mockMvc.perform(get("/api/materials/" + content[0].getId())
                            .contentType(MediaType.APPLICATION_JSON))
                    .andDo(print())
                    .andExpect(status().isOk())
                    .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                    .andExpect(jsonPath("$.part_number", matchesPattern("\\d+"))) // Part numbers are numeric strings
                    .andExpect(jsonPath("$.normalized_part_number", matchesPattern("\\d+"))) // Normalized part numbers are numeric strings
                    .andExpect(jsonPath("$.hs_code", matchesPattern("\\d{8}"))) // HS codes are 8-digit strings
                    .andExpect(jsonPath("$.name", is(not(emptyString()))))
                    .andExpect(jsonPath("$.is_deprecated", isA(Boolean.class)));
        }

        @Test
        @DisplayName("Should return specific material details by known part number")
        void shouldReturnSpecificMaterialDetailsByKnownPartNumber() throws Exception {

            MvcResult listResult = mockMvc.perform(get("/api/materials/")
                            .param("limit", "1").param("filter", "28152640129"))
                    .andExpect(status().isOk())
                    .andReturn();

            var content = objectMapper.readValue(listResult.getResponse().getContentAsString(), MaterialDTO[].class);
            assertThat(content.length).isEqualTo(1);

            // We know from test data that material with part number '28152640129' exists
            // We need to find its ID first or assume it has ID 1
            mockMvc.perform(get("/api/materials/"   + content[0].getId())
                            .contentType(MediaType.APPLICATION_JSON))
                    .andDo(print())
                    .andExpect(status().isOk())
                    .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                    .andExpect(jsonPath("$.part_number", anyOf(
                            is("28152640129"), is("8222640822"), is("3064540201")))) // Any of the test materials
                    .andExpect(jsonPath("$.hs_code", anyOf(
                            is("84839089"), is("84312000"), is("84831095")))); // Corresponding HS codes
        }

        @Test
        @DisplayName("Should return 400 for non-existent material ID")
        void shouldReturn404ForNonExistentMaterialId() throws Exception {
            mockMvc.perform(get("/api/materials/99999")
                            .contentType(MediaType.APPLICATION_JSON))
                    .andDo(print())
                    .andExpect(status().isBadRequest());
        }

        @Test
        @DisplayName("Should return 400 for negative material ID")
        void shouldReturn404ForNegativeMaterialId() throws Exception {
            mockMvc.perform(get("/api/materials/-1")
                            .contentType(MediaType.APPLICATION_JSON))
                    .andDo(print())
                    .andExpect(status().isBadRequest());
        }

        @Test
        @DisplayName("Should return 400 for invalid material ID format")
        void shouldReturn400ForInvalidMaterialIdFormat() throws Exception {
            mockMvc.perform(get("/api/materials/invalid")
                            .contentType(MediaType.APPLICATION_JSON))
                    .andDo(print())
                    .andExpect(status().isBadRequest());
        }

        @Test
        @DisplayName("Should return 400 for zero material ID")
        void shouldReturn400ForZeroMaterialId() throws Exception {
            mockMvc.perform(get("/api/materials/0")
                            .contentType(MediaType.APPLICATION_JSON))
                    .andDo(print())
                    .andExpect(status().isBadRequest()); // Assuming 0 is not a valid ID
        }
    }

    @Nested
    @DisplayName("Error Handling and Edge Cases")
    @Sql(scripts = {"classpath:master_data/countries_properties.sql", "classpath:master_data/nodes.sql", "classpath:master_data/material_packaging.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
    @Sql(scripts = {"classpath:master_data/material_packaging-cleanup.sql", "classpath:master_data/nodes-cleanup.sql", "classpath:master_data/countries_properties-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
    class ErrorHandlingTests {

        @Test
        @DisplayName("Should handle missing required headers")
        void shouldHandleMissingRequiredHeaders() throws Exception {
            mockMvc.perform(get("/api/materials/"))
                    .andDo(print())
                    .andExpect(status().isOk()); // Should still work without explicit content type
        }

        @Test
        @DisplayName("Should handle invalid parameter types for limit")
        void shouldHandleInvalidParameterTypesForLimit() throws Exception {
            mockMvc.perform(get("/api/materials/")
                            .param("limit", "invalid")
                            .contentType(MediaType.APPLICATION_JSON))
                    .andDo(print())
                    .andExpect(status().isBadRequest());
        }

        @Test
        @DisplayName("Should handle invalid parameter types for page")
        void shouldHandleInvalidParameterTypesForPage() throws Exception {
            mockMvc.perform(get("/api/materials/")
                            .param("page", "invalid")
                            .contentType(MediaType.APPLICATION_JSON))
                    .andDo(print())
                    .andExpect(status().isBadRequest());
        }

        @Test
        @DisplayName("Should handle extremely large filter string")
        void shouldHandleExtremelyLargeFilterString() throws Exception {
            String largeFilter = "x".repeat(1000);
            mockMvc.perform(get("/api/materials/")
                            .param("filter", largeFilter)
                            .contentType(MediaType.APPLICATION_JSON))
                    .andDo(print())
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$", hasSize(0))); // Should return empty result
        }

        @Test
        @DisplayName("Should handle special characters in filter")
        void shouldHandleSpecialCharactersInFilter() throws Exception {
            mockMvc.perform(get("/api/materials/")
                            .param("filter", "!@#$%^&*()")
                            .contentType(MediaType.APPLICATION_JSON))
                    .andDo(print())
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$", hasSize(0))); // Should return empty result
        }

        @Test
        @DisplayName("Should handle SQL injection attempts in filter")
        void shouldHandleSqlInjectionAttemptsInFilter() throws Exception {
            mockMvc.perform(get("/api/materials/")
                            .param("filter", "'; DROP TABLE material; --")
                            .contentType(MediaType.APPLICATION_JSON))
                    .andDo(print())
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$", hasSize(0))); // Should return empty result and not break
        }
    }

    @Nested
    @DisplayName("Performance and Load Tests")
    class PerformanceTests {

        @Test
        @DisplayName("Should handle concurrent requests efficiently")
        @Sql(scripts = {"classpath:master_data/countries_properties.sql", "classpath:master_data/nodes.sql", "classpath:master_data/material_packaging.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
        @Sql(scripts = {"classpath:master_data/material_packaging-cleanup.sql", "classpath:master_data/nodes-cleanup.sql", "classpath:master_data/countries_properties-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
        void shouldHandleConcurrentRequestsEfficiently() throws Exception {
            // Simulate multiple concurrent requests
            for (int i = 0; i < 10; i++) {
                mockMvc.perform(get("/api/materials/")
                                .param("page", String.valueOf((i % 3) + 1))
                                .param("limit", "5")
                                .contentType(MediaType.APPLICATION_JSON))
                        .andExpect(status().isOk())
                        .andExpect(header().exists("X-Total-Count"));
            }
        }

        @Test
        @DisplayName("Should respond within reasonable time limits")
        @Sql(scripts = {"classpath:master_data/countries_properties.sql", "classpath:master_data/nodes.sql", "classpath:master_data/material_packaging.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
        @Sql(scripts = {"classpath:master_data/material_packaging-cleanup.sql", "classpath:master_data/nodes-cleanup.sql", "classpath:master_data/countries_properties-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
        void shouldRespondWithinReasonableTimeLimits() throws Exception {
            long startTime = System.currentTimeMillis();

            mockMvc.perform(get("/api/materials/")
                            .contentType(MediaType.APPLICATION_JSON))
                    .andExpect(status().isOk());

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

            // Assert that response time is reasonable (less than 2 seconds)
            assert responseTime < 2000 : "Response time too slow: " + responseTime + "ms";
        }
    }
}