package com.correx.core.toolintent

import com.correx.core.events.events.ToolRequest
import com.correx.core.events.risk.RiskAction
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.toolintent.rules.ManifestContainmentRule
import com.correx.core.tools.contract.ParamRole
import com.correx.core.tools.contract.ToolCapability
import java.nio.file.Path
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue

class ManifestContainmentRuleTest {

    private val workspace = Path.of("/work/project")
    private val rule = ManifestContainmentRule()

    private class FakeProbe : WorldProbe {
        override fun exists(path: Path) = true
        override fun resolveReal(path: Path): Path = path.toAbsolutePath().normalize()
    }

    private fun input(pathArg: String, manifest: List<String>, taskScope: List<String> = emptyList()) =
        ToolCallAssessmentInput(
            request = ToolRequest(
                ToolInvocationId("i"), SessionId("s"), StageId("st"), "file_write",
                mapOf("path" to pathArg, "mode" to "0644"),
            ),
            capabilities = setOf(ToolCapability.FILE_WRITE),
            workspace = WorkspacePolicy(workspace),
            probe = FakeProbe(),
            paramRoles = mapOf("path" to ParamRole.PATH),
            writeManifest = manifest,
            session = if (taskScope.isEmpty()) SessionContext()
            else SessionContext(activeTask = ActiveTask("t1", taskScope)),
        )

    @Test
    fun `applies only to write capability`() {
        assertTrue(rule.appliesTo(setOf(ToolCapability.FILE_WRITE)))
        assertFalse(rule.appliesTo(setOf(ToolCapability.FILE_READ)))
        assertFalse(rule.appliesTo(emptySet()))
    }

    @Test
    fun `empty manifest is unrestricted`() {
        val r = rule.assess(input("/work/project/anywhere/x.kt", emptyList()))
        assertEquals(RiskAction.PROCEED, r.disposition)
        assertTrue(r.issues.isEmpty())
        assertTrue(r.observations.isEmpty())
    }

    @Test
    fun `write inside the manifest proceeds`() {
        val r = rule.assess(input("/work/project/core/a/B.kt", listOf("core/**", "docs/*.md")))
        assertEquals(RiskAction.PROCEED, r.disposition)
        assertTrue(r.issues.isEmpty())
        assertEquals("true", r.observations.single().facts["inManifest"])
    }

    @Test
    fun `write outside the manifest is blocked`() {
        val r = rule.assess(input("/work/project/apps/server/Main.kt", listOf("core/**")))
        assertEquals(RiskAction.BLOCK, r.disposition)
        assertEquals("PATH_OUTSIDE_MANIFEST", r.issues.single().code)
        assertEquals("false", r.observations.single().facts["inManifest"])
    }

    @Test
    fun `write outside manifest but inside task scope is allowed`() {
        // The scaffold dead-end: manifest lists only two files, but the claimed task's
        // affected_paths cover the whole subtree. The recorded task scope wins.
        val r = rule.assess(
            input(
                "/work/project/frontend/src/main.tsx",
                manifest = listOf("frontend/package.json", "frontend/vite.config.ts"),
                taskScope = listOf("frontend/**"),
            ),
        )
        assertEquals(RiskAction.PROCEED, r.disposition)
        assertTrue(r.issues.isEmpty())
        val facts = r.observations.single().facts
        assertEquals("false", facts["inManifest"])
        assertEquals("true", facts["inTaskScope"])
    }

    @Test
    fun `write outside both manifest and task scope is blocked with widen remedy`() {
        val r = rule.assess(
            input("/work/project/apps/x.kt", manifest = listOf("core/**"), taskScope = listOf("frontend/**")),
        )
        assertEquals(RiskAction.BLOCK, r.disposition)
        assertEquals("PATH_OUTSIDE_MANIFEST", r.issues.single().code)
        assertTrue(r.issues.single().message.contains("task_update"))
    }

    @Test
    fun `relative path arg is resolved against the workspace before matching`() {
        val r = rule.assess(input("core/a/B.kt", listOf("core/**")))
        assertEquals(RiskAction.PROCEED, r.disposition)
        assertTrue(r.issues.isEmpty())
    }

    @Test
    fun `single-star glob does not cross directories`() {
        // "docs/*.md" matches docs/x.md but not docs/sub/y.md
        val ok = rule.assess(input("/work/project/docs/x.md", listOf("docs/*.md")))
        assertEquals(RiskAction.PROCEED, ok.disposition)
        val nested = rule.assess(input("/work/project/docs/sub/y.md", listOf("docs/*.md")))
        assertEquals(RiskAction.BLOCK, nested.disposition)
    }

    @Test
    fun `out-of-workspace target is left to the containment rule`() {
        // ManifestContainmentRule only judges in-workspace writes; an outside path is
        // PathContainmentRule's job, so this rule must not also block it.
        val r = rule.assess(input("/tmp/scratch.txt", listOf("core/**")))
        assertEquals(RiskAction.PROCEED, r.disposition)
        assertTrue(r.issues.isEmpty())
        assertEquals("false", r.observations.single().facts["inWorkspace"])
    }
}
