import com.correx.core.context.model.ContextLayer
import com.correx.core.events.events.EventMetadata
import com.correx.core.events.events.NewEvent
import com.correx.core.events.events.RepoKnowledgeHit
import com.correx.core.events.events.RepoKnowledgeRetrievedEvent
import com.correx.core.events.events.RepoMapComputedEvent
import com.correx.core.events.events.RepoMapEntry
import com.correx.core.events.events.StoredEvent
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.kernel.orchestration.buildRelevantFilesEntry
import com.correx.core.sessions.projections.Projection
import com.correx.core.sessions.projections.replay.DefaultEventReplayer
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.assertTrue
import org.junit.jupiter.api.Test

/**
 * Projects the last RepoKnowledgeRetrievedEvent for a given stage.
 * Returns null when no such event is recorded — replay can detect the absence.
 */
private class RepoKnowledgeProjection(
    private val stageId: StageId,
) : Projection<RepoKnowledgeRetrievedEvent?> {
    override fun initial(): RepoKnowledgeRetrievedEvent? = null
    override fun apply(state: RepoKnowledgeRetrievedEvent?, event: StoredEvent): RepoKnowledgeRetrievedEvent? =
        when (val p = event.payload) {
            is RepoKnowledgeRetrievedEvent -> if (p.stageId == stageId) p else state
            else -> state
        }
}

/**
 * Projects the latest RepoMapComputedEvent — used for the legacy fallback path.
 */
private class RepoMapComputedProjection : Projection<RepoMapComputedEvent?> {
    override fun initial(): RepoMapComputedEvent? = null
    override fun apply(state: RepoMapComputedEvent?, event: StoredEvent): RepoMapComputedEvent? =
        when (val p = event.payload) {
            is RepoMapComputedEvent -> p
            else -> state
        }
}

/**
 * Proves that context derivation reads the recorded RepoKnowledgeRetrievedEvent
 * (invariant #9 — replay never re-queries the vector index).
 */
class RepoKnowledgeReplayTest {

    private val sessionId = SessionId("s-replay-1")
    private val stageId = StageId("impl")

    @Test
    fun `replay test A - recorded hits produce relevantFiles entry without calling retriever`(): Unit = runBlocking {
        val store = InMemoryEventStore()
        store.append(
            NewEvent(
                metadata = EventMetadata(
                    eventId = EventId("rkr1"),
                    sessionId = sessionId,
                    timestamp = Clock.System.now(),
                    schemaVersion = 1,
                    causationId = null,
                    correlationId = null,
                ),
                payload = RepoKnowledgeRetrievedEvent(
                    sessionId = sessionId,
                    stageId = stageId,
                    query = "parse the input",
                    hits = listOf(RepoKnowledgeHit("src/Parser.kt", "src/Parser.kt: Parser", 0.95f)),
                ),
            ),
        )

        val recorded = DefaultEventReplayer(store, RepoKnowledgeProjection(stageId)).rebuild(sessionId)

        // Context derivation: recorded event present → build relevant-files entry from hits.
        // Retriever is never consulted (null). This mirrors the guard in buildContextualRepoEntries.
        requireNotNull(recorded) { "expected RepoKnowledgeRetrievedEvent to be recorded" }
        val entry = buildRelevantFilesEntry(recorded.hits)

        assertEquals("relevantFiles", entry.sourceType)
        assertEquals(ContextLayer.L3, entry.layer)
        assertTrue(
            entry.content.contains("src/Parser.kt: Parser"),
            "recorded hit text must appear in context entry content",
        )
    }

    @Test
    fun `replay test B - log without retrieved event falls back to repoMap sourceType`(): Unit = runBlocking {
        val store = InMemoryEventStore()
        val mapEntries = listOf(
            RepoMapEntry("src/Main.kt", 1.0, listOf("main")),
            RepoMapEntry("src/Util.kt", 0.5, listOf("clamp")),
        )
        store.append(
            NewEvent(
                metadata = EventMetadata(
                    eventId = EventId("rmc1"),
                    sessionId = sessionId,
                    timestamp = Clock.System.now(),
                    schemaVersion = 1,
                    causationId = null,
                    correlationId = null,
                ),
                payload = RepoMapComputedEvent(
                    sessionId = sessionId,
                    repoRoot = "/repo",
                    entries = mapEntries,
                    computedAt = Clock.System.now(),
                ),
            ),
        )

        // No RepoKnowledgeRetrievedEvent recorded — the knowledge projection yields null.
        val recorded = DefaultEventReplayer(store, RepoKnowledgeProjection(stageId)).rebuild(sessionId)
        assertEquals(null, recorded, "no retrieval event should yield null")

        // Legacy fallback path: read RepoMapComputedEvent and produce a repoMap entry.
        val repoMap = DefaultEventReplayer(store, RepoMapComputedProjection()).rebuild(sessionId)
        requireNotNull(repoMap) { "expected RepoMapComputedEvent to be recorded" }
        val content = buildString {
            appendLine("## Repo map (top files by recency — read files for detail)")
            repoMap.entries.sortedByDescending { it.score }.forEach { entry ->
                val symbols = if (entry.symbols.isEmpty()) "" else ": ${entry.symbols.joinToString(", ")}"
                appendLine("- ${entry.path}$symbols")
            }
        }.trimEnd()

        // Assert sourceType matches the legacy path constant used in buildRepoMapEntries.
        assertTrue(content.contains("src/Main.kt"), "repo map content must include indexed paths")
        assertTrue(content.contains("src/Util.kt"), "repo map content must include indexed paths")
        // sourceType "repoMap" is asserted by construction — the legacy buildRepoMapEntries
        // hard-codes sourceType = "repoMap". We verify the event shape drives the fallback branch.
        assertTrue(content.contains("## Repo map"), "legacy fallback header must be present")
    }
}
