package space.nstart.pcp.slots_service.controller

import org.junit.jupiter.api.Test
import org.mockito.ArgumentMatchers.any
import org.mockito.Mockito.doAnswer
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.verify
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.web.server.LocalServerPort
import org.springframework.test.context.bean.override.mockito.MockitoBean
import org.springframework.web.reactive.function.client.WebClient
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import space.nstart.pcp.pcp_types_lib.dto.ballistics.PositionDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.RevolutionSign
import space.nstart.pcp.pcp_types_lib.dto.ballistics.StationDTO
import space.nstart.pcp.pcp_types_lib.dto.requests.ComplexPlanProcessRequestDTO
import space.nstart.pcp.pcp_types_lib.dto.requests.RequestDTO
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotCoverageStrategy
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotDTO
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteInfoDTO
import space.nstart.pcp.slots_service.service.EarthService
import space.nstart.pcp.slots_service.service.ComplexMissionService
import space.nstart.pcp.slots_service.service.DynamicPlanService
import space.nstart.pcp.slots_service.service.SlotService
import space.nstart.pcp.slots_service.service.StationService
import space.nstart.pcp.slots_service.dto.dynamicplan.DynamicPlanCalculationRequestDTO
import space.nstart.pcp.slots_service.dto.dynamicplan.DynamicPlanCalculationModeDTO
import space.nstart.pcp.slots_service.dto.dynamicplan.DynamicPlanRevolutionModeDTO
import tools.jackson.databind.ObjectMapper
import java.time.LocalDateTime
import java.util.UUID
import kotlin.test.assertEquals
import kotlin.test.assertTrue

@SpringBootTest(
    webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
    properties = [
        "spring.cloud.config.enabled=false",
        "spring.cloud.config.import-check.enabled=false",
        "spring.boot.admin.client.enabled=false"
    ]
)
class MapControllerComplexPlanTest {

    @LocalServerPort
    private var port: Int = 0

    @MockitoBean
    private lateinit var earthService: EarthService

    @MockitoBean
    private lateinit var complexMissionService: ComplexMissionService

    @MockitoBean
    private lateinit var dynamicPlanService: DynamicPlanService

    @MockitoBean
    private lateinit var stationService: StationService

    @MockitoBean
    private lateinit var slotService: SlotService

    private val objectMapper = ObjectMapper()

    private fun stationEllipseRadius(stationId: String, orbitHeightKm: Double): Double {
        val actual = WebClient.create("http://localhost:$port")
            .post()
            .uri("/requests/station/$stationId?view=true&orbitHeightKm=$orbitHeightKm")
            .retrieve()
            .bodyToMono(String::class.java)
            .map(objectMapper::readTree)
            .block()!!

        return actual[1]["ellipse"]["semiMajorAxis"].asDouble()
    }

    @Test
    fun `local post api requests delegates to earth service facade`() {
        val responseId = UUID.fromString("11111111-1111-4111-8111-111111111111")
        var capturedRequest: RequestDTO? = null
        doAnswer { invocation ->
            capturedRequest = invocation.getArgument<RequestDTO>(0)
            RequestDTO(
                requestId = responseId,
                name = "created request",
                importance = 7.5,
                contour = "POLYGON ((0 0, 0 1, 1 1, 0 0))",
            )
        }.`when`(earthService).addRequest(anyRequest())

        val actual = WebClient.create("http://localhost:$port")
            .post()
            .uri("/api/requests")
            .contentType(org.springframework.http.MediaType.APPLICATION_JSON)
            .bodyValue(
                mapOf(
                    "name" to "map request",
                    "importance" to 7.5,
                    "geoType" to "POLYGON",
                    "contour" to "POLYGON ((0 0, 0 1, 1 1, 0 0))",
                )
            )
            .retrieve()
            .bodyToMono(String::class.java)
            .map(objectMapper::readTree)
            .block()!!

        assertEquals(responseId.toString(), actual["requestId"].asText())
        assertEquals("created request", actual["name"].asText())
        assertEquals("map request", capturedRequest?.name)
        assertEquals("POLYGON ((0 0, 0 1, 1 1, 0 0))", capturedRequest?.contour)
        verify(earthService).addRequest(anyRequest())
    }

    @Test
    fun `satellite slots endpoint proxies interval request`() {
        val satelliteId = 101L
        val timeStart = LocalDateTime.of(2026, 4, 8, 10, 0)
        val timeStop = LocalDateTime.of(2026, 4, 8, 12, 0)
        doReturn(
            listOf(
                SlotDTO(
                    cycle = 2,
                    satelliteId = satelliteId,
                    tn = timeStart,
                    tk = timeStart.plusMinutes(6),
                    roll = 10.5,
                    contour = "POLYGON ((0 0, 0 1, 1 1, 0 0))",
                    revolution = 55L,
                    revolutionSign = RevolutionSign.ASC,
                    slotNumber = 9L
                )
            )
        ).`when`(slotService).allSlotsByInterval(satelliteId, timeStart, timeStop)

        val actual = WebClient.create("http://localhost:$port")
            .get()
            .uri("/api/satellites/$satelliteId/slots?timeStart=$timeStart&timeStop=$timeStop")
            .retrieve()
            .bodyToMono(String::class.java)
            .map(objectMapper::readTree)
            .block()!!

        assertEquals(1, actual.size())
        assertEquals(satelliteId, actual[0]["satelliteId"].asLong())
        assertEquals(9L, actual[0]["slotNumber"].asLong())
        verify(slotService).allSlotsByInterval(satelliteId, timeStart, timeStop)
    }

    @Test
    fun `complex plan endpoint proxies process request`() {
        val request = ComplexPlanProcessRequestDTO(
            intervalStart = LocalDateTime.of(2026, 4, 8, 10, 0),
            intervalEnd = LocalDateTime.of(2026, 4, 8, 12, 0),
            satelliteIds = listOf(101L, 202L),
            countLat = 2,
            countLong = 3,
            coverageStrategy = SlotCoverageStrategy.CONTINUOUS
        )
        val response = objectMapper.readTree(
            """
            {
              "runId": 17,
              "status": "COMPLETED",
              "durationMs": 60000,
              "calculatedModesCount": 4,
              "bookedSlotLinksCount": 9
            }
            """.trimIndent()
        )
        doReturn(response).`when`(complexMissionService).processComplexPlan(request)

        val actual = WebClient.create("http://localhost:$port")
            .post()
            .uri("/api/com-plan/process")
            .bodyValue(
                mapOf(
                    "intervalStart" to "2026-04-08T10:00:00",
                    "intervalEnd" to "2026-04-08T12:00:00",
                    "satelliteIds" to listOf(101, 202),
                    "countLat" to 2,
                    "countLong" to 3,
                    "coverageStrategy" to "CONTINUOUS"
                )
            )
            .retrieve()
            .bodyToMono(String::class.java)
            .map(objectMapper::readTree)
            .block()!!

        assertEquals(17, actual["runId"].asInt())
        assertEquals("COMPLETED", actual["status"].asText())
        assertEquals(60000, actual["durationMs"].asLong())
        assertEquals(4, actual["calculatedModesCount"].asInt())
        verify(complexMissionService).processComplexPlan(request)
    }

    @Test
    fun `complex plan page is rendered and menu contains link after rva`() {
        val body = WebClient.create("http://localhost:$port")
            .get()
            .uri("/com-plan")
            .retrieve()
            .bodyToMono(String::class.java)
            .block()!!

        assertTrue(body.contains("Ком.План"))
        assertTrue(body.contains("complex-plan-runs-body"))
        assertTrue(body.contains("complex-plan-pagination"))
        assertTrue(body.contains("complex-plan-process-form"))
        assertTrue(body.contains("complex-plan-satellite-ids"))
        assertTrue(body.contains("complex-plan-interval-start"))
        assertTrue(body.contains("complex-plan-coverage-strategy"))
        assertTrue(body.contains("complex-plan-count-lat"))
        assertTrue(body.contains("complex-plan-count-long"))
        assertTrue(body.contains("complex-plan-report"))
        assertTrue(body.contains("/complex_plan_scripts.js"))
        assertTrue(body.indexOf("/rva") < body.indexOf("/com-plan"))
    }

    @Test
    fun `complex plan script renders added survey count by satellite`() {
        val body = WebClient.create("http://localhost:$port")
            .get()
            .uri("/complex_plan_scripts.js")
            .retrieve()
            .bodyToMono(String::class.java)
            .block()!!

        assertTrue(body.contains("Добавлено съемок по спутникам"))
        assertTrue(body.contains("addedSurveyRowsBySatellite"))
        assertTrue(body.contains("source === 'COMPLAN' || source === 'MIXED'"))
    }

    @Test
    fun `dynamic plan page is rendered and menu contains separate link`() {
        val body = WebClient.create("http://localhost:$port")
            .get()
            .uri("/dynamic-plan")
            .retrieve()
            .bodyToMono(String::class.java)
            .block()!!

        assertTrue(body.contains("Dynamic Plan"))
        assertTrue(body.contains("dynamic-plan-form"))
        assertTrue(body.contains("dynamic-plan-request-id"))
        assertTrue(body.contains("dynamic-plan-satellite-ids"))
        assertTrue(body.contains("dynamic-plan-interval-start"))
        assertTrue(body.contains("dynamic-plan-interval-end"))
        assertTrue(body.contains("dynamic-plan-runs-body"))
        assertTrue(body.contains("dynamic-plan-runs-refresh"))
        assertTrue(body.contains("/dynamic_plan_map_layers.js"))
        assertTrue(body.contains("/dynamic_plan_scripts.js"))
        assertTrue(body.contains("/com-plan"))
        assertTrue(body.contains("/dynamic-plan"))
    }

    @Test
    fun `map page loads dynamic plan map layer script`() {
        doReturn(Flux.empty<RequestDTO>()).`when`(earthService).reqs()
        doReturn(Flux.empty<SatelliteInfoDTO>()).`when`(complexMissionService).satellites()
        doReturn(Flux.empty<StationDTO>()).`when`(stationService).stations()

        val body = WebClient.create("http://localhost:$port")
            .get()
            .uri("/map")
            .retrieve()
            .bodyToMono(String::class.java)
            .block()!!

        assertTrue(body.contains("cesiumContainer"))
        assertTrue(body.contains("stationOrbitHeightKm"))
        assertTrue(body.contains("Высота орбиты, км"))
        assertTrue(body.contains("dynamic_plan_map_layers.js"))
        assertTrue(body.contains("PcpDynamicPlanMapLayers"))
        verify(earthService).reqs()
    }

    @Test
    fun `station czml endpoint uses orbit height for visibility zone`() {
        val stationId = "11111111-1111-4111-8111-111111111111"
        doReturn(
            Mono.just(
                StationDTO(
                    id = UUID.fromString(stationId),
                    number = 1,
                    name = "Station 1",
                    position = PositionDTO(lat = 55.0, long = 37.0, height = 200.0),
                    elevationMin = 5.0
                )
            )
        ).`when`(stationService).station(stationId)

        val baseRadius = stationEllipseRadius(stationId, 500.0)
        val higherOrbitRadius = stationEllipseRadius(stationId, 700.0)

        assertTrue(higherOrbitRadius > baseRadius)
    }

    @Test
    fun `dynamic plan map layer keeps only one visible run`() {
        val body = WebClient.create("http://localhost:$port")
            .get()
            .uri("/dynamic_plan_map_layers.js")
            .retrieve()
            .bodyToMono(String::class.java)
            .block()!!

        assertTrue(body.contains("parsed.slice(0, 1)"))
        assertTrue(body.contains("writeRuns([normalizedRun])"))
    }

    @Test
    fun `dynamic plan endpoint proxies calculation request`() {
        val requestId = UUID.fromString("11111111-1111-4111-8111-111111111111")
        val runId = UUID.fromString("22222222-2222-4222-8222-222222222222")
        val request = DynamicPlanCalculationRequestDTO(
            requestId = requestId,
            satelliteIds = listOf(101L, 202L),
            calculationStart = LocalDateTime.of(2026, 4, 8, 10, 0),
            calculationEnd = LocalDateTime.of(2026, 4, 8, 12, 0),
            calculationMode = DynamicPlanCalculationModeDTO.GREEDY,
            revolutionMode = DynamicPlanRevolutionModeDTO.DESC,
            filterCoveredRoutes = true
        )
        val response = objectMapper.readTree(
            """
            {
              "runId": "$runId",
              "status": "PENDING",
              "requestId": "$requestId",
              "satelliteIds": [101, 202],
              "calculationStart": "2026-04-08T10:00:00",
              "calculationEnd": "2026-04-08T12:00:00",
              "calculationMode": "GREEDY",
              "revolutionMode": "DESC",
              "filterCoveredRoutes": true,
              "createdAt": "2026-04-08T09:59:59"
            }
            """.trimIndent()
        )
        doReturn(response).`when`(dynamicPlanService).calculate(request)

        val actual = WebClient.create("http://localhost:$port")
            .post()
            .uri("/api/dynamic-plan/calc")
            .bodyValue(
                mapOf(
                    "requestId" to requestId.toString(),
                    "satelliteIds" to listOf(101, 202),
                    "calculationStart" to "2026-04-08T10:00:00",
                    "calculationEnd" to "2026-04-08T12:00:00",
                    "calculationMode" to "GREEDY",
                    "revolutionMode" to "DESC",
                    "filterCoveredRoutes" to true
                )
            )
            .retrieve()
            .bodyToMono(String::class.java)
            .map(objectMapper::readTree)
            .block()!!

        assertEquals(runId.toString(), actual["runId"].asText())
        assertEquals("PENDING", actual["status"].asText())
        assertEquals("GREEDY", actual["calculationMode"].asText())
        assertEquals("DESC", actual["revolutionMode"].asText())
        assertEquals(true, actual["filterCoveredRoutes"].asBoolean())
        verify(dynamicPlanService).calculate(request)
    }

    @Test
    fun `dynamic plan runs endpoint proxies history request`() {
        val runId = UUID.fromString("22222222-2222-4222-8222-222222222222")
        val response = objectMapper.readTree(
            """
            [
              {
                "runId": "$runId",
                "status": "COMPLETED",
                "routesCount": 4
              }
            ]
            """.trimIndent()
        )
        doReturn(response).`when`(dynamicPlanService).getRuns(null, 20, 0)

        val actual = WebClient.create("http://localhost:$port")
            .get()
            .uri("/api/dynamic-plan/runs?limit=20&offset=0")
            .retrieve()
            .bodyToMono(String::class.java)
            .map(objectMapper::readTree)
            .block()!!

        assertEquals(1, actual.size())
        assertEquals(runId.toString(), actual[0]["runId"].asText())
        assertEquals("COMPLETED", actual[0]["status"].asText())
        verify(dynamicPlanService).getRuns(null, 20, 0)
    }

    @Test
    fun `dynamic plan run endpoint proxies status request`() {
        val runId = UUID.fromString("22222222-2222-4222-8222-222222222222")
        val response = objectMapper.readTree(
            """
            {
              "runId": "$runId",
              "status": "RUNNING",
              "routesCount": null
            }
            """.trimIndent()
        )
        doReturn(response).`when`(dynamicPlanService).getRun(runId)

        val actual = WebClient.create("http://localhost:$port")
            .get()
            .uri("/api/dynamic-plan/runs/$runId")
            .retrieve()
            .bodyToMono(String::class.java)
            .map(objectMapper::readTree)
            .block()!!

        assertEquals(runId.toString(), actual["runId"].asText())
        assertEquals("RUNNING", actual["status"].asText())
        verify(dynamicPlanService).getRun(runId)
    }

    @Test
    fun `dynamic plan result endpoint proxies result request`() {
        val runId = UUID.fromString("22222222-2222-4222-8222-222222222222")
        val requestId = UUID.fromString("11111111-1111-4111-8111-111111111111")
        val response = objectMapper.readTree(
            """
            {
              "status": "COMPLETED",
              "requestId": "$requestId",
              "satelliteIds": [101, 202],
              "missingSatelliteIds": [],
              "routesCount": 4,
              "durationMs": 5000
            }
            """.trimIndent()
        )
        doReturn(response).`when`(dynamicPlanService).getResult(runId)

        val actual = WebClient.create("http://localhost:$port")
            .get()
            .uri("/api/dynamic-plan/runs/$runId/result")
            .retrieve()
            .bodyToMono(String::class.java)
            .map(objectMapper::readTree)
            .block()!!

        assertEquals("COMPLETED", actual["status"].asText())
        assertEquals(4, actual["routesCount"].asInt())
        verify(dynamicPlanService).getResult(runId)
    }

    @Test
    fun `dynamic plan routes endpoint proxies routes request`() {
        val runId = UUID.fromString("22222222-2222-4222-8222-222222222222")
        val response = objectMapper.readTree(
            """
            [
              {
                "id": 1,
                "runId": "$runId",
                "requestId": "11111111-1111-4111-8111-111111111111",
                "satelliteId": 101,
                "startTime": "2026-04-08T10:10:00",
                "endTime": "2026-04-08T10:10:30",
                "duration": 30.0,
                "revolution": 55,
                "roll": 11.5,
                "contourWkt": "POLYGON ((0 0, 0 1, 1 1, 0 0))"
              }
            ]
            """.trimIndent()
        )
        doReturn(response).`when`(dynamicPlanService).getRoutes(runId, 1000, 0)

        val actual = WebClient.create("http://localhost:$port")
            .get()
            .uri("/api/dynamic-plan/runs/$runId/routes")
            .retrieve()
            .bodyToMono(String::class.java)
            .map(objectMapper::readTree)
            .block()!!

        assertEquals(1, actual.size())
        assertEquals(101, actual[0]["satelliteId"].asInt())
        verify(dynamicPlanService).getRoutes(runId, 1000, 0)
    }

    @Test
    fun `dynamic plan intervals endpoint proxies intervals request`() {
        val runId = UUID.fromString("22222222-2222-4222-8222-222222222222")
        val response = objectMapper.readTree(
            """
            [
              {
                "id": 1,
                "runId": "$runId",
                "requestId": "11111111-1111-4111-8111-111111111111",
                "intervalId": "33333333-3333-4333-8333-333333333333",
                "regionId": "44444444-4444-4444-8444-444444444444",
                "revSign": "ASC",
                "contourWkt": "POLYGON ((0 0, 0 1, 1 1, 0 0))",
                "observationParametersCount": 12
              }
            ]
            """.trimIndent()
        )
        doReturn(response).`when`(dynamicPlanService).getIntervals(runId, 1000, 0)

        val actual = WebClient.create("http://localhost:$port")
            .get()
            .uri("/api/dynamic-plan/runs/$runId/intervals")
            .retrieve()
            .bodyToMono(String::class.java)
            .map(objectMapper::readTree)
            .block()!!

        assertEquals(1, actual.size())
        assertEquals("ASC", actual[0]["revSign"].asText())
        assertEquals(12, actual[0]["observationParametersCount"].asInt())
        verify(dynamicPlanService).getIntervals(runId, 1000, 0)
    }

    @Test
    fun `complex plan runs endpoint proxies list request`() {
        val response = objectMapper.readTree(
            """
            [
              {
                "runId": 17,
                "status": "COMPLETED",
                "intervalStart": "2026-04-08T10:00:00",
                "intervalEnd": "2026-04-08T12:00:00",
                "satelliteIds": [101, 202]
              }
            ]
            """.trimIndent()
        )
        doReturn(response).`when`(complexMissionService).complexPlanRuns()

        val actual = WebClient.create("http://localhost:$port")
            .get()
            .uri("/api/com-plan/runs")
            .retrieve()
            .bodyToMono(String::class.java)
            .map(objectMapper::readTree)
            .block()!!

        assertEquals(1, actual.size())
        assertEquals(17, actual[0]["runId"].asInt())
        assertEquals("COMPLETED", actual[0]["status"].asText())
        verify(complexMissionService).complexPlanRuns()
    }

    @Test
    fun `complex plan run endpoint proxies detail request`() {
        val response = objectMapper.readTree(
            """
            {
              "runId": 17,
              "status": "COMPLETED",
              "durationMs": 60000,
              "requestPayload": "{}"
            }
            """.trimIndent()
        )
        doReturn(response).`when`(complexMissionService).complexPlanRun(17L)

        val actual = WebClient.create("http://localhost:$port")
            .get()
            .uri("/api/com-plan/runs/17")
            .retrieve()
            .bodyToMono(String::class.java)
            .map(objectMapper::readTree)
            .block()!!

        assertEquals(17, actual["runId"].asInt())
        assertEquals(60000, actual["durationMs"].asLong())
        verify(complexMissionService).complexPlanRun(17L)
    }

    @Test
    fun `complex plan run modes endpoint proxies modes request`() {
        val response = objectMapper.readTree(
            """
            [
              {
                "id": 1,
                "satelliteId": 101,
                "startTime": "2026-04-08T10:00:00",
                "endTime": "2026-04-08T10:05:00"
              }
            ]
            """.trimIndent()
        )
        doReturn(response).`when`(complexMissionService).complexPlanRunModes(17L)

        val actual = WebClient.create("http://localhost:$port")
            .get()
            .uri("/api/com-plan/runs/17/modes")
            .retrieve()
            .bodyToMono(String::class.java)
            .map(objectMapper::readTree)
            .block()!!

        assertEquals(1, actual.size())
        assertEquals(101, actual[0]["satelliteId"].asInt())
        verify(complexMissionService).complexPlanRunModes(17L)
    }

    @Test
    fun `complex plan run delete endpoint proxies delete request`() {
        WebClient.create("http://localhost:$port")
            .delete()
            .uri("/api/com-plan/runs/17")
            .retrieve()
            .toBodilessEntity()
            .block()

        verify(complexMissionService).deleteComplexPlanRun(17L)
    }

    private fun anyRequest(): RequestDTO {
        any(RequestDTO::class.java)
        return RequestDTO()
    }
}
