package org.nstart.dep265.requestservice.service

import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import org.mockito.ArgumentCaptor
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.doThrow
import org.mockito.Mockito.mock
import org.mockito.Mockito.mockingDetails
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoInteractions
import org.nstart.dep265.requestservice.dto.RouteAngleRangeDto
import org.nstart.dep265.requestservice.dto.RouteDto
import org.nstart.dep265.requestservice.entity.RequestEntity
import org.nstart.dep265.requestservice.entity.RequestRouteMatchEntity
import org.nstart.dep265.requestservice.entity.RequestStatus
import org.nstart.dep265.requestservice.entity.RequestSurveyType
import org.nstart.dep265.requestservice.repository.OutboxEventRepository
import org.nstart.dep265.requestservice.repository.RequestRepository
import org.nstart.dep265.requestservice.repository.RequestRouteMatchRepository
import org.springframework.dao.DataIntegrityViolationException
import org.springframework.transaction.TransactionStatus
import org.springframework.transaction.support.SimpleTransactionStatus
import org.springframework.transaction.support.TransactionCallback
import org.springframework.transaction.support.TransactionOperations
import java.time.LocalDateTime
import java.util.Optional
import java.util.UUID

class RouteMatchingServiceTest {

    @Test
    fun `route intersecting original and remaining stores contributing match and updates coverage`() {
        val requestRepository = mock(RequestRepository::class.java)
        val requestRouteMatchRepository = mock(RequestRouteMatchRepository::class.java)
        val outboxEventRepository = mock(OutboxEventRepository::class.java)
        val geometryService = GeometryService()
        val routeMatchingService = routeMatchingService(
            requestRepository = requestRepository,
            requestRouteMatchRepository = requestRouteMatchRepository,
            outboxEventRepository = outboxEventRepository,
            geometryService = geometryService,
        )

        val intervalBegin = LocalDateTime.now().minusMinutes(5).withNano(0)
        val intervalEnd = intervalBegin.plusHours(1)
        val request = request(
            beginDateTime = intervalBegin,
            endDateTime = intervalEnd,
            geometry = "POLYGON((0 0, 2 0, 2 2, 0 2, 0 0))",
            remainingGeometry = "POLYGON((0 0, 2 0, 2 2, 0 2, 0 0))",
            geometryArea = 4.0,
            remainingArea = 4.0,
            status = RequestStatus.ACTIVE,
        )
        val route = route(
            intervalBegin = intervalBegin.plusMinutes(1),
            intervalEnd = intervalBegin.plusMinutes(2),
            geometry = "POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))",
        )

        stubCandidate(requestRepository, requestRouteMatchRepository, request, route)

        routeMatchingService.process(route)

        val routeMatch = captureSavedRouteMatch(requestRouteMatchRepository)
        assertEquals(request.id, routeMatch.requestId)
        assertEquals(route.routeId, routeMatch.routeId)
        assertTrue(routeMatch.contributesToCoverage)
        assertTrue(routeMatch.originalIntersectionArea > 0.0)
        assertTrue(routeMatch.appliedIntersectionArea > 0.0)
        assertTrue(routeMatch.coverageDeltaPercent > 0.0)
        assertEquals(3.0, request.remainingArea)
        assertEquals(1, request.matchCount)
        assertEquals(RequestStatus.ACTIVE, request.status)
        verify(requestRepository).findByIdForUpdate(request.id)
        verify(requestRepository).save(request)
        verifyNoInteractions(outboxEventRepository)
    }

    @Test
    fun `route intersecting original but not remaining stores non contributing match without coverage update`() {
        val requestRepository = mock(RequestRepository::class.java)
        val requestRouteMatchRepository = mock(RequestRouteMatchRepository::class.java)
        val outboxEventRepository = mock(OutboxEventRepository::class.java)
        val geometryService = GeometryService()
        val routeMatchingService = routeMatchingService(
            requestRepository = requestRepository,
            requestRouteMatchRepository = requestRouteMatchRepository,
            outboxEventRepository = outboxEventRepository,
            geometryService = geometryService,
        )

        val intervalBegin = LocalDateTime.now().minusMinutes(5).withNano(0)
        val intervalEnd = intervalBegin.plusHours(1)
        val originalRemainingGeometry = "POLYGON((1 1, 2 1, 2 2, 1 2, 1 1))"
        val request = request(
            beginDateTime = intervalBegin,
            endDateTime = intervalEnd,
            geometry = "POLYGON((0 0, 2 0, 2 2, 0 2, 0 0))",
            remainingGeometry = originalRemainingGeometry,
            geometryArea = 4.0,
            remainingArea = 1.0,
            status = RequestStatus.ACTIVE,
        )
        val route = route(
            intervalBegin = intervalBegin.plusMinutes(1),
            intervalEnd = intervalBegin.plusMinutes(2),
            geometry = "POLYGON((0 0, 0.5 0, 0.5 0.5, 0 0.5, 0 0))",
        )

        stubCandidate(requestRepository, requestRouteMatchRepository, request, route)

        routeMatchingService.process(route)

        val routeMatch = captureSavedRouteMatch(requestRouteMatchRepository)
        assertEquals(request.id, routeMatch.requestId)
        assertEquals(route.routeId, routeMatch.routeId)
        assertEquals(false, routeMatch.contributesToCoverage)
        assertTrue(routeMatch.originalIntersectionArea > 0.0)
        assertNull(routeMatch.appliedIntersectionGeometry)
        assertEquals(0.0, routeMatch.appliedIntersectionArea)
        assertEquals(0.0, routeMatch.coverageDeltaPercent)
        assertEquals(originalRemainingGeometry, request.remainingGeometry)
        assertEquals(1.0, request.remainingArea)
        assertEquals(0, request.matchCount)
        verify(requestRepository, never()).save(request)
        verifyNoInteractions(outboxEventRepository)
    }

    @Test
    fun `route outside original geometry does not create match or update request`() {
        val requestRepository = mock(RequestRepository::class.java)
        val requestRouteMatchRepository = mock(RequestRouteMatchRepository::class.java)
        val outboxEventRepository = mock(OutboxEventRepository::class.java)
        val geometryService = GeometryService()
        val routeMatchingService = routeMatchingService(
            requestRepository = requestRepository,
            requestRouteMatchRepository = requestRouteMatchRepository,
            outboxEventRepository = outboxEventRepository,
            geometryService = geometryService,
        )

        val intervalBegin = LocalDateTime.now().minusMinutes(5).withNano(0)
        val request = request(
            beginDateTime = intervalBegin,
            endDateTime = intervalBegin.plusHours(1),
            status = RequestStatus.ACTIVE,
        )
        val route = route(
            intervalBegin = intervalBegin.plusMinutes(1),
            intervalEnd = intervalBegin.plusMinutes(2),
            geometry = "POLYGON((5 5, 6 5, 6 6, 5 6, 5 5))",
        )

        stubCandidate(requestRepository, requestRouteMatchRepository, request, route)

        routeMatchingService.process(route)

        verify(requestRouteMatchRepository, never()).saveAndFlush(org.mockito.ArgumentMatchers.any())
        verify(requestRepository, never()).save(request)
        verifyNoInteractions(outboxEventRepository)
        assertEquals(1.0, request.remainingArea)
        assertEquals(0, request.matchCount)
    }

    @Test
    fun `duplicate route match after lock skips geometry and coverage update`() {
        val requestRepository = mock(RequestRepository::class.java)
        val requestRouteMatchRepository = mock(RequestRouteMatchRepository::class.java)
        val outboxEventRepository = mock(OutboxEventRepository::class.java)
        val geometryService = GeometryService()
        val routeMatchingService = routeMatchingService(
            requestRepository = requestRepository,
            requestRouteMatchRepository = requestRouteMatchRepository,
            outboxEventRepository = outboxEventRepository,
            geometryService = geometryService,
        )

        val intervalBegin = LocalDateTime.now().minusMinutes(5).withNano(0)
        val request = request(
            beginDateTime = intervalBegin,
            endDateTime = intervalBegin.plusHours(1),
            status = RequestStatus.ACTIVE,
        )
        val route = route(
            intervalBegin = intervalBegin.plusMinutes(1),
            intervalEnd = intervalBegin.plusMinutes(2),
            geometry = request.geometry,
        )

        doReturn(listOf(request.id))
            .`when`(requestRepository)
            .findRequestIdsForRoute(
                listOf(RequestStatus.ACCEPTED, RequestStatus.ACTIVE),
                route.intervalBegin,
                route.intervalEnd,
            )
        doReturn(false, true)
            .`when`(requestRouteMatchRepository)
            .existsByRequestIdAndRouteId(request.id, route.routeId)
        doReturn(Optional.of(request))
            .`when`(requestRepository)
            .findByIdForUpdate(request.id)

        routeMatchingService.process(route)

        verify(requestRouteMatchRepository, never()).saveAndFlush(org.mockito.ArgumentMatchers.any())
        verify(requestRepository, never()).save(request)
        verifyNoInteractions(outboxEventRepository)
        assertEquals(1.0, request.remainingArea)
        assertEquals(0, request.matchCount)
    }

    @Test
    fun `duplicate unique violation on match insert is handled as duplicate delivery`() {
        val requestRepository = mock(RequestRepository::class.java)
        val requestRouteMatchRepository = mock(RequestRouteMatchRepository::class.java)
        val outboxEventRepository = mock(OutboxEventRepository::class.java)
        val geometryService = GeometryService()
        val routeMatchingService = routeMatchingService(
            requestRepository = requestRepository,
            requestRouteMatchRepository = requestRouteMatchRepository,
            outboxEventRepository = outboxEventRepository,
            geometryService = geometryService,
        )

        val intervalBegin = LocalDateTime.now().minusMinutes(5).withNano(0)
        val request = request(
            beginDateTime = intervalBegin,
            endDateTime = intervalBegin.plusHours(1),
            status = RequestStatus.ACTIVE,
        )
        val route = route(
            intervalBegin = intervalBegin.plusMinutes(1),
            intervalEnd = intervalBegin.plusMinutes(2),
            geometry = request.geometry,
        )

        stubCandidate(requestRepository, requestRouteMatchRepository, request, route)
        doThrow(DataIntegrityViolationException("duplicate key violates unique constraint uk_request_route_match"))
            .`when`(requestRouteMatchRepository)
            .saveAndFlush(org.mockito.ArgumentMatchers.any())

        routeMatchingService.process(route)

        verify(requestRepository, never()).save(request)
        verifyNoInteractions(outboxEventRepository)
        assertEquals(1.0, request.remainingArea)
        assertEquals(0, request.matchCount)
    }

    @Test
    fun `route completing request creates exactly one request completed outbox event`() {
        val requestRepository = mock(RequestRepository::class.java)
        val requestRouteMatchRepository = mock(RequestRouteMatchRepository::class.java)
        val outboxEventRepository = mock(OutboxEventRepository::class.java)
        val geometryService = GeometryService()
        val routeMatchingService = routeMatchingService(
            requestRepository = requestRepository,
            requestRouteMatchRepository = requestRouteMatchRepository,
            outboxEventRepository = outboxEventRepository,
            geometryService = geometryService,
        )

        val intervalBegin = LocalDateTime.now().minusMinutes(5).withNano(0)
        val request = request(
            beginDateTime = intervalBegin,
            endDateTime = intervalBegin.plusHours(1),
            status = RequestStatus.ACTIVE,
        )
        val route = route(
            intervalBegin = intervalBegin.plusMinutes(1),
            intervalEnd = intervalBegin.plusMinutes(2),
            geometry = request.geometry,
        )

        stubCandidate(requestRepository, requestRouteMatchRepository, request, route)

        routeMatchingService.process(route)

        val insertInvocations = mockingDetails(outboxEventRepository).invocations.filter {
            invocation -> invocation.method.name == "insertIfAbsent"
        }
        assertEquals(1, insertInvocations.size)
        assertEquals("REQUEST_TERMINAL_STATUS", insertInvocations.single().arguments[1])
        assertEquals(request.id, insertInvocations.single().arguments[3])
        assertEquals(RequestStatus.COMPLETED, request.status)
    }

    private fun routeMatchingService(
        requestRepository: RequestRepository,
        requestRouteMatchRepository: RequestRouteMatchRepository,
        outboxEventRepository: OutboxEventRepository,
        geometryService: GeometryService,
    ): RouteMatchingService {
        return RouteMatchingService(
            requestRepository = requestRepository,
            requestRouteMatchRepository = requestRouteMatchRepository,
            requestValidationService = RequestValidationService(geometryService),
            geometryService = geometryService,
            requestLifecycleService = RequestLifecycleService(geometryService),
            requestStatusOutboxService = requestStatusOutboxService(outboxEventRepository, geometryService),
            routeMatchingTransactionOperations = ImmediateTransactionOperations(),
        )
    }

    private fun requestStatusOutboxService(
        outboxEventRepository: OutboxEventRepository,
        geometryService: GeometryService,
    ): RequestStatusOutboxService {
        return RequestStatusOutboxService(
            outboxEventRepository = outboxEventRepository,
            objectMapper = jacksonObjectMapper()
                .registerModule(JavaTimeModule())
                .findAndRegisterModules(),
            geometryService = geometryService,
        )
    }

    private fun stubCandidate(
        requestRepository: RequestRepository,
        requestRouteMatchRepository: RequestRouteMatchRepository,
        request: RequestEntity,
        route: RouteDto,
    ) {
        doReturn(listOf(request.id))
            .`when`(requestRepository)
            .findRequestIdsForRoute(
                listOf(RequestStatus.ACCEPTED, RequestStatus.ACTIVE),
                route.intervalBegin,
                route.intervalEnd,
            )
        doReturn(false)
            .`when`(requestRouteMatchRepository)
            .existsByRequestIdAndRouteId(request.id, route.routeId)
        doReturn(Optional.of(request))
            .`when`(requestRepository)
            .findByIdForUpdate(request.id)
        doReturn(request)
            .`when`(requestRepository)
            .save(request)
    }

    private fun captureSavedRouteMatch(
        requestRouteMatchRepository: RequestRouteMatchRepository,
    ): RequestRouteMatchEntity {
        val routeMatchCaptor = ArgumentCaptor.forClass(RequestRouteMatchEntity::class.java)
        verify(requestRouteMatchRepository).saveAndFlush(routeMatchCaptor.capture())
        return routeMatchCaptor.value
    }

    private fun request(
        beginDateTime: LocalDateTime,
        endDateTime: LocalDateTime,
        geometry: String = "POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))",
        remainingGeometry: String = geometry,
        geometryArea: Double = 1.0,
        remainingArea: Double = geometryArea,
        status: RequestStatus,
        completedAt: LocalDateTime? = null,
    ): RequestEntity {
        val createdAt = beginDateTime.minusHours(1)
        return RequestEntity(
            id = UUID.randomUUID(),
            name = "Тест1",
            geometry = geometry,
            remainingGeometry = remainingGeometry,
            geometryArea = geometryArea,
            remainingArea = remainingArea,
            importance = 10.0,
            beginDateTime = beginDateTime,
            endDateTime = endDateTime,
            highPriorityTransmit = false,
            surveyType = RequestSurveyType.OPTICS,
            status = status,
            matchCount = 0,
            completedAt = completedAt,
            createdAt = createdAt,
            updatedAt = createdAt,
        )
    }

    private fun route(
        intervalBegin: LocalDateTime,
        intervalEnd: LocalDateTime,
        geometry: String,
    ): RouteDto {
        return RouteDto(
            routeId = UUID.randomUUID(),
            intervalBegin = intervalBegin,
            intervalEnd = intervalEnd,
            rollAngle = RouteAngleRangeDto(
                min = 10.0,
                max = 14.0,
            ),
            geometry = geometry,
        )
    }

    private class ImmediateTransactionOperations : TransactionOperations {
        override fun <T> execute(action: TransactionCallback<T>): T {
            val status: TransactionStatus = SimpleTransactionStatus()
            return action.doInTransaction(status)
        }
    }
}
