package io.github.js0ny.ilp_coursework.service;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import io.github.js0ny.ilp_coursework.data.common.DroneAvailability;
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.common.LngLatAlt;
import io.github.js0ny.ilp_coursework.data.common.TimeWindow;
import io.github.js0ny.ilp_coursework.data.external.Drone;
import io.github.js0ny.ilp_coursework.data.external.RestrictedArea;
import io.github.js0ny.ilp_coursework.data.external.ServicePoint;
import io.github.js0ny.ilp_coursework.data.external.ServicePointDrones;
import io.github.js0ny.ilp_coursework.data.request.MedDispatchRecRequest;
import io.github.js0ny.ilp_coursework.data.request.MedDispatchRecRequest.MedRequirement;
import io.github.js0ny.ilp_coursework.data.response.DeliveryPathResponse;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.io.IOException;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.LocalTime;
import java.util.Collections;
import java.util.List;

@ExtendWith(MockitoExtension.class)
class PathFinderServiceTest {

    private static final String DRONE_ID = "test-drone";
    private static final LngLat SERVICE_POINT_COORD = new LngLat(0.0, 0.0);
    private final ObjectMapper mapper = new ObjectMapper();

    @Mock private DroneInfoService droneInfoService;

    private PathFinderService pathFinderService;

    @BeforeEach
    void setUpPathFinder() {
        GpsCalculationService gpsCalculationService = new GpsCalculationService();

        DroneCapability capability = new DroneCapability(false, true, 5.0f, 10, 0.1f, 0.5f, 0.5f);
        Drone drone = new Drone("Test Drone", DRONE_ID, capability);
        ServicePoint servicePoint =
                new ServicePoint(
                        "Test Point",
                        1,
                        new LngLatAlt(SERVICE_POINT_COORD.lng(), SERVICE_POINT_COORD.lat(), 50.0));
        DroneAvailability availability =
                new DroneAvailability(
                        DRONE_ID,
                        new TimeWindow[] {
                            new TimeWindow(DayOfWeek.MONDAY, LocalTime.MIDNIGHT, LocalTime.MAX),
                        });
        ServicePointDrones servicePointDrones =
                new ServicePointDrones(servicePoint.id(), new DroneAvailability[] {availability});

        when(droneInfoService.fetchAllDrones()).thenReturn(List.of(drone));
        when(droneInfoService.fetchServicePoints()).thenReturn(List.of(servicePoint));
        when(droneInfoService.fetchDronesForServicePoints())
                .thenReturn(List.of(servicePointDrones));
        when(droneInfoService.fetchRestrictedAreas())
                .thenReturn(Collections.<RestrictedArea>emptyList());
        when(droneInfoService.droneMatchesRequirement(any(), any())).thenReturn(true);
        pathFinderService = new PathFinderService(gpsCalculationService, droneInfoService);
    }

    @Test
    void calculateDeliveryPath_shouldStayWithinSingleTripBudget() {
        MedDispatchRecRequest request = createSampleRequest();

        DeliveryPathResponse response =
                pathFinderService.calculateDeliveryPath(new MedDispatchRecRequest[] {request});

        assertThat(response.totalMoves()).isGreaterThan(0);
        assertThat(response.totalMoves()).isLessThanOrEqualTo(10);
        assertThat(response.dronePaths()).hasSize(1);
        DeliveryPathResponse.DronePath dronePath = response.dronePaths()[0];
        assertThat(dronePath.droneId()).isNotZero();
        assertThat(dronePath.deliveries()).hasSize(1);
        var recordedPath = dronePath.deliveries().get(0).flightPath();
        assertThat(recordedPath.get(0)).isEqualTo(SERVICE_POINT_COORD);
        assertThat(samePoint(recordedPath.get(recordedPath.size() - 1), SERVICE_POINT_COORD))
                .isTrue();
        assertThat(hasHoverAt(recordedPath, request.delivery())).isTrue();
    }

    @Test
    void calculateDeliveryPathAsGeoJson_shouldReturnFeatureCollection() throws IOException {
        MedDispatchRecRequest request = createSampleRequest();

        String geoJson =
                pathFinderService.calculateDeliveryPathAsGeoJson(
                        new MedDispatchRecRequest[] {request});

        JsonNode root = mapper.readTree(geoJson);
        assertThat(root.get("type").asText()).isEqualTo("FeatureCollection");
        JsonNode features = root.get("features");
        assertThat(features.size()).isEqualTo(1);
        JsonNode feature = features.get(0);
        assertThat(feature.get("type").asText()).isEqualTo("Feature");
        JsonNode props = feature.get("properties");
        assertThat(props.get("droneId").asInt()).isNotZero();
        assertThat(props.get("deliveryId").asInt()).isEqualTo(request.id());

        JsonNode geometry = feature.get("geometry");
        assertThat(geometry.get("type").asText()).isEqualTo("LineString");
        JsonNode coordinates = geometry.get("coordinates");
        assertThat(coordinates.size()).isGreaterThanOrEqualTo(2);

        double startLng = coordinates.get(0).get(0).asDouble();
        double startLat = coordinates.get(0).get(1).asDouble();
        assertThat(samePoint(new LngLat(startLng, startLat), SERVICE_POINT_COORD)).isTrue();
    }

    private MedDispatchRecRequest createSampleRequest() {
        return new MedDispatchRecRequest(
                101,
                LocalDate.of(2025, 1, 6),
                LocalTime.of(12, 0),
                new MedRequirement(0.5f, false, true, 50.0f),
                new LngLat(SERVICE_POINT_COORD.lng() + 0.0003, SERVICE_POINT_COORD.lat()));
    }

    private boolean hasHoverAt(List<LngLat> path, LngLat target) {
        for (int i = 0; i < path.size() - 1; i++) {
            if (samePoint(path.get(i), target) && samePoint(path.get(i + 1), target)) {
                return true;
            }
        }
        return false;
    }

    private boolean samePoint(LngLat a, LngLat b) {
        double threshold = 1e-9;
        return (Math.abs(a.lng() - b.lng()) < threshold && Math.abs(a.lat() - b.lat()) < threshold);
    }
}
