"""Tests for ClaudeHookService."""

import io
import sys
from unittest.mock import patch

from claudesprint.services.claude_hook_service import (
    ClaudeHookService,
    HookInput,
    HookResult,
    HookType,
)


class TestHookInput:
    """Tests for HookInput parsing."""

    def test_from_stdin_parses_valid_json(self) -> None:
        """Test parsing valid JSON from stdin."""
        json_input = '{"tool_name": "Bash", "tool_input": {"command": "npm test"}}'
        with patch.object(sys, "stdin", io.StringIO(json_input)):
            hook_input = HookInput.from_stdin()

        assert hook_input.tool_name == "Bash"
        assert hook_input.tool_input == {"command": "npm test"}

    def test_from_stdin_handles_empty_input(self) -> None:
        """Test handling empty stdin."""
        with patch.object(sys, "stdin", io.StringIO("")):
            hook_input = HookInput.from_stdin()

        assert hook_input.tool_name is None
        assert hook_input.tool_input is None

    def test_from_stdin_handles_invalid_json(self) -> None:
        """Test handling invalid JSON."""
        with patch.object(sys, "stdin", io.StringIO("not valid json")):
            hook_input = HookInput.from_stdin()

        assert hook_input.tool_name is None
        assert hook_input.tool_input is None

    def test_from_stdin_handles_partial_data(self) -> None:
        """Test handling JSON without all expected fields."""
        json_input = '{"tool_name": "Bash"}'
        with patch.object(sys, "stdin", io.StringIO(json_input)):
            hook_input = HookInput.from_stdin()

        assert hook_input.tool_name == "Bash"
        assert hook_input.tool_input == {}


class TestServerGuard:
    """Tests for server-guard hook."""

    def test_allows_normal_commands(self) -> None:
        """Test that normal commands are allowed."""
        service = ClaudeHookService()
        hook_input = HookInput(tool_input={"command": "npm test"})

        result = service.execute_hook(HookType.SERVER_GUARD, hook_input)

        assert result == HookResult.ALLOW

    def test_blocks_watch_flag(self) -> None:
        """Test blocking commands with --watch flag."""
        service = ClaudeHookService()
        hook_input = HookInput(tool_input={"command": "npm test --watch"})

        result = service.execute_hook(HookType.SERVER_GUARD, hook_input)

        assert result == HookResult.BLOCK

    def test_blocks_watch_word(self) -> None:
        """Test blocking commands with 'watch' keyword."""
        service = ClaudeHookService()
        hook_input = HookInput(tool_input={"command": "npm run watch"})

        result = service.execute_hook(HookType.SERVER_GUARD, hook_input)

        assert result == HookResult.BLOCK

    def test_blocks_interactive_git_rebase(self) -> None:
        """Test blocking git rebase -i."""
        service = ClaudeHookService()
        hook_input = HookInput(tool_input={"command": "git rebase -i HEAD~3"})

        result = service.execute_hook(HookType.SERVER_GUARD, hook_input)

        assert result == HookResult.BLOCK

    def test_blocks_interactive_git_add(self) -> None:
        """Test blocking git add -p."""
        service = ClaudeHookService()
        hook_input = HookInput(tool_input={"command": "git add -p"})

        result = service.execute_hook(HookType.SERVER_GUARD, hook_input)

        assert result == HookResult.BLOCK

    def test_allows_git_commit_with_message(self) -> None:
        """Test allowing git commit with -m flag."""
        service = ClaudeHookService()
        hook_input = HookInput(tool_input={"command": 'git commit -m "fix: bug"'})

        result = service.execute_hook(HookType.SERVER_GUARD, hook_input)

        assert result == HookResult.ALLOW

    def test_blocks_git_commit_without_message(self) -> None:
        """Test blocking git commit without -m (opens editor)."""
        service = ClaudeHookService()
        hook_input = HookInput(tool_input={"command": "git commit"})

        result = service.execute_hook(HookType.SERVER_GUARD, hook_input)

        assert result == HookResult.BLOCK

    def test_allows_empty_command(self) -> None:
        """Test allowing empty command."""
        service = ClaudeHookService()
        hook_input = HookInput(tool_input={"command": ""})

        result = service.execute_hook(HookType.SERVER_GUARD, hook_input)

        assert result == HookResult.ALLOW

    def test_allows_missing_command(self) -> None:
        """Test allowing when command is missing."""
        service = ClaudeHookService()
        hook_input = HookInput(tool_input={})

        result = service.execute_hook(HookType.SERVER_GUARD, hook_input)

        assert result == HookResult.ALLOW

    def test_blocks_tail_follow(self) -> None:
        """Test blocking tail -f commands."""
        service = ClaudeHookService()
        hook_input = HookInput(tool_input={"command": "tail -f /var/log/syslog"})

        result = service.execute_hook(HookType.SERVER_GUARD, hook_input)

        assert result == HookResult.BLOCK


class TestBrowserGuard:
    """Tests for browser-guard hook."""

    def test_allows_by_default(self) -> None:
        """Test that browser-guard allows operations by default."""
        service = ClaudeHookService()
        hook_input = HookInput(tool_input={"skill": "agent-browser"})

        result = service.execute_hook(HookType.BROWSER_GUARD, hook_input)

        assert result == HookResult.ALLOW


class TestAutonomousContinue:
    """Tests for autonomous-continue hook."""

    def test_allows_by_default(self) -> None:
        """Test that autonomous-continue allows stop by default."""
        service = ClaudeHookService()
        hook_input = HookInput()

        result = service.execute_hook(HookType.AUTONOMOUS_CONTINUE, hook_input)

        assert result == HookResult.ALLOW


class TestHelperMethods:
    """Tests for helper methods."""

    def test_is_watch_command(self) -> None:
        """Test is_watch_command detection."""
        service = ClaudeHookService()

        assert service.is_watch_command("npm test --watch") is True
        assert service.is_watch_command("npm run watch") is True
        assert service.is_watch_command("npm test") is False

    def test_is_interactive_git_command(self) -> None:
        """Test is_interactive_git_command detection."""
        service = ClaudeHookService()

        assert service.is_interactive_git_command("git rebase -i main") is True
        assert service.is_interactive_git_command("git add -p") is True
        assert service.is_interactive_git_command("git add file.txt") is False

    def test_is_server_command(self) -> None:
        """Test is_server_command detection."""
        service = ClaudeHookService()

        assert service.is_server_command("npm run dev") is True
        assert service.is_server_command("yarn start") is True
        assert service.is_server_command("npm test") is False


class TestServerCommandsAllowed:
    """Tests verifying server commands are ALLOWED (not blocked).

    Server commands are allowed because:
    1. The model needs flexibility to start servers when needed
    2. Duplicate prevention is handled via prompt guidance (check port before starting)
    3. See _project_discovery.xml.j2 <server_management> section
    """

    def test_allows_npm_run_dev(self) -> None:
        """Server commands are allowed - prompt handles duplicate prevention."""
        service = ClaudeHookService()
        hook_input = HookInput(tool_input={"command": "npm run dev"})

        result = service.execute_hook(HookType.SERVER_GUARD, hook_input)

        assert result == HookResult.ALLOW

    def test_allows_yarn_dev(self) -> None:
        """Server commands are allowed."""
        service = ClaudeHookService()
        hook_input = HookInput(tool_input={"command": "yarn dev"})

        result = service.execute_hook(HookType.SERVER_GUARD, hook_input)

        assert result == HookResult.ALLOW

    def test_allows_uvicorn(self) -> None:
        """Server commands are allowed."""
        service = ClaudeHookService()
        hook_input = HookInput(tool_input={"command": "uvicorn main:app --reload"})

        result = service.execute_hook(HookType.SERVER_GUARD, hook_input)

        assert result == HookResult.ALLOW

    def test_allows_flask_run(self) -> None:
        """Server commands are allowed."""
        service = ClaudeHookService()
        hook_input = HookInput(tool_input={"command": "flask run --debug"})

        result = service.execute_hook(HookType.SERVER_GUARD, hook_input)

        assert result == HookResult.ALLOW

    def test_is_server_command_detection_still_works(self) -> None:
        """The is_server_command helper still detects server commands (for other uses)."""
        service = ClaudeHookService()

        # Detection works (even though we don't block)
        assert service.is_server_command("npm run dev") is True
        assert service.is_server_command("yarn dev") is True
        assert service.is_server_command("uvicorn main:app") is True
        assert service.is_server_command("flask run") is True

        # Non-server commands
        assert service.is_server_command("npm test") is False
        assert service.is_server_command("npm run build") is False


class TestHookTypes:
    """Tests for HookType enum."""

    def test_hook_type_values(self) -> None:
        """Test hook type string values."""
        assert HookType.SERVER_GUARD.value == "server-guard"
        assert HookType.BROWSER_GUARD.value == "browser-guard"
        assert HookType.AUTONOMOUS_CONTINUE.value == "autonomous-continue"


class TestHookResult:
    """Tests for HookResult enum."""

    def test_hook_result_values(self) -> None:
        """Test hook result exit code values."""
        assert HookResult.ALLOW.value == 0
        assert HookResult.BLOCK.value == 2


class TestQuotedContentFalsePositives:
    """Tests to ensure quoted content doesn't trigger false positives."""

    def test_allows_commit_message_with_watch(self) -> None:
        """Test that 'watch' in a commit message is allowed."""
        service = ClaudeHookService()
        hook_input = HookInput(
            tool_input={"command": 'git commit -m "watch out for this bug"'}
        )

        result = service.execute_hook(HookType.SERVER_GUARD, hook_input)

        assert result == HookResult.ALLOW

    def test_allows_echo_with_watch(self) -> None:
        """Test that 'watch' in an echo statement is allowed."""
        service = ClaudeHookService()
        hook_input = HookInput(
            tool_input={"command": 'echo "Remember to watch the logs"'}
        )

        result = service.execute_hook(HookType.SERVER_GUARD, hook_input)

        assert result == HookResult.ALLOW

    def test_allows_commit_message_with_serve(self) -> None:
        """Test that 'serve' in a commit message is allowed."""
        service = ClaudeHookService()
        hook_input = HookInput(
            tool_input={"command": "git commit -m 'Add serve endpoint'"}
        )

        result = service.execute_hook(HookType.SERVER_GUARD, hook_input)

        assert result == HookResult.ALLOW

    def test_allows_grep_for_watch(self) -> None:
        """Test that grepping for 'watch' is allowed."""
        service = ClaudeHookService()
        hook_input = HookInput(tool_input={"command": 'grep "watch" package.json'})

        result = service.execute_hook(HookType.SERVER_GUARD, hook_input)

        assert result == HookResult.ALLOW

    def test_still_blocks_actual_watch_command(self) -> None:
        """Test that actual watch commands are still blocked."""
        service = ClaudeHookService()
        hook_input = HookInput(tool_input={"command": "npm run watch"})

        result = service.execute_hook(HookType.SERVER_GUARD, hook_input)

        assert result == HookResult.BLOCK

    def test_still_blocks_watch_with_quoted_args(self) -> None:
        """Test that watch commands with quoted args are still blocked."""
        service = ClaudeHookService()
        hook_input = HookInput(
            tool_input={"command": 'npm run watch --include "src/**/*.ts"'}
        )

        result = service.execute_hook(HookType.SERVER_GUARD, hook_input)

        assert result == HookResult.BLOCK

    def test_helper_ignores_quoted_watch(self) -> None:
        """Test is_watch_command ignores quoted content."""
        service = ClaudeHookService()

        assert service.is_watch_command('git commit -m "watch this"') is False
        assert service.is_watch_command("npm run watch") is True

    def test_helper_ignores_quoted_server(self) -> None:
        """Test is_server_command ignores quoted content."""
        service = ClaudeHookService()

        assert service.is_server_command('echo "npm run dev"') is False
        assert service.is_server_command("npm run dev") is True

    def test_helper_ignores_quoted_git_interactive(self) -> None:
        """Test is_interactive_git_command ignores quoted content."""
        service = ClaudeHookService()

        assert (
            service.is_interactive_git_command('echo "git rebase -i main"') is False
        )
        assert service.is_interactive_git_command("git rebase -i main") is True


class TestEscapedQuotes:
    """Tests for proper handling of escaped quotes in commands."""

    def test_allows_escaped_quotes_with_watch_inside(self) -> None:
        """Test that escaped quotes with 'watch' inside are handled correctly."""
        service = ClaudeHookService()
        # The word 'watch' appears inside escaped quotes - should be stripped
        hook_input = HookInput(
            tool_input={"command": 'echo "v1.0 \\"watch\\" release"'}
        )

        result = service.execute_hook(HookType.SERVER_GUARD, hook_input)

        assert result == HookResult.ALLOW

    def test_allows_commit_with_escaped_quotes_containing_blocked_word(self) -> None:
        """Test commit message with escaped quotes containing blocked patterns."""
        service = ClaudeHookService()
        hook_input = HookInput(
            tool_input={
                "command": 'git commit -m "Update \\"watch\\" mode documentation"'
            }
        )

        result = service.execute_hook(HookType.SERVER_GUARD, hook_input)

        assert result == HookResult.ALLOW

    def test_allows_nested_escaped_quotes_with_serve(self) -> None:
        """Test nested escaped quotes containing 'serve' keyword."""
        service = ClaudeHookService()
        hook_input = HookInput(
            tool_input={"command": 'echo "config: \\"serve\\": true"'}
        )

        result = service.execute_hook(HookType.SERVER_GUARD, hook_input)

        assert result == HookResult.ALLOW

    def test_still_blocks_watch_outside_escaped_quotes(self) -> None:
        """Test that watch command outside quotes is still blocked."""
        service = ClaudeHookService()
        # 'watch' appears outside the quoted content
        hook_input = HookInput(
            tool_input={"command": 'npm run watch --config "test.json"'}
        )

        result = service.execute_hook(HookType.SERVER_GUARD, hook_input)

        assert result == HookResult.BLOCK

    def test_handles_mixed_quote_styles(self) -> None:
        """Test handling of mixed single and double quotes."""
        service = ClaudeHookService()
        hook_input = HookInput(
            tool_input={"command": "git commit -m 'Fix \"watch\" handling'"}
        )

        result = service.execute_hook(HookType.SERVER_GUARD, hook_input)

        assert result == HookResult.ALLOW

    def test_handles_backslash_at_end_of_double_quotes(self) -> None:
        """Test backslash escape at end of double-quoted string."""
        service = ClaudeHookService()
        # Escaped backslash followed by closing quote
        hook_input = HookInput(
            tool_input={"command": 'echo "path\\\\watch\\\\"'}
        )

        result = service.execute_hook(HookType.SERVER_GUARD, hook_input)

        assert result == HookResult.ALLOW

    def test_handles_unclosed_quotes_gracefully(self) -> None:
        """Test that unclosed quotes don't cause errors."""
        service = ClaudeHookService()
        # Unclosed double quote - everything after should be treated as quoted
        hook_input = HookInput(tool_input={"command": 'echo "watch'})

        result = service.execute_hook(HookType.SERVER_GUARD, hook_input)

        # 'watch' is inside the unclosed quote, so should be stripped
        assert result == HookResult.ALLOW

    def test_handles_empty_escaped_quotes(self) -> None:
        """Test empty escaped quotes don't cause issues."""
        service = ClaudeHookService()
        hook_input = HookInput(tool_input={"command": 'echo "\\"\\"" watch'})

        result = service.execute_hook(HookType.SERVER_GUARD, hook_input)

        # 'watch' is outside quotes, should be blocked
        assert result == HookResult.BLOCK

    def test_is_watch_command_with_escaped_quotes(self) -> None:
        """Test is_watch_command helper with escaped quotes."""
        service = ClaudeHookService()

        # 'watch' inside escaped quotes should not match
        assert service.is_watch_command('echo "v1.0 \\"watch\\" release"') is False
        # Actual watch command should still match
        assert service.is_watch_command("npm test --watch") is True

    def test_is_server_command_with_escaped_quotes(self) -> None:
        """Test is_server_command helper with escaped quotes."""
        service = ClaudeHookService()

        # 'npm run dev' inside escaped quotes should not match
        assert service.is_server_command('echo "\\"npm run dev\\""') is False
        # Actual server command should still match
        assert service.is_server_command("npm run dev") is True


class TestCommandSubstitutions:
    """Tests for command substitutions with nested quotes."""

    def test_allows_watch_in_command_substitution_inside_quotes(self) -> None:
        """Test that 'watch' in a command substitution inside quotes is allowed."""
        service = ClaudeHookService()
        # The entire string is quoted, so command substitution content is stripped
        hook_input = HookInput(
            tool_input={"command": 'echo "$(git commit -m \'watch this\')"'}
        )

        result = service.execute_hook(HookType.SERVER_GUARD, hook_input)

        assert result == HookResult.ALLOW

    def test_allows_serve_in_nested_command_substitution(self) -> None:
        """Test that 'serve' in nested quotes in command substitution is allowed."""
        service = ClaudeHookService()
        hook_input = HookInput(
            tool_input={"command": 'result="$(echo \"serve endpoint\")"'}
        )

        result = service.execute_hook(HookType.SERVER_GUARD, hook_input)

        assert result == HookResult.ALLOW

    def test_blocks_watch_outside_command_substitution(self) -> None:
        """Test that watch outside a command substitution is still blocked."""
        service = ClaudeHookService()
        # watch is outside the quoted string
        hook_input = HookInput(
            tool_input={"command": 'npm run watch "$(echo test)"'}
        )

        result = service.execute_hook(HookType.SERVER_GUARD, hook_input)

        assert result == HookResult.BLOCK

    def test_handles_backtick_command_substitution(self) -> None:
        """Test that backtick command substitution with watch inside is allowed."""
        service = ClaudeHookService()
        # Note: shlex treats backticks as regular characters in POSIX mode
        # The "watch" is inside the double-quoted string
        hook_input = HookInput(
            tool_input={"command": 'echo "`git commit -m watch`"'}
        )

        result = service.execute_hook(HookType.SERVER_GUARD, hook_input)

        assert result == HookResult.ALLOW

    def test_helper_ignores_watch_in_command_substitution(self) -> None:
        """Test is_watch_command ignores content in command substitutions."""
        service = ClaudeHookService()

        # 'watch' inside quoted command substitution should not match
        assert (
            service.is_watch_command('echo "$(git commit -m \'watch\')"') is False
        )
        # Actual watch command should still match
        assert service.is_watch_command("npm run watch") is True

    def test_handles_dollar_single_quotes(self) -> None:
        """Test ANSI-C quoting ($'...') with watch inside."""
        service = ClaudeHookService()
        # shlex handles $'...' syntax in POSIX mode
        hook_input = HookInput(
            tool_input={"command": "echo $'watch\\nthis'"}
        )

        result = service.execute_hook(HookType.SERVER_GUARD, hook_input)

        # The $'...' is treated as a single-quoted string by shlex
        assert result == HookResult.ALLOW


class TestTokenBasedParsing:
    """Tests for token-based command parsing that fixes fragile regex patterns."""

    def test_blocks_git_commit_verbose_short(self) -> None:
        """git commit -v opens editor, should block."""
        service = ClaudeHookService()
        assert (
            service.is_interactive_git_command("git commit -v") is True
        ), "git commit -v opens editor and should be blocked"

    def test_blocks_git_commit_verbose_long(self) -> None:
        """git commit --verbose opens editor, should block."""
        service = ClaudeHookService()
        assert (
            service.is_interactive_git_command("git commit --verbose") is True
        ), "git commit --verbose opens editor and should be blocked"

    def test_blocks_explicit_watch_flag(self) -> None:
        """npm test --watch --verbose should block (explicit --watch)."""
        service = ClaudeHookService()
        assert (
            service.is_watch_command("npm test --watch --verbose") is True
        ), "explicit --watch flag should be blocked"

    def test_blocks_git_commit_amend_verbose(self) -> None:
        """git commit --amend -v opens editor, should block."""
        service = ClaudeHookService()
        assert (
            service.is_interactive_git_command("git commit --amend -v") is True
        ), "git commit --amend -v opens editor and should be blocked"

    def test_allows_git_commit_amend_with_message(self) -> None:
        """git commit --amend -m 'fix' should allow."""
        service = ClaudeHookService()
        assert (
            service.is_interactive_git_command('git commit --amend -m "fix"') is False
        ), "-m flag provides message non-interactively"

    def test_allows_git_commit_amend_no_edit(self) -> None:
        """git commit --amend --no-edit should allow."""
        service = ClaudeHookService()
        assert (
            service.is_interactive_git_command("git commit --amend --no-edit") is False
        ), "--no-edit skips editor"

    def test_allows_git_commit_with_message_and_verbose(self) -> None:
        """git commit -m 'msg' -v should allow (has message flag)."""
        service = ClaudeHookService()
        assert (
            service.is_interactive_git_command('git commit -m "msg" -v') is False
        ), "-m flag provides message, -v is safe alongside it"

    def test_allows_jest_workers_flag(self) -> None:
        """jest -w 4 should allow (-w sets workers, not watch mode)."""
        service = ClaudeHookService()
        assert (
            service.is_watch_command("jest -w 4 --coverage") is False
        ), "-w in jest sets worker count, not watch mode"

    def test_blocks_jest_explicit_watch(self) -> None:
        """jest --watch should block (explicit watch mode)."""
        service = ClaudeHookService()
        assert (
            service.is_watch_command("jest --watch") is True
        ), "explicit --watch should be blocked"

    def test_blocks_npm_test_watch_with_options(self) -> None:
        """npm test --watch --coverage should block."""
        service = ClaudeHookService()
        assert (
            service.is_watch_command("npm test --watch --coverage") is True
        ), "--watch should be blocked regardless of other flags"

    def test_allows_git_commit_with_file_flag(self) -> None:
        """git commit -F message.txt should allow."""
        service = ClaudeHookService()
        assert (
            service.is_interactive_git_command("git commit -F message.txt") is False
        ), "-F reads message from file non-interactively"

    def test_allows_git_commit_with_reuse_message(self) -> None:
        """git commit -C HEAD should allow."""
        service = ClaudeHookService()
        assert (
            service.is_interactive_git_command("git commit -C HEAD") is False
        ), "-C reuses message from another commit"

    def test_allows_git_commit_with_message_equals_syntax(self) -> None:
        """git commit --message='fix' should allow (--flag=value syntax)."""
        service = ClaudeHookService()
        assert (
            service.is_interactive_git_command("git commit --message='fix bug'") is False
        ), "--message=value syntax should be recognized"

    def test_allows_git_commit_with_file_equals_syntax(self) -> None:
        """git commit --file=msg.txt should allow (--flag=value syntax)."""
        service = ClaudeHookService()
        assert (
            service.is_interactive_git_command("git commit --file=msg.txt") is False
        ), "--file=value syntax should be recognized"

    def test_watch_command_ignores_watch_in_unclosed_quote(self) -> None:
        """Watch detection ignores watch keyword inside unclosed quotes."""
        service = ClaudeHookService()
        # Unclosed quote - _strip_quoted_content handles partial quotes
        # The 'watch' appears after the quote, so it's treated as quoted content
        assert (
            service.is_watch_command('echo "watch') is False
        ), "watch inside unclosed quote should not trigger block"

    def test_interactive_git_fallback_to_regex_on_unclosed_quote(self) -> None:
        """Interactive git detection falls back to regex on malformed commands."""
        service = ClaudeHookService()
        # Unclosed quote - tokenization fails, falls back to regex
        # The 'rebase -i' is inside the unclosed quote, so regex handles it
        assert (
            service.is_interactive_git_command('git log "rebase -i') is False
        ), "rebase -i inside unclosed quote should not trigger block via regex fallback"

    def test_server_guard_blocks_verbose_commit(self) -> None:
        """Server guard should block git commit -v."""
        service = ClaudeHookService()
        hook_input = HookInput(tool_input={"command": "git commit -v"})
        result = service.execute_hook(HookType.SERVER_GUARD, hook_input)
        assert result == HookResult.BLOCK, "server guard should block git commit -v"

    def test_server_guard_allows_commit_with_message(self) -> None:
        """Server guard should allow git commit -m."""
        service = ClaudeHookService()
        hook_input = HookInput(tool_input={"command": 'git commit -m "test"'})
        result = service.execute_hook(HookType.SERVER_GUARD, hook_input)
        assert result == HookResult.ALLOW, "server guard should allow git commit -m"

    def test_allows_git_log(self) -> None:
        """git log is not interactive and should be allowed."""
        service = ClaudeHookService()
        assert (
            service.is_interactive_git_command("git log") is False
        ), "git log is not interactive"
        assert (
            service.is_interactive_git_command("git log --oneline") is False
        ), "git log --oneline is not interactive"

    def test_allows_git_status(self) -> None:
        """git status is not interactive and should be allowed."""
        service = ClaudeHookService()
        assert (
            service.is_interactive_git_command("git status") is False
        ), "git status is not interactive"

    def test_allows_git_diff(self) -> None:
        """git diff is not interactive and should be allowed."""
        service = ClaudeHookService()
        assert (
            service.is_interactive_git_command("git diff") is False
        ), "git diff is not interactive"
        assert (
            service.is_interactive_git_command("git diff HEAD~1") is False
        ), "git diff HEAD~1 is not interactive"

    def test_allows_git_commit_with_combined_am_flag(self) -> None:
        """git commit -am 'msg' should allow (-a and -m combined)."""
        service = ClaudeHookService()
        assert (
            service.is_interactive_git_command('git commit -am "msg"') is False
        ), "-am combines -a (stage all) and -m (message)"

    def test_allows_git_commit_with_combined_vm_flag(self) -> None:
        """git commit -vm 'msg' should allow (-v and -m combined)."""
        service = ClaudeHookService()
        assert (
            service.is_interactive_git_command('git commit -vm "msg"') is False
        ), "-vm combines -v (verbose) and -m (message)"

    def test_allows_git_commit_with_combined_avm_flag(self) -> None:
        """git commit -avm 'msg' should allow (-a, -v, and -m combined)."""
        service = ClaudeHookService()
        assert (
            service.is_interactive_git_command('git commit -avm "msg"') is False
        ), "-avm combines -a, -v, and -m flags"

    def test_blocks_git_commit_with_combined_av_flag(self) -> None:
        """git commit -av should block (no message flag)."""
        service = ClaudeHookService()
        assert (
            service.is_interactive_git_command("git commit -av") is True
        ), "-av has no message flag, opens editor"
