"""Tests for guard scripts: guard_autonomous_bash.py and guard_autonomous_write.py.

Coverage:
- Bash guard: git read/write classification, env var bypasses, edge cases
- Write guard: protected path blocking, normal path allowing, env var precedence
"""

from __future__ import annotations

import os
from unittest.mock import patch

import pytest

# Import the check functions directly from the guard scripts
from scripts.guard_autonomous_bash import check_command
from scripts.guard_autonomous_write import check_path

# =============================================================================
# Bash Guard: Read-Only Git Commands
# =============================================================================


class TestBashGuardReadOnly:
    """Git read-only commands should always be allowed."""

    @pytest.mark.parametrize(
        "command",
        [
            "git status",
            "git diff",
            "git diff --cached",
            "git log --oneline -10",
            "git show HEAD",
            "git branch",
            "git branch -a",
            "git remote -v",
            "git describe --tags",
            "git rev-parse HEAD",
            "git ls-files",
            "git ls-tree HEAD",
            "git cat-file -p HEAD",
            "git reflog",
            "git shortlog -sn",
            "git blame src/main.py",
            "git grep 'pattern'",
            "git stash list",
            "git stash show",
            "git config --get user.name",
            "git config --list",
            "git config -l",
        ],
    )
    def test_git_read_commands_allowed(self, command):
        allowed, reason = check_command(command)
        assert allowed is True, f"Expected allowed for '{command}', got blocked: {reason}"

    def test_git_diff_with_flags(self):
        allowed, _ = check_command("git diff --stat HEAD~3..HEAD")
        assert allowed is True

    def test_git_log_with_format(self):
        allowed, _ = check_command("git log --format='%H %s' -5")
        assert allowed is True


# =============================================================================
# Bash Guard: Write Commands Blocked
# =============================================================================


class TestBashGuardWriteBlocked:
    """Git write commands should be blocked by default."""

    @pytest.mark.parametrize(
        "command",
        [
            "git commit -m 'test'",
            "git push origin main",
            "git push",
            "git reset --hard HEAD~1",
            "git reset HEAD~1",
            "git rebase main",
            "git rebase -i HEAD~3",
            "git checkout main",
            "git clean -fd",
            "git merge feature-branch",
            "git cherry-pick abc123",
            "git revert HEAD",
            "git stash drop",
            "git tag v1.0",
        ],
    )
    def test_git_write_commands_blocked(self, command):
        allowed, reason = check_command(command)
        assert allowed is False, f"Expected blocked for '{command}', got allowed: {reason}"
        assert "blocked" in reason.lower()

    @pytest.mark.parametrize(
        "command",
        [
            "git branch -d old-branch",
            "git branch -D old-branch",
            "git branch -m old new",
            "git branch -M old new",
            "git remote add upstream url",
            "git remote remove origin",
            "git remote set-url origin new-url",
        ],
    )
    def test_compound_write_subcommands_blocked(self, command):
        """Compound subcommands (branch -D, remote add) are correctly blocked
        as write operations despite the base command being in the read list."""
        allowed, reason = check_command(command)
        assert allowed is False, f"Expected blocked for '{command}', got allowed: {reason}"
        assert "blocked" in reason.lower()


# =============================================================================
# Bash Guard: Environment Variable Bypasses
# =============================================================================


class TestBashGuardBypass:
    """Environment variable overrides for bash guard."""

    def test_guard_disabled_allows_everything(self):
        with patch.dict(os.environ, {"FOUNDRY_GUARD_DISABLED": "1"}):
            allowed, reason = check_command("git push --force origin main")
            assert allowed is True
            assert "disabled" in reason.lower()

    def test_guard_disabled_zero_does_not_bypass(self):
        with patch.dict(os.environ, {"FOUNDRY_GUARD_DISABLED": "0"}, clear=False):
            allowed, _ = check_command("git push origin main")
            assert allowed is False

    def test_allow_git_commit_env(self):
        with patch.dict(os.environ, {"FOUNDRY_GUARD_ALLOW_GIT_COMMIT": "1"}):
            allowed, reason = check_command("git commit -m 'allowed'")
            assert allowed is True
            assert "FOUNDRY_GUARD_ALLOW_GIT_COMMIT" in reason

    def test_allow_git_commit_does_not_allow_push(self):
        with patch.dict(os.environ, {"FOUNDRY_GUARD_ALLOW_GIT_COMMIT": "1"}):
            allowed, _ = check_command("git push origin main")
            assert allowed is False


# =============================================================================
# Bash Guard: Edge Cases
# =============================================================================


class TestBashGuardEdgeCases:
    """Edge cases: unknown commands, flags before subcommand, shell patterns."""

    def test_bare_git_allowed(self):
        """Bare 'git' or 'git --version' should be allowed."""
        allowed, _ = check_command("git --version")
        assert allowed is True

    def test_unknown_git_subcommand_blocked(self):
        """Unknown git subcommands are blocked (fail-closed)."""
        allowed, _ = check_command("git some-unknown-command")
        assert allowed is False

    def test_non_git_commands_allowed(self):
        """Non-git commands should be allowed."""
        allowed, _ = check_command("pytest tests/ -v")
        assert allowed is True

        allowed, _ = check_command("python -m pytest")
        assert allowed is True

        allowed, _ = check_command("ls -la")
        assert allowed is True

    def test_shell_write_to_spec_blocked(self):
        """Shell writes to spec files should be blocked."""
        allowed, _ = check_command("echo 'data' > specs/test.json")
        assert allowed is False

    def test_shell_write_to_config_blocked(self):
        """Shell writes to config TOML should be blocked."""
        allowed, _ = check_command("echo 'data' >> foundry-mcp.toml")
        assert allowed is False

    def test_rm_audit_files_blocked(self):
        """Removing audit files should be blocked."""
        allowed, _ = check_command("rm .foundry-mcp/journals/entry.json")
        assert allowed is False

    def test_sed_inplace_spec_blocked(self):
        """In-place sed edits of spec files should be blocked."""
        allowed, _ = check_command("sed -i 's/old/new/' specs/test.json")
        assert allowed is False

    def test_stash_list_and_show_still_read_only(self):
        """git stash list/show remain allowed as read-only."""
        allowed, _ = check_command("git stash list")
        assert allowed is True
        allowed, _ = check_command("git stash show")
        assert allowed is True

    def test_stash_drop_and_pop_blocked(self):
        """Stash write operations should be blocked."""
        allowed, _ = check_command("git stash drop")
        assert allowed is False
        allowed, _ = check_command("git stash pop")
        assert allowed is False
        allowed, _ = check_command("git stash clear")
        assert allowed is False

    def test_bare_branch_and_remote_still_allowed(self):
        """Plain 'git branch' and 'git remote' (no args) remain read-only."""
        allowed, _ = check_command("git branch")
        assert allowed is True
        allowed, _ = check_command("git branch -a")
        assert allowed is True
        allowed, _ = check_command("git remote -v")
        assert allowed is True

    def test_git_with_flags_before_subcommand(self):
        """Git commands with flags before the subcommand.

        The token-based parser correctly skips -c key=val flags
        and finds the real subcommand.
        """
        # Bare short flags are handled correctly
        allowed, _ = check_command("git commit -m 'msg'")
        assert allowed is False

        # Flags with values (-c key=val) are correctly skipped;
        # the real subcommand "commit" is detected and blocked
        allowed, _ = check_command("git -c core.pager=cat commit -m 'msg'")
        assert allowed is False

    def test_cp_to_protected_path_blocked(self):
        """Copying files to protected paths should be blocked."""
        allowed, _ = check_command("cp bad.json specs/real.json")
        assert allowed is False

    def test_git_fetch_allowed(self):
        """git fetch is read-only from working tree perspective."""
        allowed, _ = check_command("git fetch origin")
        assert allowed is True
        allowed, _ = check_command("git fetch --all")
        assert allowed is True

    def test_git_pull_blocked(self):
        """git pull modifies working tree and is blocked."""
        allowed, _ = check_command("git pull origin main")
        assert allowed is False

    def test_git_add_blocked(self):
        """git add stages files and is blocked."""
        allowed, _ = check_command("git add .")
        assert allowed is False
        allowed, _ = check_command("git add -A")
        assert allowed is False

    def test_git_rm_mv_blocked(self):
        """git rm and git mv are write operations."""
        allowed, _ = check_command("git rm src/old.py")
        assert allowed is False
        allowed, _ = check_command("git mv src/old.py src/new.py")
        assert allowed is False

    def test_git_restore_switch_blocked(self):
        """git restore and git switch modify working tree."""
        allowed, _ = check_command("git restore --staged .")
        assert allowed is False
        allowed, _ = check_command("git switch main")
        assert allowed is False

    def test_empty_command(self):
        """Empty and whitespace-only commands are allowed."""
        allowed, _ = check_command("")
        assert allowed is True
        allowed, _ = check_command("   ")
        assert allowed is True

    def test_multiple_spaces_between_git_args(self):
        """Multiple spaces between git args should not affect detection."""
        allowed, _ = check_command("git    push   origin   main")
        assert allowed is False


# =============================================================================
# Bash Guard: Shell Wrapper Bypass Prevention
# =============================================================================


class TestBashGuardShellWrappers:
    """Shell wrapper commands that could bypass the git guard must be blocked."""

    @pytest.mark.parametrize(
        "command",
        [
            'bash -c "git push origin main"',
            "sh -c 'git push origin main'",
            "zsh -c 'git reset --hard HEAD'",
            "eval 'git push'",
            "eval git push",
            "exec git push",
        ],
    )
    def test_shell_wrapper_git_blocked(self, command):
        """Shell wrappers executing git write commands are blocked."""
        allowed, reason = check_command(command)
        assert allowed is False, f"Expected blocked for '{command}', got allowed: {reason}"
        assert "blocked" in reason.lower()

    def test_bash_without_c_flag_allowed(self):
        """Plain bash without -c flag is not a wrapper pattern."""
        allowed, _ = check_command("bash script.sh")
        assert allowed is True

    def test_python_c_with_git_blocked(self):
        """python -c with git reference is blocked."""
        allowed, _ = check_command("python3 -c \"import subprocess; subprocess.run(['git', 'push'])\"")
        assert allowed is False


# =============================================================================
# Bash Guard: Full-Path Git Binary
# =============================================================================


class TestBashGuardFullPathGit:
    """Full-path git binaries like /usr/bin/git must be detected."""

    def test_full_path_git_push_blocked(self):
        allowed, _ = check_command("/usr/bin/git push origin main")
        assert allowed is False

    def test_full_path_git_status_allowed(self):
        allowed, _ = check_command("/usr/bin/git status")
        assert allowed is True

    def test_full_path_git_commit_blocked(self):
        allowed, _ = check_command("/usr/local/bin/git commit -m 'msg'")
        assert allowed is False


# =============================================================================
# Bash Guard: Compound Subcommand Arg Extraction
# =============================================================================


class TestBashGuardCompoundArgExtraction:
    """Git flags before subcommands (e.g. -C /path) must not break classification."""

    def test_git_C_flag_branch_delete_blocked(self):
        """git -C /tmp branch -D main should be blocked."""
        allowed, _ = check_command("git -C /tmp branch -D main")
        assert allowed is False

    def test_git_c_config_before_push_blocked(self):
        """git -c core.pager=cat push should be blocked."""
        allowed, _ = check_command("git -c core.pager=cat push origin main")
        assert allowed is False

    def test_git_git_dir_flag_before_status_allowed(self):
        """git --git-dir /foo status should be allowed."""
        allowed, _ = check_command("git --git-dir /foo status")
        assert allowed is True


# =============================================================================
# Bash Guard: Bisect Classification
# =============================================================================


class TestBashGuardBisect:
    """git bisect subcommand classification (compound)."""

    @pytest.mark.parametrize(
        "command",
        [
            "git bisect start",
            "git bisect good",
            "git bisect bad",
            "git bisect reset",
            "git bisect skip",
            "git bisect run make test",
        ],
    )
    def test_bisect_write_operations_blocked(self, command):
        allowed, reason = check_command(command)
        assert allowed is False, f"Expected blocked for '{command}', got allowed: {reason}"

    @pytest.mark.parametrize(
        "command",
        [
            "git bisect log",
            "git bisect visualize",
        ],
    )
    def test_bisect_read_operations_allowed(self, command):
        allowed, reason = check_command(command)
        assert allowed is True, f"Expected allowed for '{command}', got blocked: {reason}"


# =============================================================================
# Bash Guard: Extended Sanitization
# =============================================================================


class TestBashGuardExtendedSanitization:
    """Extended sanitization: OSC, DCS, C1 codes."""

    def test_osc_hyperlink_stripped(self):
        """OSC hyperlink sequences should be stripped."""
        cmd = "\x1b]8;;http://example.com\x1b\\git status\x1b]8;;\x1b\\"
        allowed, _ = check_command(cmd)
        assert allowed is True

    def test_c1_control_codes_stripped(self):
        """8-bit C1 control codes should be stripped."""
        cmd = "\x9bgit status\x9b0m"
        allowed, _ = check_command(cmd)
        assert allowed is True


# =============================================================================
# Bash Guard: Command Chaining
# =============================================================================


class TestBashGuardCommandChaining:
    """Command chaining should check each segment independently."""

    def test_safe_and_blocked_via_double_ampersand(self):
        """git status && git push → blocked (push is write)."""
        allowed, reason = check_command("git status && git push")
        assert allowed is False
        assert "blocked" in reason.lower()

    def test_safe_and_safe_allowed(self):
        """git status && git log → allowed."""
        allowed, _ = check_command("git status && git log --oneline")
        assert allowed is True

    def test_blocked_via_or(self):
        """git status || git push → blocked."""
        allowed, _ = check_command("git status || git push")
        assert allowed is False

    def test_blocked_via_semicolon(self):
        """git log; git push → blocked."""
        allowed, _ = check_command("git log; git push origin main")
        assert allowed is False

    def test_pipe_read_only_allowed(self):
        """git log | head → allowed (both are read-only)."""
        allowed, _ = check_command("git log | head -20")
        assert allowed is True

    def test_newline_splitting(self):
        """Commands separated by newlines are checked independently."""
        allowed, _ = check_command("git status\ngit push")
        assert allowed is False

    def test_quoted_semicolons_not_split(self):
        """Semicolons inside quotes should NOT cause splitting."""
        allowed, _ = check_command("git log --format='%H;%s'")
        assert allowed is True


# =============================================================================
# Bash Guard: Sanitization
# =============================================================================


class TestBashGuardSanitization:
    """Input sanitization: null bytes, ANSI escapes."""

    def test_null_bytes_stripped(self):
        """Null bytes in command are stripped before processing."""
        allowed, _ = check_command("git\x00 status")
        assert allowed is True

    def test_ansi_escapes_stripped(self):
        """ANSI escape sequences are stripped."""
        allowed, _ = check_command("\x1b[31mgit status\x1b[0m")
        assert allowed is True


# =============================================================================
# Bash Guard: Git Config Classification
# =============================================================================


class TestBashGuardGitConfig:
    """Git config read/write classification."""

    def test_config_get_allowed(self):
        allowed, _ = check_command("git config --get user.name")
        assert allowed is True

    def test_config_list_allowed(self):
        allowed, _ = check_command("git config --list")
        assert allowed is True

    def test_config_global_read_allowed(self):
        """git config --global user.name (single key = read) → allowed."""
        allowed, _ = check_command("git config --global user.name")
        assert allowed is True

    def test_config_global_write_blocked(self):
        """git config user.name 'Tyler' (key + value = write) → blocked."""
        allowed, reason = check_command('git config user.name "Tyler"')
        assert allowed is False
        assert "blocked" in reason.lower()

    def test_config_global_key_value_blocked(self):
        """git config --global user.name 'Tyler' → blocked (write)."""
        allowed, _ = check_command("git config --global user.name Tyler")
        assert allowed is False

    def test_config_unset_blocked(self):
        """git config --unset key → blocked."""
        allowed, _ = check_command("git config --unset user.name")
        assert allowed is False


# =============================================================================
# Bash Guard: Fail-Closed for Unknown Subcommands
# =============================================================================


class TestBashGuardFailClosed:
    """Unknown git subcommands are blocked (fail-closed)."""

    def test_filter_branch_blocked(self):
        allowed, _ = check_command("git filter-branch")
        assert allowed is False

    def test_update_ref_blocked(self):
        allowed, _ = check_command("git update-ref")
        assert allowed is False

    def test_replace_blocked(self):
        allowed, _ = check_command("git replace")
        assert allowed is False


# =============================================================================
# Write Guard: Protected Paths Blocked
# =============================================================================


class TestWriteGuardBlocked:
    """Write guard should block writes to protected paths."""

    @pytest.mark.parametrize(
        "path",
        [
            "specs/active/my-spec.json",
            "/workspace/specs/active/my-spec.json",
            "specs/completed/old.json",
            "foundry-mcp.toml",
            ".foundry-mcp.toml",
            "/workspace/foundry-mcp.toml",
            ".foundry-mcp/sessions/sess-001.json",
            "/workspace/.foundry-mcp/sessions/sess-001.json",
            ".foundry-mcp/journals/entry.json",
            ".foundry-mcp/audit/event.json",
            ".foundry-mcp/proofs/proof-001.json",
        ],
    )
    def test_protected_paths_blocked(self, path):
        allowed, reason = check_path(path)
        assert allowed is False, f"Expected blocked for '{path}', got allowed: {reason}"
        assert "blocked" in reason.lower()


# =============================================================================
# Write Guard: Normal Paths Allowed
# =============================================================================


class TestWriteGuardAllowed:
    """Write guard should allow writes to normal source/test files."""

    @pytest.mark.parametrize(
        "path",
        [
            "src/main.py",
            "src/foundry_mcp/core/utils.py",
            "tests/unit/test_something.py",
            "/tmp/scratch.txt",
            "README.md",
            "pyproject.toml",
            "src/config.py",
        ],
    )
    def test_normal_paths_allowed(self, path):
        allowed, reason = check_path(path)
        assert allowed is True, f"Expected allowed for '{path}', got blocked: {reason}"


# =============================================================================
# Write Guard: Environment Variable Overrides
# =============================================================================


class TestWriteGuardBypass:
    """Environment variable overrides for write guard."""

    def test_guard_disabled_allows_everything(self):
        with patch.dict(os.environ, {"FOUNDRY_GUARD_DISABLED": "1"}):
            allowed, reason = check_path("specs/active/my-spec.json")
            assert allowed is True
            assert "disabled" in reason.lower()

    def test_extra_blocked_blocks_custom_path(self):
        with patch.dict(os.environ, {"FOUNDRY_GUARD_EXTRA_BLOCKED": "/custom/protected"}):
            allowed, reason = check_path("/custom/protected/file.txt")
            assert allowed is False
            assert "EXTRA_BLOCKED" in reason

    def test_extra_allowed_overrides_block(self):
        """EXTRA_ALLOWED takes precedence over built-in block rules."""
        with patch.dict(os.environ, {"FOUNDRY_GUARD_EXTRA_ALLOWED": "specs/active/my-spec.json"}):
            allowed, reason = check_path("specs/active/my-spec.json")
            assert allowed is True
            assert "EXTRA_ALLOWED" in reason

    def test_extra_allowed_precedence_over_extra_blocked(self):
        """EXTRA_ALLOWED is evaluated before EXTRA_BLOCKED."""
        with patch.dict(
            os.environ,
            {
                "FOUNDRY_GUARD_EXTRA_ALLOWED": "/shared/path",
                "FOUNDRY_GUARD_EXTRA_BLOCKED": "/shared/path",
            },
        ):
            allowed, reason = check_path("/shared/path/file.txt")
            assert allowed is True
            assert "EXTRA_ALLOWED" in reason
