package space.nstart.pcp.slots_service.service

import com.sun.net.httpserver.HttpServer
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.ObjectProvider
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.web.reactive.function.client.ClientResponse
import org.springframework.web.reactive.function.client.WebClient
import org.springframework.web.reactive.function.client.WebClientResponseException
import reactor.core.publisher.Mono
import space.nstart.pcp.pcp_types_lib.dto.requests.RequestDTO
import tools.jackson.databind.ObjectMapper
import java.net.InetSocketAddress
import java.net.URI
import java.nio.charset.StandardCharsets
import java.util.UUID
import java.util.concurrent.atomic.AtomicReference
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertTrue

class EarthServiceTest {
    private val objectMapper = ObjectMapper()

    @Test
    fun `cells calls v1 cells with min importance`() {
        val requestedUris = mutableListOf<URI>()
        val service = earthService(
            requestedUris = requestedUris,
            responses = mutableListOf(cellsResponse())
        )

        service.cells(3.5).toList()

        assertEquals("/v1/cells", requestedUris.single().path)
        assertEquals("minImportance=3.5&page=0&size=500", requestedUris.single().query)
        assertFalse(requestedUris.single().path.startsWith("/api/earth-grid"))
    }

    @Test
    fun `cells maps response wrapper items to earth cell dto`() {
        val service = earthService(
            responses = mutableListOf(
                cellsResponse(
                    """
                    {
                      "items": [
                        {
                          "cellNum": 42,
                          "latitude": 55.0,
                          "longitude": 37.0,
                          "importance": 10.0,
                          "contour": "POLYGON ((36 55, 37 55, 37 56, 36 56, 36 55))"
                        }
                      ],
                      "page": 0,
                      "size": 500,
                      "totalItems": 1,
                      "totalPages": 1
                    }
                    """.trimIndent()
                )
            )
        )

        val cell = service.cells(0.0).single()

        assertEquals(42L, cell.id)
        assertEquals(42L, cell.num)
        assertEquals(55.0, cell.latitude)
        assertEquals(37.0, cell.longitude)
        assertEquals(10.0, cell.importance)
        assertEquals("POLYGON ((36 55, 37 55, 37 56, 36 56, 36 55))", cell.contour)
    }

    @Test
    fun `cells does not pass count lat and count long to v1 cells`() {
        val requestedUris = mutableListOf<URI>()
        val service = earthService(
            requestedUris = requestedUris,
            responses = mutableListOf(cellsResponse())
        )

        service.cells(1.0, countLat = 2, countLong = 3).toList()

        val query = requestedUris.single().query
        assertFalse(query.contains("countLat"))
        assertFalse(query.contains("countLong"))
    }

    @Test
    fun `cells fetches all pages from v1 cells`() {
        val requestedUris = mutableListOf<URI>()
        val service = earthService(
            requestedUris = requestedUris,
            responses = mutableListOf(
                cellsResponse(
                    """
                    {
                      "items": [
                        {"cellNum": 1, "latitude": 1.0, "longitude": 2.0, "importance": 3.0, "contour": "POLYGON EMPTY"}
                      ],
                      "page": 0,
                      "size": 500,
                      "totalItems": 2,
                      "totalPages": 2
                    }
                    """.trimIndent()
                ),
                cellsResponse(
                    """
                    {
                      "items": [
                        {"cellNum": 2, "latitude": 4.0, "longitude": 5.0, "importance": 6.0, "contour": "POLYGON EMPTY"}
                      ],
                      "page": 1,
                      "size": 500,
                      "totalItems": 2,
                      "totalPages": 2
                    }
                    """.trimIndent()
                ),
            )
        )

        val cells = service.cells(0.0).toList()

        assertEquals(listOf(1L, 2L), cells.map { cell -> cell.num })
        assertEquals(listOf("page=0", "page=1"), requestedUris.map { uri -> uri.query.substringAfter("minImportance=0.0&").substringBefore("&size") })
    }

    @Test
    fun `reqs calls v1 requests with page and size instead of legacy api requests`() {
        val requestedUris = mutableListOf<URI>()
        val service = earthService(
            requestedUris = requestedUris,
            responses = mutableListOf(requestsResponse())
        )

        service.reqs().collectList().block()

        assertEquals("/v1/requests", requestedUris.single().path)
        assertEquals("page=0&size=500", requestedUris.single().query)
        assertFalse(requestedUris.any { uri -> uri.path == "/api/requests" })
    }

    @Test
    fun `reqs unwraps request list items and maps full request geometry to request dto`() {
        val requestId = UUID.fromString("11111111-1111-4111-8111-111111111111")
        val service = earthService(
            responses = mutableListOf(
                requestsResponse(
                    """
                    {
                      "items": [
                        {
                          "id": "$requestId",
                          "name": "summary request name",
                          "status": "ACCEPTED",
                          "surveyType": "OPTICS",
                          "importance": 7.0,
                          "beginDateTime": "2026-01-01T00:00:00Z",
                          "endDateTime": "2026-01-02T00:00:00Z",
                          "kpp": [1, 2],
                          "highPriorityTransmit": false,
                          "coveragePercent": 0.0
                        }
                      ],
                      "page": 0,
                      "size": 500,
                      "totalItems": 1,
                      "totalPages": 1
                    }
                    """.trimIndent()
                ),
                requestDetailsResponse(
                    """
                    {
                      "id": "$requestId",
                      "name": "real request name",
                      "geometry": "POLYGON ((30 10, 40 40, 20 40, 30 10))",
                      "importance": 7.0,
                      "status": "ACCEPTED",
                      "surveyType": "OPTICS"
                    }
                    """.trimIndent()
                ),
            )
        )

        val request = service.reqs().single().block()!!

        assertEquals(requestId, request.requestId)
        assertEquals("real request name", request.name)
        assertFalse(request.name == requestId.toString())
        assertEquals(7.0, request.importance)
        assertEquals("POLYGON ((30 10, 40 40, 20 40, 30 10))", request.contour)
    }

    @Test
    fun `addRequest posts new v1 create request and returns legacy request dto`() {
        val responseId = UUID.fromString("33333333-3333-4333-8333-333333333333")
        val responseName = "created request"
        val responseImportance = 8.25
        val requestBody = AtomicReference<String>()
        val requestMethod = AtomicReference<String>()
        val requestPath = AtomicReference<String>()
        val server = HttpServer.create(InetSocketAddress(0), 0)
        server.createContext("/") { exchange ->
            requestMethod.set(exchange.requestMethod)
            requestPath.set(exchange.requestURI.path)
            requestBody.set(exchange.requestBody.readBytes().toString(StandardCharsets.UTF_8))

            val response = """
                {
                  "id": "$responseId",
                  "name": "$responseName",
                  "status": "ACCEPTED",
                  "surveyType": "OPTICS",
                  "importance": $responseImportance,
                  "message": "Заявка успешно создана"
                }
            """.trimIndent().toByteArray(StandardCharsets.UTF_8)

            exchange.responseHeaders.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
            exchange.sendResponseHeaders(HttpStatus.CREATED.value(), response.size.toLong())
            exchange.responseBody.use { body -> body.write(response) }
        }
        server.start()

        try {
            val sourceRequest = RequestDTO(
                name = "map request",
                importance = 7.5,
                contour = "POLYGON ((30 10, 40 40, 20 40, 30 10))",
            )
            val service = EarthService(provider(WebClient.builder()), url = "http://localhost:${server.address.port}")

            val result = service.addRequest(sourceRequest)!!

            assertEquals(responseId, result.requestId)
            assertEquals(responseImportance, result.importance)
            assertEquals(sourceRequest.contour, result.contour)
            assertEquals(responseName, result.name)
            assertEquals("POST", requestMethod.get())
            assertEquals("/v1/requests", requestPath.get())
            assertFalse(requestPath.get() == "/api/requests")

            val bodyText = assertNotNull(requestBody.get())
            val body = objectMapper.readTree(bodyText)
            assertNotNull(UUID.fromString(body["id"].asText()))
            assertEquals(sourceRequest.name, body["name"].asText())
            assertEquals(sourceRequest.contour, body["geometry"].asText())
            assertEquals(sourceRequest.importance, body["importance"].asDouble())
            assertEquals("2026-01-01T00:00:00Z", body["beginDateTime"].asText())
            assertEquals("2026-12-31T23:59:59Z", body["endDateTime"].asText())
            assertEquals(0, body["kpp"].size())
            assertFalse(body["highPriorityTransmit"].asBoolean())
            assertEquals("PANCHROMATIC", body["optics"]["resultType"].asText())
            assertEquals(5.0, body["optics"]["resolution"].asDouble())
            assertEquals(10.0, body["optics"]["sunAngleMin"].asDouble())
            assertEquals(90.0, body["optics"]["sunAngleMax"].asDouble())
            assertEquals(100.0, body["optics"]["clouds"].asDouble())
            assertTrue(!bodyText.contains("app_type"))
            assertTrue(!bodyText.contains("appType"))
            assertTrue(!bodyText.contains("AppType"))
            assertTrue(!bodyText.contains("requestType"))
        } finally {
            server.stop(0)
        }
    }

    @Test
    fun `reqs fetches all pages and loads full request details for every summary`() {
        val firstId = UUID.fromString("11111111-1111-4111-8111-111111111111")
        val secondId = UUID.fromString("22222222-2222-4222-8222-222222222222")
        val requestedUris = mutableListOf<URI>()
        val service = earthService(
            requestedUris = requestedUris,
            responses = mutableListOf(
                requestsResponse(
                    """
                    {
                      "items": [
                        {
                          "id": "$firstId",
                          "name": "first request",
                          "status": "ACCEPTED",
                          "surveyType": "OPTICS",
                          "importance": 1.0,
                          "beginDateTime": "2026-01-01T00:00:00Z",
                          "endDateTime": "2026-01-02T00:00:00Z",
                          "kpp": [],
                          "highPriorityTransmit": false
                        }
                      ],
                      "page": 0,
                      "size": 500,
                      "totalItems": 2,
                      "totalPages": 2
                    }
                    """.trimIndent()
                ),
                requestsResponse(
                    """
                    {
                      "items": [
                        {
                          "id": "$secondId",
                          "name": "second request",
                          "status": "ACCEPTED",
                          "surveyType": "RSA",
                          "importance": 2.0,
                          "beginDateTime": "2026-01-03T00:00:00Z",
                          "endDateTime": "2026-01-04T00:00:00Z",
                          "kpp": [3],
                          "highPriorityTransmit": true
                        }
                      ],
                      "page": 1,
                      "size": 500,
                      "totalItems": 2,
                      "totalPages": 2
                    }
                    """.trimIndent()
                ),
                requestDetailsResponse(
                    """
                    {
                      "id": "$firstId",
                      "name": "first request",
                      "geometry": "POLYGON EMPTY",
                      "importance": 1.0
                    }
                    """.trimIndent()
                ),
                requestDetailsResponse(
                    """
                    {
                      "id": "$secondId",
                      "name": "second request",
                      "geometry": "POLYGON ((0 0, 1 0, 1 1, 0 0))",
                      "importance": 2.0
                    }
                    """.trimIndent()
                ),
            )
        )

        val requests = service.reqs().collectList().block()!!

        assertEquals(listOf(firstId, secondId), requests.map { request -> request.requestId })
        assertEquals(
            listOf(
                "/v1/requests?page=0&size=500",
                "/v1/requests?page=1&size=500",
                "/v1/requests/$firstId",
                "/v1/requests/$secondId",
            ),
            requestedUris.map { uri -> "${uri.path}${uri.query?.let { "?$it" } ?: ""}" }
        )
        assertFalse(requestedUris.any { uri -> uri.path == "/api/requests" })
    }

    @Test
    fun `reqcells calls v1 request with cells instead of legacy by number endpoint`() {
        val requestId = UUID.fromString("11111111-1111-4111-8111-111111111111")
        val requestedUris = mutableListOf<URI>()
        val service = earthService(
            requestedUris = requestedUris,
            responses = mutableListOf(requestWithCellsResponse(requestId))
        )

        service.reqcells(requestId.toString())

        assertEquals("/v1/requests/$requestId/cells", requestedUris.single().path)
        assertFalse(requestedUris.any { uri -> uri.path.startsWith("/api/") })
    }

    @Test
    fun `reqcells maps new wrapper to current request with cells dto`() {
        val requestId = UUID.fromString("11111111-1111-4111-8111-111111111111")
        val service = earthService(
            responses = mutableListOf(
                requestWithCellsResponse(
                    requestId = requestId,
                    requestName = "request name from service",
                    requestGeometry = "POLYGON ((30 10, 40 40, 20 40, 30 10))",
                    requestImportance = 10.0,
                    cellNum = 123L,
                    cellImportance = 8.5,
                    cellContour = "POLYGON ((36 55, 37 55, 37 56, 36 56, 36 55))"
                )
            )
        )

        val result = service.reqcells(requestId.toString())!!

        assertEquals(requestId, result.request.requestId)
        assertEquals("request name from service", result.request.name)
        assertFalse(result.request.name == requestId.toString())
        assertEquals(10.0, result.request.importance)
        assertEquals("POLYGON ((30 10, 40 40, 20 40, 30 10))", result.request.contour)
        assertEquals(123L, result.cells.single().id)
        assertEquals(123L, result.cells.single().num)
        assertEquals(8.5, result.cells.single().importance)
        assertEquals("POLYGON ((36 55, 37 55, 37 56, 36 56, 36 55))", result.cells.single().contour)
        assertEquals(0.0, result.cells.single().latitude)
        assertEquals(0.0, result.cells.single().longitude)
        assertFalse(result.cells.single().javaClass.declaredFields.any { field -> field.name == "name" })
    }

    @Test
    fun `reqcells propagates not found response`() {
        val requestId = UUID.fromString("11111111-1111-4111-8111-111111111111")
        val service = earthService(
            responses = mutableListOf(
                ClientResponse.create(HttpStatus.NOT_FOUND)
                    .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                    .body("""{"code":"REQUEST_NOT_FOUND","message":"Заявка не найдена"}""")
                    .build()
            )
        )

        val error = assertFailsWith<WebClientResponseException> {
            service.reqcells(requestId.toString())
        }

        assertEquals(HttpStatus.NOT_FOUND, error.statusCode)
    }

    private fun earthService(
        requestedUris: MutableList<URI> = mutableListOf(),
        responses: MutableList<ClientResponse>,
    ): EarthService {
        val builder = WebClient.builder()
            .exchangeFunction { request ->
                requestedUris += request.url()
                Mono.just(responses.removeAt(0))
            }
        return EarthService(provider(builder), url = "http://pcp-request-service")
    }

    private fun cellsResponse(
        body: String = """
            {
              "items": [],
              "page": 0,
              "size": 500,
              "totalItems": 0,
              "totalPages": 1
            }
        """.trimIndent(),
    ): ClientResponse =
        ClientResponse.create(HttpStatus.OK)
            .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
            .body(body)
            .build()

    private fun requestsResponse(
        body: String = """
            {
              "items": [],
              "page": 0,
              "size": 500,
              "totalItems": 0,
              "totalPages": 1
            }
        """.trimIndent(),
    ): ClientResponse =
        ClientResponse.create(HttpStatus.OK)
            .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
            .body(body)
            .build()

    private fun requestDetailsResponse(body: String): ClientResponse =
        ClientResponse.create(HttpStatus.OK)
            .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
            .body(body)
            .build()

    private fun requestWithCellsResponse(
        requestId: UUID,
        requestName: String = "request name",
        requestGeometry: String = "POLYGON EMPTY",
        requestImportance: Double = 1.0,
        cellNum: Long = 1L,
        cellImportance: Double = 1.0,
        cellContour: String = "POLYGON EMPTY",
    ): ClientResponse =
        ClientResponse.create(HttpStatus.OK)
            .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
            .body(
                """
                {
                  "request": {
                    "id": "$requestId",
                    "name": "$requestName",
                    "status": "ACCEPTED",
                    "surveyType": "OPTICS",
                    "geometry": "$requestGeometry",
                    "importance": $requestImportance,
                    "beginDateTime": "2026-01-01T00:00:00Z",
                    "endDateTime": "2026-01-02T00:00:00Z",
                    "kpp": [],
                    "highPriorityTransmit": false,
                    "optics": null,
                    "rsa": null,
                    "coverage": {
                      "requiredPercent": 100.0,
                      "currentPercent": 0.0
                    },
                    "createdAt": "2026-01-01T00:00:00Z",
                    "updatedAt": "2026-01-01T00:00:00Z",
                    "deletedAt": null
                  },
                  "cells": [
                    {
                      "cellNum": $cellNum,
                      "name": "cell name must be ignored",
                      "coveragePercent": 42.5,
                      "importance": $cellImportance,
                      "contour": "$cellContour"
                    }
                  ]
                }
                """.trimIndent()
            )
            .build()

    private fun provider(builder: WebClient.Builder): ObjectProvider<WebClient.Builder> =
        object : ObjectProvider<WebClient.Builder> {
            override fun getObject(vararg args: Any?): WebClient.Builder = builder
            override fun getObject(): WebClient.Builder = builder
            override fun getIfAvailable(): WebClient.Builder = builder
            override fun getIfUnique(): WebClient.Builder = builder
            override fun iterator(): MutableIterator<WebClient.Builder> = mutableListOf(builder).iterator()
        }
}
