import Foundation
import NucleicProtocol
import Testing

@testable import NucleicCore

/// `SandboxToolDisplay` is what stands between the user and a raw `mcp__nucleic__mac_vm_exec`
/// row: it turns Nucleic's own VM/container tools into a label, a working-row gerund, a summary
/// verb, and a parsed one-line body. These tests pin the parsing (so a schema change that moves
/// an argument shows up as a failing test rather than a silently blank row) and the fall-through
/// to nil for tools this file does not speak for.
@Suite("Sandbox tool display")
struct SandboxToolDisplayTests {

    // MARK: - Names

    /// Both the qualified MCP name and the bare tool name must resolve — backends differ in
    /// which form they record on the transcript, and a miss would print the wire name.
    @Test func resolvesQualifiedAndBareNames() {
        #expect(SandboxToolDisplay.label(for: "mcp__nucleic__mac_vm_exec") == "macOS VM")
        #expect(SandboxToolDisplay.label(for: "mac_vm_exec") == "macOS VM")
        #expect(SandboxToolDisplay.label(for: "mcp__nucleic__linux_container") == "Linux container")
    }

    /// Tools outside this file's remit fall through to nil so their callers keep the existing
    /// generic rendering — this is the guard against the pretty path swallowing everything.
    @Test func unrelatedToolsFallThrough() {
        #expect(SandboxToolDisplay.label(for: "Read") == nil)
        #expect(SandboxToolDisplay.gerund(for: "Read") == nil)
        #expect(SandboxToolDisplay.icon(for: "mcp__nucleic__host_exec") == nil)
        #expect(SandboxToolDisplay.detail(for: "Bash", input: .object(["command": .string("ls")])) == nil)
    }

    /// The shell tool keeps Claude Code's `Bash` wire name — every matcher in the app still
    /// switches on it — but the shell it actually runs is nash, so the *label* is the one place
    /// that rename happens. `detail` stays nil above: the transcript's own `case "Bash"` body
    /// rendering owns that row, and this file must not start competing for it.
    @Test func shellToolReadsAsNash() {
        #expect(SandboxToolDisplay.label(for: "Bash") == "Nash")
        #expect(SandboxToolDisplay.label(for: "BashOutput") == "Nash output")
        #expect(SandboxToolDisplay.gateLabel(for: "Bash") == "Nash")
    }

    /// The working-row line names where the work is happening, and distinguishes the two OSes —
    /// "a macOS VM" and "a Linux container" are the whole point of the substitution.
    @Test func gerundsNameTheEnvironment() {
        #expect(SandboxToolDisplay.gerund(for: "mac_vm_exec") == "Running on a macOS VM…")
        #expect(SandboxToolDisplay.gerund(for: "linux_vm_exec") == "Running on a Linux VM…")
        #expect(SandboxToolDisplay.gerund(for: "linux_container") == "Running in a Linux container…")
        #expect(SandboxToolDisplay.gerund(for: "mac_vm_computer") == "Operating a macOS VM…")
    }

    /// An approval names the tool it gates, so `gateLabel` must never go nil and never invent a
    /// friendly name: a tool we know reads as its label, and one we don't keeps its own identifier
    /// with only the `mcp__nucleic__` transport prefix dropped.
    @Test func gateLabelNamesEveryToolWithoutInventing() {
        #expect(SandboxToolDisplay.gateLabel(for: "mcp__nucleic__mac_vm_exec") == "macOS VM")
        #expect(SandboxToolDisplay.gateLabel(for: "mcp__nucleic__host_exec") == "host_exec")
        #expect(SandboxToolDisplay.gateLabel(for: "mcp__nucleic__check_conflict") == "check_conflict")
        #expect(SandboxToolDisplay.gateLabel(for: "Read") == "Read")
    }

    // MARK: - Bodies

    /// An exec call reads as a shell line; the summary form drops the `$` because the family
    /// verb ("Ran on a macOS VM") already supplies that framing.
    @Test func execRendersAsShellLine() {
        let input = JSONValue.object(["command": .string("swift build"), "reason": .string("Xcode")])
        #expect(SandboxToolDisplay.detail(for: "mac_vm_exec", input: input) == "$ swift build")
        #expect(SandboxToolDisplay.summaryItem(for: "mac_vm_exec", input: input) == "swift build")
    }

    /// A multi-line command collapses onto one line — a heredoc would otherwise blow out a row
    /// sized for a single line of text.
    @Test func multiLineCommandCollapses() {
        let input = JSONValue.object(["command": .string("swift build\nswift test")])
        #expect(SandboxToolDisplay.detail(for: "linux_vm_exec", input: input) == "$ swift build ⏎ swift test")
    }

    /// Computer-use actions parse per verb: a click carries its point, `type` its quoted text,
    /// `launch_app` its app, and a no-argument action stands alone.
    @Test func computerActionsParsePerVerb() {
        func detail(_ input: JSONValue) -> String? {
            SandboxToolDisplay.detail(for: "mac_vm_computer", input: input)
        }
        #expect(detail(.object(["action": .string("left_click"), "x": .number(120), "y": .number(400)]))
            == "left click (120, 400)")
        #expect(detail(.object(["action": .string("type"), "text": .string("hello")])) == "type “hello”")
        #expect(detail(.object(["action": .string("launch_app"), "text": .string("Xcode")]))
            == "launch app Xcode")
        #expect(detail(.object(["action": .string("screenshot")])) == "screenshot")
        #expect(detail(.object(["action": .string("scroll"), "scroll_direction": .string("down"),
                                "scroll_amount": .number(3)])) == "scroll down 3")
        // `ax_` means nothing to a reader, so the accessibility verbs expand rather than pass through.
        #expect(detail(.object(["action": .string("ax_dump")])) == "accessibility dump")
        #expect(detail(.object(["action": .string("ax_press"), "ref": .string("btn-7")])) == "press btn-7")
        // `open_browser` shows where it lands: the URL when there is one, the browser otherwise.
        #expect(detail(.object(["action": .string("open_browser"), "text": .string("chrome")]))
            == "open browser chrome")
        #expect(detail(.object(["action": .string("open_browser"), "text": .string("chrome"),
                                "url": .string("http://localhost:3000")]))
            == "open browser http://localhost:3000")
    }

    /// A click whose coordinates are missing still names the action rather than going blank.
    @Test func computerActionSurvivesMissingCoordinates() {
        #expect(SandboxToolDisplay.detail(for: "linux_vm_computer",
                                          input: .object(["action": .string("double_click")]))
            == "double click")
    }

    /// A batch reads as its length plus its leading verbs — what the sequence does, without
    /// unrolling every step's coordinates into the row.
    @Test func batchSummarizesStepCountAndVerbs() {
        let steps = JSONValue.array([
            .object(["action": .string("left_click"), "x": .number(1), "y": .number(2)]),
            .object(["action": .string("type"), "text": .string("hi")]),
            .object(["action": .string("key"), "text": .string("Return")]),
            .object(["action": .string("screenshot")]),
        ])
        #expect(SandboxToolDisplay.detail(for: "mac_vm_computer_batch", input: .object(["steps": steps]))
            == "4 steps — left click, type, key…")
    }

    /// A container op shows the field that op actually uses: the command for `exec`, the image
    /// for `create`, and nothing extra for `list`.
    @Test func containerOpShowsTheRelevantArgument() {
        func detail(_ input: JSONValue) -> String? {
            SandboxToolDisplay.detail(for: "linux_container", input: input)
        }
        #expect(detail(.object(["op": .string("exec"), "name": .string("c1"),
                                "command": .string("npm test")])) == "exec — $ npm test")
        #expect(detail(.object(["op": .string("create"), "image": .string("node:22")]))
            == "create — node:22")
        #expect(detail(.object(["op": .string("list")])) == "list")
    }

    /// A lifecycle op reads as an action taken on the VM, so "status" doesn't look like a state
    /// label and "kill" doesn't read as a bare verb with no object.
    @Test func controlOpsReadAsPhrases() {
        #expect(SandboxToolDisplay.detail(for: "mac_vm_control", input: .object(["op": .string("suspend")]))
            == "suspend")
        #expect(SandboxToolDisplay.detail(for: "linux_vm_control", input: .object(["op": .string("status")]))
            == "check status")
        #expect(SandboxToolDisplay.detail(for: "mac_vm_control", input: .object(["op": .string("kill")]))
            == "delete and reset")
    }

    /// The operator hand-off shows the instructions — what the user is being asked to do — not
    /// the reason, which justifies the call rather than describing it.
    @Test func operatorHandoffShowsInstructions() {
        let input = JSONValue.object([
            "instructions": .string("Solve the CAPTCHA in Safari"),
            "reason": .string("needs a human"),
        ])
        #expect(SandboxToolDisplay.detail(for: "mac_vm_request_operator", input: input)
            == "Solve the CAPTCHA in Safari")
    }

    /// `mac_vm_clear_notifications` takes no arguments, so it must yield nil rather than an
    /// empty-JSON body hanging off its label.
    @Test func argumentlessToolHasNoBody() {
        #expect(SandboxToolDisplay.detail(for: "mac_vm_clear_notifications", input: .object([:])) == nil)
    }

    // MARK: - Summary lines

    /// The tool-block headline buckets these calls under a verb that says where they ran, rather
    /// than under the raw wire name the default branch would use as both key and verb.
    @Test func familyVerbsNameTheEnvironment() {
        #expect(HeuristicSummary.actionDescription(
            ToolCall(toolCallID: "1", name: "mcp__nucleic__mac_vm_exec",
                     input: .object(["command": .string("swift test")])))
            == "ran on a macOS VM `swift test`")
    }
}
