package io.github.js0ny.ilp_coursework.service;

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

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.TimeWindow;
import io.github.js0ny.ilp_coursework.data.external.Drone;
import io.github.js0ny.ilp_coursework.data.external.ServicePointDrones;
import io.github.js0ny.ilp_coursework.data.request.MedDispatchRecRequest;

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.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.client.RestTemplate;

import java.net.URI;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.LocalTime;
import java.util.List;

@ExtendWith(MockitoExtension.class)
public class DroneInfoServiceTest {

    @Mock private RestTemplate restTemplate;

    private DroneInfoService droneInfoService;

    private final String baseUrl = "http://localhost:8080/";

    @BeforeEach
    void setUp() {
        droneInfoService = new DroneInfoService(restTemplate);
        ReflectionTestUtils.setField(droneInfoService, "baseUrl", baseUrl);
    }

    private Drone[] getMockDrones() {
        return new Drone[] {
            new Drone("Drone 1", "1", new DroneCapability(true, true, 10, 1000, 1, 1, 1)),
            new Drone("Drone 2", "2", new DroneCapability(false, true, 20, 2000, 2, 2, 2)),
            new Drone("Drone 3", "3", new DroneCapability(false, false, 30, 3000, 3, 3, 3))
        };
    }

    @Nested
    @DisplayName("dronesWithCooling(boolean) tests")
    class DronesWithCoolingTests {

        @Test
        @DisplayName("Should return drones with cooling")
        void dronesWithCooling_shouldReturnDronesWithCooling() {
            // Arrange
            when(restTemplate.getForObject(URI.create(baseUrl + "drones"), Drone[].class))
                    .thenReturn(getMockDrones());

            // Act
            List<String> result = droneInfoService.dronesWithCooling(true);

            // Assert
            assertThat(result).containsExactly("1");
        }

        @Test
        @DisplayName("Should return drones without cooling")
        void dronesWithCooling_shouldReturnDronesWithoutCooling() {
            // Arrange
            when(restTemplate.getForObject(URI.create(baseUrl + "drones"), Drone[].class))
                    .thenReturn(getMockDrones());

            // Act
            List<String> result = droneInfoService.dronesWithCooling(false);

            // Assert
            assertThat(result).containsExactly("2", "3");
        }

        @Test
        @DisplayName("Should return empty list when API returns null")
        void dronesWithCooling_shouldReturnEmptyList_whenApiReturnsNull() {
            // Arrange
            when(restTemplate.getForObject(URI.create(baseUrl + "drones"), Drone[].class))
                    .thenReturn(null);

            // Act
            List<String> result = droneInfoService.dronesWithCooling(true);

            // Assert
            assertThat(result).isEmpty();
        }
    }

    @Nested
    @DisplayName("droneDetail(String) tests")
    class DroneDetailTests {

        @Test
        @DisplayName("Should return correct drone details")
        void droneDetail_shouldReturnCorrectDrone() {
            // Arrange
            when(restTemplate.getForObject(URI.create(baseUrl + "drones"), Drone[].class))
                    .thenReturn(getMockDrones());

            // Act
            Drone result = droneInfoService.droneDetail("2");

            // Assert
            assertThat(result.id()).isEqualTo("2");
            assertThat(result.name()).isEqualTo("Drone 2");
        }

        @Test
        @DisplayName("Should throw exception for non-existent drone")
        void droneDetail_shouldThrowException_forNonExistentDrone() {
            // Arrange
            when(restTemplate.getForObject(URI.create(baseUrl + "drones"), Drone[].class))
                    .thenReturn(getMockDrones());

            // Act & Assert
            assertThatThrownBy(() -> droneInfoService.droneDetail("4"))
                    .isInstanceOf(IllegalArgumentException.class)
                    .hasMessage("drone with that ID cannot be found");
        }
    }

    @Nested
    @DisplayName("dronesMatchesRequirements(MedDispatchRecRequest[]) tests")
    class DronesMatchesRequirementsTests {
        private ServicePointDrones[] getMockServicePointDrones() {
            TimeWindow[] timeWindows = {
                new TimeWindow(DayOfWeek.MONDAY, LocalTime.of(9, 0), LocalTime.of(17, 0))
            };
            DroneAvailability drone1Avail = new DroneAvailability("1", timeWindows);
            ServicePointDrones spd =
                    new ServicePointDrones(1, new DroneAvailability[] {drone1Avail});
            return new ServicePointDrones[] {spd};
        }

        @Test
        @DisplayName("Should return drones matching a single requirement")
        void dronesMatchesRequirements_shouldReturnMatchingDrones_forSingleRequirement() {
            // Arrange
            var drones =
                    new Drone[] {
                        new Drone(
                                "Drone 1", "1", new DroneCapability(true, true, 10, 1000, 1, 1, 1)),
                        new Drone(
                                "Drone 2", "2", new DroneCapability(false, true, 5, 2000, 2, 2, 2))
                    };
            when(restTemplate.getForObject(URI.create(baseUrl + "drones"), Drone[].class))
                    .thenReturn(drones);
            when(restTemplate.getForObject(
                            URI.create(baseUrl + "drones-for-service-points"),
                            ServicePointDrones[].class))
                    .thenReturn(getMockServicePointDrones());

            var requirement = new MedDispatchRecRequest.MedRequirement(8, true, false, 100);
            var record =
                    new MedDispatchRecRequest(
                            1,
                            LocalDate.now().with(DayOfWeek.MONDAY),
                            LocalTime.of(10, 0),
                            requirement,
                            new LngLat(0, 0));

            // Act
            List<String> result =
                    droneInfoService.dronesMatchesRequirements(
                            new MedDispatchRecRequest[] {record});

            // Assert
            assertThat(result).containsExactly("1");
        }

        @Test
        @DisplayName("Should return empty list if no drones match")
        void dronesMatchesRequirements_shouldReturnEmptyList_whenNoDronesMatch() {
            // Arrange
            var drones =
                    new Drone[] {
                        new Drone(
                                "Drone 1", "1", new DroneCapability(true, true, 5, 1000, 1, 1, 1)),
                        new Drone(
                                "Drone 2", "2", new DroneCapability(false, true, 5, 2000, 2, 2, 2))
                    };
            when(restTemplate.getForObject(URI.create(baseUrl + "drones"), Drone[].class))
                    .thenReturn(drones);
            // No need to mock drones-for-service-points as it won't be called

            var requirement = new MedDispatchRecRequest.MedRequirement(10, true, false, 100);
            var record =
                    new MedDispatchRecRequest(
                            1,
                            LocalDate.now().with(DayOfWeek.MONDAY),
                            LocalTime.of(10, 0),
                            requirement,
                            new LngLat(0, 0));

            // Act
            List<String> result =
                    droneInfoService.dronesMatchesRequirements(
                            new MedDispatchRecRequest[] {record});

            // Assert
            assertThat(result).isEmpty();
        }

        @Test
        @DisplayName("Should return all drones if requirements are null or empty")
        void dronesMatchesRequirements_shouldReturnAllDrones_forNullOrEmptyRequirements() {
            // Arrange
            when(restTemplate.getForObject(URI.create(baseUrl + "drones"), Drone[].class))
                    .thenReturn(getMockDrones());

            // Act
            List<String> resultNull = droneInfoService.dronesMatchesRequirements(null);
            List<String> resultEmpty =
                    droneInfoService.dronesMatchesRequirements(new MedDispatchRecRequest[0]);

            // Assert
            assertThat(resultNull).containsExactly("1", "2", "3");
            assertThat(resultEmpty).containsExactly("1", "2", "3");
        }
    }
}
