package space.nstart.pcp.slots_service.service

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.BookedSlotStatus
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SurveySlotDTO
import space.nstart.pcp.slots_service.entity.BookedSlotEntity
import space.nstart.pcp.slots_service.entity.SlotEntity
import space.nstart.pcp.slots_service.model.satellite.TestSatelliteImpl
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime

class SlotServiceChainAggregationTest {

    private val service = SlotService()
    private val method = service.javaClass.getDeclaredMethod(
        "buildSurveyChains",
        Class.forName("space.nstart.pcp.slots_service.model.satellite.AbstractSatellite"),
        List::class.java,
        LocalDateTime::class.java,
        LocalDateTime::class.java
    ).apply { isAccessible = true }

    @Test
    fun `buildSurveyChains merges when time overlaps and geometry intersects`() {
        val satellite = satellite()
        val chains = buildSurveyChains(
            satellite,
            listOf(
                bookedSlot(
                    satelliteId = satellite.id,
                    bookedSlotId = 100,
                    slotNumber = 1,
                    start = baseTime(),
                    end = baseTime().plusSeconds(30),
                    contour = "POLYGON ((0 0, 2 0, 2 2, 0 2, 0 0))"
                ),
                bookedSlot(
                    satelliteId = satellite.id,
                    bookedSlotId = 101,
                    slotNumber = 2,
                    start = baseTime().plusSeconds(20),
                    end = baseTime().plusSeconds(60),
                    contour = "POLYGON ((1 1, 3 1, 3 3, 1 3, 1 1))"
                )
            )
        )

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

    @Test
    fun `buildSurveyChains keeps routes separate when time overlaps but geometry does not intersect`() {
        val satellite = satellite()
        val logger = LoggerFactory.getLogger(SlotService::class.java) as Logger
        val logs = captureLogs(logger) {
            val chains = buildSurveyChains(
                satellite,
                listOf(
                    bookedSlot(
                        satelliteId = satellite.id,
                        bookedSlotId = 100,
                        slotNumber = 1,
                        start = baseTime(),
                        end = baseTime().plusSeconds(30),
                        contour = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))"
                    ),
                    bookedSlot(
                        satelliteId = satellite.id,
                        bookedSlotId = 101,
                        slotNumber = 2,
                        start = baseTime().plusSeconds(20),
                        end = baseTime().plusSeconds(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 `buildSurveyChains keeps routes separate when contours only touch by boundary`() {
        val satellite = satellite()
        val logger = LoggerFactory.getLogger(SlotService::class.java) as Logger
        val logs = captureLogs(logger) {
            val chains = buildSurveyChains(
                satellite,
                listOf(
                    bookedSlot(
                        satelliteId = satellite.id,
                        bookedSlotId = 100,
                        slotNumber = 1,
                        start = baseTime(),
                        end = baseTime().plusSeconds(30),
                        contour = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))"
                    ),
                    bookedSlot(
                        satelliteId = satellite.id,
                        bookedSlotId = 101,
                        slotNumber = 2,
                        start = baseTime().plusSeconds(20),
                        end = baseTime().plusSeconds(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 `buildSurveyChains merges adjacent routes when contours are continuous`() {
        val satellite = satellite()
        val chains = buildSurveyChains(
            satellite,
            listOf(
                bookedSlot(
                    satelliteId = satellite.id,
                    bookedSlotId = 100,
                    slotNumber = 1,
                    start = baseTime(),
                    end = baseTime().plusSeconds(30),
                    contour = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))"
                ),
                bookedSlot(
                    satelliteId = satellite.id,
                    bookedSlotId = 101,
                    slotNumber = 2,
                    start = baseTime().plusSeconds(30),
                    end = baseTime().plusSeconds(60),
                    contour = "POLYGON ((1 0, 2 0, 2 1, 1 1, 1 0))"
                )
            )
        )

        assertEquals(1, chains.size)
        assertEquals(baseTime(), chains.single().tn)
        assertEquals(baseTime().plusSeconds(60), chains.single().tk)
        assertEquals(listOf(100L, 101L), chains.single().slotIds)
    }

    @Test
    fun `buildSurveyChains keeps routes separate when union returns multipolygon`() {
        val logger = LoggerFactory.getLogger(SlotService::class.java) as Logger
        val logs = captureLogs(logger) {
            val method = service.javaClass.getDeclaredMethod(
                "unionContoursIfPolygon",
                String::class.java,
                String::class.java,
                Long::class.javaPrimitiveType,
                LocalDateTime::class.java,
                LocalDateTime::class.java,
                Double::class.javaPrimitiveType,
                List::class.java,
                Class.forName("space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotDTO"),
                Long::class.javaPrimitiveType
            )
            method.isAccessible = true

            val result = method.invoke(
                service,
                "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))",
                "POLYGON ((1 1, 2 1, 2 2, 1 2, 1 1))",
                22L,
                baseTime(),
                baseTime().plusSeconds(30),
                5.0,
                listOf(100L),
                bookedSlot(
                    satelliteId = 22,
                    bookedSlotId = 101,
                    slotNumber = 2,
                    start = baseTime().plusSeconds(20),
                    end = baseTime().plusSeconds(60),
                    contour = "POLYGON ((1 1, 2 1, 2 2, 1 2, 1 1))"
                ).slot.toDTO(),
                101L
            )

            assertEquals(null, result)
        }

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

    private fun buildSurveyChains(
        satellite: TestSatelliteImpl,
        bookedSlots: List<BookedSlotEntity>
    ): List<SurveySlotDTO> {
        @Suppress("UNCHECKED_CAST")
        return method.invoke(
            service,
            satellite,
            bookedSlots,
            baseTime().minusMinutes(5),
            baseTime().plusMinutes(10)
        ) as List<SurveySlotDTO>
    }

    private fun satellite() = TestSatelliteImpl(
        id = 22,
        tnCalc = LocalDateTime.of(LocalDate.of(2026, 3, 31), LocalTime.of(0, 0)),
        durationCalc = 16
    )

    private fun bookedSlot(
        satelliteId: Long,
        bookedSlotId: Long,
        slotNumber: Long,
        start: LocalDateTime,
        end: LocalDateTime,
        contour: String
    ) = BookedSlotEntity(
        bookedSlotId = bookedSlotId,
        slot = SlotEntity(
            slotId = bookedSlotId,
            slotNum = slotNumber,
            cycle = 0,
            satelliteId = satelliteId,
            coveringType = 0,
            tn = start,
            tk = end,
            roll = 5.0,
            contour = contour,
            revolution = 101,
            revolutionSign = RevolutionSign.ASC.toString(),
            latitude = 55.75,
            longitude = 37.62
        ),
        cycle = 0,
        status = BookedSlotStatus.BOOKED.name
    )

    private fun baseTime(): LocalDateTime = LocalDateTime.of(2026, 3, 31, 10, 0)

    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)
        }
    }
}
