package space.nstart.pcp.pcp_satellites_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.Test
import org.slf4j.LoggerFactory
import space.nstart.pcp.pcp_satellites_service.entity.SatelliteModeSource
import space.nstart.pcp.pcp_satellites_service.model.BLS
import space.nstart.pcp.pcp_satellites_service.model.SatelliteModel
import space.nstart.pcp.pcp_satellites_service.model.mode.SurveyMode
import java.time.Duration
import java.time.LocalDateTime
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertTrue

class SurveyPlacementServiceTest {

    private val service = SurveyPlacementService()

    @Test
    fun `candidate fits without trimming`() {
        val surveys = mutableListOf<SurveyMode>()
        val satellite = satellite()

        val decision = service.tryPlaceSurvey(surveys, survey(start = 0, end = 40), satellite)

        assertTrue(decision.accepted)
        assertFalse(decision.trimmed)
        assertEquals(1, surveys.size)
        assertEquals(40.0, surveys.single().duration)
    }

    @Test
    fun `candidate exceeds daily max duration and is trimmed then accepted`() {
        val surveys = mutableListOf(survey(start = 600, end = 670, revolution = 2, roll = 20.0))
        val satellite = satellite(dailyMaxDuration = 100.0)

        val decision = service.tryPlaceSurvey(surveys, survey(start = 0, end = 50), satellite)

        assertTrue(decision.accepted)
        assertTrue(decision.trimmed)
        assertEquals(2, surveys.size)
        assertEquals(30.0, decision.adjustedCandidate?.duration)
    }

    @Test
    fun `candidate exceeds daily max duration and is rejected below duration min`() {
        val surveys = mutableListOf(survey(start = 600, end = 695, revolution = 2, roll = 20.0))
        val satellite = satellite(dailyMaxDuration = 100.0)

        val decision = service.tryPlaceSurvey(surveys, survey(start = 0, end = 20), satellite)

        assertFalse(decision.accepted)
        assertEquals("daily_max_duration", decision.rejectionReason)
        assertEquals(1, surveys.size)
    }

    @Test
    fun `candidate exceeds revolution max duration and is trimmed then accepted`() {
        val surveys = mutableListOf(survey(start = 600, end = 670, revolution = 5, roll = 20.0))
        val satellite = satellite(revolutionMaxDuration = 100.0)

        val decision = service.tryPlaceSurvey(
            surveys,
            survey(start = 0, end = 50, revolution = 5),
            satellite
        )

        assertTrue(decision.accepted)
        assertTrue(decision.trimmed)
        assertEquals(30.0, decision.adjustedCandidate?.duration)
    }

    @Test
    fun `candidate exceeds merged chain duration max and is trimmed then accepted`() {
        val surveys = mutableListOf(survey(start = 0, end = 80, roll = 10.0))
        val satellite = satellite(durationMax = 100.0, mmi = 10.0)

        val decision = service.tryPlaceSurvey(
            surveys,
            survey(start = 50, end = 140, roll = 10.0),
            satellite
        )

        assertTrue(decision.accepted)
        assertTrue(decision.trimmed)
        assertEquals(1, surveys.size)
        assertEquals(0, secondsFromBase(surveys.single().time))
        assertEquals(100, secondsFromBase(surveys.single().timeStop))
    }

    @Test
    fun `merge allowed when time overlaps and geometry intersects`() {
        val surveys = mutableListOf(
            survey(
                start = 0,
                end = 40,
                roll = 10.0,
                contour = "POLYGON ((0 0, 2 0, 2 2, 0 2, 0 0))"
            )
        )
        val satellite = satellite()

        val decision = service.tryPlaceSurvey(
            surveys,
            survey(
                start = 20,
                end = 70,
                roll = 10.0,
                contour = "POLYGON ((1 1, 3 1, 3 3, 1 3, 1 1))"
            ),
            satellite
        )

        assertTrue(decision.accepted)
        assertEquals(1, surveys.size)
        assertTrue(surveys.single().contourWKT.startsWith("POLYGON"))
        assertFalse(surveys.single().contourWKT.startsWith("MULTIPOLYGON"))
    }

    @Test
    fun `complan inside booked slot is not merged to mixed`() {
        val surveys = mutableListOf(
            survey(
                start = 0,
                end = 100,
                roll = 10.0,
                source = SatelliteModeSource.SLOTS,
                contour = "POLYGON ((0 0, 3 0, 3 3, 0 3, 0 0))"
            )
        )
        val satellite = satellite()

        val decision = service.tryPlaceSurvey(
            surveys,
            survey(
                start = 0,
                end = 40,
                roll = 10.0,
                source = SatelliteModeSource.COMPLAN,
                contour = "POLYGON ((1 1, 2 1, 2 2, 1 2, 1 1))"
            ),
            satellite
        )

        assertTrue(decision.accepted)
        assertEquals(2, surveys.size)
        assertTrue(decision.absorbedSurveys.isEmpty())
        assertEquals(1, surveys.count { it.source == SatelliteModeSource.SLOTS })
        assertEquals(1, surveys.count { it.source == SatelliteModeSource.COMPLAN })
        assertFalse(surveys.any { it.source == SatelliteModeSource.MIXED })
    }

    @Test
    fun `different roll conflict resolved by trimming`() {
        val surveys = mutableListOf(survey(start = 51, end = 80, roll = 20.0))
        val satellite = satellite(mmi = 10.0)

        val decision = service.tryPlaceSurvey(
            surveys,
            survey(start = 0, end = 70, roll = 10.0),
            satellite
        )

        assertTrue(decision.accepted)
        assertTrue(decision.trimmed)
        assertEquals(40.0, decision.adjustedCandidate?.duration)
    }

    @Test
    fun `different roll conflict that cannot be resolved is rejected`() {
        val surveys = mutableListOf(survey(start = 5, end = 40, roll = 20.0))
        val satellite = satellite(mmi = 10.0)

        val decision = service.tryPlaceSurvey(
            surveys,
            survey(start = 0, end = 30, roll = 10.0),
            satellite
        )

        assertFalse(decision.accepted)
        assertEquals("different_roll_conflict", decision.rejectionReason)
        assertEquals(1, surveys.size)
    }

    @Test
    fun `epsilon based roll comparison treats nearly equal rolls as same chain`() {
        val surveys = mutableListOf(
            survey(
                start = 0,
                end = 40,
                roll = 10.0,
                contour = "POLYGON ((0 0, 2 0, 2 2, 0 2, 0 0))"
            )
        )
        val satellite = satellite()

        val decision = service.tryPlaceSurvey(
            surveys,
            survey(
                start = 20,
                end = 70,
                roll = 10.005,
                contour = "POLYGON ((1 1, 3 1, 3 3, 1 3, 1 1))"
            ),
            satellite
        )

        assertTrue(decision.accepted)
        assertEquals(1, surveys.size)
        assertEquals(70, secondsFromBase(surveys.single().timeStop))
        assertNotNull(decision.appliedSurvey)
    }

    @Test
    fun `merge forbidden when time overlaps but geometry does not intersect`() {
        val surveys = mutableListOf(
            survey(
                start = 0,
                end = 40,
                contour = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))"
            )
        )
        val satellite = satellite()

        val logs = captureLogs {
            val decision = service.tryPlaceSurvey(
                surveys,
                survey(
                    start = 20,
                    end = 70,
                    contour = "POLYGON ((10 10, 11 10, 11 11, 10 11, 10 10))"
                ),
                satellite
            )

            assertTrue(decision.accepted)
        }

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

    @Test
    fun `merge forbidden when contours only touch by boundary`() {
        val surveys = mutableListOf(
            survey(
                start = 0,
                end = 40,
                contour = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))"
            )
        )
        val satellite = satellite()

        val logs = captureLogs {
            val decision = service.tryPlaceSurvey(
                surveys,
                survey(
                    start = 20,
                    end = 70,
                    contour = "POLYGON ((1 0, 2 0, 2 1, 1 1, 1 0))"
                ),
                satellite
            )

            assertTrue(decision.accepted)
        }

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

    @Test
    fun `merge forbidden when intervals do not overlap but gap is within mmi`() {
        val surveys = mutableListOf(
            survey(
                start = 0,
                end = 40,
                contour = "POLYGON ((0 0, 2 0, 2 2, 0 2, 0 0))"
            )
        )
        val satellite = satellite(mmi = 10.0)

        val decision = service.tryPlaceSurvey(
            surveys,
            survey(
                start = 45,
                end = 70,
                contour = "POLYGON ((1 1, 3 1, 3 3, 1 3, 1 1))"
            ),
            satellite
        )

        assertTrue(decision.accepted)
        assertEquals(2, surveys.size)
    }

    @Test
    fun `merge allowed when intervals are adjacent and contours are continuous`() {
        val surveys = mutableListOf(
            survey(
                start = 0,
                end = 40,
                contour = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))"
            )
        )
        val satellite = satellite()

        val decision = service.tryPlaceSurvey(
            surveys,
            survey(
                start = 40,
                end = 70,
                contour = "POLYGON ((1 0, 2 0, 2 1, 1 1, 1 0))"
            ),
            satellite
        )

        assertTrue(decision.accepted)
        assertEquals(1, surveys.size)
        assertEquals(0, secondsFromBase(surveys.single().time))
        assertEquals(70, secondsFromBase(surveys.single().timeStop))
    }

    @Test
    fun `defensive fallback skips merge when union returns multipolygon`() {
        val logs = captureLogs {
            val method = service.javaClass.getDeclaredMethod(
                "unionContoursIfPolygon",
                String::class.java,
                String::class.java,
                Long::class.javaPrimitiveType,
                SurveyMode::class.java,
                SurveyMode::class.java
            )
            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,
                survey(start = 20, end = 70, contour = "POLYGON ((1 1, 2 1, 2 2, 1 2, 1 1))"),
                survey(start = 0, end = 40, contour = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))")
            )

            assertEquals(null, result)
        }

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

    @Test
    fun `no multipolygon persisted for merged route`() {
        val surveys = mutableListOf(
            survey(
                start = 0,
                end = 40,
                contour = "POLYGON ((0 0, 2 0, 2 2, 0 2, 0 0))"
            )
        )
        val satellite = satellite()

        val decision = service.tryPlaceSurvey(
            surveys,
            survey(
                start = 20,
                end = 70,
                contour = "POLYGON ((1 1, 3 1, 3 3, 1 3, 1 1))"
            ),
            satellite
        )

        assertTrue(decision.accepted)
        assertEquals(1, surveys.size)
        assertFalse(surveys.single().contourWKT.startsWith("MULTIPOLYGON"))
    }

    private fun satellite(
        durationMin: Double = 10.0,
        durationMax: Double = 300.0,
        mmi: Double = 10.0,
        dailyMaxDuration: Double = 1_000.0,
        revolutionMaxDuration: Double = 1_000.0
    ) = SatelliteModel(
        satelliteId = 22,
        name = "T",
        bls = BLS(
            durationMin = durationMin,
            durationMax = durationMax,
            mmi = mmi,
            dailyMaxDuration = dailyMaxDuration,
            revolutionMaxDuration = revolutionMaxDuration
        )
    )

    private fun survey(
        start: Long,
        end: Long,
        revolution: Long = 1,
        roll: Double = 10.0,
        source: SatelliteModeSource = SatelliteModeSource.COMPLAN,
        contour: String = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))"
    ) = SurveyMode(
        revolution = revolution,
        time = baseTime().plusSeconds(start),
        timeStop = baseTime().plusSeconds(end),
        roll = roll,
        duration = Duration.between(baseTime().plusSeconds(start), baseTime().plusSeconds(end)).seconds.toDouble(),
        contourWKT = contour,
        source = source
    )

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

    private fun secondsFromBase(value: LocalDateTime): Long = Duration.between(baseTime(), value).seconds

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