"""Tests for the portable mirror hook.

Tests the gate logic extracted into mirror-hook.py. We import the functions
directly since the hook is also a valid Python module.
"""

import importlib.util
import json
import sys
from pathlib import Path

import pytest

# Load mirror-hook.py as a module (it's in templates, not a normal package)
HOOK_PATH = Path(__file__).parent.parent / "tokenguard" / "templates" / "hooks" / "mirror-hook.py"


@pytest.fixture(scope="module")
def hook():
    """Import the mirror hook as a module."""
    spec = importlib.util.spec_from_file_location("mirror_hook", HOOK_PATH)
    mod = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(mod)
    return mod


# =========================================================================
# Gate 0: Always Allow
# =========================================================================

class TestAlwaysAllow:
    def test_ask_user_question_allowed(self, hook):
        """AskUserQuestion should always be allowed."""
        result = hook.allow()
        assert result["hookSpecificOutput"]["permissionDecision"] == "allow"

    def test_allowed_tools_set(self, hook):
        """Verify the set of always-allowed tools."""
        expected = {"AskUserQuestion", "TodoWrite", "TaskCreate", "TaskUpdate", "TaskGet", "TaskList"}
        # These are checked in main(), verify they exist as concept
        assert "AskUserQuestion" in expected


# =========================================================================
# Gate 1: Dependency Guardian
# =========================================================================

class TestDependencyGuardian:
    def test_blocks_node_modules(self, hook):
        assert hook.is_blocked_path("/project/node_modules/express/index.js")

    def test_blocks_venv(self, hook):
        assert hook.is_blocked_path("/project/venv/lib/python3.11/site.py")

    def test_blocks_dot_venv(self, hook):
        assert hook.is_blocked_path("/project/.venv/bin/activate")

    def test_blocks_pycache(self, hook):
        assert hook.is_blocked_path("/project/src/__pycache__/mod.cpython-311.pyc")

    def test_blocks_dist(self, hook):
        assert hook.is_blocked_path("/project/dist/bundle.js")

    def test_blocks_build(self, hook):
        assert hook.is_blocked_path("/project/build/output.js")

    def test_blocks_target(self, hook):
        assert hook.is_blocked_path("/project/target/debug/binary")

    def test_blocks_vendor(self, hook):
        assert hook.is_blocked_path("/project/vendor/autoload.php")

    def test_blocks_git_objects(self, hook):
        assert hook.is_blocked_path("/project/.git/objects/ab/cdef1234")

    def test_allows_normal_path(self, hook):
        assert not hook.is_blocked_path("/project/src/main.py")

    def test_allows_empty(self, hook):
        assert not hook.is_blocked_path("")

    def test_allows_dist_in_filename(self, hook):
        """'dist' in filename should not trigger (e.g. 'distributed.md')."""
        assert not hook.is_blocked_path("/project/docs/distributed-roaming.md")

    def test_allows_build_in_filename(self, hook):
        """'build' in filename should not trigger (e.g. 'build-guide.md')."""
        # build as a directory component should trigger, but as part of filename should not
        # Actually, is_blocked_path checks path parts, so /docs/build-guide.md won't match
        assert not hook.is_blocked_path("/project/docs/build-guide.md")

    def test_allows_task_output_in_tmp(self, hook):
        assert not hook.is_blocked_path("/tmp/agent-abc.output")


# =========================================================================
# Gate 2: Destructive Git
# =========================================================================

class TestDestructiveGit:
    def test_blocks_force_push(self, hook):
        assert hook.is_destructive_git("git push origin main --force")

    def test_blocks_force_push_short(self, hook):
        assert hook.is_destructive_git("git push -f origin main")

    def test_blocks_reset_hard(self, hook):
        assert hook.is_destructive_git("git reset --hard HEAD~3")

    def test_blocks_clean_fd(self, hook):
        assert hook.is_destructive_git("git clean -fd")

    def test_blocks_checkout_dash_dash(self, hook):
        assert hook.is_destructive_git("git checkout -- src/main.py")

    def test_blocks_restore(self, hook):
        assert hook.is_destructive_git("git restore src/main.py")

    def test_allows_restore_staged(self, hook):
        assert not hook.is_destructive_git("git restore --staged src/main.py")

    def test_blocks_stash_drop(self, hook):
        assert hook.is_destructive_git("git stash drop")

    def test_blocks_branch_force_delete(self, hook):
        assert hook.is_destructive_git("git branch -D feature/old")

    def test_allows_branch_safe_delete(self, hook):
        assert not hook.is_destructive_git("git branch -d feature/merged")

    def test_allows_normal_push(self, hook):
        assert not hook.is_destructive_git("git push origin feature/branch")

    def test_allows_normal_commit(self, hook):
        assert not hook.is_destructive_git("git commit -m 'fix: stuff'")

    def test_allows_empty(self, hook):
        assert not hook.is_destructive_git("")


# =========================================================================
# Gate 3: Secrets Detection
# =========================================================================

class TestSecretsDetection:
    def test_blocks_env_file(self, hook):
        assert hook.contains_sensitive_file("git add .env")

    def test_blocks_env_local(self, hook):
        assert hook.contains_sensitive_file("git add .env.local")

    def test_allows_env_example(self, hook):
        assert not hook.contains_sensitive_file("git add .env.example")

    def test_allows_env_sample(self, hook):
        assert not hook.contains_sensitive_file("git add .env.sample")

    def test_allows_env_template(self, hook):
        assert not hook.contains_sensitive_file("git add .env.template")

    def test_blocks_pem_file(self, hook):
        assert hook.contains_sensitive_file("git add server.pem")

    def test_blocks_key_file(self, hook):
        assert hook.contains_sensitive_file("git add private.key")

    def test_blocks_id_rsa(self, hook):
        assert hook.contains_sensitive_file("git add id_rsa")

    def test_blocks_credentials_json(self, hook):
        assert hook.contains_sensitive_file("git add credentials.json")

    def test_blocks_npmrc(self, hook):
        assert hook.contains_sensitive_file("git commit -am 'add .npmrc'")

    def test_allows_normal_file(self, hook):
        assert not hook.contains_sensitive_file("git add src/main.py")

    def test_ignores_non_git_command(self, hook):
        assert not hook.contains_sensitive_file("cat .env")

    def test_allows_empty(self, hook):
        assert not hook.contains_sensitive_file("")


# =========================================================================
# Gate 2a: Git Push Gate
# =========================================================================

class TestGitPushGate:
    def test_blocks_force_push(self, hook):
        result = hook.check_git_push_gate("git push origin main --force")
        assert result is not None
        assert "force push" in result["hookSpecificOutput"]["permissionDecisionReason"]

    def test_blocks_force_push_short(self, hook):
        result = hook.check_git_push_gate("git push -f origin main")
        assert result is not None
        assert "force push" in result["hookSpecificOutput"]["permissionDecisionReason"]

    def test_blocks_protected_branch_main(self, hook):
        result = hook.check_git_push_gate("git push origin main")
        assert result is not None
        assert "protected branch" in result["hookSpecificOutput"]["permissionDecisionReason"]

    def test_blocks_protected_branch_master(self, hook):
        result = hook.check_git_push_gate("git push origin master")
        assert result is not None

    def test_blocks_protected_branch_develop(self, hook):
        result = hook.check_git_push_gate("git push origin develop")
        assert result is not None

    def test_blocks_protected_branch_release(self, hook):
        result = hook.check_git_push_gate("git push origin release")
        assert result is not None

    def test_blocks_protected_branch_production(self, hook):
        result = hook.check_git_push_gate("git push origin production")
        assert result is not None

    def test_allows_feature_branch(self, hook):
        result = hook.check_git_push_gate("git push origin feature/my-branch")
        assert result is None

    def test_allows_feature_branch_with_u(self, hook):
        result = hook.check_git_push_gate("git push -u origin feat/new-thing")
        assert result is None

    def test_returns_none_for_non_push(self, hook):
        result = hook.check_git_push_gate("git commit -m 'test'")
        assert result is None

    def test_returns_none_for_empty(self, hook):
        result = hook.check_git_push_gate("")
        assert result is None


# =========================================================================
# Gate 5: Bash Tool Substitution
# =========================================================================

class TestBashToolSubstitution:
    def test_blocks_grep(self, hook):
        result = hook.check_bash_tools("grep -r 'TODO' src/")
        assert result is not None
        assert "rg" in result["hookSpecificOutput"]["permissionDecisionReason"]

    def test_blocks_find(self, hook):
        result = hook.check_bash_tools("find . -name '*.py'")
        assert result is not None
        assert "fd" in result["hookSpecificOutput"]["permissionDecisionReason"]

    def test_blocks_cat_pipe(self, hook):
        result = hook.check_bash_tools("cat file.txt | head -5")
        assert result is not None

    def test_allows_cat_to_python(self, hook):
        """cat piped to python is a legitimate pipeline."""
        result = hook.check_bash_tools("cat data.csv | python3 process.py")
        assert result is None

    def test_allows_cat_to_jq(self, hook):
        result = hook.check_bash_tools("cat data.json | jq '.field'")
        assert result is None

    def test_blocks_rm(self, hook):
        result = hook.check_bash_tools("rm -rf build/")
        assert result is not None
        assert "trash" in result["hookSpecificOutput"]["permissionDecisionReason"]

    def test_allows_safe_command(self, hook):
        result = hook.check_bash_tools("python3 script.py")
        assert result is None

    def test_allows_empty(self, hook):
        result = hook.check_bash_tools("")
        assert result is None

    def test_heredoc_not_false_positive(self, hook):
        """grep/find inside heredoc should not trigger."""
        cmd = """gh pr create --body "$(cat <<'EOF'
feat: find and fix broken tests

This PR uses grep to find all failing tests.
EOF
)" """
        result = hook.check_bash_tools(cmd)
        assert result is None


# =========================================================================
# Gate 6: Bash Linting
# =========================================================================

class TestBashLinting:
    def test_blocks_semicolon_after_subshell(self, hook):
        result = hook.check_bash_lint("x=$(command); echo $x")
        assert result is not None
        assert "Semicolon after command substitution" in result["hookSpecificOutput"]["permissionDecisionReason"]

    def test_blocks_semicolon_in_sed(self, hook):
        result = hook.check_bash_lint("sed 's|foo|bar;baz|' file.txt")
        assert result is not None
        assert "sed" in result["hookSpecificOutput"]["permissionDecisionReason"].lower()

    def test_blocks_negation_in_double_brackets(self, hook):
        result = hook.check_bash_lint('[[ ! -f file.txt ]]')
        assert result is not None
        assert "Negation" in result["hookSpecificOutput"]["permissionDecisionReason"]

    def test_allows_normal_command(self, hook):
        result = hook.check_bash_lint("echo hello && echo world")
        assert result is None

    def test_allows_ampersand_chaining(self, hook):
        result = hook.check_bash_lint("x=$(command) && echo $x")
        assert result is None

    def test_allows_empty(self, hook):
        result = hook.check_bash_lint("")
        assert result is None


# =========================================================================
# Gate 7: Temp Script Prevention
# =========================================================================

class TestTempScriptPrevention:
    def test_blocks_tmp_script(self, hook):
        result = hook.check_temp_script("/tmp/fix.sh")
        assert result is not None
        assert "TEMP SCRIPT" in result["hookSpecificOutput"]["permissionDecisionReason"]

    def test_blocks_temp_dir_script(self, hook):
        result = hook.check_temp_script("/var/temp/run.py")
        assert result is not None

    def test_blocks_temp_prefix_script(self, hook):
        result = hook.check_temp_script("/home/user/temp_fix.js")
        assert result is not None

    def test_blocks_var_folders_script(self, hook):
        result = hook.check_temp_script("/var/folders/xx/abc/T/test.ts")
        assert result is not None

    def test_allows_non_script_in_tmp(self, hook):
        result = hook.check_temp_script("/tmp/data.json")
        assert result is None

    def test_allows_permanent_script(self, hook):
        result = hook.check_temp_script("/home/user/project/deploy.sh")
        assert result is None

    def test_allows_empty(self, hook):
        result = hook.check_temp_script("")
        assert result is None


# =========================================================================
# Gate A: Environment Variable Protection
# =========================================================================

class TestEnvVarProtection:
    def test_blocks_api_key_read(self, hook):
        result = hook.check_env_var_exposure("echo $MY_API_KEY")
        assert result is not None
        assert "ENV VAR PROTECTION" in result["hookSpecificOutput"]["permissionDecisionReason"]

    def test_blocks_secret_env(self, hook):
        result = hook.check_env_var_exposure("echo $APP_SECRET")
        assert result is not None

    def test_blocks_token_env(self, hook):
        result = hook.check_env_var_exposure("echo $AUTH_TOKEN")
        assert result is not None

    def test_blocks_password_env(self, hook):
        result = hook.check_env_var_exposure("echo $DB_PASSWORD")
        assert result is not None

    def test_blocks_printenv_grep_secret(self, hook):
        result = hook.check_env_var_exposure("printenv | grep SECRET")
        assert result is not None

    def test_blocks_env_grep_token(self, hook):
        result = hook.check_env_var_exposure("env | rg TOKEN")
        assert result is not None

    def test_blocks_aws_keys(self, hook):
        result = hook.check_env_var_exposure("echo $AWS_SECRET_KEY")
        assert result is not None

    def test_blocks_anthropic_key(self, hook):
        result = hook.check_env_var_exposure("echo $ANTHROPIC_API_KEY")
        assert result is not None

    def test_blocks_openai_key(self, hook):
        result = hook.check_env_var_exposure("echo $OPENAI_API_KEY")
        assert result is not None

    def test_blocks_github_token(self, hook):
        result = hook.check_env_var_exposure("echo $GITHUB_TOKEN")
        assert result is not None

    def test_blocks_python_environ(self, hook):
        result = hook.check_env_var_exposure("", 'os.environ.get("MY_API_KEY")')
        assert result is not None

    def test_blocks_node_process_env(self, hook):
        result = hook.check_env_var_exposure("", "process.env.MY_SECRET")
        assert result is not None

    def test_strips_github_actions_expressions(self, hook):
        """${{ secrets.KEY }} should not trigger (declarative YAML)."""
        result = hook.check_env_var_exposure("echo ${{ secrets.API_KEY }}")
        assert result is None

    def test_allows_normal_env(self, hook):
        result = hook.check_env_var_exposure("echo $HOME")
        assert result is None

    def test_allows_test_n_pattern(self, hook):
        result = hook.check_env_var_exposure('test -n "$PATH"')
        assert result is None

    def test_allows_empty(self, hook):
        result = hook.check_env_var_exposure("")
        assert result is None

    def test_content_parameter(self, hook):
        """Content param should be checked alongside command."""
        result = hook.check_env_var_exposure("", "echo $MY_API_KEY")
        assert result is not None


# =========================================================================
# Gate D: Network Binding Protection
# =========================================================================

class TestNetworkBinding:
    def test_blocks_bind_0000(self, hook):
        result = hook.check_network_binding('server.bind(("0.0.0.0", 8080))')
        assert result is not None
        assert "NETWORK BINDING" in result["hookSpecificOutput"]["permissionDecisionReason"]

    def test_blocks_host_0000(self, hook):
        result = hook.check_network_binding('app.run(host="0.0.0.0", port=5000)')
        assert result is not None

    def test_blocks_http_server_0000(self, hook):
        result = hook.check_network_binding('HTTPServer(("0.0.0.0", 8000), handler)')
        assert result is not None

    def test_blocks_listen_0000(self, hook):
        result = hook.check_network_binding('server.listen(8080, "0.0.0.0")')
        assert result is not None

    def test_blocks_inaddr_any(self, hook):
        result = hook.check_network_binding("sock.bind((socket.INADDR_ANY, port))")
        assert result is not None

    def test_allows_with_require_auth(self, hook):
        code = '''
@require_auth
def handler():
    pass
app.run(host="0.0.0.0")
'''
        result = hook.check_network_binding(code)
        assert result is None

    def test_allows_with_verify_token(self, hook):
        code = 'verify_token(request)\napp.run(host="0.0.0.0")'
        result = hook.check_network_binding(code)
        assert result is None

    def test_allows_with_authorization_header(self, hook):
        code = 'request.headers["Authorization"]\nserver.bind(("0.0.0.0", 80))'
        result = hook.check_network_binding(code)
        assert result is None

    def test_allows_localhost(self, hook):
        result = hook.check_network_binding('app.run(host="127.0.0.1", port=5000)')
        assert result is None

    def test_allows_no_binding(self, hook):
        result = hook.check_network_binding("print('hello world')")
        assert result is None


# =========================================================================
# Gate 9: Context Gate (token estimation)
# =========================================================================

class TestContextGate:
    def test_estimate_nonexistent_file(self, hook):
        assert hook.estimate_tokens_from_jsonl("/nonexistent/path.jsonl") == 0

    def test_estimate_empty_file(self, hook, tmp_path):
        f = tmp_path / "transcript.jsonl"
        f.write_text("")
        assert hook.estimate_tokens_from_jsonl(str(f)) == 0

    def test_estimate_with_messages(self, hook, tmp_path):
        f = tmp_path / "transcript.jsonl"
        lines = []
        # Each message has 400 chars = ~100 tokens
        for i in range(10):
            entry = {"message": {"content": "x" * 400}}
            lines.append(json.dumps(entry))
        f.write_text("\n".join(lines))

        estimate = hook.estimate_tokens_from_jsonl(str(f))
        # 10 messages x 400 chars / 4 = 1000 tokens
        assert estimate == 1000

    def test_estimate_caches_result(self, hook, tmp_path):
        f = tmp_path / "transcript.jsonl"
        entry = {"message": {"content": "x" * 400}}
        f.write_text(json.dumps(entry))

        # First call parses
        est1 = hook.estimate_tokens_from_jsonl(str(f))
        # Second call should use cache (same mtime+size)
        est2 = hook.estimate_tokens_from_jsonl(str(f))
        assert est1 == est2

    def test_state_persistence(self, hook, tmp_path, monkeypatch):
        # Override STATE_FILE to use tmp_path
        state_file = tmp_path / ".mirror-gate-state.json"
        monkeypatch.setattr(hook, "STATE_FILE", state_file)

        hook.save_state({"warning_issued": True, "session_id": "test-123"})
        loaded = hook.get_state()
        assert loaded["warning_issued"] is True
        assert loaded["session_id"] == "test-123"


# =========================================================================
# Helper: deny/allow response format
# =========================================================================

class TestResponseFormat:
    def test_deny_format(self, hook):
        result = hook.deny("test reason")
        assert result["hookSpecificOutput"]["hookEventName"] == "PreToolUse"
        assert result["hookSpecificOutput"]["permissionDecision"] == "deny"
        assert result["hookSpecificOutput"]["permissionDecisionReason"] == "test reason"

    def test_allow_format(self, hook):
        result = hook.allow()
        assert result["hookSpecificOutput"]["hookEventName"] == "PreToolUse"
        assert result["hookSpecificOutput"]["permissionDecision"] == "allow"

    def test_allow_with_message(self, hook):
        result = hook.allow("warning message")
        assert result["hookSpecificOutput"]["permissionDecision"] == "allow"
        assert result["systemMessage"] == "warning message"


# =========================================================================
# Safe bash fast path
# =========================================================================

class TestSafeBashFastPath:
    def test_git_status_safe(self, hook):
        assert hook._is_safe_bash("git status")

    def test_git_log_safe(self, hook):
        assert hook._is_safe_bash("git log --oneline -10")

    def test_git_diff_safe(self, hook):
        assert hook._is_safe_bash("git diff HEAD~1")

    def test_ls_safe(self, hook):
        assert hook._is_safe_bash("ls /project/src")

    def test_git_push_not_safe(self, hook):
        assert not hook._is_safe_bash("git push origin main")

    def test_python_run_not_safe(self, hook):
        assert not hook._is_safe_bash("python3 deploy.py")

    def test_empty_not_safe(self, hook):
        assert not hook._is_safe_bash("")
