package space.nstart.pcp.slots_service.model

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import org.locationtech.jts.geom.Geometry
import org.locationtech.jts.io.WKTReader
import org.slf4j.LoggerFactory
import space.nstart.pcp.pcp_types_lib.dto.ballistics.RevolutionSign
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotDTO
import space.nstart.pcp.slots_service.model.satellite.AbstractSatellite
import space.nstart.pcp.slots_service.model.satellite.TestSatelliteImpl
import space.nstart.pcp.slots_service.util.LongitudeWrapGeometry
import java.time.LocalDateTime

class ContinuousReqCoverSolverTest {

    private val satellitesById: Map<Long, AbstractSatellite> = mapOf(
        22L to TestSatelliteImpl(id = 22L, maxSurveyDuration = 300, mmi = 10),
        23L to TestSatelliteImpl(id = 23L, maxSurveyDuration = 300, mmi = 10)
    )

    private val solver = ContinuousReqCoverSolver(
        logger = LoggerFactory.getLogger("ContinuousReqCoverSolverTest"),
        satellitesById = satellitesById
    )

    private val greedySolver = OptimalReqCoverSolver(
        logger = LoggerFactory.getLogger("ContinuousReqCoverSolverTest.greedy"),
        satellitesById = satellitesById
    )

    @Test
    fun `continuous solver aggregates overlapping slots into chain-aware candidates`() {
        val mergeMethod = solver.javaClass.getDeclaredMethod("mergeSlotsToChains", List::class.java)
        mergeMethod.isAccessible = true

        @Suppress("UNCHECKED_CAST")
        val chains = mergeMethod.invoke(
            solver,
            listOf(
                slot(
                    slotNumber = 1,
                    startOffsetSeconds = 0,
                    endOffsetSeconds = 30,
                    contour = "POLYGON ((0 0, 2 0, 2 1, 0 1, 0 0))"
                ),
                slot(
                    slotNumber = 2,
                    startOffsetSeconds = 20,
                    endOffsetSeconds = 60,
                    contour = "POLYGON ((1 0, 3 0, 3 1, 1 1, 1 0))"
                ),
                slot(
                    slotNumber = 3,
                    startOffsetSeconds = 120,
                    endOffsetSeconds = 150,
                    contour = "POLYGON ((4 0, 5 0, 5 1, 4 1, 4 0))"
                )
            )
        ) as List<Any>

        assertEquals(2, chains.size)
        val slotsField = chains.first().javaClass.getDeclaredField("slots").apply { isAccessible = true }
        val representativeField = chains.first().javaClass.getDeclaredField("representative").apply { isAccessible = true }
        val firstChainSlots = slotsField.get(chains.first()) as List<*>
        val firstRepresentative = representativeField.get(chains.first()) as SlotDTO

        assertEquals(2, firstChainSlots.size)
        assertEquals(1L, firstRepresentative.slotNumber)
    }

    @Test
    fun `start selection uses continuation potential instead of leftmost fragment`() {
        val result = solver.select(
            targetWkt = "POLYGON ((0 0, 5 0, 5 1, 0 1, 0 0))",
            slots = listOf(
                slot(
                    slotNumber = 1,
                    startOffsetSeconds = 0,
                    endOffsetSeconds = 10,
                    contour = "POLYGON ((0 0, 0.4 0, 0.4 1, 0 1, 0 0))"
                ),
                slot(
                    slotNumber = 2,
                    startOffsetSeconds = 20,
                    endOffsetSeconds = 30,
                    contour = "POLYGON ((1 0, 2 0, 2 1, 1 1, 1 0))"
                ),
                slot(
                    slotNumber = 3,
                    startOffsetSeconds = 40,
                    endOffsetSeconds = 50,
                    contour = "POLYGON ((2 0, 3 0, 3 1, 2 1, 2 0))"
                ),
                slot(
                    slotNumber = 4,
                    startOffsetSeconds = 60,
                    endOffsetSeconds = 70,
                    contour = "POLYGON ((3 0, 4 0, 4 1, 3 1, 3 0))"
                )
            )
        )

        assertFalse(result.isEmpty())
        assertEquals(2L, result.first().slotNumber)
    }

    @Test
    fun `next selection prefers meaningful gain over tiny touching continuation`() {
        val result = solver.select(
            targetWkt = "POLYGON ((0 0, 4 0, 4 1, 0 1, 0 0))",
            slots = listOf(
                slot(
                    slotNumber = 1,
                    startOffsetSeconds = 0,
                    endOffsetSeconds = 10,
                    contour = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))"
                ),
                slot(
                    slotNumber = 2,
                    startOffsetSeconds = 20,
                    endOffsetSeconds = 30,
                    contour = "POLYGON ((0.999 0, 1.001 0, 1.001 1, 0.999 1, 0.999 0))"
                ),
                slot(
                    slotNumber = 3,
                    startOffsetSeconds = 40,
                    endOffsetSeconds = 50,
                    contour = "POLYGON ((1.25 0, 2.25 0, 2.25 1, 1.25 1, 1.25 0))"
                )
            )
        )

        assertEquals(listOf(1L, 3L), result.map { it.slotNumber })
    }

    @Test
    fun `continuity is evaluated by target-covered geometry rather than full contour`() {
        val result = solver.select(
            targetWkt = "POLYGON ((0 0, 3 0, 3 1, 0 1, 0 0))",
            slots = listOf(
                slot(
                    slotNumber = 1,
                    startOffsetSeconds = 0,
                    endOffsetSeconds = 10,
                    contour = "POLYGON ((0 0, 1 0, 1 2, 0 2, 0 0))"
                ),
                slot(
                    slotNumber = 2,
                    startOffsetSeconds = 20,
                    endOffsetSeconds = 30,
                    contour = "MULTIPOLYGON (((1 1.2, 1.5 1.2, 1.5 2, 1 2, 1 1.2)), ((2 0, 3 0, 3 1, 2 1, 2 0)))"
                ),
                slot(
                    slotNumber = 3,
                    startOffsetSeconds = 40,
                    endOffsetSeconds = 50,
                    contour = "POLYGON ((0.9 0, 2 0, 2 1, 0.9 1, 0.9 0))"
                )
            )
        )

        assertEquals(listOf(1L, 3L), result.map { it.slotNumber })
    }

    @Test
    fun `same longitude candidate without meaningful gain is rejected`() {
        val result = solver.select(
            targetWkt = "POLYGON ((0 0, 4 0, 4 1, 0 1, 0 0))",
            slots = listOf(
                slot(
                    slotNumber = 1,
                    startOffsetSeconds = 0,
                    endOffsetSeconds = 10,
                    contour = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))"
                ),
                slot(
                    slotNumber = 2,
                    startOffsetSeconds = 20,
                    endOffsetSeconds = 30,
                    contour = "POLYGON ((0.8 0, 1 0, 1 1, 0.8 1, 0.8 0))"
                ),
                slot(
                    slotNumber = 3,
                    startOffsetSeconds = 40,
                    endOffsetSeconds = 50,
                    contour = "POLYGON ((1 0, 2 0, 2 1, 1 1, 1 0))"
                )
            )
        )

        assertEquals(listOf(1L, 3L), result.map { it.slotNumber })
    }

    @Test
    fun `same longitude candidate is allowed only when it adds meaningful new coverage`() {
        val result = solver.select(
            targetWkt = "POLYGON ((0 0, 1 0, 1 2, 0 2, 0 0))",
            slots = listOf(
                slot(
                    slotNumber = 1,
                    startOffsetSeconds = 0,
                    endOffsetSeconds = 10,
                    contour = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))"
                ),
                slot(
                    slotNumber = 2,
                    startOffsetSeconds = 20,
                    endOffsetSeconds = 30,
                    contour = "POLYGON ((0 1, 1 1, 1 2, 0 2, 0 1))"
                )
            )
        )

        assertEquals(listOf(1L, 2L), result.map { it.slotNumber })
    }

    @Test
    fun `continuous coverage remains competitive with greedy on long strip`() {
        val target = "POLYGON ((0 0, 6 0, 6 1, 0 1, 0 0))"
        val slots = listOf(
            slot(1, 0, 10, contour = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))"),
            slot(2, 20, 30, contour = "POLYGON ((1 0, 2 0, 2 1, 1 1, 1 0))"),
            slot(3, 40, 50, contour = "POLYGON ((2 0, 3 0, 3 1, 2 1, 2 0))"),
            slot(4, 60, 70, contour = "POLYGON ((3 0, 4 0, 4 1, 3 1, 3 0))"),
            slot(5, 80, 90, contour = "POLYGON ((4 0, 5 0, 5 1, 4 1, 4 0))"),
            slot(11, 100, 110, contour = "POLYGON ((0.999 0, 1.001 0, 1.001 1, 0.999 1, 0.999 0))"),
            slot(12, 120, 130, contour = "POLYGON ((1.999 0, 2.001 0, 2.001 1, 1.999 1, 1.999 0))")
        )

        val greedy = greedySolver.select(target, slots)
        val continuous = solver.select(target, slots)
        val greedyMetrics = coverageMetrics(target, greedy)
        val continuousMetrics = coverageMetrics(target, continuous)

        assertTrue(continuousMetrics.coveragePercent >= greedyMetrics.coveragePercent * 0.95)
        assertTrue(continuousMetrics.redundancyArea <= greedyMetrics.redundancyArea + 0.1)
        assertTrue(continuous.size <= greedy.size + 1)
    }

    @Test
    fun `continuous does not collapse on competing branches fixture`() {
        val target = "POLYGON ((0 0, 5 0, 5 1, 0 1, 0 0))"
        val slots = listOf(
            slot(1, 0, 10, contour = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))"),
            slot(2, 20, 30, contour = "POLYGON ((1 0, 2 0, 2 1, 1 1, 1 0))"),
            slot(3, 40, 50, contour = "POLYGON ((2 0, 3 0, 3 1, 2 1, 2 0))"),
            slot(10, 15, 25, revolutionSign = RevolutionSign.DESC, contour = "POLYGON ((0.8 0, 2.4 0, 2.4 1, 0.8 1, 0.8 0))"),
            slot(11, 35, 45, revolutionSign = RevolutionSign.DESC, contour = "POLYGON ((2.4 0, 4.0 0, 4.0 1, 2.4 1, 2.4 0))"),
            slot(12, 55, 65, contour = "POLYGON ((3 0, 4 0, 4 1, 3 1, 3 0))")
        )

        val greedy = greedySolver.select(target, slots)
        val continuous = solver.select(target, slots)
        val greedyMetrics = coverageMetrics(target, greedy)
        val continuousMetrics = coverageMetrics(target, continuous)

        assertTrue(continuousMetrics.coveragePercent >= greedyMetrics.coveragePercent * 0.85)
        assertTrue(continuousMetrics.coveragePercent >= 75.0)
    }

    private fun coverageMetrics(targetWkt: String, slots: List<SlotDTO>): CoverageMetrics {
        val reader = WKTReader()
        val target = LongitudeWrapGeometry.normalizeToContinuous360(reader.read(targetWkt))
        val coveredParts = slots.map { slot ->
            val geometry = LongitudeWrapGeometry.alignToReference(reader.read(slot.contour), target)
            geometry.intersection(target)
        }.filter { !it.isEmpty }

        val union = coveredParts.reduceOrNull(Geometry::union)
        val coveredArea = union?.area ?: 0.0
        val totalArea = target.area
        val redundancyArea = coveredParts.sumOf { it.area } - coveredArea

        return CoverageMetrics(
            coveragePercent = if (totalArea == 0.0) 0.0 else coveredArea * 100.0 / totalArea,
            redundancyArea = redundancyArea
        )
    }

    private data class CoverageMetrics(
        val coveragePercent: Double,
        val redundancyArea: Double
    )

    private fun slot(
        slotNumber: Long,
        startOffsetSeconds: Long,
        endOffsetSeconds: Long,
        satelliteId: Long = 22L,
        cycle: Long = 1L,
        roll: Double = 5.0,
        revolutionSign: RevolutionSign = RevolutionSign.ASC,
        contour: String
    ): SlotDTO {
        val start = LocalDateTime.of(2026, 3, 30, 10, 0)
        return SlotDTO(
            cycle = cycle,
            satelliteId = satelliteId,
            tn = start.plusSeconds(startOffsetSeconds),
            tk = start.plusSeconds(endOffsetSeconds),
            roll = roll,
            contour = contour,
            revolution = 101,
            revolutionSign = revolutionSign,
            slotNumber = slotNumber,
            latitude = 0.0,
            longitude = 0.0
        )
    }
}
