package de.avatic.lcc.controller.configuration;

import org.junit.jupiter.api.*;
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.jdbc.core.JdbcTemplate;
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.MockMvcResultMatchers.*;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
@Transactional
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class PropertyControllerIntegrationTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private JdbcTemplate jdbcTemplate;

    // ========================================================================================
    // Property Set Tests
    // ========================================================================================

    @Test
    @Order(1)
    @DisplayName("Should return properties with default property_set parameter")
    @Sql(scripts = {"classpath:master_data/countries_properties.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
    @Sql(scripts = {"classpath:master_data/countries_properties-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
    void shouldReturnPropertiesWithDefaultPropertySet() throws Exception {
        mockMvc.perform(get("/api/properties/"))
                .andExpect(status().isOk())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                // Check that response is not empty
                .andExpect(jsonPath("$").isArray())
                .andExpect(jsonPath("$").isNotEmpty())
                // Verify some specific properties we know should be present from the test data
                .andExpect(jsonPath("$[?(@.external_mapping_id=='WORKDAYS')].current_value").value("210"))
                .andExpect(jsonPath("$[?(@.external_mapping_id=='WORKDAYS')].draft_value").value(contains(nullValue())))
                .andExpect(jsonPath("$[?(@.external_mapping_id=='INTEREST_RATE')].current_value").value("0.12"))
                .andExpect(jsonPath("$[?(@.external_mapping_id=='INTEREST_RATE')].draft_value").value(contains(nullValue())))
                .andReturn();
    }

    @Test
    @Order(2)
    @DisplayName("Should return properties for specific property_set")
    @Sql(scripts = {"classpath:master_data/countries_properties.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
    @Sql(scripts = {"classpath:master_data/countries_properties-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
    void shouldReturnPropertiesForSpecificPropertySet() throws Exception {

        Integer propertySetId = getExpiredPropertySet();

        // When & Then
        mockMvc.perform(get("/api/properties/")
                        .param("property_set", propertySetId.toString()))
                .andExpect(status().isOk())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                .andExpect(jsonPath("$", hasSize(greaterThan(0))))
                .andExpect(jsonPath("$[?(@.external_mapping_id=='TRUCK_UTIL')].current_value").value("0.8"))
                .andReturn();
    }

    private Integer getExpiredPropertySet() {
        return jdbcTemplate.queryForObject(
                "SELECT id FROM property_set WHERE state = 'EXPIRED' LIMIT 1",
                Integer.class);
    }


    @Test
    @Order(3)
    @DisplayName("Should return empty list for non-existent property_set")
    @Sql(scripts = {"classpath:master_data/countries_properties.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
    @Sql(scripts = {"classpath:master_data/countries_properties-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
    void shouldReturnEmptyListForNonExistentPropertySet() throws Exception {
        // When & Then
        mockMvc.perform(get("/api/properties/")
                        .param("property_set", "99999"))
                .andExpect(status().isBadRequest())
                .andReturn();
    }

    // ========================================================================================
    // Validity Period Tests
    // ========================================================================================

    @Test
    @Order(4)
    @DisplayName("Should return all validity periods")
    @Sql(scripts = {"classpath:master_data/countries_properties.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
    @Sql(scripts = {"classpath:master_data/countries_properties-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
    void shouldReturnAllValidityPeriods() throws Exception {
        // Given: Test data includes validity periods

        // When & Then
        mockMvc.perform(get("/api/properties/periods/"))
                .andExpect(status().isOk())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                .andExpect(jsonPath("$").isArray())
                .andExpect(jsonPath("$", hasSize(2)))
                .andExpect(jsonPath("$[0].id").exists())
                .andExpect(jsonPath("$[0].start_date").exists())
                .andExpect(jsonPath("$[0].state").exists());

    }

    @Test
    @Order(5)
    @DisplayName("Should invalidate validity period successfully")
    @Sql(scripts = {"classpath:master_data/countries_properties.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
    @Sql(scripts = {"classpath:master_data/countries_properties-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
    void shouldInvalidateValidityPeriod() throws Exception {
        // Given: Create a valid period
        Integer propertySetId = getExpiredPropertySet();

        // When
        mockMvc.perform(delete("/api/properties/periods/" + propertySetId.toString()))
                .andExpect(status().isOk());

        // Then: Verify state change in database
        mockMvc.perform(get("/api/properties/periods/"))
                .andExpect(status().isOk())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                .andExpect(jsonPath("$").isArray())
                .andExpect(jsonPath("$[?(@.id == " + propertySetId + ")].state").value("INVALID"));

    }

    @Test
    @Order(6)
    @DisplayName("Should return error for non-existent validity period/ not expired validity period")
    @Sql(scripts = {"classpath:master_data/countries_properties.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
    @Sql(scripts = {"classpath:master_data/countries_properties-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
    void shouldReturn404ForNonExistentValidityPeriod() throws Exception {

        // When & Then
        mockMvc.perform(delete("/api/properties/periods/2")) // <- non expired
                .andExpect(status().isBadRequest());

        // When & Then
        mockMvc.perform(delete("/api/properties/periods/99999")) // <- non existent
                .andExpect(status().isBadRequest());
    }

    // ========================================================================================
    // Country Property Tests
    // ========================================================================================

    @Test
    @Order(7)
    @DisplayName("Should update existing country property")
    @Sql(scripts = {"classpath:master_data/countries_properties.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
    @Sql(scripts = {"classpath:master_data/countries_properties-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
    void shouldUpdateExistingCountryProperty() throws Exception {
        // Given: Country and property type exist
        String isoCode = "DE";
        String externalMappingId = "SAFETY_STOCK";
        String value = "19";

        // check if draft value is null
        mockMvc.perform(get("/api/countries/" + isoCode)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(value))
                .andExpect(status().isOk())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                .andExpect(jsonPath("$.properties[?(@.external_mapping_id=='"+externalMappingId+"')].draft_value").value(contains(nullValue())));


        // When
        mockMvc.perform(put("/api/properties/country/" + isoCode + "/" + externalMappingId)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(value))
                .andExpect(status().isOk());


        // Then
        mockMvc.perform(get("/api/countries/" + isoCode)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(value))
                .andExpect(status().isOk())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                .andExpect(jsonPath("$.properties[?(@.external_mapping_id=='"+externalMappingId+"')].draft_value").value("19"));

    }


    @Test
    @Order(9)
    @DisplayName("Should reject invalid ISO code")
    @Sql(scripts = {"classpath:master_data/countries_properties.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
    @Sql(scripts = {"classpath:master_data/countries_properties-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
    void shouldRejectInvalidIsoCode() throws Exception {
        String invalidIsoCode = "INV";
        String validIsoCode = "DE";
        String externalMappingId = "SAFETY_STOCK";
        String value = "19";
        String invalidValue = "none";


        // When & Then
        mockMvc.perform(put("/api/properties/country/" + invalidIsoCode + "/" + externalMappingId)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(value))
                .andExpect(status().is(not(200)));

        // When & Then
        mockMvc.perform(put("/api/properties/country/" + validIsoCode + "/" + externalMappingId)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(invalidValue))
                .andExpect(status().is(not(200)));
    }

    // ========================================================================================
    // System Property Tests
    // ========================================================================================

    @Test
    @Order(10)
    @DisplayName("Should set system property successfully")
    @Sql(scripts = {"classpath:master_data/countries_properties.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
    @Sql(scripts = {"classpath:master_data/countries_properties-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
    void shouldSetSystemProperty() throws Exception {
        // Given
        String externalMappingId = "RADIUS_REGION";
        String value = "120";


        // When & Then
        mockMvc.perform(get("/api/properties/staged_changes"))
                .andExpect(status().isOk())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                .andExpect(content().string("false"));

        // When
        mockMvc.perform(put("/api/properties/system/" + externalMappingId)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(value))
                .andExpect(status().isOk());

        // Then
        mockMvc.perform(get("/api/properties/")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(value))
                .andExpect(status().isOk())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                .andExpect(jsonPath("$[?(@.external_mapping_id=='"+externalMappingId+"')].draft_value").value(value));

        // When & Then
        mockMvc.perform(get("/api/properties/staged_changes"))
                .andExpect(status().isOk())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                .andExpect(content().string("true"));

    }

    @Test
    @Order(11)
    @DisplayName("Should validate property data type")
    @Sql(scripts = {"classpath:master_data/countries_properties.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
    @Sql(scripts = {"classpath:master_data/countries_properties-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
    void shouldValidatePropertyDataType() throws Exception {
        // Given: Property type with INT data type
        String externalMappingId = "INTEREST_RATE";
        String invalidValue = "none";

        // When & Then
        mockMvc.perform(put("/api/properties/system/" + externalMappingId)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(invalidValue))
                .andExpect(status().isBadRequest());
    }

    // ========================================================================================
    // Staged Changes Tests
    // ========================================================================================


    @Test
    @Order(14)
    @DisplayName("Should approve staged changes successfully")
    @Sql(scripts = {"classpath:master_data/countries_properties.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
    @Sql(scripts = {"classpath:master_data/countries_properties-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
    void shouldApproveStagedChangesSuccessfully() throws Exception {

        // Given
        String externalMappingId = "RADIUS_REGION";
        String value = "120";

        mockMvc.perform(get("/api/properties/periods")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(value))
                .andExpect(status().isOk())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                .andExpect(jsonPath("$[?(@.state == 'DRAFT')]").isEmpty());

        // When & Then
        mockMvc.perform(get("/api/properties/staged_changes"))
                .andExpect(status().isOk())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                .andExpect(content().string("false"));

        // When
        mockMvc.perform(put("/api/properties/system/" + externalMappingId)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(value))
                .andExpect(status().isOk());

        // Then
        mockMvc.perform(get("/api/properties/")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(value))
                .andExpect(status().isOk())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                .andExpect(jsonPath("$[?(@.external_mapping_id=='"+externalMappingId+"')].draft_value").value(value));

        // Then
        mockMvc.perform(get("/api/properties/periods")
                        .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                .andExpect(jsonPath("$[?(@.state == 'DRAFT')]", hasSize(1)));

        // When & Then
        mockMvc.perform(get("/api/properties/staged_changes"))
                .andExpect(status().isOk())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                .andExpect(content().string("true"));

        // get draft set id from db:
        Integer draftPeriodId = getDraftPropertySet();

        // make draft set new valid set.
        mockMvc.perform(put("/api/properties/staged_changes"))
                .andExpect(status().isOk());

        // Then
        mockMvc.perform(get("/api/properties/periods")
                        .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                .andExpect(jsonPath("$[?(@.id == " + draftPeriodId + " && @.state == 'VALID')]", hasSize(1)));
    }

    private Integer getDraftPropertySet() {
        return jdbcTemplate.queryForObject(
                "SELECT id FROM property_set WHERE state = 'DRAFT' LIMIT 1",
                Integer.class);
    }


}