package de.avatic.lcc.controller.calculation;


import com.fasterxml.jackson.databind.ObjectMapper;
import de.avatic.lcc.dto.calculation.edit.destination.DestinationCreateDTO;
import de.avatic.lcc.dto.calculation.edit.destination.DestinationUpdateDTO;
import org.assertj.core.api.InstanceOfAssertFactories;
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.context.annotation.Import;
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 java.math.BigDecimal;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;

import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
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.status;

@SpringBootTest
@AutoConfigureMockMvc
@Transactional
@Import({PremiseControllerTestData.class, PremiseTestsHelper.class})

public class DestinationIntegrationTest {

    @Autowired
    protected JdbcTemplate jdbcTemplate;
    @Autowired
    private MockMvc mockMvc;
    @Autowired
    private ObjectMapper objectMapper;
    @Autowired
    private PremiseControllerTestData premiseTestData;
    @Autowired
    private PremiseTestsHelper testHelper;

    @Nested
    @Sql(scripts = {"classpath:master_data/alldata.sql", "classpath:master_data/users.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS)
    @Sql(scripts = {"classpath:master_data/users-cleanup.sql", "classpath:master_data/alldata-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_CLASS)
    @Sql(scripts = {"classpath:master_data/premises.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
    @Sql(scripts = {"classpath:master_data/premises-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
    class CreateDestinationTests {

        @Test
        @DisplayName("POST /api/calculation/destination/ - happy path (create destination)")
        public void createDestinationCase1() throws Exception {

            var nodeId = testHelper.getNodeIdByExternalMappingId("AB");

            var premisesBeforeUpdate = testHelper.getPremisesFromDb();
            var premise1 = premisesBeforeUpdate.stream().filter(p -> p.getHuUnitCount() == 2).findFirst().orElseThrow();
            var premise3 = premisesBeforeUpdate.stream().filter(p -> p.getHuUnitCount() == 3).findFirst().orElseThrow();

            var dto = new DestinationCreateDTO();

            var map = new HashMap<Integer, List<Integer>>();
            map.put(premise1.getId(), List.of(nodeId));
            map.put(premise3.getId(), List.of(nodeId));
            dto.setDestinationNodeIds(map);

            mockMvc.perform(post("/api/calculation/destination")
                    .content(objectMapper.writeValueAsString(dto))
                    .contentType(MediaType.APPLICATION_JSON)).andExpect(status().isOk()).andDo(print());

            // TODO check ...

        }
    }

    @Nested
    @Sql(scripts = {"classpath:master_data/alldata.sql", "classpath:master_data/users.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS)
    @Sql(scripts = {"classpath:master_data/users-cleanup.sql", "classpath:master_data/alldata-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_CLASS)
    @Sql(scripts = {"classpath:master_data/premises.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
    @Sql(scripts = {"classpath:master_data/premises-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
    class GetDestinationTests {

        @BeforeEach
        void setUp() {
            premiseTestData.generateData();
        }

        @AfterEach
        void cleanUp() {
            premiseTestData.deleteAll();
        }

        @Test
        @DisplayName("GET /api/calculation/destination - happy path (get destination)")
        public void getDestinationCase1() throws Exception {

            var premisesFromDb = testHelper.getPremisesFromDb();
            var premise1 = premisesFromDb.stream().filter(p -> p.getHuUnitCount() == 2).findFirst().orElseThrow();

            var destIds = testHelper.getDestinationIdsByPremiseId(premise1.getId());


            mockMvc.perform(get("/api/calculation/destination/" + destIds.getFirst()))
                    .andExpect(status().isOk()).andDo(print());

            // TODO check ...

        }
    }

    @Nested
    @Sql(scripts = {"classpath:master_data/alldata.sql", "classpath:master_data/users.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS)
    @Sql(scripts = {"classpath:master_data/users-cleanup.sql", "classpath:master_data/alldata-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_CLASS)
    @Sql(scripts = {"classpath:master_data/premises.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
    @Sql(scripts = {"classpath:master_data/premises-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
    class UpdateDestinationTests {

        @BeforeEach
        void setUp() {
            premiseTestData.generateData();
        }

        @AfterEach
        void cleanUp() {
            premiseTestData.deleteAll();
        }

        @Test
        @DisplayName("PUT /api/calculation/destination - happy path (update destination)")
        public void getDestinationCase1() throws Exception {

            var premisesFromDb = testHelper.getPremisesFromDb();
            var premise1 = premisesFromDb.stream().filter(p -> p.getHuUnitCount() == 2).findFirst().orElseThrow();

            List<Integer> destId = testHelper.getDestinationIdsByPremiseId(premise1.getId());
            assertThat(destId).asInstanceOf(InstanceOfAssertFactories.LIST).isNotEmpty();

            var destinationBefore = testHelper.getDestinationById(destId.getFirst());
            var routesBefore = testHelper.getRoutesByDestinationId(destId.getFirst());

            var dto = new DestinationUpdateDTO();
            dto.setDisposalCost(666.6);
            dto.setHandlingCost(777.7);
            dto.setRepackingCost(888.8);
            dto.setRouteSelectedId(routesBefore.getFirst().getId());

            mockMvc.perform(put("/api/calculation/destination/" + destId.getFirst())
                            .content(objectMapper.writeValueAsString(dto)).contentType(MediaType.APPLICATION_JSON))
                    .andExpect(status().isOk())
                    .andDo(print());

            var destinationAfter = testHelper.getDestinationById(destId.getFirst());
            var routesAfter = testHelper.getRoutesByDestinationId(destId.getFirst());

            assertThat(destinationAfter).isNotEmpty();
            assertThat(destinationAfter.get().getDisposalCost()).isEqualByComparingTo(BigDecimal.valueOf(666.6));
            assertThat(destinationAfter.get().getHandlingCost()).isEqualByComparingTo(BigDecimal.valueOf(777.7));
            assertThat(destinationAfter.get().getRepackingCost()).isEqualByComparingTo(BigDecimal.valueOf(888.8));
            assertThat(routesAfter.getFirst().getSelected()).isEqualTo(true);
            assertThat(routesAfter.getLast().getSelected()).isEqualTo(false);
        }

        @Test
        @DisplayName("PUT /api/calculation/destination - happy path (partial update destination)")
        public void getDestinationCase2() throws Exception {

            var premisesFromDb = testHelper.getPremisesFromDb();
            var premise1 = premisesFromDb.stream().filter(p -> p.getHuUnitCount() == 2).findFirst().orElseThrow();

            List<Integer> destId = testHelper.getDestinationIdsByPremiseId(premise1.getId());
            assertThat(destId).asInstanceOf(InstanceOfAssertFactories.LIST).isNotEmpty();

            var destinationBefore = testHelper.getDestinationById(destId.getFirst()).orElseThrow();
            var routesBefore = testHelper.getRoutesByDestinationId(destId.getFirst());

            var dto = new DestinationUpdateDTO();
            dto.setDisposalCost(666.6);
            dto.setHandlingCost(null);
            dto.setRepackingCost(null);
            dto.setRouteSelectedId(routesBefore.getLast().getId());

            mockMvc.perform(put("/api/calculation/destination/" + destId.getFirst())
                            .content(objectMapper.writeValueAsString(dto)).contentType(MediaType.APPLICATION_JSON))
                    .andExpect(status().isOk())
                    .andDo(print());

            var destinationAfter = testHelper.getDestinationById(destId.getFirst());
            var routesAfter = testHelper.getRoutesByDestinationId(destId.getFirst());

            assertThat(destinationAfter).isNotEmpty();
            assertThat(destinationAfter.get().getDisposalCost()).isEqualByComparingTo(BigDecimal.valueOf(666.6));
            assertThat(destinationAfter.get().getHandlingCost()).isEqualByComparingTo(destinationBefore.getHandlingCost());
            assertThat(destinationAfter.get().getRepackingCost()).isEqualByComparingTo(destinationBefore.getHandlingCost());
            assertThat(routesAfter.getFirst().getSelected()).isEqualTo(false);
            assertThat(routesAfter.getLast().getSelected()).isEqualTo(true);
        }

        @Test
        @DisplayName("PUT /api/calculation/destination - error case (negative value)")
        public void getDestinationCase3() throws Exception {

            var premisesFromDb = testHelper.getPremisesFromDb();
            var premise1 = premisesFromDb.stream().filter(p -> p.getHuUnitCount() == 2).findFirst().orElseThrow();

            List<Integer> destId = testHelper.getDestinationIdsByPremiseId(premise1.getId());
            assertThat(destId).asInstanceOf(InstanceOfAssertFactories.LIST).isNotEmpty();

            var destinationBefore = testHelper.getDestinationById(destId.getFirst()).orElseThrow();
            var routesBefore = testHelper.getRoutesByDestinationId(destId.getFirst());

            var dto = new DestinationUpdateDTO();
            dto.setDisposalCost(-666.6);
            dto.setHandlingCost(null);
            dto.setRepackingCost(null);
            dto.setRouteSelectedId(routesBefore.getLast().getId());

            mockMvc.perform(put("/api/calculation/destination/" + destId.getFirst())
                            .content(objectMapper.writeValueAsString(dto)).contentType(MediaType.APPLICATION_JSON))
                    .andExpect(status().isBadRequest())
                    .andDo(print());

            var destinationAfter = testHelper.getDestinationById(destId.getFirst());
            var routesAfter = testHelper.getRoutesByDestinationId(destId.getFirst());

            assertThat(destinationAfter).isNotEmpty();
            assertThat(destinationAfter.get().getDisposalCost()).isEqualByComparingTo(destinationBefore.getHandlingCost());
            assertThat(destinationAfter.get().getHandlingCost()).isEqualByComparingTo(destinationBefore.getHandlingCost());
            assertThat(destinationAfter.get().getRepackingCost()).isEqualByComparingTo(destinationBefore.getHandlingCost());
            assertThat(routesAfter.getFirst().getSelected()).isEqualTo(routesBefore.getFirst().getSelected());
            assertThat(routesAfter.getLast().getSelected()).isEqualTo(routesBefore.getLast().getSelected());
        }

    }

    @Nested
    @Sql(scripts = {"classpath:master_data/alldata.sql", "classpath:master_data/users.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS)
    @Sql(scripts = {"classpath:master_data/users-cleanup.sql", "classpath:master_data/alldata-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_CLASS)
    @Sql(scripts = {"classpath:master_data/premises.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
    @Sql(scripts = {"classpath:master_data/premises-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
    class DeleteDestinationTests {

        @BeforeEach
        void setUp() {
            premiseTestData.generateData();
        }

        @AfterEach
        void cleanUp() {
            premiseTestData.deleteAll();
        }


        @Test
        @DisplayName("DELETE /api/calculation/destination - happy path (delete single destination)")
        public void deleteDestinationCase1() throws Exception {

            var premisesFromDb = testHelper.getPremisesFromDb();
            var premise1 = premisesFromDb.stream().filter(p -> p.getHuUnitCount() == 2).findFirst().orElseThrow();

            List<Integer> destIdsBefore = testHelper.getDestinationIdsByPremiseId(premise1.getId());
            assertThat(destIdsBefore).asInstanceOf(InstanceOfAssertFactories.LIST).hasSize(2);


            mockMvc.perform(delete("/api/calculation/destination/" + destIdsBefore.getFirst()))
                    .andExpect(status().isOk())
                    .andDo(print());

            List<Integer> destIdsAfter = testHelper.getDestinationIdsByPremiseId(premise1.getId());
            assertThat(destIdsAfter).asInstanceOf(InstanceOfAssertFactories.LIST).hasSize(1);

            var destinationAfter = testHelper.getDestinationById(destIdsBefore.getFirst());
            assertThat(destinationAfter).isEmpty();

        }


        @Test
        @DisplayName("DELETE /api/calculation/destination - happy path (delete all destination)")
        public void deleteDestinationCase2() throws Exception {

            var premisesFromDb = testHelper.getPremisesFromDb();
            var premise1 = premisesFromDb.stream().filter(p -> p.getHuUnitCount() == 2).findFirst().orElseThrow();

            List<Integer> destIdsBefore = testHelper.getDestinationIdsByPremiseId(premise1.getId());
            assertThat(destIdsBefore).asInstanceOf(InstanceOfAssertFactories.LIST).hasSize(2);

            assertThat(testHelper.getRouteCount()).isNotEqualTo(0);
            assertThat(testHelper.getRouteSectionCount()).isNotEqualTo(0);
            assertThat(testHelper.getRouteNodeCount()).isNotEqualTo(0);


            mockMvc.perform(delete("/api/calculation/destination/" + destIdsBefore.getFirst()))
                    .andExpect(status().isOk())
                    .andDo(print());

            mockMvc.perform(delete("/api/calculation/destination/" + destIdsBefore.getLast()))
                    .andExpect(status().isOk())
                    .andDo(print());

            List<Integer> destIdsAfter = testHelper.getDestinationIdsByPremiseId(premise1.getId());
            assertThat(destIdsAfter).asInstanceOf(InstanceOfAssertFactories.LIST).hasSize(0);

            assertThat(testHelper.getDestinationById(destIdsBefore.getFirst())).isEmpty();
            assertThat(testHelper.getDestinationById(destIdsBefore.getLast())).isEmpty();

            assertThat(testHelper.getRouteCount()).isEqualTo(0);
            assertThat(testHelper.getRouteSectionCount()).isEqualTo(0);
            assertThat(testHelper.getRouteNodeCount()).isEqualTo(0);
        }


        @Test
        @DisplayName("DELETE /api/calculation/destination - error case (unknown id)")
        public void deleteDestinationCase3() throws Exception {

            var premisesFromDb = testHelper.getPremisesFromDb();
            var premise1 = premisesFromDb.stream().filter(p -> p.getHuUnitCount() == 2).findFirst().orElseThrow();

            List<Integer> destIdsBefore = testHelper.getDestinationIdsByPremiseId(premise1.getId());
            assertThat(destIdsBefore).asInstanceOf(InstanceOfAssertFactories.LIST).hasSize(2);

            mockMvc.perform(delete("/api/calculation/destination/" + 9999))
                    .andExpect(status().isForbidden())
                    .andDo(print());

            List<Integer> destIdsAfter = testHelper.getDestinationIdsByPremiseId(premise1.getId());
            assertThat(destIdsAfter).asInstanceOf(InstanceOfAssertFactories.LIST).hasSize(2);

        }

    }


}
