package org.nstart.dep265.requestservice.service

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.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoInteractions
import org.nstart.dep265.requestservice.dto.CreateRequestDto
import org.nstart.dep265.requestservice.dto.RequestListFilter
import org.nstart.dep265.requestservice.dto.OpticsParamsDto
import org.nstart.dep265.requestservice.dto.OpticsResultTypeDto
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.springframework.data.domain.PageImpl
import org.springframework.data.domain.PageRequest
import org.springframework.data.domain.Pageable
import java.time.LocalDateTime
import java.util.Optional
import java.util.UUID
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith

class RequestServiceTest {
    private val requestRepository = mock(RequestRepository::class.java)
    private val requestOpticsParamsRepository = mock(RequestOpticsParamsRepository::class.java)
    private val requestRsaParamsRepository = mock(RequestRsaParamsRepository::class.java)
    private val requestCellRepository = mock(RequestCellRepository::class.java)
    private val requestGridProjectionService = mock(RequestGridProjectionService::class.java)
    private val entityManager = mock(EntityManager::class.java)

    private val service = RequestService(
        requestRepository = requestRepository,
        requestOpticsParamsRepository = requestOpticsParamsRepository,
        requestRsaParamsRepository = requestRsaParamsRepository,
        requestCellRepository = requestCellRepository,
        geometryService = GeometryService(),
        requestGridProjectionService = requestGridProjectionService,
        requestLifecycleService = RequestLifecycleService(GeometryService()),
        entityManager = entityManager,
    )

    init {
        doAnswer { invocation -> invocation.getArgument<RequestOpticsParamsEntity>(0) }
            .`when`(requestOpticsParamsRepository)
            .save(any(RequestOpticsParamsEntity::class.java))
        doAnswer { invocation -> invocation.getArgument<RequestRsaParamsEntity>(0) }
            .`when`(requestRsaParamsRepository)
            .save(any(RequestRsaParamsEntity::class.java))
        doReturn(Optional.empty<RequestOpticsParamsEntity>())
            .`when`(requestOpticsParamsRepository)
            .findById(any())
        doReturn(Optional.empty<RequestRsaParamsEntity>())
            .`when`(requestRsaParamsRepository)
            .findById(any())
        doReturn(emptyList<RequestCellEntity>())
            .`when`(requestCellRepository)
            .findWithCellByRequestId(uuidMatcher())
    }

    @Test
    fun `create request inserts new request with default required coverage and input importance`() {
        val persistedRequests = mutableListOf<RequestEntity>()
        doReturn(false)
            .`when`(requestRepository)
            .existsById(REQUEST_ID)
        doAnswer { invocation ->
            persistedRequests += invocation.getArgument<RequestEntity>(0)
            null
        }.`when`(entityManager).persist(any(RequestEntity::class.java))

        val response = service.createRequest(createRequestDto())

        assertEquals(REQUEST_ID, response.id)
        assertEquals(REQUEST_NAME, response.name)
        assertEquals(REQUEST_IMPORTANCE, response.importance)
        assertEquals(1, persistedRequests.size)
        assertEquals(REQUEST_NAME, persistedRequests.single().name)
        assertEquals(100.0, persistedRequests.single().coverageRequiredPercent)
        assertEquals(REQUEST_IMPORTANCE, persistedRequests.single().importance)
        verify(requestRepository, never()).save(any(RequestEntity::class.java))
        verify(requestGridProjectionService).rebuildProjection(persistedRequests.single())
    }

    @Test
    fun `create request passes through explicit coverage required percent from api`() {
        val persistedRequests = mutableListOf<RequestEntity>()
        doReturn(false)
            .`when`(requestRepository)
            .existsById(REQUEST_ID)
        doAnswer { invocation ->
            persistedRequests += invocation.getArgument<RequestEntity>(0)
            null
        }.`when`(entityManager).persist(any(RequestEntity::class.java))

        service.createRequest(createRequestDto(coverageRequiredPercent = 80.0))

        assertEquals(80.0, persistedRequests.single().coverageRequiredPercent)
    }

    @Test
    fun `create request coalesces null coverage required percent to default`() {
        val persistedRequests = mutableListOf<RequestEntity>()
        doReturn(false)
            .`when`(requestRepository)
            .existsById(REQUEST_ID)
        doAnswer { invocation ->
            persistedRequests += invocation.getArgument<RequestEntity>(0)
            null
        }.`when`(entityManager).persist(any(RequestEntity::class.java))

        // Явный null (клиент прислал coverageRequiredPercent: null) трактуем как «взять дефолт».
        service.createRequest(createRequestDto(coverageRequiredPercent = null))

        assertEquals(100.0, persistedRequests.single().coverageRequiredPercent)
    }

    @Test
    fun `get request by id returns name and importance`() {
        val request = existingRequest()
        doReturn(Optional.of(request))
            .`when`(requestRepository)
            .findById(REQUEST_ID)

        val response = service.getRequestById(REQUEST_ID)

        assertEquals(REQUEST_NAME, response?.name)
        assertEquals(REQUEST_IMPORTANCE, response?.importance)
    }

    @Test
    fun `get request with cells returns request read model and request cell projection fields`() {
        val request = existingRequest()
        val earthCell = EarthCellEntity(
            cellId = 101,
            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(request))
            .`when`(requestRepository)
            .findById(REQUEST_ID)
        doReturn(
            listOf(
                RequestCellEntity(
                    requestId = REQUEST_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(REQUEST_ID)

        val response = service.getRequestWithCells(REQUEST_ID)

        assertEquals(REQUEST_ID, response?.request?.id)
        assertEquals(REQUEST_NAME, response?.request?.name)
        assertEquals(REQUEST_IMPORTANCE, response?.request?.importance)

        val cell = response?.cells?.single()
        assertEquals(32400L, cell?.cellNum)
        assertEquals(25.0, cell?.coveragePercent)
        assertEquals(7.0, cell?.importance)
        assertEquals("POLYGON ((36.5 55.5, 37 55.5, 37 56, 36.5 56, 36.5 55.5))", cell?.contour)
        verify(requestCellRepository).findWithCellByRequestId(REQUEST_ID)
    }

    @Test
    fun `get request with cells returns empty cells for request without projections`() {
        doReturn(Optional.of(existingRequest()))
            .`when`(requestRepository)
            .findById(REQUEST_ID)
        doReturn(emptyList<RequestCellEntity>())
            .`when`(requestCellRepository)
            .findWithCellByRequestId(REQUEST_ID)

        val response = service.getRequestWithCells(REQUEST_ID)

        assertEquals(REQUEST_ID, response?.request?.id)
        assertEquals(emptyList(), response?.cells)
    }

    @Test
    fun `get request with cells returns deleted request with empty cells`() {
        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(REQUEST_ID)
        doReturn(emptyList<RequestCellEntity>())
            .`when`(requestCellRepository)
            .findWithCellByRequestId(REQUEST_ID)

        val response = service.getRequestWithCells(REQUEST_ID)

        assertEquals(RequestStatus.DELETED.name, response?.request?.status?.name)
        assertEquals(emptyList(), response?.cells)
    }

    @Test
    fun `get request with cells returns null when request is absent`() {
        doReturn(Optional.empty<RequestEntity>())
            .`when`(requestRepository)
            .findById(REQUEST_ID)

        val response = service.getRequestWithCells(REQUEST_ID)

        assertEquals(null, response)
        verify(requestCellRepository, never()).findWithCellByRequestId(REQUEST_ID)
    }

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

        val response = service.listRequests(RequestListFilter())

        assertEquals(REQUEST_NAME, response.items.single().name)
        assertEquals(REQUEST_IMPORTANCE, response.items.single().importance)
    }

    @Test
    fun `list requests with geometry returns expanded summary`() {
        doReturn(PageImpl(listOf(existingRequest())))
            .`when`(requestRepository)
            .findRequests(
                anyBoolean(),
                deletedStatusMatcher(),
                isNull(),
                isNull(),
                isNull(),
                isNull(),
                isNull(),
                isNull(),
                isNull(),
                isNull(),
                pageableMatcher(),
            )

        val response = service.listRequestsForMap(RequestListFilter())

        assertEquals(REQUEST_NAME, response.items.single().name)
        assertEquals("POLYGON ((37.5 55.5, 37.7 55.5, 37.7 55.7, 37.5 55.7, 37.5 55.5))", response.items.single().geometry)
        assertEquals(REQUEST_IMPORTANCE, response.items.single().importance)
        assertEquals(50.0, response.items.single().coveragePercent)
    }

    @Test
    fun `create request rejects duplicate id without touching existing request state`() {
        doReturn(true)
            .`when`(requestRepository)
            .existsById(REQUEST_ID)

        assertFailsWith<RequestAlreadyExistsException> {
            service.createRequest(createRequestDto())
        }

        verify(entityManager, never()).persist(any(RequestEntity::class.java))
        verify(requestRepository, never()).save(any(RequestEntity::class.java))
        verifyNoInteractions(requestGridProjectionService)
        verify(requestCellRepository, never()).deleteByRequestId(REQUEST_ID)
    }

    @Test
    fun `delete request soft deletes request and removes request cells projection`() {
        val request = existingRequest()
        doReturn(Optional.of(request))
            .`when`(requestRepository)
            .findById(REQUEST_ID)
        doAnswer { invocation -> invocation.getArgument<RequestEntity>(0) }
            .`when`(requestRepository)
            .save(any(RequestEntity::class.java))

        val response = service.deleteRequest(REQUEST_ID)

        assertEquals(REQUEST_ID, response?.id)
        assertEquals(RequestStatus.DELETED, request.status)
        verify(requestCellRepository).deleteByRequestId(REQUEST_ID)
    }

    private fun createRequestDto(coverageRequiredPercent: Double? = 100.0): CreateRequestDto {
        return CreateRequestDto(
            id = REQUEST_ID,
            name = REQUEST_NAME,
            geometry = "POLYGON ((37.5 55.5, 37.7 55.5, 37.7 55.7, 37.5 55.7, 37.5 55.5))",
            importance = REQUEST_IMPORTANCE,
            beginDateTime = LocalDateTime.of(2026, 1, 1, 0, 0, 0),
            endDateTime = LocalDateTime.of(2026, 1, 31, 23, 59, 59),
            coverageRequiredPercent = coverageRequiredPercent,
            optics = OpticsParamsDto(
                resultType = OpticsResultTypeDto.PANCHROMATIC,
                resolution = 1.0,
            ),
        )
    }

    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 = REQUEST_ID,
            name = REQUEST_NAME,
            geometry = geometry,
            remainingGeometry = geometry,
            geometryArea = 1.0,
            remainingArea = 0.5,
            coverageRequiredPercent = 100.0,
            importance = REQUEST_IMPORTANCE,
            beginDateTime = LocalDateTime.parse("2026-01-01T00:00:00"),
            endDateTime = LocalDateTime.parse("2026-01-31T23:59:59"),
            surveyType = RequestSurveyType.OPTICS,
            status = RequestStatus.ACTIVE,
            matchCount = 1,
            completedAt = null,
            createdAt = LocalDateTime.parse("2025-12-31T00:00:00"),
            updatedAt = LocalDateTime.parse("2025-12-31T00:00:00"),
        )
    }

    private companion object {
        val REQUEST_ID: UUID = UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afa6")
        const val REQUEST_NAME = "Тест1"
        const val REQUEST_IMPORTANCE = 10.0
    }

    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 REQUEST_ID
    }
}
