import Testing

@testable import NucleicCore

@Suite struct GitCommandSummaryTests {
    /// The motivating case: an autoship-style commit pipeline collapses to the commit's
    /// subject, ignoring the `cd`, `git add`, and trailing `git log`.
    @Test func commitPipelineUsesHeredocSubject() {
        let command = """
            cd "/Users/me/proj" && git add -A && git commit -q -F - <<'EOF'
            Render bash summary lines as inline Markdown (code spans)

            Bash tool summaries are model-rewritten purpose lines.
            EOF
            git log --oneline -1
            """
        #expect(
            GitCommandSummary.summary(for: command)
                == "Commit: Render bash summary lines as inline Markdown (code spans)")
    }

    /// A long subject is clipped on a word boundary so the title stays glanceable.
    @Test func longCommitSubjectIsClipped() {
        let command = #"git commit -m "Render bash summary lines as inline Markdown code spans everywhere now""#
        #expect(
            GitCommandSummary.summary(for: command)
                == "Commit: Render bash summary lines as inline Markdown code spans")
    }

    @Test func commitInlineMessage() {
        #expect(
            GitCommandSummary.summary(for: #"git commit -m "Fix the login crash""#)
                == "Commit: Fix the login crash")
        #expect(
            GitCommandSummary.summary(for: "git commit --message='Tidy up imports'")
                == "Commit: Tidy up imports")
    }

    @Test func commitWithoutMessage() {
        #expect(GitCommandSummary.summary(for: "git commit") == "Commit changes")
        #expect(GitCommandSummary.summary(for: "git commit --amend --no-edit") == "Amend commit")
        #expect(
            GitCommandSummary.summary(for: #"git commit --amend -m "Reword""#)
                == "Amend commit: Reword")
    }

    @Test func addLabels() {
        #expect(GitCommandSummary.summary(for: "git add -A") == "Stage all changes")
        #expect(GitCommandSummary.summary(for: "git add .") == "Stage all changes")
        #expect(GitCommandSummary.summary(for: "git add src/main.swift") == "Stage src/main.swift")
    }

    @Test func pushAndPull() {
        #expect(GitCommandSummary.summary(for: "git push origin main") == "Push to origin/main")
        #expect(GitCommandSummary.summary(for: "git push") == "Push changes")
        #expect(
            GitCommandSummary.summary(for: "git push --force origin feature")
                == "Push to origin/feature")
        #expect(GitCommandSummary.summary(for: "git pull origin") == "Pull from origin")
        #expect(GitCommandSummary.summary(for: "git fetch origin") == "Fetch from origin")
    }

    @Test func branchesAndCheckout() {
        #expect(GitCommandSummary.summary(for: "git checkout main") == "Check out main")
        #expect(GitCommandSummary.summary(for: "git checkout -b feature/x") == "Create branch feature/x")
        #expect(GitCommandSummary.summary(for: "git switch dev") == "Switch to dev")
        #expect(GitCommandSummary.summary(for: "git branch -d old") == "Delete branch old")
        #expect(GitCommandSummary.summary(for: "git branch") == "List branches")
        #expect(GitCommandSummary.summary(for: "git branch new-thing") == "Create branch new-thing")
    }

    @Test func readOnlyOps() {
        #expect(GitCommandSummary.summary(for: "git status") == "Check git status")
        #expect(GitCommandSummary.summary(for: "git log --oneline -10") == "Show recent commits")
        #expect(GitCommandSummary.summary(for: "git diff HEAD~1") == "Show changes")
    }

    @Test func stashAndMerge() {
        #expect(GitCommandSummary.summary(for: "git stash") == "Stash changes")
        #expect(GitCommandSummary.summary(for: "git stash pop") == "Pop stashed changes")
        #expect(GitCommandSummary.summary(for: "git merge feature") == "Merge feature")
        #expect(GitCommandSummary.summary(for: "git merge") == "Merge branches")
        #expect(GitCommandSummary.summary(for: "git rebase main") == "Rebase onto main")
    }

    @Test func clone() {
        #expect(
            GitCommandSummary.summary(for: "git clone https://github.com/org/repo.git")
                == "Clone repo")
        // Regression: a value option preceding the URL must not be read as the repo name.
        // `git clone --depth 1 <url>` used to mislabel as "Clone 1" (the `--depth` value),
        // hiding the repo and confusing the Control activity feed.
        #expect(
            GitCommandSummary.summary(for: "git clone --depth 1 https://github.com/org/repo.git")
                == "Clone repo")
        #expect(
            GitCommandSummary.summary(for: "git clone --branch main --depth 1 https://github.com/org/repo.git")
                == "Clone repo")
        // The interceptor classifies a raw argv (the shim's report) the same way.
        #expect(
            GitCommandSummary.classify(argv: ["clone", "--depth", "1", "git@github.com:org/repo.git"])?.label
                == "Clone repo")
        // `--depth=1` (self-contained) already parsed correctly — keep it working.
        #expect(
            GitCommandSummary.summary(for: "git clone --depth=1 https://github.com/org/repo.git")
                == "Clone repo")
    }

    @Test func worktreeAndRevParse() {
        #expect(
            GitCommandSummary.summary(for: "git worktree add --detach /tmp/x HEAD")
                == "Add worktree at /tmp/x")
        #expect(GitCommandSummary.summary(for: "git worktree remove /tmp/x") == "Remove worktree /tmp/x")
        #expect(GitCommandSummary.summary(for: "git worktree list") == "List worktrees")
        #expect(GitCommandSummary.summary(for: "git rev-parse HEAD") == "Resolve a git revision")
        // `worktree add` mutates repo state; `rev-parse` is a pure read.
        #expect(GitCommandSummary.classify(argv: ["worktree", "add", "/tmp/x"])?.isMutating == true)
        #expect(GitCommandSummary.classify(argv: ["rev-parse", "HEAD"])?.isMutating == false)
    }

    /// Global pre-subcommand options (`-C <path>`, `-c k=v`) are skipped to find the verb.
    @Test func globalOptionsSkipped() {
        #expect(
            GitCommandSummary.summary(for: #"git -C /tmp/repo -c user.name=x commit -m "Y""#)
                == "Commit: Y")
    }

    /// The most salient op wins across a pipeline regardless of order.
    @Test func salienceAcrossPipeline() {
        #expect(
            GitCommandSummary.summary(for: "git status && git add -A && git push origin main")
                == "Push to origin/main")
    }

    /// Non-git commands, and git mentioned as a mere argument, are not recognized.
    @Test func nonGitReturnsNil() {
        #expect(GitCommandSummary.summary(for: "ls -la") == nil)
        #expect(GitCommandSummary.summary(for: "swift build") == nil)
        #expect(GitCommandSummary.summary(for: #"echo "git commit""#) == nil)
        #expect(GitCommandSummary.summary(for: "rm -rf build/") == nil)
    }

    /// An unknown git subcommand isn't mislabeled — it falls through to nil so the caller
    /// shows the command verbatim.
    @Test func unknownSubcommandReturnsNil() {
        #expect(GitCommandSummary.summary(for: "git frobnicate --all") == nil)
    }

    /// `sudo` and leading env assignments don't hide the git op.
    @Test func leadingPrefixesIgnored() {
        #expect(GitCommandSummary.summary(for: "GIT_EDITOR=true git commit --amend") == "Amend commit")
    }

    // MARK: - Structured block

    @Test func blockParsesCommitSubjectAndBody() {
        let command = """
            cd "/Users/me/proj" && git add -A && git commit -q -F - <<'EOF'
            Nucleic: Unify sidebar panes + fix queue-pane height

            Extract a shared design pattern for the three swappable sidebar panes
            into SidebarPane.swift.

            - Recents keeps its hard cap of 8.
            - Intelligence and Interlock shrink to fit.
            EOF
            echo "=== committed ===" && git log --oneline -1 | cat
            """
        let block = GitCommandSummary.block(for: command)
        #expect(block != nil)
        #expect(block?.isCommit == true)
        #expect(block?.commit?.subject == "Nucleic: Unify sidebar panes + fix queue-pane height")
        #expect(block?.commit?.amend == false)
        // The body keeps its paragraphs and bullet list for the Markdown renderer.
        #expect(block?.commit?.body.contains("Extract a shared design pattern") == true)
        #expect(block?.commit?.body.contains("- Recents keeps its hard cap of 8.") == true)
        // The steps cover every git op in run order — add, commit, log — with concise labels.
        #expect(block?.steps.map(\.label) == ["Stage all changes", "Commit", "Show recent commits"])
    }

    @Test func blockInlineMessageHasNoBody() {
        let block = GitCommandSummary.block(for: #"git commit -m "Fix the login crash""#)
        #expect(block?.commit?.subject == "Fix the login crash")
        #expect(block?.commit?.body == "")
        #expect(block?.steps.map(\.label) == ["Commit"])
    }

    @Test func blockAmendIsFlagged() {
        let block = GitCommandSummary.block(for: "git commit --amend --no-edit")
        #expect(block?.commit?.amend == true)
        #expect(block?.steps.map(\.label) == ["Amend commit"])
    }

    /// A non-commit pipeline still parses into steps, but isn't a commit card.
    @Test func blockWithoutCommitIsNotACommitCard() {
        let block = GitCommandSummary.block(for: "git add -A && git push origin main")
        #expect(block?.isCommit == false)
        #expect(block?.commit == nil)
        #expect(block?.steps.map(\.label) == ["Stage all changes", "Push to origin/main"])
    }

    @Test func blockNonGitReturnsNil() {
        #expect(GitCommandSummary.block(for: "swift build && swift test") == nil)
    }

    /// The approval title routes git ops through the summary while leaving other commands
    /// on the "Run: …" path.
    @Test func riskClassifierTitleUsesGitSummary() {
        #expect(
            RiskClassifier.title(toolName: "Bash", input: ["command": "git status"])
                == "Check git status")
        #expect(
            RiskClassifier.title(toolName: "Bash", input: ["command": "ls -la"])
                == "Run: ls -la")
    }
}
