package com.correx.core.kernel.orchestration

import com.correx.core.approvals.Tier
import com.correx.core.events.events.EventMetadata
import com.correx.core.events.events.EventPayload
import com.correx.core.events.events.FileWrittenEvent
import com.correx.core.events.events.LspDiagnostic
import com.correx.core.events.events.LspDiagnosticsCompletedEvent
import com.correx.core.events.events.StoredEvent
import com.correx.core.events.events.ToolInvocationRequestedEvent
import com.correx.core.events.events.ToolRequest
import com.correx.core.events.types.EventId
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import com.correx.core.events.types.ToolInvocationId
import kotlinx.datetime.Instant
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test

/** #309: same-fingerprint loop-breaker + repair-ledger data source, unit-tested as pure folds. */
class RecoveryFileLoopBreakTest {

    private val stage = StageId("recovery")
    private val session = SessionId("s1")
    private var seq = 0L

    @Test
    fun `a file rewritten past the limit with a diagnostic that never clears trips the breaker`() {
        val events = buildList {
            repeat(3) { addAll(writeThenDiagnose("SessionsList.tsx", cleared = false, code = "TS6133")) }
        }
        val reason = recoveryFileLoopBreakPure(events, stage, limit = 3)
        assertNotNull(reason)
        assertTrue(reason!!.contains("SessionsList.tsx"), "reason: $reason")
        assertTrue(reason.contains("3x"), "reason: $reason")
        assertTrue(reason.contains("TS6133"), "reason: $reason")
    }

    @Test
    fun `a file whose diagnostic clears before the limit does not trip the breaker`() {
        val events = buildList {
            addAll(writeThenDiagnose("MainLayout.tsx", cleared = false, code = "TS1005"))
            addAll(writeThenDiagnose("MainLayout.tsx", cleared = true))
        }
        assertNull(recoveryFileLoopBreakPure(events, stage, limit = 3))
    }

    @Test
    fun `ledger annotates an unresolved file distinctly from a resolved one`() {
        val events = buildList {
            repeat(3) { addAll(writeThenDiagnose("SessionsList.tsx", cleared = false, code = "TS6133")) }
            addAll(writeThenDiagnose("MainLayout.tsx", cleared = false, code = "TS1005"))
            addAll(writeThenDiagnose("MainLayout.tsx", cleared = true))
        }
        val outcomes = fileRepairOutcomes(events, stage).associateBy { it.path }
        val stuck = outcomes.getValue("SessionsList.tsx")
        assertEquals(false, stuck.resolved)
        assertEquals(3, stuck.writeCount)
        assertEquals(setOf("TS6133"), stuck.persistentCodes)
        assertTrue(
            describeFileRepairOutcome(stuck).let {
                it.contains("written 3x") && it.contains("TS6133") &&
                    it.contains("Re-writing has not changed the result")
            },
        )

        val fixed = outcomes.getValue("MainLayout.tsx")
        assertEquals(true, fixed.resolved)
        assertEquals(2, fixed.writeCount)
        assertEquals(2, fixed.clearedAtWrite)
        assertTrue(describeFileRepairOutcome(fixed).let { it.contains("cleared after write 2") })
    }

    @Test
    fun `a write with no diagnostic run after it reads as unchecked, never as resolved`() {
        val events = buildList {
            repeat(3) { addAll(writeThenDiagnose("SessionsList.tsx", cleared = false, code = "TS6133")) }
            addAll(writeOnly("SessionsList.tsx"))
        }
        val outcome = fileRepairOutcomes(events, stage).single()
        assertTrue(outcome.unchecked)
        assertEquals(false, outcome.resolved, "an unverified write must not read as clean")
        assertTrue(describeFileRepairOutcome(outcome).contains("not re-checked"))
        // and it must not terminally kill the run on the absence of evidence
        assertNull(recoveryFileLoopBreakPure(events, stage, limit = 3))
    }

    // recoveryFileLoopBreak is an extension on DefaultSessionOrchestrator that reads the event store;
    // its pure core is fileRepairOutcomes, exercised directly here for the same result without needing
    // to stand up an orchestrator instance.
    private fun recoveryFileLoopBreakPure(events: List<StoredEvent>, stageId: StageId, limit: Int): String? {
        val stuck = fileRepairOutcomes(events, stageId)
            .firstOrNull { !it.resolved && !it.unchecked && it.writeCount >= limit }
            ?: return null
        val codes = stuck.persistentCodes.takeIf { it.isNotEmpty() }?.joinToString(", ") ?: "its diagnostic"
        return "recovery stage ${stageId.value} rewrote '${stuck.path}' ${stuck.writeCount}x without " +
            "clearing $codes — the same fix has not changed the result. A materially different fix is " +
            "required; escalating instead of continuing to loop."
    }

    private fun ev(payload: EventPayload) = StoredEvent(
        metadata = EventMetadata(
            eventId = EventId("e${seq++}"),
            sessionId = session,
            timestamp = Instant.parse("2026-01-01T00:00:00Z"),
            schemaVersion = 1,
            causationId = null,
            correlationId = null,
        ),
        sequence = seq,
        sessionSequence = seq,
        payload = payload,
    )

    private fun writeOnly(path: String): List<StoredEvent> = writeThenDiagnose(path, cleared = true).dropLast(1)

    private fun writeThenDiagnose(path: String, cleared: Boolean, code: String? = null): List<StoredEvent> {
        val inv = ToolInvocationId("inv-${seq}")
        val req = ev(
            ToolInvocationRequestedEvent(
                invocationId = inv, sessionId = session, stageId = stage,
                toolName = "file_edit", tier = Tier.T2,
                request = ToolRequest(inv, session, stage, "file_edit", mapOf("path" to path)),
            ),
        )
        val write = ev(
            FileWrittenEvent(
                invocationId = inv, sessionId = session, path = path,
                postImageHash = "h${seq}", preExisted = true, timestampMs = 0,
            ),
        )
        val diagnostics = if (cleared) {
            emptyList()
        } else {
            listOf(LspDiagnostic(path = path, line = 1, character = 1, severity = "error", code = code, message = "m"))
        }
        val diag = ev(LspDiagnosticsCompletedEvent(session, stage, "tsserver", diagnostics))
        return listOf(req, write, diag)
    }
}
