package space.nstart.pcp.slots_service.model

import ch.qos.logback.classic.Logger
import ch.qos.logback.classic.spi.ILoggingEvent
import ch.qos.logback.core.read.ListAppender
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.slf4j.LoggerFactory
import space.nstart.pcp.pcp_types_lib.dto.ballistics.RevolutionSign
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotDTO
import java.time.LocalDateTime

class OptimalReqCoverSolverTest {

    @Test
    fun `mergeSlotsToChains preserves representative coordinates`() {
        val solver = OptimalReqCoverSolver(
            logger = LoggerFactory.getLogger("OptimalReqCoverSolverTest.coordinates"),
            satellitesById = emptyMap()
        )
        val mergeMethod = solver.javaClass.getDeclaredMethod("mergeSlotsToChains", List::class.java)
        mergeMethod.isAccessible = true
        val start = LocalDateTime.of(2026, 3, 30, 10, 0)
        val slots = listOf(
            SlotDTO(
                cycle = 1,
                satelliteId = 22,
                tn = start,
                tk = start.plusSeconds(30),
                roll = 5.0,
                contour = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))",
                revolution = 101,
                revolutionSign = RevolutionSign.ASC,
                slotNumber = 1,
                latitude = 55.75,
                longitude = 37.62
            ),
            SlotDTO(
                cycle = 1,
                satelliteId = 22,
                tn = start.plusSeconds(20),
                tk = start.plusSeconds(60),
                roll = 5.0,
                contour = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))",
                revolution = 101,
                revolutionSign = RevolutionSign.ASC,
                slotNumber = 2,
                latitude = 56.0,
                longitude = 38.0
            )
        )

        @Suppress("UNCHECKED_CAST")
        val chains = mergeMethod.invoke(solver, slots) as List<Any>
        val representativeField = chains.single().javaClass.getDeclaredField("representative")
        representativeField.isAccessible = true
        val representative = representativeField.get(chains.single()) as SlotDTO

        assertEquals(55.75, representative.latitude)
        assertEquals(37.62, representative.longitude)
    }

    @Test
    fun `mergeSlotsToChains merges when time overlaps and geometry intersects`() {
        val solver = OptimalReqCoverSolver(
            logger = LoggerFactory.getLogger("OptimalReqCoverSolverTest.mergeAllowed"),
            satellitesById = emptyMap()
        )
        val chains = mergeChains(
            solver,
            listOf(
                slot(
                    startOffsetSeconds = 0,
                    endOffsetSeconds = 30,
                    contour = "POLYGON ((0 0, 2 0, 2 2, 0 2, 0 0))"
                ),
                slot(
                    slotNumber = 2,
                    startOffsetSeconds = 20,
                    endOffsetSeconds = 60,
                    contour = "POLYGON ((1 1, 3 1, 3 3, 1 3, 1 1))"
                )
            )
        )

        assertEquals(1, chains.size)
        val representative = representative(chains.single())
        assertTrue(representative.contour.startsWith("POLYGON"))
        assertFalse(representative.contour.startsWith("MULTIPOLYGON"))
    }

    @Test
    fun `mergeSlotsToChains keeps chains separate when time overlaps but geometry does not intersect`() {
        val logger = LoggerFactory.getLogger("OptimalReqCoverSolverTest.noGeometry") as Logger
        val logs = captureLogs(logger) {
            val solver = OptimalReqCoverSolver(
                logger = logger,
                satellitesById = emptyMap()
            )
            val chains = mergeChains(
                solver,
                listOf(
                    slot(
                        startOffsetSeconds = 0,
                        endOffsetSeconds = 30,
                        contour = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))"
                    ),
                    slot(
                        slotNumber = 2,
                        startOffsetSeconds = 20,
                        endOffsetSeconds = 60,
                        contour = "POLYGON ((10 10, 11 10, 11 11, 10 11, 10 10))"
                    )
                )
            )

            assertEquals(2, chains.size)
        }

        assertTrue(logs.any { it.formattedMessage.contains("contours lack sufficient areal intersection despite temporal overlap") })
    }

    @Test
    fun `mergeSlotsToChains keeps chains separate when contours only touch by boundary`() {
        val logger = LoggerFactory.getLogger("OptimalReqCoverSolverTest.touchingBoundary") as Logger
        val logs = captureLogs(logger) {
            val solver = OptimalReqCoverSolver(
                logger = logger,
                satellitesById = emptyMap()
            )
            val chains = mergeChains(
                solver,
                listOf(
                    slot(
                        startOffsetSeconds = 0,
                        endOffsetSeconds = 30,
                        contour = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))"
                    ),
                    slot(
                        slotNumber = 2,
                        startOffsetSeconds = 20,
                        endOffsetSeconds = 60,
                        contour = "POLYGON ((1 0, 2 0, 2 1, 1 1, 1 0))"
                    )
                )
            )

            assertEquals(2, chains.size)
        }

        assertTrue(logs.any { it.formattedMessage.contains("contours lack sufficient areal intersection despite temporal overlap") })
    }

    @Test
    fun `mergeSlotsToChains does not merge when intervals do not overlap`() {
        val solver = OptimalReqCoverSolver(
            logger = LoggerFactory.getLogger("OptimalReqCoverSolverTest.noTimeOverlap"),
            satellitesById = emptyMap()
        )
        val chains = mergeChains(
            solver,
            listOf(
                slot(
                    startOffsetSeconds = 0,
                    endOffsetSeconds = 30,
                    contour = "POLYGON ((0 0, 2 0, 2 2, 0 2, 0 0))"
                ),
                slot(
                    slotNumber = 2,
                    startOffsetSeconds = 31,
                    endOffsetSeconds = 60,
                    contour = "POLYGON ((1 1, 3 1, 3 3, 1 3, 1 1))"
                )
            )
        )

        assertEquals(2, chains.size)
    }

    @Test
    fun `mergeSlotsToChains skips merge when union returns multipolygon`() {
        val logger = LoggerFactory.getLogger("OptimalReqCoverSolverTest.multiPolygon") as Logger
        val logs = captureLogs(logger) {
            val solver = OptimalReqCoverSolver(
                logger = logger,
                satellitesById = emptyMap()
            )
            val method = solver.javaClass.getDeclaredMethod(
                "unionContoursIfPolygon",
                String::class.java,
                String::class.java,
                SlotDTO::class.java,
                SlotDTO::class.java
            )
            method.isAccessible = true

            val result = method.invoke(
                solver,
                "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))",
                "POLYGON ((1 1, 2 1, 2 2, 1 2, 1 1))",
                slot(
                    startOffsetSeconds = 0,
                    endOffsetSeconds = 30,
                    contour = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))"
                ),
                slot(
                    slotNumber = 2,
                    startOffsetSeconds = 20,
                    endOffsetSeconds = 60,
                    contour = "POLYGON ((1 1, 2 1, 2 2, 1 2, 1 1))"
                )
            )

            assertEquals(null, result)
        }

        assertTrue(logs.any { it.formattedMessage.contains("union produced non-polygon geometry") })
    }

    private fun mergeChains(solver: OptimalReqCoverSolver, slots: List<SlotDTO>): List<Any> {
        val mergeMethod = solver.javaClass.getDeclaredMethod("mergeSlotsToChains", List::class.java)
        mergeMethod.isAccessible = true
        @Suppress("UNCHECKED_CAST")
        return mergeMethod.invoke(solver, slots) as List<Any>
    }

    private fun representative(chain: Any): SlotDTO {
        val representativeField = chain.javaClass.getDeclaredField("representative")
        representativeField.isAccessible = true
        return representativeField.get(chain) as SlotDTO
    }

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

    private fun captureLogs(logger: Logger, block: () -> Unit): List<ILoggingEvent> {
        val appender = ListAppender<ILoggingEvent>()
        appender.start()
        logger.addAppender(appender)
        try {
            block()
            return appender.list.toList()
        } finally {
            logger.detachAppender(appender)
        }
    }
}
