import Testing

@testable import NucleicCore

@Suite struct HostCommandSummaryTests {
    /// The motivating case: the exact shape the agent runs to build the app — a `cd` to the repo
    /// root, an env-prefixed `swift build` with paired value-flags. It decomposes into a named
    /// working directory, one `swift build` invocation with its env and flags parsed, and a
    /// command-derived purpose that names the product and channel.
    @Test func realWorldSwiftBuild() {
        let command = #"cd "$(git rev-parse --show-toplevel 2>/dev/null || pwd)" && NUCLEIC_CHANNEL=dev swift build --build-system native --product nucleic-local"#
        let summary = HostCommandSummary.summary(for: command)
        #expect(summary?.workingDirectory == "the repository root")
        #expect(summary?.invocations.count == 1)
        let inv = summary?.invocations.first
        #expect(inv?.program == "swift")
        #expect(inv?.actions == ["build"])
        #expect(inv?.env.map(\.display) == ["NUCLEIC_CHANNEL=dev"])
        #expect(inv?.flags.map(\.display) == ["--build-system native", "--product nucleic-local"])
        #expect(inv?.arguments.isEmpty == true)
        #expect(summary?.purpose == "Build the Swift product `nucleic-local` (dev channel)")
    }

    /// A value-flag written `--flag value` pairs with its value (not read as an operand), and the
    /// filter surfaces in the purpose. A release configuration is called out too.
    @Test func swiftTestAndConfiguration() {
        let test = HostCommandSummary.summary(for: "swift test --filter HostCommandSummaryTests")
        #expect(test?.invocations.first?.actions == ["test"])
        #expect(test?.invocations.first?.flags.map(\.display) == ["--filter HostCommandSummaryTests"])
        #expect(test?.purpose == "Run the Swift test suite (filter: HostCommandSummaryTests)")

        let release = HostCommandSummary.summary(for: "swift build -c release")
        #expect(release?.purpose == "Build the Swift package in release")
    }

    /// make targets are open-ended actions (no fixed vocabulary), so several targets all parse.
    @Test func makeTargets() {
        #expect(HostCommandSummary.summary(for: "make")?.purpose == "Run the default make target")
        let multi = HostCommandSummary.summary(for: "make clean build")
        #expect(multi?.invocations.first?.actions == ["clean", "build"])
        #expect(multi?.purpose == "Run the make targets `clean, build`")
    }

    /// `npm run <script>` keeps `run` as the action and the script name as an operand (not a
    /// second action), and the purpose names the script.
    @Test func npmRunScript() {
        let summary = HostCommandSummary.summary(for: "npm run build")
        #expect(summary?.invocations.first?.program == "npm")
        #expect(summary?.invocations.first?.actions == ["run"])
        #expect(summary?.invocations.first?.arguments == ["build"])
        #expect(summary?.purpose == "Run the `build` npm script")
    }

    /// A git host command is parsed without swallowing the remote/branch operands as actions, and
    /// its purpose reuses the shared git phrasing.
    @Test func gitReusesGitPhrasing() {
        let summary = HostCommandSummary.summary(for: "git push origin main")
        #expect(summary?.invocations.first?.actions == ["push"])
        #expect(summary?.invocations.first?.arguments == ["origin", "main"])
        #expect(summary?.purpose == "Push to origin/main")
    }

    /// A destructive `rm` dominates the headline (highest salience) even amid other ops, and is
    /// flagged on its invocation — exactly what a user must notice before allowing a host command.
    @Test func destructiveRemoveLeads() {
        let summary = HostCommandSummary.summary(for: "swift build && rm -rf .build")
        #expect(summary?.isDestructive == true)
        #expect(summary?.invocations.contains { $0.destructive && $0.program == "rm" } == true)
        #expect(summary?.purpose == "Delete .build on the host")
    }

    /// A `sudo` prefix is stripped to find the real program but recorded as an elevation flag.
    @Test func sudoIsFlagged() {
        let summary = HostCommandSummary.summary(for: "sudo make install")
        #expect(summary?.isElevated == true)
        #expect(summary?.invocations.first?.program == "make")
        #expect(summary?.invocations.first?.actions == ["install"])
    }

    /// A bare script invocation: the program is its basename, a script operand isn't mistaken for
    /// an action, and the purpose names the script.
    @Test func interpreterScripts() {
        let py = HostCommandSummary.summary(for: "python3 tools/gen.py --check")
        #expect(py?.invocations.first?.program == "python3")
        #expect(py?.invocations.first?.actions.isEmpty == true)
        #expect(py?.invocations.first?.arguments == ["tools/gen.py"])
        #expect(py?.purpose == "Run the Python script `tools/gen.py`")

        let sh = HostCommandSummary.summary(for: "./scripts/package-app.sh dev")
        #expect(sh?.invocations.first?.program == "package-app.sh")
        #expect(sh?.purpose == "Run `package-app.sh dev`")
    }

    /// An unrecognized read-only command falls back to a restated purpose, ranked low so it never
    /// outshouts a real op in a pipeline.
    @Test func unknownAndReadOnlyFallback() {
        #expect(HostCommandSummary.summary(for: "ls -la")?.purpose == "Inspect the host (`ls`)")
        #expect(HostCommandSummary.summary(for: "")?.purpose == nil)  // blank → no summary
        #expect(HostCommandSummary.summary(for: "   ") == nil)
    }

    /// A `for` loop reads as its structure: the header is one control-flow step (not a bogus
    /// `for i`/`do`/`done` sequence), the body command parses normally, and the `do`/`done`
    /// keyword-only segments drop out.
    @Test func forLoopParsesAsControlFlow() {
        let invs = HostCommandSummary.summary(for: "for i in $(seq 1 50); do sleep 5; done")?.invocations ?? []
        #expect(invs.count == 2)
        #expect(invs.first?.control?.kind == .loop)
        #expect(invs.first?.headline == "for i in $(seq 1 50)")
        #expect(invs.last?.control == nil)
        #expect(invs.last?.program == "sleep")
    }

    /// The motivating screenshot: a poll loop followed by an `if`/`then`/`else` block. The loop and
    /// conditional headers each read as one control step; the `do`/`done`/`break`/`fi` keywords are
    /// dropped; and the `then`/`else` bodies parse as their real commands (`cat`, `echo`).
    @Test func loopAndConditionalStructure() {
        let command = """
            for i in $(seq 1 50); do [ -f /tmp/compat3.done ] && break; sleep 5; done; \
            if [ -f /tmp/compat3.done ]; then cat /tmp/compat3.done; else echo STILL_RUNNING; fi
            """
        let invs = HostCommandSummary.summary(for: command)?.invocations ?? []
        // Keyword-only segments never render as steps.
        #expect(invs.allSatisfy { !["do", "done", "break", "then", "else", "fi"].contains($0.headline) })
        // The loop and conditional headers each read as one control step, verbatim.
        #expect(invs.contains { $0.control?.kind == .loop && $0.headline == "for i in $(seq 1 50)" })
        #expect(invs.contains { $0.control?.kind == .conditional && $0.headline == "if [ -f /tmp/compat3.done ]" })
        // The `then`/`else` bodies parse as their real commands.
        #expect(invs.contains { $0.program == "cat" && $0.arguments == ["/tmp/compat3.done"] })
        #expect(invs.contains { $0.headline == "echo STILL_RUNNING" })
    }

    /// A `while` loop is a loop too, and a delete inside a `then` body still surfaces as a
    /// destructive step — the keyword can't hide the `rm` from the risk banner.
    @Test func whileLoopAndDeleteInThenBody() {
        let loop = HostCommandSummary.summary(for: "while true; do echo hi; done")
        #expect(loop?.invocations.first?.control?.kind == .loop)

        let cond = HostCommandSummary.summary(for: "if [ -d build ]; then rm -rf build; fi")
        #expect(cond?.isDestructive == true)
        #expect(cond?.invocations.contains { $0.destructive && $0.program == "rm" } == true)
        #expect(cond?.purpose == "Delete build on the host")
    }

    /// `$PWD` and `${PWD}` expand to the working directory the card is told the command runs in, so
    /// the user reads the real path instead of an opaque `$PWD`. Both the bare and braced forms
    /// resolve, and the expanded command parses through to a named working directory.
    @Test func expandsPWD() {
        let env = ["PWD": "/Users/dev/project"]
        #expect(HostCommandSummary.expand("cd $PWD && swift build", environment: env)
            == "cd /Users/dev/project && swift build")
        #expect(HostCommandSummary.expand("cat ${PWD}/log.txt", environment: env)
            == "cat /Users/dev/project/log.txt")

        let summary = HostCommandSummary.summary(
            for: HostCommandSummary.expand("cd $PWD", environment: env))
        #expect(summary?.workingDirectory == "/Users/dev/project")
    }

    /// Expansion mirrors shell quoting and never fabricates a value: single quotes suppress it,
    /// double quotes don't, an unset variable is left as its literal `$NAME`, and a `$(…)` command
    /// substitution is untouched (we can't run it).
    @Test func expandRespectsQuotingAndUnsetVars() {
        let env = ["HOME": "/Users/dev", "PWD": "/w"]
        // Single-quoted: verbatim. Double-quoted and bare: expanded.
        #expect(HostCommandSummary.expand("echo '$HOME' \"$HOME\" $HOME", environment: env)
            == "echo '$HOME' \"/Users/dev\" /Users/dev")
        // Unset variable stays literal rather than blanking.
        #expect(HostCommandSummary.expand("echo $NOPE done", environment: env)
            == "echo $NOPE done")
        // Command substitution is left alone.
        #expect(HostCommandSummary.expand(#"cd "$(git rev-parse --show-toplevel)""#, environment: env)
            == #"cd "$(git rev-parse --show-toplevel)""#)
        // Nothing to do when there's no `$`.
        #expect(HostCommandSummary.expand("swift build", environment: env) == "swift build")
    }
}
