"""Tests for tools/self_repo_guard.py — the running-source-checkout git guard."""

import subprocess
from pathlib import Path

import pytest

from tools.self_repo_guard import (
    detect_self_repo_git_mutation,
    get_running_source_root,
)


@pytest.fixture
def repo(tmp_path):
    root = tmp_path / "hermes-agent"
    root.mkdir()
    subprocess.run(["git", "init", "-q", str(root)], check=True)
    (root / "agent").mkdir()
    return root.resolve()


def _detect(command, cwd, root):
    return detect_self_repo_git_mutation(command, str(cwd), source_root=root)


class TestBlocksMutationsInSourceRepo:
    @pytest.mark.parametrize(
        "sub",
        [
            "checkout pr-51020",
            "switch main",
            "bisect start",
            "bisect good HEAD~10",
            "reset --hard origin/main",
            "reset --har origin/main",
            "rebase origin/main",
            "merge origin/main",
            "pull",
            "restore .",
            "stash",
            "stash pop",
            "clean -fd",
            "cherry-pick abc123",
            "revert HEAD",
        ],
    )
    def test_cwd_inside_repo(self, repo, sub):
        hit, msg = _detect(f"git {sub}", repo, repo)
        assert hit is True
        assert str(repo) in msg

    def test_cwd_in_repo_subdirectory(self, repo):
        hit, _ = _detect("git checkout main", repo / "agent", repo)
        assert hit is True

    def test_dash_c_targeting_repo_from_outside(self, repo, tmp_path):
        hit, _ = _detect(f"git -C {repo} checkout pr-51020", tmp_path, repo)
        assert hit is True

    def test_cd_into_repo_then_checkout(self, repo, tmp_path):
        hit, _ = _detect(f"cd {repo} && git checkout pr-51020", tmp_path, repo)
        assert hit is True

    def test_relative_cd_into_repo(self, repo):
        hit, _ = _detect("cd hermes-agent && git pull", repo.parent, repo)
        assert hit is True

    def test_mutation_after_safe_command(self, repo):
        hit, _ = _detect("git status; git reset --hard HEAD~1", repo, repo)
        assert hit is True

    def test_wrapped_in_sudo_env(self, repo):
        hit, _ = _detect("sudo env GIT_PAGER=cat git checkout main", repo, repo)
        assert hit is True

    @pytest.mark.parametrize(
        "command",
        [
            "sudo -u root git checkout main",
            "env -u GIT_PAGER git switch main",
            "/usr/bin/git checkout main",
            "sh -c 'git checkout main'",
            "bash -lc 'git switch main'",
            "bash -o pipefail -c 'git checkout main'",
            "bash +O extglob -c 'git checkout main'",
            "zsh -yc 'git checkout main'",
            "dash -Vc 'git checkout main'",
            "ksh -Gc 'git checkout main'",
        ],
    )
    def test_wrappers_and_nested_shells(self, repo, command):
        hit, _ = _detect(command, repo, repo)
        assert hit is True

    @pytest.mark.parametrize(
        "command",
        [
            "gh pr checkout 51020",
            "hub pr checkout 51020",
        ],
    )
    def test_pr_checkout_clients(self, repo, command):
        hit, _ = _detect(command, repo, repo)
        assert hit is True

    def test_explicit_work_tree_targeting_repo(self, repo, tmp_path):
        command = f"git --git-dir={repo / '.git'} --work-tree={repo} checkout main"
        hit, _ = _detect(command, tmp_path, repo)
        assert hit is True

    def test_git_environment_targeting_repo(self, repo, tmp_path):
        command = f"GIT_DIR={repo / '.git'} GIT_WORK_TREE={repo} git checkout main"
        hit, _ = _detect(command, tmp_path, repo)
        assert hit is True

    def test_inline_git_alias(self, repo):
        hit, _ = _detect("git -c alias.co=checkout co main", repo, repo)
        assert hit is True

    def test_configured_git_alias(self, repo):
        subprocess.run(
            ["git", "-C", str(repo), "config", "alias.co", "checkout"],
            check=True,
        )
        hit, _ = _detect("git co main", repo, repo)
        assert hit is True

    def test_mutation_in_command_substitution(self, repo):
        hit, _ = _detect('echo "$(git checkout main)"', repo, repo)
        assert hit is True

    @pytest.mark.parametrize(
        "command",
        [
            'echo "$(echo ready && git checkout main)"',
            "echo `git checkout main`",
            'echo "`git checkout main`"',
        ],
    )
    def test_nested_command_lists(self, repo, command):
        hit, _ = _detect(command, repo, repo)
        assert hit is True

    def test_shell_heredoc_is_executed(self, repo):
        command = "bash <<'EOF'\ngit checkout main\nEOF\n"
        hit, _ = _detect(command, repo, repo)
        assert hit is True

    def test_tilde_dash_c_path(self, repo, monkeypatch, tmp_path):
        monkeypatch.setenv("HOME", str(repo.parent))
        hit, _ = _detect("git -C ~/hermes-agent checkout main", tmp_path, repo)
        assert hit is True


class TestAllowsSafeCommands:
    @pytest.mark.parametrize(
        "cmd",
        [
            "git status",
            "git log --oneline -5",
            "git diff main...HEAD",
            "git branch --show-current",
            "git stash list",
            "git stash show -p",
            "git stash create",
            "git stash store abc123",
            "git stash drop",
            "git stash clear",
            "git reset --soft HEAD~1",
            "git reset --mixed HEAD~1",
            "git restore --staged pyproject.toml",
            "git clean --dry-run -fd",
            "git clean -nd",
            "git commit -m 'msg'",
            "git add -A",
            "git fetch origin main",
            "git worktree add /tmp/wt feature-branch",
            "git push fork feature-branch",
            "ls -la",
            "grep -rn checkout tools/",
        ],
    )
    def test_read_only_and_dev_loop_in_repo(self, repo, cmd):
        hit, _ = _detect(cmd, repo, repo)
        assert hit is False

    def test_mutation_in_other_repo(self, repo, tmp_path):
        other = tmp_path / "other-project"
        other.mkdir()
        hit, _ = _detect("git checkout main", other, repo)
        assert hit is False

    def test_dash_c_redirects_out_of_repo(self, repo, tmp_path):
        hit, _ = _detect(f"git -C {tmp_path} checkout main", repo, repo)
        assert hit is False

    def test_cd_out_of_repo_then_checkout(self, repo, tmp_path):
        hit, _ = _detect(f"cd {tmp_path} && git checkout main", repo, repo)
        assert hit is False

    def test_mentioning_repo_path_without_targeting_it(self, repo, tmp_path):
        hit, _ = _detect(f"echo {repo} && git checkout main", tmp_path, repo)
        assert hit is False

    def test_checkout_as_grep_pattern_not_git(self, repo):
        hit, _ = _detect("grep checkout file.txt", repo, repo)
        assert hit is False

    def test_pr_checkout_words_in_other_gh_command_are_safe(self, repo):
        hit, _ = _detect("gh api /repos/example/pr/checkout", repo, repo)
        assert hit is False

    @pytest.mark.parametrize(
        "command",
        [
            'echo "safe | git checkout main"',
            "echo '$(git checkout main)'",
            "printf '%s\\n' 'git checkout main'",
        ],
    )
    def test_quoted_git_text_is_not_executed(self, repo, command):
        hit, _ = _detect(command, repo, repo)
        assert hit is False

    @pytest.mark.parametrize(
        "command",
        [
            "cat > script.sh <<'EOF'\ngit checkout main\nEOF\n",
            "python - <<'PY'\nprint('git checkout main')\nPY\n",
        ],
    )
    def test_data_heredoc_is_not_executed_as_shell(self, repo, command):
        hit, _ = _detect(command, repo, repo)
        assert hit is False

    def test_subshell_cd_does_not_leak(self, repo):
        command = f"(cd {repo} && git status); git checkout main"
        hit, _ = _detect(command, repo.parent, repo)
        assert hit is False

    def test_pipeline_cd_does_not_leak(self, repo):
        command = f"cd {repo} | cat; git checkout main"
        hit, _ = _detect(command, repo.parent, repo)
        assert hit is False

    def test_successful_cd_or_branch_does_not_run(self, repo):
        command = f"cd {repo} || git checkout main"
        hit, _ = _detect(command, repo.parent, repo)
        assert hit is False

    def test_empty_command(self, repo):
        hit, _ = _detect("", repo, repo)
        assert hit is False

    def test_packaged_install_is_inert(self, monkeypatch, tmp_path):
        import tools.self_repo_guard as mod

        monkeypatch.setattr(mod, "get_running_source_root", lambda: None)
        hit, msg = mod.detect_self_repo_git_mutation("git checkout main", str(tmp_path))
        assert hit is False
        assert msg is None


class TestWorktreeTargetingSourceRoot:
    @pytest.mark.parametrize(
        "sub",
        [
            "remove .",
            "remove -f .",
            "remove --force .",
            "remove -- .",
            "move . {other}",
            "move -f . {other}",
        ],
    )
    def test_blocks_relative_target_from_inside(self, repo, tmp_path, sub):
        command = f"git worktree {sub.format(other=tmp_path / 'moved')}"
        hit, msg = _detect(command, repo, repo)
        assert hit is True
        assert str(repo) in msg

    @pytest.mark.parametrize("action", ["remove", "remove -f", "remove --force"])
    def test_blocks_absolute_target_from_outside(self, repo, tmp_path, action):
        hit, _ = _detect(f"git worktree {action} {repo}", tmp_path, repo)
        assert hit is True

    def test_blocks_move_of_root_from_outside(self, repo, tmp_path):
        command = f"git worktree move {repo} {tmp_path / 'moved'}"
        hit, _ = _detect(command, tmp_path, repo)
        assert hit is True

    def test_blocks_dash_c_worktree_remove(self, repo, tmp_path):
        hit, _ = _detect(f"git -C {tmp_path} worktree remove {repo}", tmp_path, repo)
        assert hit is True

    def test_blocks_parent_relative_target_from_subdirectory(self, repo):
        hit, _ = _detect("git worktree remove ..", repo / "agent", repo)
        assert hit is True

    def test_blocks_sibling_relative_target(self, repo):
        hit, _ = _detect(f"git worktree remove ../{repo.name}", repo, repo)
        assert hit is True

    @pytest.mark.parametrize(
        "sub",
        [
            "add {other}",
            "add -b feature {other}",
            "list",
            "list --porcelain",
            "prune",
            "lock {other}",
            "unlock {other}",
            "remove {other}",
            "move {other} {other}-dest",
        ],
    )
    def test_allows_other_worktrees_and_add(self, repo, tmp_path, sub):
        command = f"git worktree {sub.format(other=tmp_path / 'other-wt')}"
        hit, _ = _detect(command, repo, repo)
        assert hit is False

    @pytest.mark.parametrize("sub", ["", "remove", "move", "-f"])
    def test_incomplete_worktree_command_is_not_blocked(self, repo, sub):
        hit, _ = _detect(f"git worktree {sub}".strip(), repo, repo)
        assert hit is False


class TestSourceRootResolution:
    def test_resolves_to_repo_when_git_dir_present(self):
        root = get_running_source_root()
        if root is not None:
            assert (root / ".git").exists()

    def test_worktree_git_file_counts(self, tmp_path, monkeypatch):
        import tools.self_repo_guard as mod

        root = tmp_path / "wt"
        root.mkdir()
        (root / ".git").write_text("gitdir: /somewhere/.git/worktrees/wt\n")
        (root / "tools").mkdir()
        fake_file = root / "tools" / "self_repo_guard.py"
        fake_file.write_text("")
        monkeypatch.setattr(mod, "__file__", str(fake_file))
        assert mod.get_running_source_root() == root.resolve()


class TestUnparseableCommands:
    def test_unbalanced_quotes_fall_back(self, repo):
        hit, _ = _detect('git checkout "unterminated', repo, repo)
        assert hit is True

    def test_subshell_syntax_does_not_crash(self, repo):
        hit, _ = _detect("VAL=$(git rev-parse HEAD) git checkout main", repo, repo)
        assert hit is True


class TestBlockMessageGuidance:
    """The block message must steer agents to a disk-backed scratch clone,
    not a bare "temporary clone" (agents defaulted to /tmp, which is tmpfs
    on most distros — parallel salvage clones running npm ci filled a 32GB
    tmpfs to 97% in one campaign)."""

    def test_message_recommends_shared_clone_on_disk(self, repo):
        hit, msg = _detect("git rebase origin/main", repo, repo)
        assert hit is True
        assert "git clone --shared" in msg
        assert "scratch" in msg

    def test_message_warns_against_tmp_for_dep_installs(self, repo):
        hit, msg = _detect("git rebase origin/main", repo, repo)
        assert hit is True
        assert "tmpfs" in msg
        assert "Delete the clone" in msg

    def test_scratch_hint_honors_hermes_home(self, repo, monkeypatch):
        monkeypatch.setenv("HERMES_HOME", "/custom/hermes-home")
        hit, msg = _detect("git rebase origin/main", repo, repo)
        assert hit is True
        assert "/custom/hermes-home/scratch" in msg
