package space.nstart.pcp_tgu_service.service

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.Mockito.mock
import org.mockito.Mockito.`when`
import org.springframework.data.jpa.repository.Query
import org.springframework.transaction.PlatformTransactionManager
import org.springframework.transaction.TransactionDefinition
import org.springframework.transaction.TransactionStatus
import org.springframework.transaction.support.SimpleTransactionStatus
import org.springframework.transaction.support.TransactionTemplate
import space.nstart.pcp_tgu_service.config.PlanningProperties
import space.nstart.pcp_tgu_service.entity.JobStatus
import space.nstart.pcp_tgu_service.entity.PlanRecalculationJobEntity
import space.nstart.pcp_tgu_service.entity.VisibilityRefreshJobEntity
import space.nstart.pcp_tgu_service.integration.api.ExternalPointsClient
import space.nstart.pcp_tgu_service.repository.PlanRecalculationJobRepository
import space.nstart.pcp_tgu_service.repository.SpacecraftSnapshotRepository
import space.nstart.pcp_tgu_service.repository.VisibilityRefreshJobRepository
import java.time.LocalDateTime
import java.util.Optional

class JobRerunFlowTest {

    @Test
    fun `enqueue does not reset running visibility job lock`() {
        val query = visibilityUpsertQuery()

        assertTrue(query.contains("WHEN visibility_refresh_job.status = 'RUNNING' THEN visibility_refresh_job.status"))
        assertTrue(query.contains("WHEN visibility_refresh_job.status = 'RUNNING' THEN visibility_refresh_job.locked_by"))
        assertTrue(query.contains("WHEN visibility_refresh_job.status = 'RUNNING' THEN visibility_refresh_job.locked_until"))
    }

    @Test
    fun `enqueue does not reset running recalculation job lock`() {
        val query = recalculationUpsertQuery()

        assertTrue(query.contains("WHEN plan_recalculation_job.status = 'RUNNING' THEN plan_recalculation_job.status"))
        assertTrue(query.contains("WHEN plan_recalculation_job.status = 'RUNNING' THEN plan_recalculation_job.locked_by"))
        assertTrue(query.contains("WHEN plan_recalculation_job.status = 'RUNNING' THEN plan_recalculation_job.locked_until"))
    }

    @Test
    fun `enqueue running job sets rerunRequested to true`() {
        assertTrue(
            visibilityUpsertQuery()
                .contains("WHEN visibility_refresh_job.status = 'RUNNING' THEN TRUE")
        )
        assertTrue(
            recalculationUpsertQuery()
                .contains("WHEN plan_recalculation_job.status = 'RUNNING' THEN TRUE")
        )
    }

    @Test
    fun `visibility markDone with rerunRequested moves job to pending`() {
        val spacecraftId = "SC-1"
        val jobStore = linkedMapOf(
            spacecraftId to VisibilityRefreshJobEntity(
                spacecraftId = spacecraftId,
                noradId = 25544L,
                status = JobStatus.PENDING,
                rerunRequested = true
            )
        )
        val repository = visibilityJobRepository(jobStore)
        val externalPointsClient = mock(ExternalPointsClient::class.java)
        val snapshotService = mock(SpacecraftVisibilitySnapshotService::class.java)
        `when`(
            externalPointsClient.fetchRawVisibilityPayload(
                org.mockito.ArgumentMatchers.anyLong(),
                anyNonNull(),
                anyNonNull()
            )
        ).thenReturn("[]")
        val worker = VisibilityRefreshWorker(
            planningProperties = planningProperties(),
            visibilityRefreshJobRepository = repository,
            spacecraftSnapshotRepository = mock(SpacecraftSnapshotRepository::class.java),
            externalPointsClient = externalPointsClient,
            spacecraftVisibilitySnapshotService = snapshotService,
            transactionTemplate = transactionTemplate()
        )

        worker.processPendingJobs()

        val job = jobStore.getValue(spacecraftId)
        assertEquals(JobStatus.PENDING, job.status)
        assertEquals(false, job.rerunRequested)
        assertNull(job.lockedBy)
        assertNull(job.lockedUntil)
    }

    @Test
    fun `visibility markDone without rerunRequested moves job to done`() {
        val spacecraftId = "SC-2"
        val jobStore = linkedMapOf(
            spacecraftId to VisibilityRefreshJobEntity(
                spacecraftId = spacecraftId,
                noradId = 25544L,
                status = JobStatus.PENDING
            )
        )
        val repository = visibilityJobRepository(jobStore)
        val externalPointsClient = mock(ExternalPointsClient::class.java)
        val snapshotService = mock(SpacecraftVisibilitySnapshotService::class.java)
        `when`(
            externalPointsClient.fetchRawVisibilityPayload(
                org.mockito.ArgumentMatchers.anyLong(),
                anyNonNull(),
                anyNonNull()
            )
        ).thenReturn("[]")
        val worker = VisibilityRefreshWorker(
            planningProperties = planningProperties(),
            visibilityRefreshJobRepository = repository,
            spacecraftSnapshotRepository = mock(SpacecraftSnapshotRepository::class.java),
            externalPointsClient = externalPointsClient,
            spacecraftVisibilitySnapshotService = snapshotService,
            transactionTemplate = transactionTemplate()
        )

        worker.processPendingJobs()

        val job = jobStore.getValue(spacecraftId)
        assertEquals(JobStatus.DONE, job.status)
        assertEquals(false, job.rerunRequested)
        assertNull(job.lockedBy)
        assertNull(job.lockedUntil)
    }

    @Test
    fun `recalculation markDone with rerunRequested moves job to pending`() {
        val spacecraftId = "SC-3"
        val jobStore = linkedMapOf(
            spacecraftId to PlanRecalculationJobEntity(
                spacecraftId = spacecraftId,
                status = JobStatus.PENDING,
                rerunRequested = true
            )
        )
        val repository = recalculationJobRepository(jobStore)
        val rebuildPlanScheduleUseCase = mock(RebuildPlanScheduleUseCase::class.java)
        val worker = PlanRecalculationWorker(
            planningProperties = planningProperties(),
            planRecalculationJobRepository = repository,
            rebuildPlanScheduleUseCase = rebuildPlanScheduleUseCase,
            transactionTemplate = transactionTemplate()
        )

        worker.processPendingJobs()

        val job = jobStore.getValue(spacecraftId)
        assertEquals(JobStatus.PENDING, job.status)
        assertEquals(false, job.rerunRequested)
        assertNull(job.lockedBy)
        assertNull(job.lockedUntil)
    }

    @Test
    fun `recalculation markDone without rerunRequested moves job to done`() {
        val spacecraftId = "SC-4"
        val jobStore = linkedMapOf(
            spacecraftId to PlanRecalculationJobEntity(
                spacecraftId = spacecraftId,
                status = JobStatus.PENDING
            )
        )
        val repository = recalculationJobRepository(jobStore)
        val rebuildPlanScheduleUseCase = mock(RebuildPlanScheduleUseCase::class.java)
        val worker = PlanRecalculationWorker(
            planningProperties = planningProperties(),
            planRecalculationJobRepository = repository,
            rebuildPlanScheduleUseCase = rebuildPlanScheduleUseCase,
            transactionTemplate = transactionTemplate()
        )

        worker.processPendingJobs()

        val job = jobStore.getValue(spacecraftId)
        assertEquals(JobStatus.DONE, job.status)
        assertEquals(false, job.rerunRequested)
        assertNull(job.lockedBy)
        assertNull(job.lockedUntil)
    }

    private fun visibilityUpsertQuery(): String =
        VisibilityRefreshJobRepository::class.java
            .getMethod(
                "upsertPending",
                String::class.java,
                java.lang.Long::class.java,
                String::class.java,
                LocalDateTime::class.java
            )
            .getAnnotation(Query::class.java)
            .value

    private fun recalculationUpsertQuery(): String =
        PlanRecalculationJobRepository::class.java
            .getMethod(
                "upsertPending",
                String::class.java,
                String::class.java,
                LocalDateTime::class.java
            )
            .getAnnotation(Query::class.java)
            .value

    private fun visibilityJobRepository(
        store: MutableMap<String, VisibilityRefreshJobEntity>
    ): VisibilityRefreshJobRepository {
        val repository = mock(VisibilityRefreshJobRepository::class.java)
        `when`(
            repository.findPendingForUpdate(
                anyNonNull(),
                org.mockito.ArgumentMatchers.anyInt()
            )
        ).thenAnswer { invocation ->
            val now = invocation.arguments[0] as LocalDateTime
            val limit = invocation.arguments[1] as Int
            store.values
                .filter { job -> job.status == JobStatus.PENDING || isExpiredRunning(job.status, job.lockedUntil, now) }
                .sortedBy { job -> job.requestedAt }
                .take(limit)
        }
        `when`(repository.findById(anyNonNull())).thenAnswer { invocation ->
            Optional.ofNullable(store[invocation.arguments[0]])
        }
        `when`(repository.save(anyNonNull())).thenAnswer { invocation ->
            val job = invocation.arguments[0] as VisibilityRefreshJobEntity
            store[job.spacecraftId] = job
            job
        }
        `when`(repository.saveAll<VisibilityRefreshJobEntity>(anyNonNull())).thenAnswer { invocation ->
            val jobs = invocation.arguments[0] as Iterable<VisibilityRefreshJobEntity>
            jobs.forEach { job -> store[job.spacecraftId] = job }
            jobs.toList()
        }
        return repository
    }

    private fun recalculationJobRepository(
        store: MutableMap<String, PlanRecalculationJobEntity>
    ): PlanRecalculationJobRepository {
        val repository = mock(PlanRecalculationJobRepository::class.java)
        `when`(
            repository.findPendingForUpdate(
                anyNonNull(),
                org.mockito.ArgumentMatchers.anyInt()
            )
        ).thenAnswer { invocation ->
            val now = invocation.arguments[0] as LocalDateTime
            val limit = invocation.arguments[1] as Int
            store.values
                .filter { job -> job.status == JobStatus.PENDING || isExpiredRunning(job.status, job.lockedUntil, now) }
                .sortedBy { job -> job.requestedAt }
                .take(limit)
        }
        `when`(repository.findById(anyNonNull())).thenAnswer { invocation ->
            Optional.ofNullable(store[invocation.arguments[0]])
        }
        `when`(repository.save(anyNonNull())).thenAnswer { invocation ->
            val job = invocation.arguments[0] as PlanRecalculationJobEntity
            store[job.spacecraftId] = job
            job
        }
        `when`(repository.saveAll<PlanRecalculationJobEntity>(anyNonNull())).thenAnswer { invocation ->
            val jobs = invocation.arguments[0] as Iterable<PlanRecalculationJobEntity>
            jobs.forEach { job -> store[job.spacecraftId] = job }
            jobs.toList()
        }
        return repository
    }

    private fun isExpiredRunning(status: JobStatus, lockedUntil: LocalDateTime?, now: LocalDateTime): Boolean =
        status == JobStatus.RUNNING &&
            lockedUntil != null &&
            lockedUntil.isBefore(now)

    private fun planningProperties(): PlanningProperties =
        PlanningProperties(30, 180, 60, false, 3, 14, 0, 30_000, 5, 300)

    private fun transactionTemplate(): TransactionTemplate =
        TransactionTemplate(
            object : PlatformTransactionManager {
                override fun getTransaction(definition: TransactionDefinition?): TransactionStatus = SimpleTransactionStatus()
                override fun commit(status: TransactionStatus) = Unit
                override fun rollback(status: TransactionStatus) = Unit
            }
        )

    @Suppress("UNCHECKED_CAST")
    private fun <T> anyNonNull(): T {
        org.mockito.ArgumentMatchers.any<T>()
        return null as T
    }
}
