package org.nstart.dep265.requestservice.controller

import com.fasterxml.jackson.databind.SerializationFeature
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import jakarta.persistence.EntityManager
import org.junit.jupiter.api.Test
import org.mockito.ArgumentMatchers.any
import org.mockito.ArgumentMatchers.anyBoolean
import org.mockito.ArgumentMatchers.isNull
import org.mockito.Mockito.doAnswer
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.mock
import org.nstart.dep265.requestservice.dto.CreateRequestDto
import org.nstart.dep265.requestservice.entity.EarthCellEntity
import org.nstart.dep265.requestservice.entity.RequestEntity
import org.nstart.dep265.requestservice.entity.RequestCellEntity
import org.nstart.dep265.requestservice.entity.RequestOpticsParamsEntity
import org.nstart.dep265.requestservice.entity.RequestRsaParamsEntity
import org.nstart.dep265.requestservice.entity.RequestStatus
import org.nstart.dep265.requestservice.entity.RequestSurveyType
import org.nstart.dep265.requestservice.repository.RequestCellRepository
import org.nstart.dep265.requestservice.repository.RequestOpticsParamsRepository
import org.nstart.dep265.requestservice.repository.RequestRepository
import org.nstart.dep265.requestservice.repository.RequestRsaParamsRepository
import org.nstart.dep265.requestservice.service.GeometryService
import org.nstart.dep265.requestservice.service.RequestGridProjectionService
import org.nstart.dep265.requestservice.service.RequestLifecycleService
import org.nstart.dep265.requestservice.service.RequestService
import org.springframework.data.domain.PageImpl
import org.springframework.data.domain.PageRequest
import org.springframework.data.domain.Pageable
import org.springframework.http.MediaType
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
import org.springframework.test.web.servlet.setup.MockMvcBuilders
import java.time.LocalDateTime
import java.util.Optional
import java.util.UUID

class RequestControllerOpenApiSkeletonTest {
    private val requestRepository = mock(RequestRepository::class.java).also { repository ->
        doReturn(Optional.empty<RequestEntity>())
            .`when`(repository)
            .findById(any())
        doReturn(PageImpl(emptyList<RequestEntity>(), PageRequest.of(0, 50), 0))
            .`when`(repository)
            .findRequests(
                anyBoolean(),
                deletedStatusMatcher(),
                isNull(),
                isNull(),
                isNull(),
                isNull(),
                isNull(),
                isNull(),
                isNull(),
                isNull(),
                pageableMatcher(),
            )
        doAnswer { invocation -> invocation.getArgument<RequestEntity>(0) }
            .`when`(repository)
            .save(any(RequestEntity::class.java))
    }
    private val requestOpticsParamsRepository = mock(RequestOpticsParamsRepository::class.java).also { repository ->
        doAnswer { invocation -> invocation.getArgument<RequestOpticsParamsEntity>(0) }
            .`when`(repository)
            .save(any(RequestOpticsParamsEntity::class.java))
        doReturn(Optional.empty<RequestOpticsParamsEntity>())
            .`when`(repository)
            .findById(any())
    }
    private val requestRsaParamsRepository = mock(RequestRsaParamsRepository::class.java).also { repository ->
        doAnswer { invocation -> invocation.getArgument<RequestRsaParamsEntity>(0) }
            .`when`(repository)
            .save(any(RequestRsaParamsEntity::class.java))
        doReturn(Optional.empty<RequestRsaParamsEntity>())
            .`when`(repository)
            .findById(any())
    }
    private val requestCellRepository = mock(RequestCellRepository::class.java).also { repository ->
        doReturn(emptyList<RequestCellEntity>())
            .`when`(repository)
            .findWithCellByRequestId(uuidMatcher())
    }
    private val entityManager = mock(EntityManager::class.java)

    private val mockMvc = MockMvcBuilders
        .standaloneSetup(
            RequestController(
                RequestService(
                    requestRepository = requestRepository,
                    requestOpticsParamsRepository = requestOpticsParamsRepository,
                    requestRsaParamsRepository = requestRsaParamsRepository,
                    requestCellRepository = requestCellRepository,
                    geometryService = GeometryService(),
                    requestGridProjectionService = mock(RequestGridProjectionService::class.java),
                    requestLifecycleService = RequestLifecycleService(GeometryService()),
                    entityManager = entityManager,
                )
            )
        )
        .setControllerAdvice(RequestApiExceptionHandler())
        .setMessageConverters(
            MappingJackson2HttpMessageConverter(
                jacksonObjectMapper()
                    .registerModule(JavaTimeModule())
                    .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS),
            ),
        )
        .build()

    @Test
    fun `post v1 requests returns create response`() {
        // Окно в будущем → приём детерминированно даёт ACCEPTED (createRequest зовёт refreshStatus).
        mockMvc.perform(
            post("/v1/requests")
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON)
                .content(
                    """
                    {
                      "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "name": "Тест1",
                      "geometry": "POLYGON ((37.5 55.5, 37.7 55.5, 37.7 55.7, 37.5 55.7, 37.5 55.5))",
                      "importance": 10.0,
                      "beginDateTime": "2090-01-01T00:00:00Z",
                      "endDateTime": "2090-01-31T23:59:59Z",
                      "kpp": [1, 2, 3],
                      "highPriorityTransmit": false,
                      "optics": {
                        "resultType": "PANCHROMATIC",
                        "resolution": 1.0
                      }
                    }
                    """.trimIndent(),
                ),
        )
            .andExpect(status().isCreated)
            .andExpect(jsonPath("$.id").value("3fa85f64-5717-4562-b3fc-2c963f66afa6"))
            .andExpect(jsonPath("$.name").value("Тест1"))
            .andExpect(jsonPath("$.status").value("ACCEPTED"))
            .andExpect(jsonPath("$.surveyType").value("OPTICS"))
            .andExpect(jsonPath("$.importance").value(10.0))
    }

    @Test
    fun `post v1 requests returns conflict when request id already exists`() {
        val id = UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afa6")
        doReturn(true)
            .`when`(requestRepository)
            .existsById(id)

        mockMvc.perform(
            post("/v1/requests")
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON)
                .content(
                    """
                    {
                      "id": "$id",
                      "name": "Тест1",
                      "geometry": "POLYGON ((37.5 55.5, 37.7 55.5, 37.7 55.7, 37.5 55.7, 37.5 55.5))",
                      "importance": 10.0,
                      "beginDateTime": "2026-01-01T00:00:00Z",
                      "endDateTime": "2026-01-31T23:59:59Z",
                      "optics": {
                        "resultType": "PANCHROMATIC",
                        "resolution": 1.0
                      }
                    }
                    """.trimIndent(),
                ),
        )
            .andExpect(status().isConflict)
            .andExpect(jsonPath("$.code").value("REQUEST_ALREADY_EXISTS"))
    }

    @Test
    fun `post v1 requests rejects missing name`() {
        mockMvc.perform(
            post("/v1/requests")
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON)
                .content(
                    """
                    {
                      "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "geometry": "POLYGON ((37.5 55.5, 37.7 55.5, 37.7 55.7, 37.5 55.7, 37.5 55.5))",
                      "importance": 10.0,
                      "beginDateTime": "2026-01-01T00:00:00Z",
                      "endDateTime": "2026-01-31T23:59:59Z",
                      "optics": {
                        "resultType": "PANCHROMATIC",
                        "resolution": 1.0
                      }
                    }
                    """.trimIndent(),
                ),
        )
            .andExpect(status().isBadRequest)
            .andExpect(jsonPath("$.code").value("VALIDATION_ERROR"))
    }

    @Test
    fun `post v1 requests rejects blank name`() {
        mockMvc.perform(
            post("/v1/requests")
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON)
                .content(
                    """
                    {
                      "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "name": "   ",
                      "geometry": "POLYGON ((37.5 55.5, 37.7 55.5, 37.7 55.7, 37.5 55.7, 37.5 55.5))",
                      "importance": 10.0,
                      "beginDateTime": "2026-01-01T00:00:00Z",
                      "endDateTime": "2026-01-31T23:59:59Z",
                      "optics": {
                        "resultType": "PANCHROMATIC",
                        "resolution": 1.0
                      }
                    }
                    """.trimIndent(),
                ),
        )
            .andExpect(status().isBadRequest)
            .andExpect(jsonPath("$.code").value("VALIDATION_ERROR"))
    }

    @Test
    fun `post v1 requests rejects missing importance`() {
        mockMvc.perform(
            post("/v1/requests")
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON)
                .content(
                    """
                    {
                      "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "name": "Тест1",
                      "geometry": "POLYGON ((37.5 55.5, 37.7 55.5, 37.7 55.7, 37.5 55.7, 37.5 55.5))",
                      "beginDateTime": "2026-01-01T00:00:00Z",
                      "endDateTime": "2026-01-31T23:59:59Z",
                      "optics": {
                        "resultType": "PANCHROMATIC",
                        "resolution": 1.0
                      }
                    }
                    """.trimIndent(),
                ),
        )
            .andExpect(status().isBadRequest)
            .andExpect(jsonPath("$.code").value("VALIDATION_ERROR"))
    }

    @Test
    fun `post v1 requests rejects negative importance`() {
        mockMvc.perform(
            post("/v1/requests")
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON)
                .content(
                    """
                    {
                      "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "name": "Тест1",
                      "geometry": "POLYGON ((37.5 55.5, 37.7 55.5, 37.7 55.7, 37.5 55.7, 37.5 55.5))",
                      "importance": -1.0,
                      "beginDateTime": "2026-01-01T00:00:00Z",
                      "endDateTime": "2026-01-31T23:59:59Z",
                      "optics": {
                        "resultType": "PANCHROMATIC",
                        "resolution": 1.0
                      }
                    }
                    """.trimIndent(),
                ),
        )
            .andExpect(status().isBadRequest)
            .andExpect(jsonPath("$.code").value("VALIDATION_ERROR"))
    }

    @Test
    fun `post v1 requests rejects missing optics and rsa`() {
        mockMvc.perform(
            post("/v1/requests")
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON)
                .content(
                    """
                    {
                      "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "name": "Тест1",
                      "geometry": "POLYGON ((37.5 55.5, 37.7 55.5, 37.7 55.7, 37.5 55.7, 37.5 55.5))",
                      "importance": 10.0,
                      "beginDateTime": "2026-01-01T00:00:00Z",
                      "endDateTime": "2026-01-31T23:59:59Z"
                    }
                    """.trimIndent(),
                ),
        )
            .andExpect(status().isBadRequest)
            .andExpect(jsonPath("$.code").value("VALIDATION_ERROR"))
    }

    @Test
    fun `get v1 requests returns list response`() {
        mockMvc.perform(
            get("/v1/requests")
                .queryParam("page", "2")
                .queryParam("size", "25")
                .accept(MediaType.APPLICATION_JSON),
        )
            .andExpect(status().isOk)
            .andExpect(jsonPath("$.items").isArray)
            .andExpect(jsonPath("$.page").value(2))
            .andExpect(jsonPath("$.size").value(25))
            .andExpect(jsonPath("$.totalItems").value(0))
            .andExpect(jsonPath("$.totalPages").value(0))
    }

    @Test
    fun `get v1 requests returns importance in summary`() {
        doReturn(PageImpl(listOf(existingRequest())))
            .`when`(requestRepository)
            .findRequests(
                anyBoolean(),
                deletedStatusMatcher(),
                isNull(),
                isNull(),
                isNull(),
                isNull(),
                isNull(),
                isNull(),
                isNull(),
                isNull(),
                pageableMatcher(),
            )

        mockMvc.perform(get("/v1/requests").accept(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk)
            .andExpect(jsonPath("$.items[0].name").value("Тест1"))
            .andExpect(jsonPath("$.items[0].importance").value(10.0))
    }

    @Test
    fun `get v1 requests map returns map items with geometry`() {
        doReturn(PageImpl(listOf(existingRequest())))
            .`when`(requestRepository)
            .findRequests(
                anyBoolean(),
                deletedStatusMatcher(),
                isNull(),
                isNull(),
                isNull(),
                isNull(),
                isNull(),
                isNull(),
                isNull(),
                isNull(),
                pageableMatcher(),
            )

        mockMvc.perform(get("/v1/requests/map").accept(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk)
            .andExpect(jsonPath("$.items[0].name").value("Тест1"))
            .andExpect(jsonPath("$.items[0].geometry").value("POLYGON ((37.5 55.5, 37.7 55.5, 37.7 55.7, 37.5 55.7, 37.5 55.5))"))
            .andExpect(jsonPath("$.items[0].importance").value(10.0))
            .andExpect(jsonPath("$.items[0].coveragePercent").value(50.0))
    }

    @Test
    fun `get v1 requests rejects unsupported sort field`() {
        mockMvc.perform(
            get("/v1/requests")
                .queryParam("sort", "geometry,asc")
                .accept(MediaType.APPLICATION_JSON),
        )
            .andExpect(status().isBadRequest)
            .andExpect(jsonPath("$.code").value("VALIDATION_ERROR"))
    }

    @Test
    fun `get v1 requests by id returns importance`() {
        val id = UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afa6")
        doReturn(Optional.of(existingRequest()))
            .`when`(requestRepository)
            .findById(id)

        mockMvc.perform(get("/v1/requests/$id").accept(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk)
            .andExpect(jsonPath("$.name").value("Тест1"))
            .andExpect(jsonPath("$.importance").value(10.0))
    }

    @Test
    fun `get v1 request cells returns request and cell projection fields`() {
        val id = UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afa6")
        val earthCell = EarthCellEntity(
            cellId = 1,
            cellNum = 32400,
            latitude = 55.0,
            longitude = 37.0,
            importance = 5.0,
            contour = "POLYGON ((36 55, 37 55, 37 56, 36 56, 36 55))",
        )
        doReturn(Optional.of(existingRequest()))
            .`when`(requestRepository)
            .findById(id)
        doReturn(
            listOf(
                RequestCellEntity(
                    requestId = id,
                    coveragePercent = 25.0,
                    importance = 7.0,
                    contour = "POLYGON ((36.5 55.5, 37 55.5, 37 56, 36.5 56, 36.5 55.5))",
                    cell = earthCell,
                )
            )
        )
            .`when`(requestCellRepository)
            .findWithCellByRequestId(id)

        mockMvc.perform(get("/v1/requests/$id/cells").accept(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk)
            .andExpect(jsonPath("$.request.id").value(id.toString()))
            .andExpect(jsonPath("$.request.name").value("Тест1"))
            .andExpect(jsonPath("$.request.importance").value(10.0))
            .andExpect(jsonPath("$.request.geometry").value("POLYGON ((37.5 55.5, 37.7 55.5, 37.7 55.7, 37.5 55.7, 37.5 55.5))"))
            .andExpect(jsonPath("$.request.coverage.currentPercent").value(50.0))
            .andExpect(jsonPath("$.cells[0].cellNum").value(32400))
            .andExpect(jsonPath("$.cells[0].coveragePercent").value(25.0))
            .andExpect(jsonPath("$.cells[0].importance").value(7.0))
            .andExpect(jsonPath("$.cells[0].contour").value("POLYGON ((36.5 55.5, 37 55.5, 37 56, 36.5 56, 36.5 55.5))"))
    }

    @Test
    fun `get v1 request cells returns empty cells for request without projections`() {
        val id = UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afa6")
        doReturn(Optional.of(existingRequest()))
            .`when`(requestRepository)
            .findById(id)
        doReturn(emptyList<RequestCellEntity>())
            .`when`(requestCellRepository)
            .findWithCellByRequestId(id)

        mockMvc.perform(get("/v1/requests/$id/cells").accept(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk)
            .andExpect(jsonPath("$.request.id").value(id.toString()))
            .andExpect(jsonPath("$.cells").isEmpty)
    }

    @Test
    fun `get v1 request cells returns deleted request with empty cells`() {
        val id = UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afa6")
        val request = existingRequest().also { existingRequest ->
            existingRequest.status = RequestStatus.DELETED
            existingRequest.deletedAt = LocalDateTime.parse("2026-02-01T00:00:00")
        }
        doReturn(Optional.of(request))
            .`when`(requestRepository)
            .findById(id)
        doReturn(emptyList<RequestCellEntity>())
            .`when`(requestCellRepository)
            .findWithCellByRequestId(id)

        mockMvc.perform(get("/v1/requests/$id/cells").accept(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk)
            .andExpect(jsonPath("$.request.status").value("DELETED"))
            .andExpect(jsonPath("$.request.deletedAt").exists())
            .andExpect(jsonPath("$.cells").isEmpty)
    }

    @Test
    fun `get and delete by id return not found when request is absent`() {
        val id = "3fa85f64-5717-4562-b3fc-2c963f66afa6"

        mockMvc.perform(get("/v1/requests/$id").accept(MediaType.APPLICATION_JSON))
            .andExpect(status().isNotFound)

        mockMvc.perform(get("/v1/requests/$id/cells").accept(MediaType.APPLICATION_JSON))
            .andExpect(status().isNotFound)

        mockMvc.perform(delete("/v1/requests/$id").accept(MediaType.APPLICATION_JSON))
            .andExpect(status().isNotFound)
    }

    @Test
    fun `legacy request with cells endpoint is not restored`() {
        mockMvc.perform(
            get("/api/requests/by-number/3fa85f64-5717-4562-b3fc-2c963f66afa6/with-cells")
                .accept(MediaType.APPLICATION_JSON),
        )
            .andExpect(status().isNotFound)
    }

    private fun existingRequest(): RequestEntity {
        val geometry = "POLYGON ((37.5 55.5, 37.7 55.5, 37.7 55.7, 37.5 55.7, 37.5 55.5))"
        return RequestEntity(
            id = UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afa6"),
            name = "Тест1",
            geometry = geometry,
            remainingGeometry = geometry,
            geometryArea = 1.0,
            remainingArea = 0.5,
            coverageRequiredPercent = 100.0,
            importance = 10.0,
            beginDateTime = LocalDateTime.parse("2026-01-01T00:00:00"),
            endDateTime = LocalDateTime.parse("2026-01-31T23:59:59"),
            highPriorityTransmit = false,
            surveyType = RequestSurveyType.OPTICS,
            status = RequestStatus.ACTIVE,
            createdAt = LocalDateTime.parse("2025-12-31T00:00:00"),
            updatedAt = LocalDateTime.parse("2025-12-31T00:00:00"),
        )
    }

    private fun deletedStatusMatcher(): RequestStatus {
        any(RequestStatus::class.java)
        return RequestStatus.DELETED
    }

    private fun pageableMatcher(): Pageable {
        any(Pageable::class.java)
        return PageRequest.of(0, 50)
    }

    private fun uuidMatcher(): UUID {
        any(UUID::class.java)
        return UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afa6")
    }
}
