import com.correx.core.artifacts.kind.ConfigArtifactKind
import com.correx.core.artifacts.kind.FileWrittenKind
import com.correx.core.artifacts.kind.JsonSchema
import com.correx.core.artifacts.kind.TypedArtifactSlot
import com.correx.core.context.model.ContextLayer
import com.correx.core.context.model.EntryRole
import com.correx.core.events.events.FileWrittenEvent
import com.correx.core.events.events.RefinementIterationEvent
import com.correx.core.events.events.RetryAttemptedEvent
import com.correx.core.events.events.ToolInvocationRequestedEvent
import com.correx.core.events.events.ToolRequest
import com.correx.core.events.types.ToolInvocationId
import com.correx.core.approvals.ApprovalOutcome
import com.correx.core.approvals.ApprovalStatus
import com.correx.core.approvals.Tier
import com.correx.core.events.events.ApprovalDecisionResolvedEvent
import com.correx.core.events.events.ApprovalRequestedEvent
import com.correx.core.events.types.ApprovalDecisionId
import com.correx.core.events.types.ApprovalRequestId
import com.correx.core.events.types.ArtifactId
import com.correx.core.events.types.ValidationReportId
import com.correx.core.kernel.orchestration.buildRejectionFeedbackEntry
import kotlinx.datetime.Instant
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import com.correx.core.events.types.TransitionId
import com.correx.core.events.events.RepoKnowledgeHit
import com.correx.core.kernel.orchestration.buildAgentInstructionsEntry
import com.correx.core.kernel.orchestration.buildArtifactKindVocabularyEntry
import com.correx.core.kernel.orchestration.buildProjectProfileEntry
import com.correx.core.kernel.orchestration.buildRelevantFilesEntry
import com.correx.core.kernel.orchestration.buildRetryFeedbackEntry
import com.correx.core.kernel.orchestration.criticArtifactIds
import com.correx.core.sessions.BoundAgentInstructions
import com.correx.core.sessions.BoundProjectProfile
import com.correx.core.transitions.graph.StageConfig
import com.correx.core.transitions.graph.TransitionEdge
import com.correx.core.transitions.graph.WorkflowGraph
import com.correx.testing.fixtures.EventFixtures.stored
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test

class ContextFeedbackTest {

    @Test
    fun `no retry events yields null`() {
        assertNull(buildRetryFeedbackEntry(emptyList(), StageId("impl")))
    }

    @Test
    fun `latest retry event for the stage becomes a labeled L1 user repair mandate`() {
        val events = listOf(
            stored(payload = RetryAttemptedEvent(SessionId("s"), StageId("impl"), 1, 3, "old reason")),
            stored(payload = RetryAttemptedEvent(SessionId("s"), StageId("other"), 1, 3, "other stage")),
            stored(payload = RetryAttemptedEvent(SessionId("s"), StageId("impl"), 2, 3, "tests failed: NPE in FooTest")),
        )
        val entry = buildRetryFeedbackEntry(events, StageId("impl"))!!
        assertEquals("retryFeedback", entry.sourceType)
        assertEquals(ContextLayer.L1, entry.layer)
        // #293: USER role so it renders as the trailing repair mandate, not folded into leading system.
        assertEquals(EntryRole.USER, entry.role)
        assertTrue(entry.content.contains("Attempt 2 of 3"), "content: ${entry.content}")
        assertTrue(entry.content.contains("tests failed: NPE in FooTest"), "content: ${entry.content}")
    }

    @Test
    fun `retry repair state names authoritative CAS images of files this stage already wrote`() {
        val inv = ToolInvocationId("inv-1")
        val events = listOf(
            stored(
                payload = ToolInvocationRequestedEvent(
                    invocationId = inv, sessionId = SessionId("s"), stageId = StageId("impl"),
                    toolName = "file_write", tier = Tier.T2,
                    request = ToolRequest(
                        invocationId = inv, sessionId = SessionId("s"), stageId = StageId("impl"),
                        toolName = "file_write", parameters = mapOf("path" to "frontend/src/hooks/queries.ts"),
                    ),
                ),
            ),
            stored(
                payload = FileWrittenEvent(
                    invocationId = inv, sessionId = SessionId("s"), path = "frontend/src/hooks/queries.ts",
                    postImageHash = "cafebabe", preExisted = true, timestampMs = 0,
                ),
            ),
            stored(
                payload = RetryAttemptedEvent(
                    SessionId("s"), StageId("impl"), 2, 3,
                    "build gate: queries.ts(39,3): error TS1005: '}' expected", gate = "execution",
                ),
            ),
        )
        val entry = buildRetryFeedbackEntry(events, StageId("impl"))!!
        assertTrue(entry.content.contains("## Retry repair state"), "content: ${entry.content}")
        assertTrue(entry.content.contains("gate 'execution'"), "content: ${entry.content}")
        // Path only: the raw CAS hash is opaque noise the model can't act on and confuses it into
        // reasoning about hashes — it patches by path via file_read/file_write.
        assertTrue(entry.content.contains("- frontend/src/hooks/queries.ts"), "content: ${entry.content}")
        assertFalse(entry.content.contains("cafebabe"), "content: ${entry.content}")
        assertTrue(entry.content.contains("do NOT re-read"), "content: ${entry.content}")
    }

    @Test
    fun `retry entry for other stage is not returned for queried stage`() {
        val events = listOf(
            stored(payload = RetryAttemptedEvent(SessionId("s"), StageId("other"), 1, 3, "reason")),
        )
        assertNull(buildRetryFeedbackEntry(events, StageId("impl")))
    }

    @Test
    fun `rejection feedback is one stage-scoped entry naming tool preview tier and reason`() {
        val req = ApprovalRequestId("req-1")
        val events = listOf(
            stored(
                payload = ApprovalRequestedEvent(
                    requestId = req, tier = Tier.T2,
                    validationReportId = ValidationReportId("vr-1"), riskSummaryId = null,
                    sessionId = SessionId("s"), stageId = StageId("impl"), projectId = null,
                    toolName = "shell", preview = "rm -rf build/",
                ),
            ),
            stored(
                payload = ApprovalDecisionResolvedEvent(
                    decisionId = ApprovalDecisionId("d-1"), requestId = req,
                    outcome = ApprovalOutcome.REJECTED, status = ApprovalStatus.COMPLETED,
                    tier = Tier.T2, resolutionTimestamp = Instant.fromEpochMilliseconds(0),
                    reason = "destructive; do not delete build output",
                ),
            ),
        )
        val entry = buildRejectionFeedbackEntry(events, StageId("impl"))!!
        assertEquals("rejectionFeedback", entry.sourceType)
        assertEquals(ContextLayer.L2, entry.layer)
        assertTrue(entry.content.contains("## Rejected tool calls this stage"), entry.content)
        assertTrue(entry.content.contains("shell (tier T2) rm -rf build/"), entry.content)
        assertTrue(entry.content.contains("destructive; do not delete build output"), entry.content)
    }

    @Test
    fun `rejection feedback ignores other stages and non-rejected decisions`() {
        val reqOther = ApprovalRequestId("req-o")
        val events = listOf(
            stored(
                payload = ApprovalRequestedEvent(
                    requestId = reqOther, tier = Tier.T1,
                    validationReportId = ValidationReportId("vr-2"), riskSummaryId = null,
                    sessionId = SessionId("s"), stageId = StageId("other"), projectId = null,
                    toolName = "file_write", preview = "x",
                ),
            ),
            stored(
                payload = ApprovalDecisionResolvedEvent(
                    decisionId = ApprovalDecisionId("d-2"), requestId = reqOther,
                    outcome = ApprovalOutcome.REJECTED, status = ApprovalStatus.COMPLETED,
                    tier = Tier.T1, resolutionTimestamp = Instant.fromEpochMilliseconds(0), reason = null,
                ),
            ),
        )
        assertNull(buildRejectionFeedbackEntry(events, StageId("impl")))
    }

    @Test
    fun `project profile renders as single L0 entry`() {
        val entry = buildProjectProfileEntry(
            BoundProjectProfile(
                about = "Event-sourced kernel",
                conventions = listOf("no bare try-catch"),
                commands = mapOf("test" to "./gradlew check"),
            ),
        )
        assertEquals(ContextLayer.L0, entry.layer)
        assertEquals(EntryRole.SYSTEM, entry.role)
        assertTrue(entry.content.startsWith("## Project profile"), "content: ${entry.content}")
        assertTrue(entry.content.contains("- no bare try-catch"), "content: ${entry.content}")
        assertTrue(entry.content.contains("test: ./gradlew check"), "content: ${entry.content}")
    }

    @Test
    fun `project profile with only about renders without conventions or commands sections`() {
        val entry = buildProjectProfileEntry(
            BoundProjectProfile(
                about = "Simple project",
                conventions = emptyList(),
                commands = emptyMap(),
            ),
        )
        assertTrue(entry.content.contains("Simple project"), "content: ${entry.content}")
        assertTrue(!entry.content.contains("### Conventions"), "should not contain Conventions: ${entry.content}")
        assertTrue(!entry.content.contains("### Commands"), "should not contain Commands: ${entry.content}")
        assertEquals("projectProfile", entry.sourceType)
    }

    @Test
    fun `agent instructions render as single L0 entry`() {
        val entry = buildAgentInstructionsEntry(
            BoundAgentInstructions(
                sources = listOf("CLAUDE.md", "AGENTS.md"),
                content = "# CLAUDE.md\n\nbe careful\n\n# AGENTS.md\n\nuse tools",
            ),
        )
        assertEquals(ContextLayer.L0, entry.layer)
        assertEquals(EntryRole.SYSTEM, entry.role)
        assertEquals("agentInstructions", entry.sourceType)
        assertTrue(entry.content.contains("# CLAUDE.md"), "content: ${entry.content}")
        assertTrue(entry.content.contains("# AGENTS.md"), "content: ${entry.content}")
    }

    @Test
    fun `criticArtifactIds resolves the from-stage produces on a back-edge`() {
        val graph = graphWith(
            "implement" to producesSlots("patch"),
            "review" to producesSlots("review_report"),
        )
        val events = listOf(
            stored(payload = RefinementIterationEvent(SessionId("s"), "review->implement", 1, 3)),
        )
        assertEquals(setOf(ArtifactId("review_report")), criticArtifactIds(events, graph, StageId("implement")))
    }

    @Test
    fun `vocabulary entry enumerates kind ids and flags llm-emitted`() {
        val plain = ConfigArtifactKind(id = "plan", schema = JsonSchema(type = "object"), llmEmitted = false)
        val emitted = ConfigArtifactKind(id = "report", schema = JsonSchema(type = "object"), llmEmitted = true)
        val entry = buildArtifactKindVocabularyEntry(listOf(plain, emitted))
        assertEquals("artifactKindVocabulary", entry.sourceType)
        assertEquals(ContextLayer.L1, entry.layer)
        assertEquals(EntryRole.SYSTEM, entry.role)
        assertTrue(entry.content.contains("plan"), "content should contain plain id: ${entry.content}")
        assertTrue(entry.content.contains("report (llm-emitted)"), "content should flag llm-emitted: ${entry.content}")
    }

    @Test
    fun `buildRelevantFilesEntry produces L3 user entry with Relevant files header`() {
        val hits = listOf(
            RepoKnowledgeHit(path = "src/Parser.kt", text = "src/Parser.kt: Parser, Lexer", score = 0.9f),
            RepoKnowledgeHit(path = "src/Main.kt", text = "src/Main.kt: main", score = 0.7f),
        )
        val entry = buildRelevantFilesEntry(hits)
        assertEquals(ContextLayer.L3, entry.layer)
        assertEquals(EntryRole.USER, entry.role)
        assertEquals("relevantFiles", entry.sourceType)
        assertTrue(entry.content.startsWith("## Relevant files"), "content: ${entry.content}")
        assertTrue(entry.content.contains("src/Parser.kt"), "content: ${entry.content}")
    }

    @Test
    fun `no refinement events yields empty set`() {
        assertTrue(criticArtifactIds(emptyList(), graphWith(), StageId("implement")).isEmpty())
    }

    @Test
    fun `criticArtifactIds ignores events targeting a different stage`() {
        val graph = graphWith(
            "implement" to producesSlots("patch"),
            "review" to producesSlots("review_report"),
        )
        val events = listOf(
            stored(payload = RefinementIterationEvent(SessionId("s"), "review->other", 1, 3)),
        )
        assertTrue(criticArtifactIds(events, graph, StageId("implement")).isEmpty())
    }
}

private fun producesSlots(vararg names: String): List<TypedArtifactSlot> =
    names.map { TypedArtifactSlot(name = ArtifactId(it), kind = FileWrittenKind) }

private fun graphWith(vararg stages: Pair<String, List<TypedArtifactSlot>>): WorkflowGraph {
    val stageMap = stages.associate { (id, slots) ->
        StageId(id) to StageConfig(produces = slots)
    }.toMutableMap()
    if (stageMap.isEmpty()) stageMap[StageId("start")] = StageConfig()
    return WorkflowGraph(
        id = "test-graph",
        stages = stageMap,
        transitions = emptySet(),
        start = stageMap.keys.first(),
    )
}
