package space.nstart.pcp.pcp_mission_planing_service.service

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.mockito.Mockito.mock
import org.mockito.Mockito.`when`
import org.springframework.web.server.ResponseStatusException
import space.nstart.pcp.pcp_mission_planing_service.controller.MissionCloneRequest
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.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_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.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.routes.SurveyModeStatus
import java.time.LocalDateTime
import java.util.Optional
import java.util.UUID

class MissionCloneTest {

    private val missionStore = linkedMapOf<UUID, MissionEntity>()
    private val planStore = linkedMapOf<Long, PlanEntity>()
    private val surveyStore = linkedMapOf<Long, SurveyModeEntity>()
    private val dropStore = linkedMapOf<Long, DropModeEntity>()
    private val dropSurveyStore = mutableListOf<DropModeSurveyEntity>()

    private var missionAutoId = 1L
    private var planAutoId = 100L
    private var modeAutoId = 10L

    private val missionRepository = mock(MissionRepository::class.java).also { repo ->
        `when`(repo.findById(anyUUID())).thenAnswer { inv ->
            Optional.ofNullable(missionStore[inv.arguments[0]])
        }
        `when`(repo.save(anyNonNull<MissionEntity>())).thenAnswer { inv ->
            val m = inv.arguments[0] as MissionEntity
            missionStore[m.missionId] = m
            m
        }
    }

    private val planRepository = mock(PlanRepository::class.java).also { repo ->
        `when`(repo.save(anyNonNull<PlanEntity>())).thenAnswer { inv ->
            val p = inv.arguments[0] as PlanEntity
            val id = ++planAutoId
            val saved = PlanEntity(id = id, mission = p.mission, planNumber = p.planNumber, planStart = p.planStart, planStop = p.planStop)
            planStore[id] = saved
            saved
        }
    }

    private val modeRepository = mock(ModeRepository::class.java).also { repo ->
        `when`(repo.findAllByPlan_Mission_MissionIdOrderByTimeStartAsc(anyUUID())).thenAnswer { inv ->
            val missionId = inv.arguments[0] as UUID
            (surveyStore.values.filter { it.plan?.mission?.missionId == missionId } +
                dropStore.values.filter { it.plan?.mission?.missionId == missionId })
        }
    }

    private val surveyModeRepository = mock(SurveyModeRepository::class.java).also { repo ->
        `when`(repo.save(anyNonNull<SurveyModeEntity>())).thenAnswer { inv ->
            val s = inv.arguments[0] as SurveyModeEntity
            val id = ++modeAutoId
            val saved = SurveyModeEntity(id = id, plan = s.plan, timeStart = s.timeStart, revolution = s.revolution,
                lat = s.lat, long = s.long, duration = s.duration, contourWkt = s.contourWkt, roll = s.roll,
                status = s.status, source = s.source)
            surveyStore[id] = saved
            saved
        }
    }

    private val dropModeRepository = mock(DropModeRepository::class.java).also { repo ->
        `when`(repo.save(anyNonNull<DropModeEntity>())).thenAnswer { inv ->
            val d = inv.arguments[0] as DropModeEntity
            val id = ++modeAutoId
            val saved = DropModeEntity(id = id, plan = d.plan, timeStart = d.timeStart, revolution = d.revolution,
                station = d.station, duration = d.duration)
            dropStore[id] = saved
            saved
        }
    }

    private val dropModeSurveyRepository = mock(DropModeSurveyRepository::class.java).also { repo ->
        `when`(repo.findAllByDropMode_IdOrderBySurveyMode_TimeStartAsc(anyLong())).thenAnswer { inv ->
            val dropId = inv.arguments[0] as Long
            dropSurveyStore.filter { it.dropMode?.id == dropId }
        }
        `when`(repo.save(anyNonNull<DropModeSurveyEntity>())).thenAnswer { inv ->
            val link = inv.arguments[0] as DropModeSurveyEntity
            dropSurveyStore.add(link)
            link
        }
    }

    private val service = MissionPlaningService(
        missionRepository = missionRepository,
        modeRepository = modeRepository,
        planRepository = planRepository,
        surveyModeRepository = surveyModeRepository,
        dropModeRepository = dropModeRepository,
        dropModeSurveyRepository = dropModeSurveyRepository,
        modeBookingRepository = mock(ModeBookingRepository::class.java),
        bookedSlotsEventPublisher = mock(BookedSlotsEventPublisher::class.java),
        ballisticsService = mock(BallisticsService::class.java),
        complexMissionService = mock(ComplexMissionService::class.java),
        satelliteCatalogClient = mock(space.nstart.pcp.pcp_mission_planing_service.service.catalog.SatelliteCatalogClient::class.java),
        satelliteMissionTypeFactory = SatelliteMissionTypeFactory(
            listOf(DefaultSatelliteMissionType(), EmissioSatelliteMissionType(), ReflixioSatelliteMissionType()),
            DefaultSatelliteMissionType()
        )
    )

    // --- replace: полный клон ---

    @Test
    fun `replace clone copies all survey and drop modes with remapped survey links`() {
        val (missionId, _, survey1, survey2, drop) = buildSourceMission(t(1), t(5))
        addDropLink(drop, survey1)
        addDropLink(drop, survey2)

        val result = service.cloneMission(missionId, MissionCloneRequest(mode = "replace"))

        val clonedSurveys = surveyStore.values.filter { it.plan?.mission?.missionId == result.newMissionId }
        val clonedDrops = dropStore.values.filter { it.plan?.mission?.missionId == result.newMissionId }
        assertEquals(2, clonedSurveys.size)
        assertEquals(1, clonedDrops.size)
        // Ссылки переведены на новые survey IDs
        val clonedLinks = dropSurveyStore.filter { it.dropMode?.plan?.mission?.missionId == result.newMissionId }
        assertEquals(2, clonedLinks.size)
        assertTrue(clonedLinks.all { it.surveyMode?.plan?.mission?.missionId == result.newMissionId })
    }

    // --- fork-forward: фильтрация по окну ---

    @Test
    fun `fork-forward clone includes mode that starts before window but ends inside`() {
        // Включение начинается за 5 мин до окна и заканчивается внутри → пересекается.
        // buildSourceMission создаёт 2 survey (оба перекрывают окно) + 1 drop.
        val (missionId) = buildSourceMission(t(0).minusMinutes(5), t(0).plusMinutes(10))
        val result = service.cloneMission(
            missionId,
            MissionCloneRequest(mode = "fork-forward", windowStart = t(0), windowStop = t(1))
        )
        val cloned = surveyStore.values.filter { it.plan?.mission?.missionId == result.newMissionId }
        assertEquals(2, cloned.size)
    }

    @Test
    fun `fork-forward clone excludes mode whose stop equals window start (closed left is open right semantics)`() {
        // Включение заканчивается ровно в windowStart → не пересекается (modeStop > windowStart is false).
        // Явно создаём одну съёмку длительностью 30 минут, заканчивающуюся в t(0).
        val source = MissionEntity(satelliteId = 100)
        missionStore[source.missionId] = source
        val plan = PlanEntity(id = ++planAutoId, mission = source, planNumber = 1)
        planStore[plan.id!!] = plan
        // duration 1800s = 30 min; начало: t(0) - 30min = строго до окна, конец ровно в t(0).
        survey(plan, t(0).minusMinutes(30), duration = 1800.0)

        val result = service.cloneMission(
            source.missionId,
            MissionCloneRequest(mode = "fork-forward", windowStart = t(0), windowStop = t(1))
        )
        val cloned = surveyStore.values.filter { it.plan?.mission?.missionId == result.newMissionId }
        assertTrue(cloned.isEmpty())
    }

    @Test
    fun `fork-forward clone excludes mode starting exactly at window stop (open upper bound)`() {
        // Включение начинается ровно в windowStop → не пересекается (open right bound [start, stop)).
        // Явно создаём одну съёмку: start = t(1) (= windowStop), duration 10min.
        val source = MissionEntity(satelliteId = 100)
        missionStore[source.missionId] = source
        val plan = PlanEntity(id = ++planAutoId, mission = source, planNumber = 1)
        planStore[plan.id!!] = plan
        survey(plan, t(1), duration = 600.0)

        val result = service.cloneMission(
            source.missionId,
            MissionCloneRequest(mode = "fork-forward", windowStart = t(0), windowStop = t(1))
        )
        val cloned = surveyStore.values.filter { it.plan?.mission?.missionId == result.newMissionId }
        assertTrue(cloned.isEmpty())
    }

    @Test
    fun `fork-forward drops drop mode when all its surveys are outside window`() {
        // Две съёмки вне окна; сброс на них тоже не клонируется (пустой drop).
        val source = MissionEntity(satelliteId = 100)
        missionStore[source.missionId] = source
        val sourcePlan = PlanEntity(id = ++planAutoId, mission = source, planNumber = 1)
        planStore[sourcePlan.id!!] = sourcePlan
        val outSurvey = survey(sourcePlan, start = t(0).minusHours(2), duration = 60.0)
        val drop = drop(sourcePlan, start = t(0).minusMinutes(30))
        addDropLink(drop, outSurvey)

        val result = service.cloneMission(
            source.missionId,
            MissionCloneRequest(mode = "fork-forward", windowStart = t(1), windowStop = t(2))
        )
        val clonedDrops = dropStore.values.filter { it.plan?.mission?.missionId == result.newMissionId }
        assertTrue(clonedDrops.isEmpty())
    }

    @Test
    fun `fork-forward keeps a survey even when its drop falls outside the window`() {
        // Съёмка внутри окна [t(1), t(2)), но её сброс — до окна. Сброс не клонируется,
        // съёмка остаётся (будет пересброшена в новом плане) — намеренная семантика fork-forward.
        val source = MissionEntity(satelliteId = 100)
        missionStore[source.missionId] = source
        val plan = PlanEntity(id = ++planAutoId, mission = source, planNumber = 1)
        planStore[plan.id!!] = plan
        val inSurvey = survey(plan, start = t(1).plusMinutes(10), duration = 600.0)
        val outDrop = drop(plan, start = t(0))
        addDropLink(outDrop, inSurvey)

        val result = service.cloneMission(
            source.missionId,
            MissionCloneRequest(mode = "fork-forward", windowStart = t(1), windowStop = t(2))
        )
        val clonedSurveys = surveyStore.values.filter { it.plan?.mission?.missionId == result.newMissionId }
        val clonedDrops = dropStore.values.filter { it.plan?.mission?.missionId == result.newMissionId }
        assertEquals(1, clonedSurveys.size)
        assertTrue(clonedDrops.isEmpty())
    }

    @Test
    fun `fork-forward requires windowStart and windowStop`() {
        val (missionId) = buildSourceMission(t(0), t(1))
        assertThrows<ResponseStatusException> {
            service.cloneMission(missionId, MissionCloneRequest(mode = "fork-forward"))
        }
    }

    // --- helpers ---

    data class SourceFixture(
        val missionId: UUID,
        val plan: PlanEntity,
        val survey1: SurveyModeEntity,
        val survey2: SurveyModeEntity,
        val drop: DropModeEntity
    )

    private fun buildSourceMission(surveyStart: LocalDateTime, surveyStop: LocalDateTime): SourceFixture {
        val mission = MissionEntity(satelliteId = 100)
        missionStore[mission.missionId] = mission
        val plan = PlanEntity(id = ++planAutoId, mission = mission, planNumber = 1)
        planStore[plan.id!!] = plan
        val duration = java.time.Duration.between(surveyStart, surveyStop).toMillis().toDouble() / 1000.0
        val s1 = survey(plan, surveyStart, duration)
        val s2 = survey(plan, surveyStart.plusMinutes(1), duration)
        val d = drop(plan, surveyStart.plusMinutes(5))
        return SourceFixture(mission.missionId, plan, s1, s2, d)
    }

    private fun survey(plan: PlanEntity, start: LocalDateTime, duration: Double = 300.0): SurveyModeEntity {
        val id = ++modeAutoId
        val s = SurveyModeEntity(id = id, plan = plan, timeStart = start, revolution = id,
            lat = 55.0, long = 37.0, duration = duration, roll = 0.0, source = SurveyModeSource.MANUAL,
            status = SurveyModeStatus.PLANNED)
        surveyStore[id] = s
        return s
    }

    private fun drop(plan: PlanEntity, start: LocalDateTime): DropModeEntity {
        val id = ++modeAutoId
        val d = DropModeEntity(id = id, plan = plan, timeStart = start,
            revolution = id, station = "102", duration = 60.0)
        dropStore[id] = d
        return d
    }

    private fun addDropLink(drop: DropModeEntity, survey: SurveyModeEntity) {
        dropSurveyStore.add(DropModeSurveyEntity(dropMode = drop, surveyMode = survey))
    }

    private fun t(hour: Int): LocalDateTime = LocalDateTime.of(2026, 6, 10, hour, 0, 0)

    @Suppress("UNCHECKED_CAST")
    private fun <T> anyNonNull(): T { org.mockito.ArgumentMatchers.any<T>(); return null as T }
    private fun anyUUID(): UUID { org.mockito.ArgumentMatchers.any<UUID>(); return UUID.randomUUID() }
    private fun anyLong(): Long { org.mockito.ArgumentMatchers.anyLong(); return 0L }
}
