package com.correx.core.kernel.orchestration

import com.correx.core.artifactstore.ArtifactStore
import com.correx.core.events.events.EventPayload
import com.correx.core.events.events.JournalCompactedEvent
import com.correx.core.events.types.SessionId
import com.correx.core.journal.model.DecisionJournalState
import com.correx.core.journal.model.DecisionKind
import com.correx.core.journal.model.DecisionRecord
import com.correx.core.utils.TypeId
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withTimeout
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

class JournalCompactionServiceTest {

    private val fixedArtifactId = TypeId("artifact-001")

    private fun fakeArtifactStore(): ArtifactStore = object : ArtifactStore {
        override suspend fun put(bytes: ByteArray) = fixedArtifactId
        override suspend fun get(id: TypeId): ByteArray? = null
        override suspend fun flushBefore(commit: suspend () -> Unit) = commit()
    }

    private fun stateWithRecords(vararg records: DecisionRecord) =
        DecisionJournalState(records = records.toList())

    private fun makeRecord(seq: Long, kind: DecisionKind) =
        DecisionRecord(sequence = seq, kind = kind, summary = "summary of $kind")

    @Test
    fun `returns false when token estimate is below threshold`(): Unit = runBlocking {
        val svc = JournalCompactionService(fakeArtifactStore(), { it }, tokenThreshold = { 2000 })
        val state = stateWithRecords(makeRecord(1, DecisionKind.INTENT))
        val emitted = mutableListOf<EventPayload>()
        val result = svc.compactIfNeeded(SessionId("s1"), state, renderedTokenEstimate = 500) { emitted += it }
        assertFalse(result)
        assertTrue(emitted.isEmpty())
    }

    @Test
    fun `returns false when records is empty even if estimate is high`(): Unit = runBlocking {
        val svc = JournalCompactionService(fakeArtifactStore(), { it }, tokenThreshold = { 2000 })
        val state = DecisionJournalState(records = emptyList())
        val emitted = mutableListOf<EventPayload>()
        val result = svc.compactIfNeeded(SessionId("s1"), state, renderedTokenEstimate = 9999) { emitted += it }
        assertFalse(result)
        assertTrue(emitted.isEmpty())
    }

    @Test
    fun `emits JournalCompactedEvent with correct coversThroughSequence and lowSalienceOmittedCount`():
        Unit = runBlocking {
        val svc = JournalCompactionService(fakeArtifactStore(), { "summary" }, tokenThreshold = { 100 })
        val state = stateWithRecords(
            makeRecord(1, DecisionKind.INTENT),      // HIGH
            makeRecord(2, DecisionKind.TRANSITION),  // LOW
            makeRecord(3, DecisionKind.APPROVAL),    // HIGH
            makeRecord(4, DecisionKind.RETRY),       // LOW
        )
        val emitted = mutableListOf<EventPayload>()
        val result = svc.compactIfNeeded(SessionId("s1"), state, renderedTokenEstimate = 500) { emitted += it }
        assertTrue(result)
        assertEquals(1, emitted.size)
        val event = emitted.first() as JournalCompactedEvent
        assertEquals(4L, event.coversThroughSequence)
        assertEquals(2, event.lowSalienceOmittedCount)
        assertEquals(fixedArtifactId, event.summaryArtifactId)
    }

    // Regression: append() flushes artifacts inside the SAME non-reentrant Mutex that flushBefore
    // holds. If compaction wraps its emit in flushBefore, the emit->append->flushBefore re-acquires
    // that Mutex and self-deadlocks. This store mirrors production locking so the deadlock is real;
    // the fake above cannot catch it because its flushBefore takes no lock.
    private fun reentrantHostileArtifactStore(): ArtifactStore = object : ArtifactStore {
        val mutex = Mutex()
        override suspend fun put(bytes: ByteArray) = fixedArtifactId
        override suspend fun get(id: TypeId): ByteArray? = null
        override suspend fun flushBefore(commit: suspend () -> Unit) = mutex.withLock { commit() }
    }

    @Test
    fun `does not deadlock when emit re-enters the artifact store (as append does)`(): Unit = runBlocking {
        val store = reentrantHostileArtifactStore()
        val svc = JournalCompactionService(store, { "summary" }, tokenThreshold = { 100 })
        val state = stateWithRecords(makeRecord(1, DecisionKind.INTENT))
        // emit simulates SqliteEventStore.append: it flushes artifacts inside the store mutex.
        withTimeout(2000) {
            svc.compactIfNeeded(SessionId("s1"), state, renderedTokenEstimate = 500) {
                store.flushBefore { /* persist event row */ }
            }
        }
    }

    @Test
    fun `only HIGH-salience records are included in the summarize prompt`(): Unit = runBlocking {
        val capturedPrompts = mutableListOf<String>()
        val svc = JournalCompactionService(
            artifactStore = fakeArtifactStore(),
            summarize = { prompt -> capturedPrompts += prompt; "ok" },
            tokenThreshold = { 100 },
        )
        val state = stateWithRecords(
            makeRecord(1, DecisionKind.INTENT),      // HIGH
            makeRecord(2, DecisionKind.TRANSITION),  // LOW — should not appear in prompt
            makeRecord(3, DecisionKind.ARTIFACT),    // LOW — should not appear in prompt
            makeRecord(4, DecisionKind.FAILURE),     // HIGH
        )
        val emitted = mutableListOf<EventPayload>()
        svc.compactIfNeeded(SessionId("s1"), state, renderedTokenEstimate = 500) { emitted += it }
        assertEquals(1, capturedPrompts.size)
        val prompt = capturedPrompts.first()
        assertTrue(prompt.contains("INTENT"), "Prompt should contain INTENT")
        assertTrue(prompt.contains("FAILURE"), "Prompt should contain FAILURE")
        assertFalse(prompt.contains("TRANSITION"), "Prompt must not contain TRANSITION")
        assertFalse(prompt.contains("ARTIFACT"), "Prompt must not contain ARTIFACT")
    }
}
