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 io.github.js0ny.ilp_coursework.data.common.Angle;
import io.github.js0ny.ilp_coursework.data.common.LngLat;
import io.github.js0ny.ilp_coursework.data.common.Region;
import io.github.js0ny.ilp_coursework.data.request.DistanceRequest;
import io.github.js0ny.ilp_coursework.data.request.MovementRequest;
import io.github.js0ny.ilp_coursework.data.request.RegionCheckRequest;
import io.github.js0ny.ilp_coursework.service.GpsCalculationService;

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 org.springframework.test.web.servlet.result.MockMvcResultMatchers;

import java.util.List;

@WebMvcTest(ApiController.class)
public class ApiControllerTest {

    @Autowired private MockMvc mockMvc;

    @Autowired private ObjectMapper objectMapper;

    @MockitoBean private GpsCalculationService service;

    @Nested
    @DisplayName("GET /uid")
    class GetUidTests {

        @Test
        @DisplayName("GET /uid -> 200 OK")
        void getUid_shouldReturn200AndStudentIdFromService() throws Exception {
            String endpoint = "/api/v1/uid";
            String expected = "s2522255";
            var mock = mockMvc.perform(get(endpoint));
            mock.andExpect(status().isOk());
            mock.andExpect(content().string(expected));
        }
    }

    @Nested
    @DisplayName("POST /distanceTo")
    class GetDistanceTests {

        @Test
        @DisplayName("POST /distanceTo -> 200 OK")
        void getDistance_shouldReturn200AndDistance_whenCorrectInput() throws Exception {
            double expected = 5.0;
            String endpoint = "/api/v1/distanceTo";
            LngLat p1 = new LngLat(0, 4.0);
            LngLat p2 = new LngLat(3.0, 0);
            var req = new DistanceRequest(p1, p2);
            when(service.calculateDistance(any(LngLat.class), any(LngLat.class)))
                    .thenReturn(expected);
            var mock =
                    mockMvc.perform(
                            post(endpoint)
                                    .contentType(MediaType.APPLICATION_JSON)
                                    .content(objectMapper.writeValueAsString(req)));

            mock.andExpect(status().isOk());
            mock.andExpect(content().string(String.valueOf(expected)));
        }

        @Test
        @DisplayName("POST /distanceTo -> 400 Bad Request: Missing Field")
        void getDistance_shouldReturn400_whenMissingField() throws Exception {
            String endpoint = "/api/v1/distanceTo";
            String req =
                    """
                    {
                        "position1": {
                            "lng": 3.0,
                            "lat": 4.0
                        }
                    }
                    """;
            when(service.calculateDistance(any(LngLat.class), isNull()))
                    .thenThrow(new NullPointerException());
            mockMvc.perform(post(endpoint).contentType(MediaType.APPLICATION_JSON).content(req))
                    .andExpect(status().isBadRequest());
        }

        @Test
        @DisplayName("POST /distanceTo -> 400 Bad Request: Semantic errors")
        void getDistance_shouldReturn400_whenInvalidInput() throws Exception {
            String endpoint = "/api/v1/distanceTo";
            String req =
                    """
                    { "position1": { "lng": -300.192473, "lat": 550.946233 }, "position2": { "lng": -3202.192473, "lat": 5533.942617 } }
                    """;
            when(service.calculateDistance(any(LngLat.class), isNull()))
                    .thenThrow(new NullPointerException());
            mockMvc.perform(post(endpoint).contentType(MediaType.APPLICATION_JSON).content(req))
                    .andExpect(status().isBadRequest());
        }
    }

    @Nested
    @DisplayName("POST /isCloseTo")
    class IsCloseToTests {

        @Test
        @DisplayName("POST /isCloseTo -> 200 OK")
        void getIsCloseTo_shouldReturn200AndBoolean_whenCorrectInput() throws Exception {
            boolean expected = false;
            String endpoint = "/api/v1/isCloseTo";
            LngLat p1 = new LngLat(0, 4.0);
            LngLat p2 = new LngLat(3.0, 0);
            var req = new DistanceRequest(p1, p2);
            when(service.isCloseTo(any(LngLat.class), any(LngLat.class))).thenReturn(expected);
            var mock =
                    mockMvc.perform(
                            post(endpoint)
                                    .contentType(MediaType.APPLICATION_JSON)
                                    .content(objectMapper.writeValueAsString(req)));

            mock.andExpect(status().isOk());
            mock.andExpect(content().string(String.valueOf(expected)));
        }

        @Test
        @DisplayName("POST /isCloseTo -> 400 Bad Request: Malformed JSON ")
        void getIsCloseTo_shouldReturn400_whenJsonIsMalformed() throws Exception {
            // json without a bracket
            String malformedJson =
                    """
                    {
                        "position1": { "lng": 0.0, "lat": 3.0 }
                    """;
            mockMvc.perform(
                            post("/api/v1/isCloseTo")
                                    .contentType(MediaType.APPLICATION_JSON)
                                    .content(malformedJson))
                    .andExpect(status().isBadRequest());
        }

        @Test
        @DisplayName("POST /isCloseTo -> 400 Bad Request: Semantic errors")
        void getIsCloseTo_shouldReturn400_whenInvalidInput() throws Exception {
            String endpoint = "/api/v1/isCloseTo";
            String req =
                    """
                    { "position1": { "lng": -3004.192473, "lat": 550.946233 }, "position2": { "lng": -390.192473, "lat": 551.942617 } }
                    """;
            when(service.calculateDistance(any(LngLat.class), isNull()))
                    .thenThrow(new NullPointerException());
            mockMvc.perform(post(endpoint).contentType(MediaType.APPLICATION_JSON).content(req))
                    .andExpect(status().isBadRequest());
        }
    }

    @Nested
    @DisplayName("POST /nextPosition")
    class GetNextPositionTests {

        String endpoint = "/api/v1/nextPosition";

        @Test
        @DisplayName("POST /nextPosition -> 200 OK")
        void getNextPosition_shouldReturn200AndCoordinate_whenCorrectInput() throws Exception {
            LngLat expected = new LngLat(0.00015, 0.0);
            LngLat p = new LngLat(0, 0);
            var req = new MovementRequest(p, 0);
            when(service.nextPosition(any(LngLat.class), any(Angle.class))).thenReturn(expected);
            var mock =
                    mockMvc.perform(
                            post(endpoint)
                                    .contentType(MediaType.APPLICATION_JSON)
                                    .content(objectMapper.writeValueAsString(req)));

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

        @Test
        @DisplayName("POST /nextPosition -> 400 Bad Request: Missing Field")
        void getNextPosition_shouldReturn400_whenKeyNameError() throws Exception {
            // "position" should be "start"
            String malformedJson =
                    """
                    {
                        "position": { "lng": 0.0, "lat": 3.0 },
                        "angle": 180
                    }
                    """;
            when(service.nextPosition(isNull(), any(Angle.class)))
                    .thenThrow(new NullPointerException());
            mockMvc.perform(
                            post("/api/v1/nextPosition")
                                    .contentType(MediaType.APPLICATION_JSON)
                                    .content(malformedJson))
                    .andExpect(MockMvcResultMatchers.status().isBadRequest());
        }

        @Test
        @DisplayName("POST /nextPosition -> 400 Bad Request: Semantic errors")
        void getNextPosition_shouldReturn400_whenInvalidInput() throws Exception {
            String endpoint = "/api/v1/nextPosition";
            String req =
                    """
                    {
                      "start": {
                        "lng": -3.192473,
                        "lat": 55.946233
                      },
                      "angle": 900
                    }
                    """;
            when(service.calculateDistance(any(LngLat.class), isNull()))
                    .thenThrow(new NullPointerException());
            mockMvc.perform(post(endpoint).contentType(MediaType.APPLICATION_JSON).content(req))
                    .andExpect(status().isBadRequest());
        }
    }

    @Nested
    @DisplayName("POST /isInRegion")
    class GetIsInRegionTests {

        @Test
        @DisplayName("POST /isInRegion -> 200 OK")
        void getIsInRegion_shouldReturn200AndBoolean_whenCorrectInput() throws Exception {
            boolean expected = false;
            String endpoint = "/api/v1/isInRegion";
            var position = new LngLat(1.234, 1.222);
            var region =
                    new Region(
                            "central",
                            List.of(
                                    new LngLat(-3.192473, 55.946233),
                                    new LngLat(-3.192473, 55.942617),
                                    new LngLat(-3.184319, 55.942617),
                                    new LngLat(-3.184319, 55.946233),
                                    new LngLat(-3.192473, 55.946233)));
            var req = new RegionCheckRequest(position, region);
            when(service.checkIsInRegion(any(LngLat.class), any(Region.class)))
                    .thenReturn(expected);
            var mock =
                    mockMvc.perform(
                            post(endpoint)
                                    .contentType(MediaType.APPLICATION_JSON)
                                    .content(objectMapper.writeValueAsString(req)));

            mock.andExpect(status().isOk());
            mock.andExpect(content().string(String.valueOf(expected)));
        }

        @Test
        @DisplayName(
                "POST /isInRegion -> 400 Bad Request: Passing a list of empty vertices to"
                        + " isInRegion")
        void getIsInRegion_shouldReturn400_whenPassingIllegalArguments() throws Exception {
            var position = new LngLat(1, 1);
            var region = new Region("illegal", List.of());
            var request = new RegionCheckRequest(position, region);
            when(service.checkIsInRegion(any(LngLat.class), any(Region.class)))
                    .thenThrow(new IllegalArgumentException("Region is not closed."));
            mockMvc.perform(
                            post("/api/v1/isInRegion")
                                    .contentType(MediaType.APPLICATION_JSON)
                                    .content(objectMapper.writeValueAsString(request)))
                    .andExpect(status().isBadRequest());
        }

        @Test
        @DisplayName(
                "POST /isInRegion -> 400 Bad Request: Passing a list of not-closing vertices to"
                        + " isInRegion")
        void getIsInRegion_shouldReturn400_whenPassingNotClosingVertices() throws Exception {
            var position = new LngLat(1, 1);
            var region =
                    new Region(
                            "illegal",
                            List.of(
                                    new LngLat(1, 2),
                                    new LngLat(3, 4),
                                    new LngLat(5, 6),
                                    new LngLat(7, 8),
                                    new LngLat(9, 10)));
            var request = new RegionCheckRequest(position, region);
            when(service.checkIsInRegion(any(LngLat.class), any(Region.class)))
                    .thenThrow(new IllegalArgumentException("Region is not closed."));
            mockMvc.perform(
                            post("/api/v1/isInRegion")
                                    .contentType(MediaType.APPLICATION_JSON)
                                    .content(objectMapper.writeValueAsString(request)))
                    .andExpect(status().isBadRequest());
        }
    }
}
