package de.avatic.lcc.controller.configuration;

import com.fasterxml.jackson.databind.ObjectMapper;
import de.avatic.lcc.dto.configuration.nodes.update.NodeUpdateDTO;
import de.avatic.lcc.dto.configuration.nodes.userNodes.AddUserNodeDTO;
import de.avatic.lcc.dto.generic.CountryDTO;
import de.avatic.lcc.dto.generic.LocationDTO;
import de.avatic.lcc.dto.generic.NodeType;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
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.transaction.annotation.Transactional;

import static org.hamcrest.Matchers.*;
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 NodeControllerIntegrationTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @Nested
    @DisplayName("GET /api/nodes/ - List Nodes")
    class ListNodesTests {

        @Test
        @DisplayName("Should return list of nodes with default pagination")
        @Sql(scripts = {"classpath:master_data/countries_properties.sql", "classpath:master_data/nodes.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
        @Sql(scripts = {"classpath:master_data/nodes-cleanup.sql", "classpath:master_data/countries_properties-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
        void shouldReturnListOfNodesWithDefaultPagination() throws Exception {
            mockMvc.perform(get("/api/nodes/"))
                    .andExpect(status().isOk())
                    .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                    .andDo(print())
                    .andExpect(header().exists("X-Total-Count"))
                    .andExpect(header().exists("X-Page-Count"))
                    .andExpect(header().exists("X-Current-Page"))
                    .andExpect(jsonPath("$", isA(java.util.List.class)))
                    .andExpect(jsonPath("$", hasSize(greaterThan(0))))
                    .andExpect(jsonPath("$[0].id", notNullValue()))
                    .andExpect(jsonPath("$[0].country", notNullValue()))
                    .andExpect(jsonPath("$[0].country.id", notNullValue()))
                    .andExpect(jsonPath("$[0].country.iso_code", notNullValue()))
                    .andExpect(jsonPath("$[0].country.region_code", notNullValue()))
                    .andExpect(jsonPath("$[0].address", notNullValue()))
                    .andExpect(jsonPath("$[0].location", notNullValue()))
                    .andExpect(jsonPath("$[0].location.longitude", notNullValue()))
                    .andExpect(jsonPath("$[0].location.latitude", notNullValue()))
                    .andExpect(jsonPath("$[0].types", isA(java.util.List.class)));
        }

        @Test
        @DisplayName("Should return filtered nodes when filter parameter is provided")
        @Sql(scripts = {"classpath:master_data/countries_properties.sql", "classpath:master_data/nodes.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
        @Sql(scripts = {"classpath:master_data/nodes-cleanup.sql", "classpath:master_data/countries_properties-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
        void shouldReturnFilteredNodes() throws Exception {
            mockMvc.perform(get("/api/nodes/")
                            .param("filter", "Shanghai"))
                    .andExpect(status().isOk())
                    .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                    .andExpect(jsonPath("$", isA(java.util.List.class)))
                    .andExpect(jsonPath("$[*].name", everyItem(containsStringIgnoringCase("Shanghai"))));
        }

        @Test
        @DisplayName("Should respect pagination parameters")
        @Sql(scripts = {"classpath:master_data/countries_properties.sql", "classpath:master_data/nodes.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
        @Sql(scripts = {"classpath:master_data/nodes-cleanup.sql", "classpath:master_data/countries_properties-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
        void shouldRespectPaginationParameters() throws Exception {
            mockMvc.perform(get("/api/nodes/")
                            .param("page", "1")
                            .param("limit", "5"))
                    .andExpect(status().isOk())
                    .andDo(print())
                    .andExpect(header().string("X-Current-Page", "1"))
                    .andExpect(jsonPath("$", hasSize(lessThanOrEqualTo(5))));
        }

        @Test
        @DisplayName("Should return empty list when no nodes match filter")
        @Sql(scripts = {"classpath:master_data/countries_properties.sql", "classpath:master_data/nodes.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
        @Sql(scripts = {"classpath:master_data/nodes-cleanup.sql", "classpath:master_data/countries_properties-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
        void shouldReturnEmptyListWhenNoNodesMatchFilter() throws Exception {
            mockMvc.perform(get("/api/nodes/")
                            .param("filter", "NonExistentCity"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$", hasSize(0)))
                    .andExpect(header().string("X-Total-Count", "0"));
        }
    }

    @Nested
    @DisplayName("GET /api/nodes/search - Search Nodes")
    class SearchNodesTests {

        @Test
        @DisplayName("Should search nodes without type filter")
        @Sql(scripts = {"classpath:master_data/countries_properties.sql", "classpath:master_data/nodes.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
        @Sql(scripts = {"classpath:master_data/nodes-cleanup.sql", "classpath:master_data/countries_properties-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
        void shouldSearchNodesWithoutTypeFilter() throws Exception {
            mockMvc.perform(get("/api/nodes/search")
                            .param("filter", "KION")
                            .param("limit", "10"))
                    .andExpect(status().isOk())
                    .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                    .andExpect(jsonPath("$", isA(java.util.List.class)))
                    .andExpect(jsonPath("$", hasSize(lessThanOrEqualTo(10))));
        }

        @Test
        @DisplayName("Should search nodes with specific node type")
        @Sql(scripts = {"classpath:master_data/countries_properties.sql", "classpath:master_data/nodes.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
        @Sql(scripts = {"classpath:master_data/nodes-cleanup.sql", "classpath:master_data/countries_properties-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
        void shouldSearchNodesWithSpecificNodeType() throws Exception {
            mockMvc.perform(get("/api/nodes/search")
                            .param("node_type", "SOURCE")
                            .param("limit", "10"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$", isA(java.util.List.class)))
                    .andExpect(jsonPath("$[*].types", everyItem(hasItem("source"))));
        }

        @Test
        @DisplayName("Should include user nodes when requested")
        @Sql(scripts = {"classpath:master_data/countries_properties.sql", "classpath:master_data/nodes.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
        @Sql(scripts = {"classpath:master_data/nodes-cleanup.sql", "classpath:master_data/countries_properties-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
        void shouldIncludeUserNodesWhenRequested() throws Exception {
            mockMvc.perform(get("/api/nodes/search")
                            .param("include_user_node", "true")
                            .param("limit", "20"))
                    .andExpect(status().isOk())
                    .andDo(print())
                    .andExpect(jsonPath("$", isA(java.util.List.class)));
        }
    }

    @Nested
    @DisplayName("GET /api/nodes/{id} - Get Node Details")
    class GetNodeDetailsTests {

        @Test
        @DisplayName("Should return node details for existing node")
        @Sql(scripts = {"classpath:master_data/countries_properties.sql", "classpath:master_data/nodes.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
        @Sql(scripts = {"classpath:master_data/nodes-cleanup.sql", "classpath:master_data/countries_properties-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
        void shouldReturnNodeDetailsForExistingNode() throws Exception {
            // Using node ID 16 (LX - Linde China) from test data
            mockMvc.perform(get("/api/nodes/16"))
                    .andExpect(status().isOk())
                    .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                    .andDo(print())
                    .andExpect(jsonPath("$.id", is(16)))
                    .andExpect(jsonPath("$.country", notNullValue()))
                    .andExpect(jsonPath("$.country.iso_code", is("CN")))
                    .andExpect(jsonPath("$.name", notNullValue()))
                    .andExpect(jsonPath("$.address", notNullValue()))
                    .andExpect(jsonPath("$.location", notNullValue()))
                    .andExpect(jsonPath("$.location.longitude", notNullValue()))
                    .andExpect(jsonPath("$.location.latitude", notNullValue()))
                    .andExpect(jsonPath("$.types", isA(java.util.List.class)))
                    .andExpect(jsonPath("$.types", hasItem("source")))
                    .andExpect(jsonPath("$.predecessors", notNullValue()))
                    .andExpect(jsonPath("$.outbound_countries", isA(java.util.List.class)))
                    .andExpect(jsonPath("$.is_deprecated", is(false)));
        }

        @Test
        @DisplayName("Should return bad request for non-existing node")
        @Sql(scripts = {"classpath:master_data/countries_properties.sql", "classpath:master_data/nodes.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
        @Sql(scripts = {"classpath:master_data/nodes-cleanup.sql", "classpath:master_data/countries_properties-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
        void shouldReturn400ForNonExistingNode() throws Exception {
            mockMvc.perform(get("/api/nodes/99999")).andExpect(status().isBadRequest());
        }

        @Test
        @DisplayName("Should return node with predecessor chains")
        @Sql(scripts = {"classpath:master_data/countries_properties.sql", "classpath:master_data/nodes.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
        @Sql(scripts = {"classpath:master_data/nodes-cleanup.sql", "classpath:master_data/countries_properties-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
        void shouldReturnNodeWithPredecessorChains() throws Exception {
            // Using node ID 24 (CTT - Châtellerault) which has predecessor chains
            mockMvc.perform(get("/api/nodes/24"))
                    .andExpect(status().isOk())
                    .andDo(print())
                    .andExpect(jsonPath("$.id", is(24)))
                    .andExpect(jsonPath("$.predecessors", notNullValue()))
                    .andExpect(jsonPath("$.predecessors", not(empty())));
        }
    }

    @Nested
    @DisplayName("PUT /api/nodes/{id} - Update Node")
    class UpdateNodeTests {

        @Test
        @DisplayName("Should update existing node successfully")
        @Sql(scripts = {"classpath:master_data/countries_properties.sql", "classpath:master_data/nodes.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
        @Sql(scripts = {"classpath:master_data/nodes-cleanup.sql", "classpath:master_data/countries_properties-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
        void shouldUpdateExistingNodeSuccessfully() throws Exception {
            NodeUpdateDTO updateDTO = new NodeUpdateDTO();
            updateDTO.setId(16);
            updateDTO.setAddress("Updated Address for Linde China");
            // Set other required fields based on your DTO structure

            mockMvc.perform(put("/api/nodes/16")
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(updateDTO)))
                    .andExpect(status().isOk())
                    .andExpect(content().string("16"));

            // Verify the update
            mockMvc.perform(get("/api/nodes/16"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.address", is("Updated Address for Linde China")));
        }

        @Test
        @DisplayName("Should return 400 when ID in path doesn't match ID in body")
        @Sql(scripts = {"classpath:master_data/countries_properties.sql", "classpath:master_data/nodes.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
        @Sql(scripts = {"classpath:master_data/nodes-cleanup.sql", "classpath:master_data/countries_properties-cleanup.sql"
        }, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
        void shouldReturn400WhenIdMismatch() throws Exception {
            NodeUpdateDTO updateDTO = new NodeUpdateDTO();
            updateDTO.setId(17); // Different from path parameter
            updateDTO.setAddress("Some address");

            mockMvc.perform(put("/api/nodes/16")
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(updateDTO)))
                    .andExpect(status().isBadRequest());
        }

        @Test
        @DisplayName("Should return 400 when trying to update non-existing node")
        @Sql(scripts = {"classpath:master_data/countries_properties.sql", "classpath:master_data/nodes.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
        @Sql(scripts = {"classpath:master_data/nodes-cleanup.sql", "classpath:master_data/countries_properties-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
        void shouldReturn400WhenUpdatingNonExistingNode() throws Exception {
            NodeUpdateDTO updateDTO = new NodeUpdateDTO();
            updateDTO.setId(99999);
            updateDTO.setAddress("Some address");

            mockMvc.perform(put("/api/nodes/99999")
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(updateDTO)))
                    .andExpect(status().isBadRequest());
        }
    }

    @Nested
    @DisplayName("DELETE /api/nodes/{id} - Delete Node")
    class DeleteNodeTests {

        @Test
        @DisplayName("Should mark node as deprecated successfully")
        @Sql(scripts = {"classpath:master_data/countries_properties.sql", "classpath:master_data/nodes.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
        @Sql(scripts = {"classpath:master_data/nodes-cleanup.sql", "classpath:master_data/countries_properties-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
        void shouldMarkNodeAsDeprecatedSuccessfully() throws Exception {
            mockMvc.perform(delete("/api/nodes/16"))
                    .andExpect(status().isOk())
                    .andExpect(content().string("16"));

            // Verify the node is no longer in the regular list (since deprecated nodes are filtered out)
            mockMvc.perform(get("/api/nodes/"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$[?(@.id == '16')]", hasSize(0)));
        }

        @Test
        @DisplayName("Should return 404 when trying to delete non-existing node")
        @Sql(scripts = {"classpath:master_data/countries_properties.sql", "classpath:master_data/nodes.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
        @Sql(scripts = {"classpath:master_data/nodes-cleanup.sql", "classpath:master_data/countries_properties-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
        void shouldReturn404WhenDeletingNonExistingNode() throws Exception {
            mockMvc.perform(delete("/api/nodes/99999"))
                    .andExpect(status().isBadRequest());
        }
    }

    @Nested
    @DisplayName("GET /api/nodes/locate - Locate Node")
    class LocateNodeTests {

        @Test
        @DisplayName("Should locate node by address")
        void shouldLocateNodeByAddress() throws Exception {
            mockMvc.perform(get("/api/nodes/locate")
                            .param("address", "Berlin, Germany"))
                    .andExpect(status().isOk())
                    .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                    .andExpect(jsonPath("$", notNullValue()));
            // Note: This test depends on the GeoApiService implementation
            // You might want to mock the service for more predictable results
        }

        @Test
        @DisplayName("Should return 400 when address parameter is missing")
        void shouldReturn400WhenAddressParameterIsMissing() throws Exception {
            mockMvc.perform(get("/api/nodes/locate"))
                    .andExpect(status().isBadRequest());
        }

        @Test
        @DisplayName("Should handle invalid addresses gracefully")
        void shouldHandleInvalidAddressesGracefully() throws Exception {
            mockMvc.perform(get("/api/nodes/locate")
                            .param("address", "InvalidAddressThatDoesNotExist123"))
                    .andExpect(status().isOk()); // Depends on GeoApiService implementation
            // The actual behavior depends on how your GeoApiService handles invalid addresses
        }
    }

    @Nested
    @DisplayName("PUT /api/nodes/ - Add User Node")
    class AddUserNodeTests {

        @Test
        @DisplayName("Should add user node successfully")
        @Sql(scripts = {"classpath:master_data/countries_properties.sql", "classpath:master_data/nodes.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
        @Sql(scripts = {"classpath:master_data/nodes-cleanup.sql", "classpath:master_data/countries_properties-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
        void shouldAddUserNodeSuccessfully() throws Exception {

            CountryDTO countryDTO = new CountryDTO();
            countryDTO.setIsoCode("DE");

            LocationDTO locationDTO = new LocationDTO(52.5139,13.4079);

            AddUserNodeDTO addUserNodeDTO = new AddUserNodeDTO();
            addUserNodeDTO.setName("MyUserNode");
            addUserNodeDTO.setAddress("Rathausstraße 5, 10178 Berlin");
            addUserNodeDTO.setCountry(countryDTO);
            addUserNodeDTO.setLocation(locationDTO);


            mockMvc.perform(put("/api/nodes/")
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(addUserNodeDTO)))
                    .andExpect(status().isOk());
        }

        @Test
        @DisplayName("Should return 400 for invalid user node data")
        void shouldReturn400ForInvalidUserNodeData() throws Exception {
            AddUserNodeDTO invalidDTO = new AddUserNodeDTO();
            // Leave required fields empty to trigger validation errors

            mockMvc.perform(put("/api/nodes/")
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(invalidDTO)))
                    .andExpect(status().isBadRequest());
        }
    }

    @Nested
    @DisplayName("Error Handling Tests")
    class ErrorHandlingTests {

        @Test
        @DisplayName("Should handle malformed JSON in request body")
        void shouldHandleMalformedJsonInRequestBody() throws Exception {
            String malformedJson = "{ \"id\": \"invalid\" malformed }";

            mockMvc.perform(put("/api/nodes/16")
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(malformedJson))
                    .andExpect(status().isBadRequest());
        }

        @Test
        @DisplayName("Should handle non-numeric node ID in path")
        void shouldHandleNonNumericNodeIdInPath() throws Exception {
            mockMvc.perform(get("/api/nodes/invalid-id"))
                    .andExpect(status().isBadRequest());
        }

        @Test
        @DisplayName("Should handle negative pagination parameters")
        void shouldHandleNegativePaginationParameters() throws Exception {
            mockMvc.perform(get("/api/nodes/")
                            .param("page", "-1")
                            .param("limit", "-5"))
                    .andExpect(status().isBadRequest());
        }
    }

    @Nested
    @DisplayName("Security Tests")
    class SecurityTests {

        @Test
        @DisplayName("Should reject requests with XSS attempts in filter")
        void shouldRejectRequestsWithXssAttemptsInFilter() throws Exception {
            String xssAttempt = "<script>alert('xss')</script>";

            mockMvc.perform(get("/api/nodes/")
                            .param("filter", xssAttempt))
                    .andExpect(status().isOk()) // Should not fail but should sanitize
                    .andExpect(jsonPath("$[*].address", not(containsString("<script>"))));
        }

        @Test
        @DisplayName("Should handle SQL injection attempts in filter")
        void shouldHandleSqlInjectionAttemptsInFilter() throws Exception {
            String sqlInjection = "'; DROP TABLE node; --";

            mockMvc.perform(get("/api/nodes/")
                            .param("filter", sqlInjection))
                    .andExpect(status().isOk()); // Should not cause database issues
        }
    }

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

        @Test
        @DisplayName("Should handle large page sizes efficiently")
        @Sql(scripts = {
                "classpath:master_data/countries_properties.sql",
                "classpath:master_data/nodes.sql"
        }, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
        @Sql(scripts = {
                "classpath:master_data/nodes-cleanup.sql",
                "classpath:master_data/countries_properties-cleanup.sql"
        }, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
        void shouldHandleLargePageSizesEfficiently() throws Exception {
            long startTime = System.currentTimeMillis();

            mockMvc.perform(get("/api/nodes/")
                            .param("limit", "1000"))
                    .andExpect(status().isOk());

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

            // Assert that the request completes within reasonable time (adjust as needed)
            assert executionTime < 5000; // 5 seconds max
        }
    }
}