package io.github.js0ny.ilp_coursework.controller;

import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;

import io.github.js0ny.ilp_coursework.data.common.DroneCapability;
import io.github.js0ny.ilp_coursework.data.common.LngLat;
import io.github.js0ny.ilp_coursework.data.external.Drone;
import io.github.js0ny.ilp_coursework.data.request.AttrQueryRequest;
import io.github.js0ny.ilp_coursework.data.request.MedDispatchRecRequest;
import io.github.js0ny.ilp_coursework.service.DroneAttrComparatorService;
import io.github.js0ny.ilp_coursework.service.DroneInfoService;
import io.github.js0ny.ilp_coursework.service.PathFinderService;

import org.junit.jupiter.api.BeforeEach;
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.WebMvcTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;

import java.time.LocalDate;
import java.time.LocalTime;
import java.util.List;
import java.util.Map;

@WebMvcTest(DroneController.class)
public class DroneControllerTest {

    @Autowired private MockMvc mockMvc;

    private ObjectMapper objectMapper;

    @MockitoBean private DroneInfoService droneInfoService;

    @MockitoBean private DroneAttrComparatorService droneAttrComparatorService;

    @MockitoBean private PathFinderService pathFinderService;

    @BeforeEach
    void setUp() {
        objectMapper = new ObjectMapper();
        objectMapper.registerModule(new JavaTimeModule());
    }

    @Nested
    @DisplayName("GET /dronesWithCooling/{state}")
    class GetDronesWithCoolingTest {

        final String API_ENDPOINT_BASE = "/api/v1/dronesWithCooling/";

        @Test
        @DisplayName("true -> 200 OK")
        void getDronesWithCooling_shouldReturn200AndArrayOfString_whenStateIsTrue()
                throws Exception {
            String endpoint = API_ENDPOINT_BASE + "true";
            List<String> expected = List.of("1", "5", "8", "9");
            when(droneInfoService.dronesWithCooling(anyBoolean())).thenReturn(expected);
            var mock = mockMvc.perform(get(endpoint).contentType(MediaType.APPLICATION_JSON));

            mock.andExpect(status().isOk());
            mock.andExpect(content().json(objectMapper.writeValueAsString(expected)));
        }

        @Test
        @DisplayName("false -> 200 OK")
        void getDronesWithCooling_shouldReturn200AndArrayOfString_whenStateIsFalse()
                throws Exception {
            String endpoint = API_ENDPOINT_BASE + "false";
            List<String> expected = List.of("2", "3", "4", "6", "7", "10");
            when(droneInfoService.dronesWithCooling(anyBoolean())).thenReturn(expected);
            var mock = mockMvc.perform(get(endpoint).contentType(MediaType.APPLICATION_JSON));

            mock.andExpect(status().isOk());
            mock.andExpect(content().json(objectMapper.writeValueAsString(expected)));
        }

        @Test
        @DisplayName("-> 400 Bad Request")
        void getDronesWithCooling_shouldReturn400_whenStateIsInvalid() throws Exception {
            String endpoint = API_ENDPOINT_BASE + "invalid";
            mockMvc.perform(get(endpoint).contentType(MediaType.APPLICATION_JSON))
                    .andExpect(status().isBadRequest());
        }
    }

    @Nested
    @DisplayName("GET /droneDetails/{droneId}")
    class DroneDetailsTest {

        static final String API_ENDPOINT_BASE = "/api/v1/droneDetails/";

        @Test
        @DisplayName("-> 200 OK")
        void getDroneDetails_shouldReturn200AndJson_whenCorrectInput() throws Exception {
            Drone expected =
                    new Drone(
                            "Drone 1",
                            "1",
                            new DroneCapability(true, true, 4.0f, 2000, 0.01f, 4.3f, 6.5f));
            String endpoint = API_ENDPOINT_BASE + "1";
            when(droneInfoService.droneDetail(anyString())).thenReturn(expected);
            var mock = mockMvc.perform(get(endpoint).contentType(MediaType.APPLICATION_JSON));

            mock.andExpect(status().isOk());
            mock.andExpect(content().json(objectMapper.writeValueAsString(expected)));
        }

        @Test
        @DisplayName("-> 404 Not Found")
        void getDroneDetails_shouldReturn404_whenDroneNotFound() throws Exception {
            String endpoint = API_ENDPOINT_BASE + "invalidDroneId";
            when(droneInfoService.droneDetail(anyString()))
                    .thenThrow(new IllegalArgumentException());
            mockMvc.perform(get(endpoint).contentType(MediaType.APPLICATION_JSON))
                    .andExpect(status().isNotFound());
        }
    }

    @Nested
    @DisplayName("GET /queryAsPath/{attrName}/{attrVal}")
    class GetQueryAsPathTests {

        final String API_ENDPOINT_BASE = "/api/v1/queryAsPath/";

        @Test
        @DisplayName("capacity = 8 -> 200 OK")
        void getQueryAsPath_shouldReturn200AndArrayOfString_whenCapacityIs8() throws Exception {
            String attrName = "capacity";
            String attrVal = "8";
            List<String> expected = List.of("2", "4", "7", "9");
            when(droneAttrComparatorService.dronesWithAttribute(attrName, attrVal))
                    .thenReturn(expected);

            mockMvc.perform(
                            get(API_ENDPOINT_BASE + attrName + "/" + attrVal)
                                    .contentType(MediaType.APPLICATION_JSON))
                    .andExpect(status().isOk())
                    .andExpect(content().json(objectMapper.writeValueAsString(expected)));
        }

        @Test
        @DisplayName("heating = true -> 200 OK")
        void getQueryAsPath_shouldReturn200AndArrayOfString_whenHeatingIsTrue() throws Exception {
            String attrName = "heating";
            String attrVal = "true";
            List<String> expected = List.of("1", "2", "4", "5", "6", "7", "9");
            when(droneAttrComparatorService.dronesWithAttribute(attrName, attrVal))
                    .thenReturn(expected);

            mockMvc.perform(
                            get(API_ENDPOINT_BASE + attrName + "/" + attrVal)
                                    .contentType(MediaType.APPLICATION_JSON))
                    .andExpect(status().isOk())
                    .andExpect(content().json(objectMapper.writeValueAsString(expected)));
        }

        @Test
        @DisplayName("cooling = false -> 200 OK")
        void getQueryAsPath_shouldReturn200AndArrayOfString_whenCoolingIsFalse() throws Exception {
            String attrName = "cooling";
            String attrVal = "false";
            List<String> expected = List.of("2", "3", "4", "6", "7", "10");
            when(droneAttrComparatorService.dronesWithAttribute(attrName, attrVal))
                    .thenReturn(expected);

            mockMvc.perform(
                            get(API_ENDPOINT_BASE + attrName + "/" + attrVal)
                                    .contentType(MediaType.APPLICATION_JSON))
                    .andExpect(status().isOk())
                    .andExpect(content().json(objectMapper.writeValueAsString(expected)));
        }

        @Test
        @DisplayName("maxMoves = 1000 -> 200 OK")
        void getQueryAsPath_shouldReturn200AndArrayOfString_whenMaxMovesIs1000() throws Exception {
            String attrName = "maxMoves";
            String attrVal = "1000";
            List<String> expected = List.of("2", "4", "7", "9");
            when(droneAttrComparatorService.dronesWithAttribute(attrName, attrVal))
                    .thenReturn(expected);

            mockMvc.perform(
                            get(API_ENDPOINT_BASE + attrName + "/" + attrVal)
                                    .contentType(MediaType.APPLICATION_JSON))
                    .andExpect(status().isOk())
                    .andExpect(content().json(objectMapper.writeValueAsString(expected)));
        }

        @Test
        @DisplayName("invalid = null -> 200 OK (empty list)")
        void getQueryAsPath_shouldReturn200AndEmptyArrayOfString_whenInvalidAttribute()
                throws Exception {
            String attrName = "invalid";
            String attrVal = "null";
            List<String> expected = List.of();
            when(droneAttrComparatorService.dronesWithAttribute(attrName, attrVal))
                    .thenReturn(expected);

            mockMvc.perform(
                            get(API_ENDPOINT_BASE + attrName + "/" + attrVal)
                                    .contentType(MediaType.APPLICATION_JSON))
                    .andExpect(status().isOk())
                    .andExpect(content().json(objectMapper.writeValueAsString(expected)));
        }
    }

    @Nested
    @DisplayName("POST /query")
    class PostQueryTests {

        final String API_ENDPOINT = "/api/v1/query";

        @Test
        @DisplayName("three eqs -> 200 OK")
        void postQuery_shouldReturn200AndArrayOfString_whenThreeEqualsConditions()
                throws Exception {
            AttrQueryRequest req1 = new AttrQueryRequest("capacity", "=", "20");
            AttrQueryRequest req2 = new AttrQueryRequest("heating", "=", "false");
            AttrQueryRequest req3 = new AttrQueryRequest("cooling", "=", "true");
            AttrQueryRequest[] requestBody = {req1, req2, req3};
            List<String> expected = List.of("8");

            when(droneAttrComparatorService.dronesSatisfyingAttributes(
                            any(AttrQueryRequest[].class)))
                    .thenReturn(expected);

            mockMvc.perform(
                            post(API_ENDPOINT)
                                    .contentType(MediaType.APPLICATION_JSON)
                                    .content(objectMapper.writeValueAsString(requestBody)))
                    .andExpect(status().isOk())
                    .andExpect(content().json(objectMapper.writeValueAsString(expected)));
        }

        @Test
        @DisplayName("GT LT -> 200 OK")
        void postQuery_shouldReturn200AndArrayOfString_whenGtLtConditions() throws Exception {
            AttrQueryRequest req1 = new AttrQueryRequest("capacity", ">", "8");
            AttrQueryRequest req2 = new AttrQueryRequest("maxMoves", "<", "2000");
            AttrQueryRequest[] requestBody = {req1, req2};
            List<String> expected = List.of("5", "10");

            when(droneAttrComparatorService.dronesSatisfyingAttributes(
                            any(AttrQueryRequest[].class)))
                    .thenReturn(expected);

            mockMvc.perform(
                            post(API_ENDPOINT)
                                    .contentType(MediaType.APPLICATION_JSON)
                                    .content(objectMapper.writeValueAsString(requestBody)))
                    .andExpect(status().isOk())
                    .andExpect(content().json(objectMapper.writeValueAsString(expected)));
        }

        @Test
        @DisplayName("GT LT -contradict -> 200 OK (empty list)")
        void postQuery_shouldReturn200AndEmptyArrayOfString_whenContradictoryConditions()
                throws Exception {
            AttrQueryRequest req1 = new AttrQueryRequest("capacity", ">", "8");
            AttrQueryRequest req2 = new AttrQueryRequest("capacity", "<", "8");
            AttrQueryRequest[] requestBody = {req1, req2};
            List<String> expected = List.of();

            when(droneAttrComparatorService.dronesSatisfyingAttributes(
                            any(AttrQueryRequest[].class)))
                    .thenReturn(expected);

            mockMvc.perform(
                            post(API_ENDPOINT)
                                    .contentType(MediaType.APPLICATION_JSON)
                                    .content(objectMapper.writeValueAsString(requestBody)))
                    .andExpect(status().isOk())
                    .andExpect(content().json(objectMapper.writeValueAsString(expected)));
        }

        @Test
        @DisplayName("GT LT EQ -> 200 OK")
        void postQuery_shouldReturn200AndArrayOfString_whenGtLtEqConditions() throws Exception {
            AttrQueryRequest req1 = new AttrQueryRequest("capacity", ">", "8");
            AttrQueryRequest req2 = new AttrQueryRequest("maxMoves", "<", "2000");
            AttrQueryRequest req3 = new AttrQueryRequest("cooling", "=", "true");
            AttrQueryRequest[] requestBody = {req1, req2, req3};
            List<String> expected = List.of("5");

            when(droneAttrComparatorService.dronesSatisfyingAttributes(
                            any(AttrQueryRequest[].class)))
                    .thenReturn(expected);

            mockMvc.perform(
                            post(API_ENDPOINT)
                                    .contentType(MediaType.APPLICATION_JSON)
                                    .content(objectMapper.writeValueAsString(requestBody)))
                    .andExpect(status().isOk())
                    .andExpect(content().json(objectMapper.writeValueAsString(expected)));
        }
    }

    @Nested
    @DisplayName("POST /queryAvailableDrones")
    class PostQueryAvailableDronesTests {

        final String API_ENDPOINT = "/api/v1/queryAvailableDrones";

        @Test
        @DisplayName("Example -> 200 OK")
        void postQueryAvailableDrones_shouldReturn200AndArrayOfString_whenExampleRequest()
                throws Exception {
            var reqs = new MedDispatchRecRequest.MedRequirement(0.75f, false, true, 13.5f);
            var delivery = new LngLat(-3.00, 55.121);
            var record =
                    new MedDispatchRecRequest(
                            123,
                            LocalDate.parse("2025-12-22"),
                            LocalTime.parse("14:30"),
                            reqs,
                            delivery);
            MedDispatchRecRequest[] requestBody = {record};
            List<String> expected = List.of("1", "2", "6", "7", "9");

            when(droneInfoService.dronesMatchesRequirements(any(MedDispatchRecRequest[].class)))
                    .thenReturn(expected);

            mockMvc.perform(
                            post(API_ENDPOINT)
                                    .contentType(MediaType.APPLICATION_JSON)
                                    .content(objectMapper.writeValueAsString(requestBody)))
                    .andExpect(status().isOk())
                    .andExpect(content().json(objectMapper.writeValueAsString(expected)));
        }

        @Test
        @DisplayName("Treat Null as False (Cooling) -> 200 OK")
        void postQueryAvailableDrones_shouldReturn200AndArrayOfString_whenCoolingIsNull()
                throws Exception {
            var requestMap =
                    Map.of(
                            "id",
                            123,
                            "date",
                            "2025-12-22",
                            "time",
                            "14:30",
                            "requirements",
                            Map.of(
                                    "capacity", 0.75,
                                    "heating", true,
                                    "maxCost", 13.5));
            List<String> expected = List.of("1", "2", "6", "7", "9");

            when(droneInfoService.dronesMatchesRequirements(any(MedDispatchRecRequest[].class)))
                    .thenReturn(expected);

            mockMvc.perform(
                            post(API_ENDPOINT)
                                    .contentType(MediaType.APPLICATION_JSON)
                                    .content(
                                            objectMapper.writeValueAsString(
                                                    new Object[] {requestMap})))
                    .andExpect(status().isOk())
                    .andExpect(content().json(objectMapper.writeValueAsString(expected)));
        }

        @Test
        @DisplayName("Complex -> 200 OK")
        void postQueryAvailableDrones_shouldReturn200AndArrayOfString_whenComplexRequest()
                throws Exception {
            var reqs1 = new MedDispatchRecRequest.MedRequirement(0.75f, false, true, 13.5f);
            var delivery1 = new LngLat(-3.00, 55.121);
            var record1 =
                    new MedDispatchRecRequest(
                            123,
                            LocalDate.parse("2025-12-22"),
                            LocalTime.parse("14:30"),
                            reqs1,
                            delivery1);

            var reqs2 = new MedDispatchRecRequest.MedRequirement(0.75f, false, true, 13.5f);
            var delivery2 = new LngLat(-3.00, 55.121);
            var record2 =
                    new MedDispatchRecRequest(
                            456,
                            LocalDate.parse("2025-12-25"),
                            LocalTime.parse("11:30"),
                            reqs2,
                            delivery2);

            MedDispatchRecRequest[] requestBody = {record1, record2};
            List<String> expected = List.of("2", "7", "9");

            when(droneInfoService.dronesMatchesRequirements(any(MedDispatchRecRequest[].class)))
                    .thenReturn(expected);

            mockMvc.perform(
                            post(API_ENDPOINT)
                                    .contentType(MediaType.APPLICATION_JSON)
                                    .content(objectMapper.writeValueAsString(requestBody)))
                    .andExpect(status().isOk())
                    .andExpect(content().json(objectMapper.writeValueAsString(expected)));
        }
    }
}
