package org.nstart.dep265.requestservice.service

import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.mockito.ArgumentMatchers.any
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.nstart.dep265.requestservice.entity.EarthCellEntity
import org.nstart.dep265.requestservice.entity.EarthGridSettingsEntity
import org.nstart.dep265.requestservice.entity.RequestCellEntity
import org.nstart.dep265.requestservice.repository.EarthCellRepository
import org.nstart.dep265.requestservice.repository.EarthGridSettingsRepository
import org.springframework.data.domain.PageImpl
import org.springframework.data.domain.PageRequest
import org.springframework.data.domain.Pageable
import java.util.Optional
import java.util.UUID
import kotlin.test.assertEquals

class CellsQueryServiceTest {
    @Test
    fun `list cells uses repository pageable query and does not call find all`() {
        val earthCellRepository = emptyCellsRepository()
        val service = service(earthCellRepository)

        service.listCells(
            minImportance = null,
            countLat = null,
            countLong = null,
            page = 0,
            size = 50,
            sort = null,
        )

        verify(earthCellRepository).findCells(isNull(), pageableMatcher())
        verify(earthCellRepository, never()).findAll()
    }

    @Test
    fun `list cells builds default sort`() {
        val capturedPageables = mutableListOf<Pageable>()
        val earthCellRepository = emptyCellsRepository(capturedPageables)
        val service = service(earthCellRepository)

        service.listCells(
            minImportance = null,
            countLat = null,
            countLong = null,
            page = 0,
            size = 50,
            sort = null,
        )

        val sort = capturedPageables.single().sort.toList()
        assertEquals("importance", sort[0].property)
        assertEquals(false, sort[0].isAscending)
        assertEquals("cellNum", sort[1].property)
        assertEquals(true, sort[1].isAscending)
    }

    @Test
    fun `list cells rejects invalid query values`() {
        val service = service(emptyCellsRepository())

        assertThrows<InvalidCellsQueryException> {
            service.listCells(
                minImportance = -1.0,
                countLat = null,
                countLong = null,
                page = 0,
                size = 50,
                sort = null,
            )
        }

        assertThrows<InvalidCellsQueryException> {
            service.listCells(
                minImportance = null,
                countLat = null,
                countLong = null,
                page = -1,
                size = 50,
                sort = null,
            )
        }

        assertThrows<InvalidCellsQueryException> {
            service.listCells(
                minImportance = null,
                countLat = null,
                countLong = null,
                page = 0,
                size = 501,
                sort = null,
            )
        }
    }

    @Test
    fun `list cells with requests returns fragments from request cells`() {
        val requestId = UUID.fromString("00000000-0000-0000-0000-000000000101")
        val cellWithRequest = cell(cellNum = 10, importance = 5.0)
        cellWithRequest.requestProjections = mutableListOf(
            RequestCellEntity(
                requestId = requestId,
                contour = "POLYGON ((1 1, 2 1, 2 2, 1 2, 1 1))",
                coveragePercent = 25.0,
                importance = 7.0,
                cell = cellWithRequest,
            )
        )
        val cellWithoutRequests = cell(cellNum = 11, importance = 0.0)
        val earthCellRepository = emptyCellsRepository().also { repository ->
            doReturn(listOf(cellWithRequest, cellWithoutRequests))
                .`when`(repository)
                .findCellsWithRequestProjections(isNull())
        }
        val service = service(earthCellRepository)

        val response = service.listCellsPriorityMap(
            minImportance = null,
            countLat = null,
            countLong = null,
        )

        assertEquals(listOf(10L, 11L), response.items.map { cell -> cell.cellNum })
        assertEquals(emptyList(), response.items[1].requests)

        val fragment = response.items.first().requests.single()
        assertEquals(requestId, fragment.requestId)
        assertEquals("POLYGON ((1 1, 2 1, 2 2, 1 2, 1 1))", fragment.contour)
        assertEquals(25.0, fragment.coveragePercent)
        assertEquals(7.0, fragment.importance)
        verify(earthCellRepository).findCellsWithRequestProjections(isNull())
        verify(earthCellRepository, never()).findCells(isNull(), pageableMatcher())
        verify(earthCellRepository, never()).findAll()
    }

    @Test
    fun `list cells with requests passes min importance to repository`() {
        val earthCellRepository = emptyCellsRepository().also { repository ->
            doReturn(listOf(cell(cellNum = 10, importance = 5.0)))
                .`when`(repository)
                .findCellsWithRequestProjections(2.0)
        }
        val service = service(earthCellRepository)

        val response = service.listCellsPriorityMap(
            minImportance = 2.0,
            countLat = null,
            countLong = null,
        )

        assertEquals(listOf(10L), response.items.map { cell -> cell.cellNum })
        verify(earthCellRepository).findCellsWithRequestProjections(2.0)
    }

    @Test
    fun `list cells with requests rejects partial aggregation parameters`() {
        val service = service(emptyCellsRepository())

        assertThrows<InvalidCellsQueryException> {
            service.listCellsPriorityMap(
                minImportance = null,
                countLat = 2,
                countLong = null,
            )
        }

        assertThrows<InvalidCellsQueryException> {
            service.listCellsPriorityMap(
                minImportance = null,
                countLat = null,
                countLong = 2,
            )
        }
    }

    @Test
    fun `list cells with requests delegates to existing aggregation when both counts are present`() {
        val earthCellRepository = emptyCellsRepository().also { repository ->
            doReturn(
                listOf(
                    cell(cellNum = 10, latitude = -88.0, longitude = 0.0, importance = 1.0),
                    cell(cellNum = 11, latitude = -88.0, longitude = 2.0, importance = 2.0),
                )
            )
                .`when`(repository)
                .findCellsWithRequestProjections(isNull())
        }
        val service = service(earthCellRepository)

        val response = service.listCellsPriorityMap(
            minImportance = null,
            countLat = 1,
            countLong = 2,
        )

        assertEquals(1, response.items.size)
        assertEquals(10L, response.items.first().cellNum)
        assertEquals(3.0, response.items.first().importance)
        assertEquals("POLYGON ((-2.0 -90.0, 2.0 -90.0, 2.0 -88.0, -2.0 -88.0, -2.0 -90.0))", response.items.first().contour)
    }

    private fun emptyCellsRepository(capturedPageables: MutableList<Pageable> = mutableListOf()): EarthCellRepository {
        return mock(EarthCellRepository::class.java).also { repository ->
            doAnswer { invocation ->
                capturedPageables += invocation.getArgument<Pageable>(1)
                PageImpl(emptyList<EarthCellEntity>(), PageRequest.of(0, 50), 0)
            }
                .`when`(repository)
                .findCells(isNull(), pageableMatcher())
            doReturn(emptyList<EarthCellEntity>())
                .`when`(repository)
                .findCellsWithRequestProjections(isNull())
        }
    }

    private fun service(earthCellRepository: EarthCellRepository): CellsQueryService {
        val earthGridSettingsRepository = mock(EarthGridSettingsRepository::class.java).also { repository ->
            doReturn(Optional.of(EarthGridSettingsEntity(calculationStep = 2.0)))
                .`when`(repository)
                .findById(EarthGridSettingsEntity.DEFAULT_SETTINGS_ID)
        }
        val earthGridCatalogService = EarthGridCatalogService(
            earthCellRepository = earthCellRepository,
            earthGridSettingsRepository = earthGridSettingsRepository,
            compatGeometryService = CompatGeometryService(GeometryService()),
            gridEnabled = false,
        )
        return CellsQueryService(earthCellRepository, earthGridCatalogService)
    }

    private fun cell(
        cellNum: Long,
        latitude: Double = 55.0,
        longitude: Double = 37.0,
        importance: Double = 0.0,
        contour: String = "POLYGON ((36 55, 37 55, 37 56, 36 56, 36 55))",
    ): EarthCellEntity {
        return EarthCellEntity(
            cellNum = cellNum,
            latitude = latitude,
            longitude = longitude,
            importance = importance,
            contour = contour,
        )
    }

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