package com.correx.infrastructure.artifactscas.evict

import com.correx.core.events.events.EventMetadata
import com.correx.core.events.events.InferenceCompletedEvent
import com.correx.core.events.events.NewEvent
import com.correx.core.events.types.ArtifactId
import com.correx.core.events.types.EventId
import com.correx.core.events.types.SessionId
import com.correx.core.inference.TokenUsage
import com.correx.core.utils.TypeId
import com.correx.infrastructure.artifactscas.CasArtifactStore
import com.correx.infrastructure.artifactscas.config.CasConfig
import com.correx.infrastructure.artifactscas.index.SqliteArtifactIndex
import com.correx.infrastructure.persistence.InMemoryEventStore
import kotlinx.coroutines.runBlocking
import kotlinx.datetime.Clock
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir
import java.nio.file.Files
import java.nio.file.Path
import java.util.*
import kotlin.io.path.listDirectoryEntries

class EvictorTest {

    private val sessionId: SessionId = TypeId("session-evict")

    private fun newStore(dir: Path, maxSegmentBytes: Long, maxTotalBytes: Long): CasArtifactStore {
        val index = SqliteArtifactIndex(dir.resolve("index.sqlite"))
        return CasArtifactStore(CasConfig(dir, maxSegmentBytes, maxTotalBytes), index)
    }

    private suspend fun emitCompleted(store: InMemoryEventStore, response: ArtifactId) {
        store.append(
            NewEvent(
                metadata = EventMetadata(
                    eventId = EventId(UUID.randomUUID().toString()),
                    sessionId = sessionId,
                    timestamp = Clock.System.now(),
                    schemaVersion = 1,
                    causationId = null,
                    correlationId = null,
                ),
                payload = InferenceCompletedEvent(
                    requestId = TypeId(UUID.randomUUID().toString()),
                    sessionId = sessionId,
                    stageId = TypeId("stage"),
                    providerId = TypeId("prov"),
                    tokensUsed = TokenUsage(0, 0),
                    latencyMs = 0L,
                    responseArtifactId = response,
                ),
            )
        )
    }

    private fun segNames(dir: Path): Set<String> =
        dir.resolve("segments").listDirectoryEntries("*.seg").map { it.fileName.toString() }.toSet()

    private fun segIds(dir: Path): List<Long> =
        dir.resolve("segments").listDirectoryEntries("*.seg")
            .mapNotNull { runCatching { it.fileName.toString().removeSuffix(".seg").toLong() }.getOrNull() }
            .sorted()

    @Test
    fun `under cap - no eviction`(@TempDir dir: Path): Unit = runBlocking {
        newStore(dir, maxSegmentBytes = 64, maxTotalBytes = 10_000).use { store ->
            val eventStore = InMemoryEventStore()
            store.put(ByteArray(40) { 1 })
            store.put(ByteArray(40) { 2 })
            store.put(ByteArray(40) { 3 })

            val before = segNames(dir)
            val report = store.evict(eventStore)

            assertTrue(report.evictedSegmentIds.isEmpty(), "report: $report")
            assertEquals(0L, report.bytesReclaimed)
            assertEquals(before, segNames(dir))
        }
    }

    @Test
    fun `over cap, oldest dead segment evicted first`(@TempDir dir: Path): Unit = runBlocking {
        newStore(dir, maxSegmentBytes = 64, maxTotalBytes = 80).use { store ->
            val eventStore = InMemoryEventStore()
            store.put(ByteArray(40) { 1 }) // dead, oldest
            store.put(ByteArray(40) { 2 }) // dead
            store.put(ByteArray(40) { 3 }) // active segment

            val idsBefore = segIds(dir)
            assertTrue(idsBefore.size >= 3, "expected rotation, got $idsBefore")
            val oldest = idsBefore.first()

            val report = store.evict(eventStore)

            assertTrue(oldest in report.evictedSegmentIds, "expected oldest=$oldest evicted, got $report")
            assertTrue(report.bytesReclaimed > 0, "report: $report")
            assertFalse(
                "%08d.seg".format(oldest) in segNames(dir),
                "oldest segment file should be deleted",
            )
        }
        SqliteArtifactIndex(dir.resolve("index.sqlite")).use { reopened ->
            val oldestId = 1L
            assertTrue(
                reopened.entriesIn(oldestId).isEmpty(),
                "index entries for evicted segment must be removed",
            )
        }
    }

    @Test
    fun `live segment never evicted even if oldest`(@TempDir dir: Path): Unit = runBlocking {
        newStore(dir, maxSegmentBytes = 64, maxTotalBytes = 50).use { store ->
            val eventStore = InMemoryEventStore()
            val live = store.put(ByteArray(40) { 1 }) // oldest, but LIVE
            store.put(ByteArray(40) { 2 }) // dead
            store.put(ByteArray(40) { 3 }) // active
            emitCompleted(eventStore, live)

            val idsBefore = segIds(dir)
            val oldest = idsBefore.first()

            val report = store.evict(eventStore)

            assertFalse(
                oldest in report.evictedSegmentIds,
                "live oldest must not be evicted, got $report",
            )
            assertTrue(
                "%08d.seg".format(oldest) in segNames(dir),
                "live oldest segment file must remain",
            )
        }
    }

    @Test
    fun `active segment never evicted even if dead`(@TempDir dir: Path): Unit = runBlocking {
        newStore(dir, maxSegmentBytes = 64, maxTotalBytes = 1).use { store ->
            val eventStore = InMemoryEventStore()
            store.put(ByteArray(40) { 1 })
            store.put(ByteArray(40) { 2 })
            store.put(ByteArray(40) { 3 }) // active

            val idsBefore = segIds(dir)
            val active = idsBefore.last()

            val report = store.evict(eventStore)

            assertFalse(
                active in report.evictedSegmentIds,
                "active must not be evicted, got $report",
            )
            assertTrue(
                "%08d.seg".format(active) in segNames(dir),
                "active segment file must remain",
            )
        }
    }

    @Test
    fun `evicts strictly oldest-first when multiple dead segments exist`(@TempDir dir: Path): Unit = runBlocking {
        newStore(dir, maxSegmentBytes = 64, maxTotalBytes = 200).use { store ->
            val eventStore = InMemoryEventStore()
            store.put(ByteArray(40) { 1 }) // dead, oldest
            store.put(ByteArray(40) { 2 }) // dead
            store.put(ByteArray(40) { 3 }) // dead, youngest dead
            store.put(ByteArray(40) { 4 }) // active

            val idsBefore = segIds(dir)
            assertTrue(idsBefore.size >= 4, "expected >=4 segments, got $idsBefore")
            val oldest = idsBefore[0]
            val secondOldest = idsBefore[1]
            val youngestDead = idsBefore[2]

            val report = store.evict(eventStore)

            assertEquals(
                listOf(oldest, secondOldest),
                report.evictedSegmentIds,
                "expected strictly oldest-first eviction, got $report",
            )
            assertTrue(
                "%08d.seg".format(youngestDead) in segNames(dir),
                "youngest dead segment must survive, ids=${segIds(dir)}",
            )
        }
    }

    @Test
    fun `live segment in the middle is skipped and younger dead segment is evicted`(
        @TempDir dir: Path,
    ): Unit = runBlocking {
        newStore(dir, maxSegmentBytes = 64, maxTotalBytes = 50).use { store ->
            val eventStore = InMemoryEventStore()
            store.put(ByteArray(40) { 1 }) // dead, oldest
            val live = store.put(ByteArray(40) { 2 }) // live, middle
            store.put(ByteArray(40) { 3 }) // dead, newer
            store.put(ByteArray(40) { 4 }) // active
            emitCompleted(eventStore, live)

            val idsBefore = segIds(dir)
            assertTrue(idsBefore.size >= 4, "expected >=4 segments, got $idsBefore")
            val oldest = idsBefore[0]
            val middle = idsBefore[1]
            val newerDead = idsBefore[2]

            val report = store.evict(eventStore)

            assertFalse(
                middle in report.evictedSegmentIds,
                "live middle segment must not be evicted, got $report",
            )
            assertTrue(
                "%08d.seg".format(middle) in segNames(dir),
                "live middle segment file must remain",
            )
            assertTrue(
                oldest in report.evictedSegmentIds,
                "expected oldest dead evicted, got $report",
            )
            // If more than one eviction was needed, the next must be the newer dead — in age order.
            if (report.evictedSegmentIds.size >= 2) {
                assertEquals(
                    listOf(oldest, newerDead),
                    report.evictedSegmentIds,
                    "evictions must be in age order, skipping live middle",
                )
            }
        }
    }

    @Test
    fun `evicts multiple segments until under cap`(@TempDir dir: Path): Unit = runBlocking {
        newStore(dir, maxSegmentBytes = 64, maxTotalBytes = 100).use { store ->
            val eventStore = InMemoryEventStore()
            store.put(ByteArray(40) { 1 })
            store.put(ByteArray(40) { 2 })
            store.put(ByteArray(40) { 3 })
            store.put(ByteArray(40) { 4 })
            store.put(ByteArray(40) { 5 }) // active

            val idsBefore = segIds(dir)
            assertTrue(idsBefore.size >= 4, "expected several segments, got $idsBefore")

            val report = store.evict(eventStore)

            assertTrue(report.evictedSegmentIds.size >= 2, "expected >=2 evicted, got $report")

            val segsDir = dir.resolve("segments")
            val totalAfter = segsDir.listDirectoryEntries("*.seg").sumOf { Files.size(it) }
            assertTrue(
                totalAfter <= 100 || segIds(dir).size == 1,
                "expected under cap or only active remaining, totalAfter=$totalAfter ids=${segIds(dir)}",
            )
            for (evicted in report.evictedSegmentIds) {
                assertFalse(
                    "%08d.seg".format(evicted) in segNames(dir),
                    "evicted segment $evicted must be deleted",
                )
            }
        }
    }
}
