package space.nstart.pcp.pcp_mission_planing_service.service

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.Test
import org.mockito.ArgumentMatchers.anyList
import org.mockito.Mockito.mock
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoInteractions
import org.mockito.Mockito.`when`
import org.springframework.http.HttpStatus
import org.springframework.web.server.ResponseStatusException
import space.nstart.pcp.pcp_types_lib.configuration.CustomErrorException
import space.nstart.pcp.pcp_types_lib.dto.routes.AngleRangeDto
import space.nstart.pcp.pcp_types_lib.dto.routes.RoutePassportDto
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.BookedSlotStatus
import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.DropModeResponseDTO
import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.SatelliteModeResponseDTO
import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.SatelliteModeSnapshotResponseDTO
import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.SurveyModeResponseDTO
import space.nstart.pcp.pcp_mission_planing_service.entity.DropModeEntity
import space.nstart.pcp.pcp_mission_planing_service.entity.DropModeSurveyEntity
import space.nstart.pcp.pcp_mission_planing_service.entity.MissionEntity
import space.nstart.pcp.pcp_mission_planing_service.entity.ModeBookingEntity
import space.nstart.pcp.pcp_mission_planing_service.entity.PlanEntity
import space.nstart.pcp.pcp_mission_planing_service.entity.SurveyModeEntity
import space.nstart.pcp.pcp_mission_planing_service.entity.SurveyModeSource
import space.nstart.pcp.pcp_types_lib.dto.routes.SurveyModeStatus
import space.nstart.pcp.pcp_mission_planing_service.repository.DropModeRepository
import space.nstart.pcp.pcp_mission_planing_service.repository.DropModeSurveyRepository
import space.nstart.pcp.pcp_mission_planing_service.repository.MissionRepository
import space.nstart.pcp.pcp_mission_planing_service.repository.ModeBookingRepository
import space.nstart.pcp.pcp_mission_planing_service.repository.ModeRepository
import space.nstart.pcp.pcp_mission_planing_service.repository.PlanRepository
import space.nstart.pcp.pcp_mission_planing_service.repository.SurveyModeRepository
import space.nstart.pcp.pcp_mission_planing_service.service.catalog.SatelliteCatalogClient
import space.nstart.pcp.pcp_mission_planing_service.service.satellite_type.DefaultSatelliteMissionType
import space.nstart.pcp.pcp_mission_planing_service.service.satellite_type.EmissioSatelliteMissionType
import space.nstart.pcp.pcp_mission_planing_service.service.satellite_type.ReflixioSatelliteMissionType
import space.nstart.pcp.pcp_mission_planing_service.service.satellite_type.SatelliteMissionTypeFactory
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteDTO
import java.time.LocalDateTime
import java.util.UUID
import java.math.BigDecimal

class MissionPlaningServiceTest {

    private val missionRepository = mock(MissionRepository::class.java)
    private val modeRepository = mock(ModeRepository::class.java)
    private val planRepository = mock(PlanRepository::class.java)
    private val surveyModeRepository = mock(SurveyModeRepository::class.java)
    private val dropModeRepository = mock(DropModeRepository::class.java)
    private val dropModeSurveyRepository = mock(DropModeSurveyRepository::class.java)
    private val modeBookingRepository = mock(ModeBookingRepository::class.java)
    private val bookedSlotsEventPublisher = mock(BookedSlotsEventPublisher::class.java)
    private val ballisticsService = mock(BallisticsService::class.java)
    private val complexMissionService = mock(ComplexMissionService::class.java)
    private val satelliteCatalogClient = mock(SatelliteCatalogClient::class.java)
    private val defaultSatelliteMissionType = DefaultSatelliteMissionType()
    private val satelliteMissionTypeFactory = SatelliteMissionTypeFactory(
        listOf(defaultSatelliteMissionType, EmissioSatelliteMissionType(), ReflixioSatelliteMissionType()),
        defaultSatelliteMissionType
    )

    private val service = MissionPlaningService(
        missionRepository = missionRepository,
        modeRepository = modeRepository,
        planRepository = planRepository,
        surveyModeRepository = surveyModeRepository,
        dropModeRepository = dropModeRepository,
        dropModeSurveyRepository = dropModeSurveyRepository,
        modeBookingRepository = modeBookingRepository,
        bookedSlotsEventPublisher = bookedSlotsEventPublisher,
        ballisticsService = ballisticsService,
        complexMissionService = complexMissionService,
        satelliteCatalogClient = satelliteCatalogClient,
        satelliteMissionTypeFactory = satelliteMissionTypeFactory
    )

    @Test
    fun `modesBySatelliteAndInterval returns only intersecting modes`() {
        val mission = MissionEntity(
            missionId = UUID.randomUUID(),
            satelliteId = 77
        )
        val plan = PlanEntity(id = 101L, mission = mission, planNumber = 1)
        val intervalStart = LocalDateTime.of(2026, 3, 16, 10, 0, 0)
        val intervalStop = LocalDateTime.of(2026, 3, 16, 10, 30, 0)

        val overlappingSurvey = SurveyModeEntity(
            id = 1L,
            plan = plan,
            timeStart = intervalStart.minusMinutes(5),
            revolution = 10,
            lat = 55.0,
            long = 37.0,
            duration = 600.0,
            contourWkt = "POLYGON((0 0,1 0,1 1,0 0))",
            roll = 2.0,
            source = SurveyModeSource.SLOT
        )
        val inWindowSurvey = SurveyModeEntity(
            id = 2L,
            plan = plan,
            timeStart = intervalStart.plusMinutes(10),
            revolution = 11,
            lat = 56.0,
            long = 38.0,
            duration = 300.0,
            contourWkt = "POLYGON((0 0,1 0,1 1,0 0))",
            roll = 1.0,
            source = SurveyModeSource.SLOT
        )
        val inWindowDrop = DropModeEntity(
            id = 3L,
            plan = plan,
            timeStart = intervalStart.plusMinutes(20),
            revolution = 12,
            station = "station-1",
            duration = 120.0
        )

        `when`(
            modeRepository.findAllByPlan_Mission_SatelliteIdAndTimeStartIsNotNullAndTimeStartLessThanEqualOrderByTimeStartAsc(
                77,
                intervalStop
            )
        ).thenReturn(listOf(overlappingSurvey, inWindowSurvey, inWindowDrop))
        `when`(modeBookingRepository.findAllByMode_IdIn(listOf(1L, 2L))).thenReturn(
            listOf(
                ModeBookingEntity(mode = overlappingSurvey, slotId = 1001L),
                ModeBookingEntity(mode = overlappingSurvey, slotId = 1001L),
                ModeBookingEntity(mode = inWindowSurvey, slotId = 2002L)
            )
        )
        `when`(dropModeSurveyRepository.findAllByDropMode_IdOrderBySurveyMode_TimeStartAsc(3L)).thenReturn(
            listOf(
                DropModeSurveyEntity(
                    dropMode = inWindowDrop,
                    surveyMode = inWindowSurvey
                )
            )
        )

        val result = service.modesBySatelliteAndInterval(77, intervalStart, intervalStop)

        assertEquals(listOf(1L, 2L, 3L), result.map { it.id })
        assertEquals(listOf(1001L), (result[0] as SurveyModeResponseDTO).bookedSlotIds)
        assertEquals(listOf(2002L), (result[1] as SurveyModeResponseDTO).bookedSlotIds)
        assertEquals(listOf(2L), (result[2] as DropModeResponseDTO).surveys)
    }

    @Test
    fun `modesBySatelliteAndInterval rejects inverted interval`() {
        val exception = assertThrows(ResponseStatusException::class.java) {
            service.modesBySatelliteAndInterval(
                satelliteId = 77,
                timeStart = LocalDateTime.of(2026, 3, 16, 10, 30, 0),
                timeStop = LocalDateTime.of(2026, 3, 16, 10, 0, 0)
            )
        }

        assertEquals(HttpStatus.BAD_REQUEST, exception.statusCode)
    }

    @Test
    fun `findMode searches by satellite number revolution and time start with one second tolerance`() {
        val routePassport = RoutePassportDto(
            routeId = UUID.randomUUID(),
            kaShort = "22",
            kaFull = "SATELLITE-22",
            routeNameFull = "ROUTE-001",
            routeNameShort = "R-001",
            orbitNumber = 321,
            orbitState = "ASC",
            intervalBegin = LocalDateTime.of(2026, 4, 3, 12, 0, 0),
            intervalEnd = LocalDateTime.of(2026, 4, 3, 12, 5, 0),
            rollAngle = AngleRangeDto(BigDecimal.ZERO, BigDecimal.ONE),
            visirAngle = AngleRangeDto(BigDecimal.ZERO, BigDecimal.ONE),
            resolutionRange = BigDecimal.ONE,
            resolutionAzimuth = BigDecimal.ONE,
            polarisation = listOf("VV"),
            processingLevel = "L1",
            geometry = "POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))",
        )
        val exactMode = SurveyModeEntity(
            id = 10L,
            timeStart = routePassport.intervalBegin,
            revolution = 321
        )
        val nearMode = SurveyModeEntity(
            id = 11L,
            timeStart = routePassport.intervalBegin.plusSeconds(1),
            revolution = 321
        )

        `when`(
            surveyModeRepository.findAllBySatelliteNumberAndRevolutionAndTimeStartBetween(
                "22",
                321,
                routePassport.intervalBegin.minusSeconds(1),
                routePassport.intervalBegin.plusSeconds(1)
            )
        ).thenReturn(listOf(nearMode, exactMode))

        val result = service.findMode(routePassport)

        assertEquals(10L, result?.id)
    }

    @Test
    fun `loadSlots reads persisted stage4 modes and preserves source bookings and traceability`() {
        val missionId = UUID.randomUUID()
        val missionStart = LocalDateTime.of(2026, 3, 18, 10, 0)
        val missionStop = LocalDateTime.of(2026, 3, 18, 12, 0)
        val mission = MissionEntity(
            missionId = missionId,
            satelliteId = 77,
            missionStart = missionStart,
            missionStop = missionStop
        )
        val plan = PlanEntity(id = 101L, mission = mission, planStart = missionStart, planStop = missionStop, planNumber = 1)
        val stage4Mode = SatelliteModeResponseDTO(
            id = 9001L,
            satelliteId = 77,
            source = "MANUAL",
            startTime = missionStart.plusMinutes(5),
            endTime = missionStart.plusMinutes(15),
            revolution = 321,
            lat = 55.0,
            longitude = 37.0,
            duration = 600.0,
            contourWkt = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))",
            roll = 4.0,
            bookedSlotIds = listOf(501L, 502L),
            snapshotId = 7001L
        )
        val snapshot = SatelliteModeSnapshotResponseDTO(
            id = 7001L,
            satelliteId = 77,
            intervalStart = missionStart.minusHours(1),
            intervalEnd = missionStop.plusHours(1),
            createdAt = missionStart.minusHours(1),
            isActive = true
        )
        var savedSurveyModes: List<SurveyModeEntity> = emptyList()
        var savedModeBookings: List<ModeBookingEntity> = emptyList()

        `when`(missionRepository.findById(missionId)).thenReturn(java.util.Optional.of(mission))
        `when`(planRepository.findFirstByMissionMissionIdAndPlanNumber(missionId, 1)).thenReturn(java.util.Optional.of(plan))
        `when`(satelliteCatalogClient.getSatellite(77L)).thenReturn(SatelliteDTO(id = 77L, typeCode = "Emissio"))
        `when`(complexMissionService.activeSnapshot(77L)).thenReturn(snapshot)
        `when`(complexMissionService.snapshotModes(snapshot.id, missionStart, missionStop)).thenReturn(listOf(stage4Mode))
        `when`(surveyModeRepository.saveAll(anyList())).thenAnswer {
            savedSurveyModes = it.arguments[0] as List<SurveyModeEntity>
            savedSurveyModes
        }
        `when`(modeBookingRepository.saveAll(anyList())).thenAnswer {
            savedModeBookings = it.arguments[0] as List<ModeBookingEntity>
            savedModeBookings
        }

        val response = service.loadSlots(missionId)

        verify(complexMissionService).activeSnapshot(77L)
        verify(complexMissionService).snapshotModes(snapshot.id, missionStart, missionStop)
        verifyNoInteractions(bookedSlotsEventPublisher)

        val savedSurvey = savedSurveyModes.single()
        assertEquals(SurveyModeSource.MANUAL, savedSurvey.source)
        assertEquals(9001L, savedSurvey.satelliteModeId)
        assertEquals(SurveyModeStatus.PLANNED, savedSurvey.status)
        assertEquals(snapshot.id, plan.stage4SnapshotId)
        assertEquals(listOf(501L, 502L), savedModeBookings.map { it.slotId })
        assertEquals(true, response.completed)
        assertEquals(snapshot.id, response.comPlanSnapshotId)
        assertEquals(1, response.surveyModesCount)
        assertEquals(1, response.manualModesCount)
        assertEquals(0, response.complanModesCount)
        assertEquals(0, response.slotsModesCount)
        assertEquals(0, response.mixedModesCount)
        assertEquals(2, response.bookedSlotIdsCount)
    }

    @Test
    fun `loadSlots uses explicit satellite and interval overrides for stage5 calculation`() {
        val missionId = UUID.randomUUID()
        val mission = MissionEntity(
            missionId = missionId,
            satelliteId = 77,
            missionStart = LocalDateTime.of(2026, 3, 19, 10, 0),
            missionStop = LocalDateTime.of(2026, 3, 19, 12, 0)
        )
        val overrideStart = LocalDateTime.of(2026, 3, 20, 8, 0)
        val overrideStop = LocalDateTime.of(2026, 3, 20, 9, 0)
        val overrideSatelliteId = 88L
        val plan = PlanEntity(id = 111L, mission = mission, planNumber = 1)
        val snapshot = SatelliteModeSnapshotResponseDTO(
            id = 7002L,
            satelliteId = overrideSatelliteId,
            intervalStart = overrideStart,
            intervalEnd = overrideStop,
            createdAt = overrideStart.minusHours(1),
            isActive = true
        )

        `when`(missionRepository.findById(missionId)).thenReturn(java.util.Optional.of(mission))
        `when`(planRepository.findFirstByMissionMissionIdAndPlanNumber(missionId, 1))
            .thenReturn(java.util.Optional.of(plan))
        `when`(satelliteCatalogClient.getSatellite(overrideSatelliteId)).thenReturn(SatelliteDTO(id = overrideSatelliteId, typeCode = "Reflixio"))
        `when`(complexMissionService.activeSnapshot(overrideSatelliteId)).thenReturn(snapshot)
        `when`(complexMissionService.snapshotModes(snapshot.id, overrideStart, overrideStop)).thenReturn(emptyList())

        val response = service.loadSlots(missionId, overrideSatelliteId, overrideStart, overrideStop)

        verify(complexMissionService).activeSnapshot(overrideSatelliteId)
        verify(complexMissionService).snapshotModes(snapshot.id, overrideStart, overrideStop)
        verify(missionRepository).save(mission)
        assertEquals(overrideSatelliteId, mission.satelliteId)
        assertEquals(overrideStart, mission.missionStart)
        assertEquals(overrideStop, mission.missionStop)
        assertEquals(snapshot.id, plan.stage4SnapshotId)
        assertEquals(snapshot.id, response.comPlanSnapshotId)
        assertEquals(0, response.surveyModesCount)
        verifyNoInteractions(bookedSlotsEventPublisher)
    }

    @Test
    fun `loadSlots uses explicit com plan snapshot as primary source`() {
        val missionId = UUID.randomUUID()
        val missionStart = LocalDateTime.of(2026, 3, 21, 10, 0)
        val missionStop = LocalDateTime.of(2026, 3, 21, 12, 0)
        val mission = MissionEntity(
            missionId = missionId,
            satelliteId = 77,
            missionStart = missionStart,
            missionStop = missionStop
        )
        val plan = PlanEntity(id = 112L, mission = mission, planStart = missionStart, planStop = missionStop, planNumber = 1)
        val snapshot = SatelliteModeSnapshotResponseDTO(
            id = 7003L,
            satelliteId = 77,
            intervalStart = missionStart,
            intervalEnd = missionStop,
            createdAt = missionStart.minusHours(1),
            isActive = false
        )

        `when`(missionRepository.findById(missionId)).thenReturn(java.util.Optional.of(mission))
        `when`(planRepository.findFirstByMissionMissionIdAndPlanNumber(missionId, 1)).thenReturn(java.util.Optional.of(plan))
        `when`(satelliteCatalogClient.getSatellite(77L)).thenReturn(SatelliteDTO(id = 77L, typeCode = ""))
        `when`(complexMissionService.snapshot(snapshot.id)).thenReturn(snapshot)
        `when`(complexMissionService.snapshotModes(snapshot.id, missionStart, missionStop)).thenReturn(emptyList())

        val response = service.loadSlots(missionId, comPlanSnapshotId = snapshot.id)

        verify(complexMissionService).snapshot(snapshot.id)
        verify(complexMissionService).snapshotModes(snapshot.id, missionStart, missionStop)
        verify(complexMissionService, never()).activeSnapshot(77L)
        assertEquals(snapshot.id, plan.stage4SnapshotId)
        assertEquals(snapshot.id, response.comPlanSnapshotId)
        assertEquals(0, response.surveyModesCount)
    }

    @Test
    fun `loadSlots fails when active stage4 snapshot is not found`() {
        val missionId = UUID.randomUUID()
        val missionStart = LocalDateTime.of(2026, 3, 22, 10, 0)
        val missionStop = LocalDateTime.of(2026, 3, 22, 12, 0)
        val mission = MissionEntity(
            missionId = missionId,
            satelliteId = 77,
            missionStart = missionStart,
            missionStop = missionStop
        )
        val plan = PlanEntity(id = 113L, mission = mission, planStart = missionStart, planStop = missionStop, planNumber = 1)

        `when`(missionRepository.findById(missionId)).thenReturn(java.util.Optional.of(mission))
        `when`(satelliteCatalogClient.getSatellite(77L)).thenReturn(SatelliteDTO(id = 77L, typeCode = "UNKNOWN"))
        `when`(planRepository.findFirstByMissionMissionIdAndPlanNumber(missionId, 1)).thenReturn(java.util.Optional.of(plan))
        `when`(complexMissionService.activeSnapshot(77L)).thenReturn(null)

        val exception = assertThrows(CustomErrorException::class.java) {
            service.loadSlots(missionId)
        }

        assertEquals(
            "Active stage4 snapshot not found for satelliteId=77 and interval=[$missionStart - $missionStop]",
            exception.message
        )
        verify(complexMissionService).activeSnapshot(77L)
    }

    @Test
    fun `confirmMission publishes processed booked slots event`() {
        val missionId = UUID.randomUUID()
        val mission = MissionEntity(missionId = missionId, satelliteId = 77)
        val surveyMode = SurveyModeEntity(id = 11L)

        `when`(missionRepository.findById(missionId)).thenReturn(java.util.Optional.of(mission))
        `when`(missionRepository.save(mission)).thenReturn(mission)
        `when`(surveyModeRepository.findAllByPlan_Mission_MissionIdOrderByTimeStartAsc(missionId))
            .thenReturn(listOf(surveyMode))
        `when`(modeBookingRepository.findAllByMode_IdIn(listOf(11L)))
            .thenReturn(listOf(ModeBookingEntity(mode = surveyMode, slotId = 1001L)))

        val result = service.confirmMission(missionId)

        assertEquals(listOf(1001L), result)
        verify(bookedSlotsEventPublisher).publish(
            listOf(1001L),
            BookedSlotStatus.PROCESSED,
            missionId,
            null,
            77L
        )
    }

    @Test
    fun `updateSurveyModeStatus publishes terminal booked slot event`() {
        val missionId = UUID.randomUUID()
        val mission = MissionEntity(missionId = missionId, satelliteId = 77)
        val plan = PlanEntity(id = 101L, mission = mission, planNumber = 1)
        val surveyMode = SurveyModeEntity(id = 99L, plan = plan, status = SurveyModeStatus.PLANNED)

        `when`(surveyModeRepository.findById(99L)).thenReturn(java.util.Optional.of(surveyMode))
        `when`(surveyModeRepository.save(surveyMode)).thenReturn(surveyMode)
        `when`(modeBookingRepository.findAllByMode_IdIn(listOf(99L)))
            .thenReturn(listOf(ModeBookingEntity(mode = surveyMode, slotId = 501L)))

        service.updateSurveyModeStatus(99L, SurveyModeStatus.PROCESSED)

        assertEquals(SurveyModeStatus.PROCESSED, surveyMode.status)
        verify(bookedSlotsEventPublisher).publish(
            listOf(501L),
            BookedSlotStatus.FINISHED,
            missionId,
            99L,
            77L
        )
    }

    @Test
    fun `updateSurveyModeStatus without bookings does not publish event`() {
        val surveyMode = SurveyModeEntity(id = 77L, status = SurveyModeStatus.PLANNED)

        `when`(surveyModeRepository.findById(77L)).thenReturn(java.util.Optional.of(surveyMode))
        `when`(surveyModeRepository.save(surveyMode)).thenReturn(surveyMode)
        `when`(modeBookingRepository.findAllByMode_IdIn(listOf(77L))).thenReturn(emptyList())

        service.updateSurveyModeStatus(77L, SurveyModeStatus.FAILED)

        assertEquals(SurveyModeStatus.FAILED, surveyMode.status)
        verifyNoInteractions(bookedSlotsEventPublisher)
    }
}
