package space.nstart.pcp_tgu_service.service

import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import io.micrometer.core.instrument.simple.SimpleMeterRegistry
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.Test
import org.mockito.Mockito.mock
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import space.nstart.pcp_tgu_service.config.ClassifierProperties
import space.nstart.pcp_tgu_service.dto.NsiPlatformDataDto
import space.nstart.pcp_tgu_service.dto.NsiPlatformDto
import space.nstart.pcp_tgu_service.integration.api.OrdinisUnavailableException
import space.nstart.pcp_tgu_service.integration.api.PlatformsClassifierClient
import java.time.Clock
import java.time.Duration
import java.time.Instant
import java.time.ZoneId
import java.time.ZoneOffset

class PlatformServiceTest {

    @Test
    fun `loadPlatforms maps NSI DTO and filters by status`() {
        val client = mock(PlatformsClassifierClient::class.java)
        `when`(client.fetchPlatforms()).thenReturn(
            listOf(
                platformDto(id = "operational-id", status = "OPERATIONAL", noradId = 25544L),
                platformDto(id = "standby-id", status = "STANDBY", noradId = 41386L),
                platformDto(id = "lost-id", status = "LOST", noradId = 39186L),
                platformDto(id = null, status = "OPERATIONAL", noradId = 2L)
            )
        )
        val service = service(client)

        val allPlatforms = service.loadPlatforms()
        val platforms = service.loadPlatforms(status = "STANDBY")

        assertEquals(listOf("operational-id", "standby-id", "lost-id"), allPlatforms.map { it.nsiId })
        assertEquals(1, platforms.size)
        assertEquals("standby-id", platforms.single().nsiId)
        assertEquals("business-standby-id", platforms.single().businessKey)
        assertEquals("Satellite standby-id", platforms.single().name)
        assertEquals("STANDBY", platforms.single().status)
        assertEquals(41386L, platforms.single().noradId)
        assertEquals("MISSION", platforms.single().mission)
        assertEquals(Instant.parse("2026-01-01T00:00:00Z"), platforms.single().validFrom)
        assertEquals(Instant.parse("2026-12-31T00:00:00Z"), platforms.single().validTo)
        verify(client, times(1)).fetchPlatforms()
    }

    @Test
    fun `first loadPlatforms call fetches platforms from client`() {
        val client = mock(PlatformsClassifierClient::class.java)
        `when`(client.fetchPlatforms()).thenReturn(
            listOf(platformDto(id = "operational-id", status = "OPERATIONAL", noradId = 1L))
        )
        val service = service(client)

        val platforms = service.loadPlatforms()

        assertEquals(listOf("operational-id"), platforms.map { it.nsiId })
        verify(client, times(1)).fetchPlatforms()
    }

    @Test
    fun `second loadPlatforms call within ttl uses cached platforms`() {
        val client = mock(PlatformsClassifierClient::class.java)
        `when`(client.fetchPlatforms()).thenReturn(
            listOf(platformDto(id = "cached-id", status = "OPERATIONAL", noradId = 1L))
        )
        val service = service(client)

        val firstCall = service.loadPlatforms()
        val secondCall = service.loadPlatforms()

        assertEquals(listOf("cached-id"), firstCall.map { it.nsiId })
        assertEquals(listOf("cached-id"), secondCall.map { it.nsiId })
        verify(client, times(1)).fetchPlatforms()
    }

    @Test
    fun `loadPlatforms refreshes cache after fifteen minutes`() {
        val clock = MutableClock(Instant.parse("2026-05-26T12:00:00Z"))
        val client = mock(PlatformsClassifierClient::class.java)
        `when`(client.fetchPlatforms())
            .thenReturn(listOf(platformDto(id = "first-id", status = "OPERATIONAL", noradId = 1L)))
            .thenReturn(listOf(platformDto(id = "refreshed-id", status = "OPERATIONAL", noradId = 2L)))
        val service = service(client, clock)

        val firstCall = service.loadPlatforms()
        clock.advance(Duration.ofMinutes(15))
        val refreshedCall = service.loadPlatforms()

        assertEquals(listOf("first-id"), firstCall.map { it.nsiId })
        assertEquals(listOf("refreshed-id"), refreshedCall.map { it.nsiId })
        verify(client, times(2)).fetchPlatforms()
    }

    @Test
    fun `loadPlatforms keeps stale cache when Ordinis is unavailable`() {
        val clock = MutableClock(Instant.parse("2026-05-26T12:00:00Z"))
        val meterRegistry = SimpleMeterRegistry()
        val client = mock(PlatformsClassifierClient::class.java)
        `when`(client.fetchPlatforms())
            .thenReturn(listOf(platformDto(id = "cached-id", status = "OPERATIONAL", noradId = 1L)))
            .thenThrow(ordinisUnavailable())
        val service = service(
            client = client,
            clock = clock,
            ordinisAvailabilityMetrics = OrdinisAvailabilityMetrics(meterRegistry)
        )

        val firstCall = service.loadPlatforms()
        clock.advance(Duration.ofMinutes(15))
        val staleCall = service.loadPlatforms()

        assertEquals(listOf("cached-id"), firstCall.map { it.nsiId })
        assertEquals(listOf("cached-id"), staleCall.map { it.nsiId })
        assertEquals(1.0, meterRegistry.get(OrdinisAvailabilityMetrics.UNAVAILABLE).gauge().value())
        assertEquals(1.0, meterRegistry.get(OrdinisAvailabilityMetrics.UNAVAILABLE_TOTAL).counter().count())
        verify(client, times(2)).fetchPlatforms()
    }

    @Test
    fun `loadPlatforms rethrows Ordinis outage when cache is empty`() {
        val meterRegistry = SimpleMeterRegistry()
        val client = mock(PlatformsClassifierClient::class.java)
        `when`(client.fetchPlatforms()).thenThrow(ordinisUnavailable())
        val service = service(
            client = client,
            ordinisAvailabilityMetrics = OrdinisAvailabilityMetrics(meterRegistry)
        )

        assertThrows(OrdinisUnavailableException::class.java) {
            service.loadPlatforms()
        }

        assertEquals(1.0, meterRegistry.get(OrdinisAvailabilityMetrics.UNAVAILABLE).gauge().value())
        assertEquals(1.0, meterRegistry.get(OrdinisAvailabilityMetrics.UNAVAILABLE_TOTAL).counter().count())
        verify(client, times(1)).fetchPlatforms()
    }

    @Test
    fun `loadAllowedPlatforms uses cached platforms and configured filtering`() {
        val now = Instant.parse("2026-05-26T12:00:00Z")
        val client = mock(PlatformsClassifierClient::class.java)
        `when`(client.fetchPlatforms()).thenReturn(
            listOf(
                platformDto(id = "operational-id", status = "OPERATIONAL", noradId = 1L, mission = null),
                platformDto(id = "standby-id", status = "STANDBY", noradId = 2L),
                platformDto(id = "lost-id", status = "LOST", noradId = 3L),
                platformDto(id = "without-norad-id", status = "OPERATIONAL", noradId = null),
                platformDto(id = "future-id", status = "OPERATIONAL", noradId = 4L, validFrom = "2026-06-01T00:00:00Z"),
                platformDto(id = "expired-id", status = "OPERATIONAL", noradId = 5L, validTo = "2026-05-26T12:00:00Z"),
                platformDto(id = "without-valid-from", status = "OPERATIONAL", noradId = 6L, validFrom = null),
                platformDto(id = "without-valid-to", status = "OPERATIONAL", noradId = 7L, validTo = null)
            )
        )
        val service = service(client)

        val cachedPlatforms = service.loadPlatforms()
        val allowedPlatforms = service.loadAllowedPlatforms(now)

        assertEquals(8, cachedPlatforms.size)
        assertEquals(listOf("operational-id"), allowedPlatforms.map { it.nsiId })
        verify(client, times(1)).fetchPlatforms()
    }

    @Test
    fun `loadPlatformsFromNsiPayload replaces cached platforms from file payload`() {
        val client = mock(PlatformsClassifierClient::class.java)
        `when`(client.fetchPlatforms()).thenReturn(
            listOf(platformDto(id = "from-nsi-id", status = "OPERATIONAL", noradId = 1L))
        )
        val service = service(client)

        val fromNsi = service.loadPlatforms()
        val fromFile = service.loadPlatformsFromNsiPayload(
            """
            [
              {
                "id": "from-file-id",
                "businessKey": "business-from-file-id",
                "data": {
                  "name": "Satellite from-file-id",
                  "status": "OPERATIONAL",
                  "norad_id": 25544,
                  "mission": "MISSION"
                },
                "validFrom": "2026-01-01T00:00:00Z",
                "validTo": "2026-12-31T00:00:00Z"
              }
            ]
            """.trimIndent()
        )
        val cachedAfterFileLoad = service.loadPlatforms()

        assertEquals(listOf("from-nsi-id"), fromNsi.map { it.nsiId })
        assertEquals(listOf("from-file-id"), fromFile.map { it.nsiId })
        assertEquals(listOf("from-file-id"), cachedAfterFileLoad.map { it.nsiId })
        verify(client, times(1)).fetchPlatforms()
    }

    private fun service(
        client: PlatformsClassifierClient,
        clock: Clock = Clock.fixed(Instant.parse("2026-05-26T12:00:00Z"), ZoneOffset.UTC),
        ordinisAvailabilityMetrics: OrdinisAvailabilityMetrics = OrdinisAvailabilityMetrics(SimpleMeterRegistry())
    ): PlatformService =
        PlatformService(
            platformsClassifierClient = client,
            classifierProperties = ClassifierProperties(
                platformsUrl = "http://localhost",
                platformsCacheTtl = Duration.ofMinutes(15),
                allowedPlatformStatuses = setOf("OPERATIONAL")
            ),
            objectMapper = jacksonObjectMapper().findAndRegisterModules(),
            ordinisAvailabilityMetrics = ordinisAvailabilityMetrics,
            clock = clock
        )

    private fun ordinisUnavailable(): OrdinisUnavailableException =
        OrdinisUnavailableException(
            classifierUrl = "http://localhost",
            statusCode = null,
            cause = RuntimeException("Connection refused")
        )

    private fun platformDto(
        id: String?,
        status: String,
        noradId: Long?,
        validFrom: String? = "2026-01-01T00:00:00Z",
        validTo: String? = "2026-12-31T00:00:00Z",
        mission: String? = "MISSION"
    ): NsiPlatformDto =
        NsiPlatformDto(
            id = id,
            businessKey = id?.let { "business-$it" },
            data = NsiPlatformDataDto(
                name = id?.let { "Satellite $it" },
                status = status,
                noradId = noradId,
                mission = mission
            ),
            validFrom = validFrom?.let(Instant::parse),
            validTo = validTo?.let(Instant::parse)
        )

    private class MutableClock(
        private var currentInstant: Instant
    ) : Clock() {
        override fun getZone(): ZoneId = ZoneOffset.UTC

        override fun withZone(zone: ZoneId): Clock = this

        override fun instant(): Instant = currentInstant

        fun advance(duration: Duration) {
            currentInstant = currentInstant.plus(duration)
        }
    }
}
