package com.correx.core.kernel.concept

import com.correx.core.events.events.ConceptPromotedEvent
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.RetryAttemptedEvent
import com.correx.core.events.events.StageCompletedEvent
import com.correx.core.events.events.StageFailedEvent
import com.correx.core.events.events.StoredEvent
import com.correx.core.events.events.WorkflowFailedEvent
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 com.correx.core.events.types.TransitionId
import kotlinx.datetime.Instant
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test

class ConceptCompilerProjectionTest {

    private val projection = ConceptCompilerProjection()
    private var seq = 0L

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

    private fun retry(fp: String, stage: String = "impl", session: String = "s1", reason: String = "boom\ndetail") =
        stored(
            RetryAttemptedEvent(
                sessionId = SessionId(session),
                stageId = StageId(stage),
                attemptNumber = 1,
                maxAttempts = 3,
                failureReason = reason,
                gate = "lint",
                fingerprint = fp,
            ),
            session,
        )

    private fun completed(stage: String = "impl", session: String = "s1") =
        stored(StageCompletedEvent(SessionId(session), StageId(stage), TransitionId("t")), session)

    private fun failed(stage: String = "impl", session: String = "s1") =
        stored(StageFailedEvent(SessionId(session), StageId(stage), TransitionId("t"), "x"), session)

    private fun wrote(path: String, hash: String? = "cas1", session: String = "s1") =
        stored(
            FileWrittenEvent(
                invocationId = ToolInvocationId("inv"),
                sessionId = SessionId(session),
                path = path,
                postImageHash = hash,
                preExisted = true,
                timestampMs = 1L,
            ),
            session,
        )

    private fun fold(events: List<StoredEvent>) =
        events.fold(projection.initial(), projection::apply)

    // All helpers default to reason "boom\ndetail" on gate "lint" → the single class key "lint:boom".
    private val ConceptCompilerState.only get() = clusters.values.single()

    @Test
    fun `retry then StageCompleted counts one validated fix`() {
        val state = fold(listOf(retry("fp1"), completed()))
        assertEquals(1, state.only.validatedFixes)
        assertTrue(state.promotable().isEmpty())
    }

    @Test
    fun `three cross-session validated fixes cross the threshold`() {
        val state = fold(
            listOf(
                retry("fp1", session = "sa"), completed(session = "sa"),
                retry("fp1", session = "sb"), completed(session = "sb"),
                retry("fp1", session = "sc"), completed(session = "sc"),
            ),
        )
        assertEquals(3, state.only.validatedFixes)
        assertEquals(listOf("lint:boom"), state.promotable().map { it.classKey })
    }

    @Test
    fun `distinct fingerprints of the same failure class aggregate into one concept`() {
        // Same normalized signature, different volatile tokens (paths/line numbers) → one class key.
        val state = fold(
            listOf(
                retry("fpA", session = "sa", reason = "TS2322 in 'src/App.tsx:12'"), completed(session = "sa"),
                retry("fpB", session = "sb", reason = "TS2322 in 'src/Nav.tsx:88'"), completed(session = "sb"),
                retry("fpC", session = "sc", reason = "TS2322 in 'src/Home.tsx:5'"), completed(session = "sc"),
            ),
        )
        assertEquals(1, state.clusters.size)
        assertEquals(3, state.only.validatedFixes)
        assertEquals(listOf("lint:ts# in <str>"), state.promotable().map { it.classKey })
    }

    @Test
    fun `StageFailed while class open contradicts the cluster`() {
        val state = fold(
            listOf(
                retry("fp1", session = "sa"), completed(session = "sa"),
                retry("fp1", session = "sb"), completed(session = "sb"),
                retry("fp1", session = "sc"), failed(session = "sc"),
            ),
        )
        assertTrue(state.only.contradicted)
        assertTrue(state.promotable().isEmpty())
    }

    @Test
    fun `WorkflowFailed contradicts every open class in that session`() {
        val state = fold(
            listOf(
                retry("fp1", stage = "a", session = "sa", reason = "alpha broke"),
                retry("fp2", stage = "b", session = "sa", reason = "beta broke"),
                stored(WorkflowFailedEvent(SessionId("sa"), StageId("a"), "dead", true), "sa"),
            ),
        )
        assertEquals(2, state.clusters.size)
        assertTrue(state.clusters.values.all { it.contradicted })
    }

    @Test
    fun `promotion is idempotent - a promoted class is not promotable again`() {
        val base = listOf(
            retry("fp1", session = "sa"), completed(session = "sa"),
            retry("fp1", session = "sb"), completed(session = "sb"),
            retry("fp1", session = "sc"), completed(session = "sc"),
        )
        val promoted = base + stored(ConceptPromotedEvent(SessionId("sc"), "fp1", "lint:boom", "lint", "concept", 3))
        assertTrue(fold(promoted).promotable().isEmpty())
    }

    @Test
    fun `a file written between retry and completion is captured as the validated fix ref`() {
        val state = fold(listOf(retry("fp1"), wrote("src/App.tsx", "hashA"), completed()))
        assertEquals("src/App.tsx", state.only.fixPath)
        assertEquals("hashA", state.only.fixHash)
    }

    @Test
    fun `first validated fix ref wins and a later stage's writes do not back-attribute`() {
        val state = fold(
            listOf(
                retry("fp1", session = "sa"), wrote("first.kt", "h1", session = "sa"), completed(session = "sa"),
                retry("fp1", session = "sb"), wrote("second.kt", "h2", session = "sb"), completed(session = "sb"),
            ),
        )
        assertEquals("first.kt", state.only.fixPath)
        assertEquals("h1", state.only.fixHash)
    }

    @Test
    fun `signature is the first line of the failure reason`() {
        val state = fold(listOf(retry("fp1", reason = "detekt: MagicNumber\ntrace..."), completed()))
        assertEquals("detekt: MagicNumber", state.only.signature)
    }
}
