# ruff: noqa: RUF001, RUF002, RUF003
"""Tests for auto_yes.detector module."""

from auto_yes.detector import PromptDetector  # noqa: I001


# ==================================================================
# generic patterns (always loaded)
# ==================================================================


class TestGenericBrackets:
    def setup_method(self):
        self.det = PromptDetector()

    def test_y_n_lower(self):
        assert self.det.detect("Continue? [y/n]") is not None

    def test_Y_n(self):
        assert self.det.detect("Proceed? [Y/n]") is not None

    def test_y_N(self):
        assert self.det.detect("Delete file? [y/N]") is not None

    def test_parens_y_n(self):
        assert self.det.detect("Remove package? (y/n)") is not None

    def test_yes_no_brackets(self):
        result = self.det.detect("Overwrite config? [yes/no]")
        assert result is not None
        assert result.suggested_response == "yes"

    def test_yes_no_parens(self):
        result = self.det.detect("Replace all? (yes/no)")
        assert result is not None
        assert result.suggested_response == "yes"

    def test_conda_default_y(self):
        assert self.det.detect("Proceed ([y]/n)?") is not None

    def test_conda_default_n(self):
        assert self.det.detect("Proceed (y/[n])?") is not None

    def test_standalone_y_npm(self):
        assert self.det.detect("Ok to proceed? (y)") is not None

    def test_bare_y_n(self):
        assert self.det.detect("Continue? y/n:") is not None

    def test_bare_y_n_no_colon(self):
        assert self.det.detect("Continue? y/n") is not None

    def test_bracket_yes_no_style(self):
        assert self.det.detect("[Y]es / [N]o") is not None

    def test_bracket_yes_lower_no_style(self):
        assert self.det.detect("[Y]es / [n]o") is not None


class TestGenericSentences:
    def setup_method(self):
        self.det = PromptDetector()

    def test_do_you_want_to_continue(self):
        assert self.det.detect("Do you want to continue?") is not None

    def test_are_you_sure(self):
        assert self.det.detect("Are you sure?") is not None

    def test_proceed_question(self):
        assert self.det.detect("Proceed?") is not None

    def test_overwrite_excluded(self):
        assert self.det.detect("Overwrite output.txt?") is None

    def test_remove_excluded(self):
        assert self.det.detect("Remove directory /tmp/foo?") is None

    def test_would_you_like_to(self):
        assert self.det.detect("Would you like to install it?") is not None

    def test_do_you_agree(self):
        assert self.det.detect("Do you agree to the terms?") is not None

    def test_do_you_accept(self):
        assert self.det.detect("Do you accept the license?") is not None


class TestGenericSSH:
    def setup_method(self):
        self.det = PromptDetector()

    def test_ssh_yes_no(self):
        prompt = "Are you sure you want to " "continue connecting (yes/no)?"
        result = self.det.detect(prompt)
        assert result is not None
        assert result.suggested_response == "yes"

    def test_ssh_yes_no_fingerprint(self):
        prompt = "Are you sure you want to " "continue connecting (yes/no/[fingerprint])?"
        result = self.det.detect(prompt)
        assert result is not None
        assert result.suggested_response == "yes"


class TestGenericActionVerbs:
    def setup_method(self):
        self.det = PromptDetector()

    def test_allow(self):
        assert self.det.detect("Allow access to /tmp?") is not None

    def test_approve(self):
        assert self.det.detect("Approve this change?") is not None

    def test_accept(self):
        assert self.det.detect("Accept incoming connection?") is not None

    def test_download(self):
        assert self.det.detect("Download 150MB package?") is not None

    def test_upgrade_excluded(self):
        assert self.det.detect("Upgrade to version 3.0?") is None

    def test_update_excluded(self):
        assert self.det.detect("Update all packages?") is None

    def test_enable(self):
        assert self.det.detect("Enable auto-updates?") is not None

    def test_install_excluded(self):
        assert self.det.detect("Install missing dependencies?") is None

    def test_create(self):
        assert self.det.detect("Create directory /opt/app?") is not None

    def test_merge_excluded(self):
        assert self.det.detect("Merge branch 'feature' into main?") is None

    def test_restart_excluded(self):
        assert self.det.detect("Restart the service?") is None

    def test_reboot_excluded(self):
        assert self.det.detect("Reboot now?") is None


class TestGenericLicenseAgreement:
    def setup_method(self):
        self.det = PromptDetector()

    def test_accept_license(self):
        assert self.det.detect("Accept the license agreement") is not None

    def test_agree_to_terms(self):
        assert self.det.detect("Agree to the terms of service") is not None


class TestGenericSpecial:
    def setup_method(self):
        self.det = PromptDetector()

    def test_type_yes_to_continue(self):
        result = self.det.detect("Type 'yes' to continue:")
        assert result is not None
        assert result.suggested_response == "yes"

    def test_type_uppercase_yes(self):
        result = self.det.detect("Type 'YES' to confirm:")
        assert result is not None
        assert result.suggested_response == "yes"

    def test_enter_yes_to_confirm(self):
        result = self.det.detect("Enter 'yes' to proceed:")
        assert result is not None
        assert result.suggested_response == "yes"

    def test_terraform_only_yes_accepted(self):
        result = self.det.detect("Only 'yes' will be accepted to confirm.")
        assert result is not None
        assert result.suggested_response == "yes"

    def test_press_enter(self):
        result = self.det.detect("Press Enter to continue")
        assert result is not None
        assert result.suggested_response == ""

    def test_press_any_key(self):
        result = self.det.detect("Press any key to continue")
        assert result is not None
        assert result.suggested_response == ""

    def test_default_y_paren(self):
        result = self.det.detect("Use cache? (default: Y)")
        assert result is not None
        assert result.suggested_response == ""

    def test_default_yes_bracket(self):
        result = self.det.detect("Use cache? [default=yes]")
        assert result is not None
        assert result.suggested_response == ""

    def test_is_this_ok(self):
        assert self.det.detect("Is this ok [y/d/N]:") is not None

    def test_ok_to_proceed(self):
        assert self.det.detect("Ok to proceed?") is not None


class TestNeedRequirePrompts:
    def setup_method(self):
        self.det = PromptDetector()

    def test_need_to_install(self):
        assert self.det.detect("Need to install the following packages?") is not None

    def test_do_you_need_to(self):
        assert self.det.detect("Do you need to download dependencies?") is not None

    def test_required_question(self):
        assert self.det.detect("Installation required?") is not None

    def test_requires_question(self):
        assert self.det.detect("This action requires elevated access, continue?") is not None

    def test_necessary_question(self):
        assert self.det.detect("Is this step necessary?") is not None

    def test_need_no_question_mark_no_match(self):
        assert self.det.detect("You will need to restart later.") is None


# ==================================================================
# false-positive prevention
# ==================================================================


class TestNoFalsePositives:
    def setup_method(self):
        self.det = PromptDetector()

    def test_empty_string(self):
        assert self.det.detect("") is None

    def test_whitespace_only(self):
        assert self.det.detect("   \n   \n") is None

    def test_regular_output_with_newline(self):
        text = "Setting y/n flag in config\nDone.\n"
        assert self.det.detect(text) is None

    def test_prompt_in_middle_still_detected(self):
        # full-buffer matching catches prompts on non-last lines so that
        # multi-line menus are not missed.  the runner's buffer-clear-on-input
        # and timing guards prevent stale matches in practice.
        text = "Question: continue? [y/n]\nUser chose yes.\nProcessing...\n"
        assert self.det.detect(text) is not None

    def test_plain_statement(self):
        assert self.det.detect("Downloading files...") is None

    def test_progress_line(self):
        assert self.det.detect("Installing: 45% complete") is None


# ==================================================================
# AI CLI categories
# ==================================================================


class TestClaudePatterns:
    """Claude Code CLI patterns — covers both ASCII > and Unicode ❯ (U+276F)."""

    def setup_method(self):
        self.det = PromptDetector(categories=["generic", "claude"])

    # --- trust folder ---

    def test_trust_folder_ascii(self):
        assert self.det.detect("> 1. Yes, I trust this folder") is not None

    def test_trust_folder_unicode(self):
        assert self.det.detect("❯ 1. Yes, I trust this folder") is not None

    # --- API key ---

    def test_api_key(self):
        assert self.det.detect("Do you want to use this API key?") is not None

    # --- numbered Yes (any digit) ---

    def test_numbered_yes_ascii(self):
        assert self.det.detect("> 1. Yes") is not None

    def test_numbered_yes_unicode(self):
        assert self.det.detect("❯ 1. Yes") is not None

    def test_numbered_yes_option_2(self):
        assert self.det.detect("❯ 2. Yes") is not None

    # --- real Claude Code permission prompt (full TUI output) ---

    def test_permission_prompt_full(self):
        text = (
            " Bash command\n"
            "\n"
            "   nvidia-smi 2>&1\n"
            "   Check GPU status\n"
            "\n"
            " Permission rule Bash requires confirmation for this command.\n"
            "\n"
            " Do you want to proceed?\n"
            " ❯ 1. Yes\n"
            "   2. No\n"
        )
        assert self.det.detect(text) is not None

    def test_permission_prompt_with_crlf(self):
        text = (
            " Do you want to proceed?\r\n"
            " ❯ 1. Yes\r\n"
            "   2. No\r\n"
        )
        assert self.det.detect(text) is not None

    # --- press enter ---

    def test_press_enter(self):
        r = self.det.detect("Press Enter to continue")
        assert r is not None
        assert r.suggested_response == ""

    def test_press_enter_with_ellipsis(self):
        r = self.det.detect("Press Enter to continue…")
        assert r is not None

    # --- dark mode setup ---

    def test_dark_mode_ascii(self):
        assert self.det.detect("> 1. Dark mode") is not None

    def test_dark_mode_unicode(self):
        assert self.det.detect("❯ 1. Dark mode") is not None

    # --- yes allow ---

    def test_yes_allow_unicode(self):
        assert self.det.detect("❯ Yes, allow this tool") is not None

    def test_yes_allow_no_prefix(self):
        assert self.det.detect("Yes, allow") is not None

    # --- allow once / always ---

    def test_allow_once_ascii(self):
        assert self.det.detect("> Allow once") is not None

    def test_allow_once_unicode(self):
        assert self.det.detect("❯ Allow once") is not None

    def test_allow_always_unicode(self):
        assert self.det.detect("❯ Allow always") is not None

    # --- allow variants: numbered menu items (newer Claude Code formats) ---

    def test_allow_for_this_session_bare(self):
        assert self.det.detect("❯ Allow for this session") is not None

    def test_allow_once_numbered(self):
        assert self.det.detect("❯ 1. Allow once") is not None

    def test_allow_for_session_numbered(self):
        assert self.det.detect("❯ 2. Allow for this session") is not None

    def test_always_allow_numbered(self):
        assert self.det.detect("❯ 3. Always allow for this project") is not None

    def test_always_allow_bare(self):
        assert self.det.detect("❯ Always allow") is not None

    def test_allow_numbered_generic(self):
        # numbered Allow with unexpected suffix should still match
        assert self.det.detect("❯ 1. Allow all") is not None

    # --- do you want to proceed ---

    def test_do_want_to_proceed(self):
        assert self.det.detect("Do you want to proceed?") is not None

    def test_do_want_to_proceed_with_spaces(self):
        assert self.det.detect(" Do you want to proceed? ") is not None

    # --- full allow dialog (realistic Claude Code output) ---

    def test_allow_menu_bare_dialog(self):
        """Full allow-once dialog without numbers (permission popup)."""
        text = (
            " Claude wants to run a command:\n"
            "\n"
            "   bash: git log --oneline -10\n"
            "\n"
            " ❯ Allow once\n"
            "   Allow for this session\n"
            "   Always allow\n"
            "   No\n"
        )
        assert self.det.detect(text) is not None

    def test_allow_menu_numbered_dialog(self):
        """Full numbered allow dialog (permission rule dialog)."""
        text = (
            " Claude wants to run a bash command.\n"
            "\n"
            " ❯ 1. Allow once\n"
            "   2. Allow for this session\n"
            "   3. Always allow for this project\n"
            "   4. No\n"
        )
        assert self.det.detect(text) is not None

    # --- no false positives ---

    def test_no_match_plain_text(self):
        assert self.det.detect("Compiling project...") is None

    def test_no_match_numbered_no(self):
        # "2. No" should not match any Claude pattern
        det_claude_only = PromptDetector(categories=["claude"])
        assert det_claude_only.detect("  2. No") is None

    def test_no_match_allow_without_indicator(self):
        # "Allow" without ❯/> prefix should not match
        det_claude_only = PromptDetector(categories=["claude"])
        assert det_claude_only.detect("  Allow for this session") is None


class TestGeminiPatterns:
    def setup_method(self):
        self.det = PromptDetector(categories=["generic", "gemini"])

    def test_allow_once(self):
        assert self.det.detect("│ ● 1. Allow once") is not None

    def test_yes_allow(self):
        assert self.det.detect("│ ● 1. Yes, allow once") is not None

    def test_allow_for_session(self):
        assert self.det.detect("│ ● 2. Allow for this session") is not None

    def test_always_allow(self):
        assert self.det.detect("│ ● 3. Always allow") is not None


class TestCodexPatterns:
    def setup_method(self):
        self.det = PromptDetector(categories=["generic", "codex"])

    def test_approve_and_run(self):
        assert self.det.detect("> 1. Approve and run now") is not None

    def test_yes_allow_codex(self):
        assert self.det.detect("> 1. Yes, allow Codex to work") is not None


class TestCursorPatterns:
    """Cursor Agent CLI patterns — (y) catch-all + trust workspace."""

    def setup_method(self):
        self.det = PromptDetector(categories=["generic", "cursor"])

    # --- (y) catch-all: every action type ---

    def test_run_once_with_enter(self):
        assert self.det.detect("→ Run (once) (y) (enter)") is not None

    def test_run_once_without_enter(self):
        assert self.det.detect("→ Run (once) (y)") is not None

    def test_delete_y(self):
        assert self.det.detect("→ Delete (y)") is not None

    def test_edit_y(self):
        assert self.det.detect("→ Edit (y)") is not None

    def test_write_y(self):
        assert self.det.detect("→ Write (y)") is not None

    def test_create_y(self):
        assert self.det.detect("→ Create (y)") is not None

    def test_execute_y(self):
        assert self.det.detect("→ Execute (y)") is not None

    # --- (y) inside box-drawing TUI ---

    def test_run_once_inside_box(self):
        assert self.det.detect(" │  → Run (once) (y) (enter)  │") is not None

    def test_run_once_inside_box_no_enter(self):
        assert self.det.detect(" │  → Run (once) (y)  │") is not None

    def test_delete_inside_box(self):
        assert self.det.detect(" │  → Delete (y)  │") is not None

    # --- response is empty string (just Enter) ---

    def test_response_is_enter_only(self):
        r = self.det.detect("→ Run (once) (y)")
        assert r is not None
        assert r.suggested_response == ""

    def test_delete_response_is_enter(self):
        r = self.det.detect("→ Delete (y)")
        assert r is not None
        assert r.suggested_response == ""

    # --- full approval dialog with \r\n ---

    def test_approval_dialog_full(self):
        text = (
            " Waiting for approval...\n"
            " │ Run this command?\n"
            " │  → Run (once) (y) (enter)\n"
            " │    Skip (esc or n)      "
        )
        assert self.det.detect(text) is not None

    def test_approval_dialog_crlf(self):
        text = (
            " │ Delete this file?\r\n"
            " │  → Delete (y)\r\n"
            " │    Keep (n)\r\n"
        )
        assert self.det.detect(text) is not None

    def test_approval_dialog_delete(self):
        text = (
            " ┌─────────────────────────────┐\n"
            " │ Delete file: test.py         │\n"
            " └─────────────────────────────┘\n"
            " ┌─────────────────────────────┐\n"
            " │ Delete this file?            │\n"
            " │  → Delete (y)               │\n"
            " │    Keep (n)                 │\n"
            " └─────────────────────────────┘\n"
        )
        assert self.det.detect(text) is not None

    # --- trust workspace ---

    def test_trust_workspace(self):
        r = self.det.detect("▶ [a] Trust this workspace")
        assert r is not None
        assert r.suggested_response == "a"

    def test_trust_workspace_inside_box(self):
        r = self.det.detect(" │ ▶ [a] Trust this workspace  │")
        assert r is not None
        assert r.suggested_response == "a"

    # --- no false positives ---

    def test_no_match_plain_text(self):
        det = PromptDetector(categories=["cursor"])
        assert det.detect("Compiling main.py...") is None

    def test_no_match_parenthesized_word(self):
        det = PromptDetector(categories=["cursor"])
        assert det.detect("see documentation (yesterday)") is None

    def test_no_match_keep_n(self):
        det = PromptDetector(categories=["cursor"])
        assert det.detect("    Keep (n)") is None


class TestCopilotPatterns:
    def setup_method(self):
        self.det = PromptDetector(categories=["generic", "copilot"])

    def test_yes_proceed(self):
        assert self.det.detect("│ \u276f 1. Yes, proceed") is not None

    def test_allow_copilot(self):
        assert self.det.detect("Allow Copilot to run this command?") is not None


class TestAiderPatterns:
    def setup_method(self):
        self.det = PromptDetector(categories=["generic", "aider"])

    def test_yes_no_style(self):
        assert self.det.detect("Run shell command? (Y)es/(N)o/(D)on't ask again [Yes]:") is not None

    def test_add_to_chat(self):
        assert self.det.detect("Add main.py to the chat?") is not None

    def test_run_shell_command(self):
        assert self.det.detect("Run shell command?") is not None

    def test_apply_edit(self):
        assert self.det.detect("Apply edit to src/utils.py?") is not None

    def test_create_new_file(self):
        assert self.det.detect("Create new file tests/test_api.py?") is not None

    def test_fix_lint(self):
        assert self.det.detect("Attempt to fix lint errors?") is not None

    def test_drop_from_chat(self):
        assert self.det.detect("Drop old_file.py from the chat?") is not None


class TestOpenHandsPatterns:
    def setup_method(self):
        self.det = PromptDetector(categories=["generic", "openhands"])

    def test_execute_action(self):
        assert self.det.detect("Do you want to execute this action?") is not None

    def test_approve(self):
        assert self.det.detect("> Approve") is not None


class TestWindsurfPatterns:
    def setup_method(self):
        self.det = PromptDetector(categories=["generic", "windsurf"])

    def test_accept_changes(self):
        assert self.det.detect("Accept all changes?") is not None

    def test_run_command(self):
        assert self.det.detect("Run this command?") is not None


class TestQwenPatterns:
    def setup_method(self):
        self.det = PromptDetector(categories=["generic", "qwen"])

    def test_numbered_yes(self):
        assert self.det.detect("> 1. Yes") is not None

    def test_approve_execution(self):
        assert self.det.detect("Approve execution?") is not None


class TestAmazonQPatterns:
    def setup_method(self):
        self.det = PromptDetector(categories=["generic", "amazonq"])

    def test_approve_action(self):
        assert self.det.detect("Do you approve this action?") is not None

    def test_accept_suggestion(self):
        assert self.det.detect("Accept suggestion?") is not None


class TestLoadAll:
    def test_all_categories_loadable(self):
        from auto_yes.patterns import REGISTRY

        all_cats = list(REGISTRY.keys())
        det = PromptDetector(categories=all_cats)
        assert len(det.pattern_strings) > 0

    def test_generic_still_works_with_all(self):
        from auto_yes.patterns import REGISTRY

        det = PromptDetector(categories=list(REGISTRY.keys()))
        assert det.detect("Continue? [y/n]") is not None

    def test_all_categories_no_empty(self):
        from auto_yes.patterns import REGISTRY

        for name, entry in REGISTRY.items():
            assert len(entry["patterns"]) > 0, f"category '{name}' has no patterns"


# ==================================================================
# custom patterns & runtime mutation
# ==================================================================


class TestCustomPatterns:
    def test_extra_pattern_init(self):
        det = PromptDetector(extra_patterns=[r"custom_prompt\?"])
        assert det.detect("custom_prompt?") is not None

    def test_add_pattern_runtime(self):
        det = PromptDetector()
        assert det.detect("do_thing?") is None
        det.add_pattern(r"do_thing\?")
        assert det.detect("do_thing?") is not None

    def test_load_category_runtime(self):
        det = PromptDetector()
        assert det.detect("> 1. Yes, I trust this folder") is None
        det.load_category("claude")
        assert det.detect("> 1. Yes, I trust this folder") is not None

    def test_pattern_strings_includes_custom(self):
        det = PromptDetector(extra_patterns=[r"my_pat"])
        assert r"my_pat" in det.pattern_strings


class TestAnsiInPrompt:
    def test_colored_prompt_detected(self):
        det = PromptDetector()
        colored = "\x1b[1;33mContinue? [y/n]\x1b[0m"
        assert det.detect(colored) is not None


# ==================================================================
# Parallel agent / subagent / overnight-loop scenarios
# ==================================================================


class TestClaudeSubagentScenarios:
    """Verify detection works for parallel-agent and subagent patterns.

    These cover the overnight-loop use case: Claude Code spawning subagents
    via the Agent tool, each needing its own set of permissions.  The buffer
    seen by the detector may contain long chains of output followed by a
    permission prompt at the bottom.
    """

    def setup_method(self):
        self.det = PromptDetector(categories=["generic", "claude"])

    def test_agent_tool_bash_permission(self):
        """Agent tool triggers bash permission dialog (most common overnight block)."""
        text = (
            " Bash command\n"
            "\n"
            "   nvidia-smi --query-gpu=utilization.gpu --format=csv,noheader\n"
            "   Check GPU utilization\n"
            "\n"
            " Permission rule  Bash requires confirmation for this command.\n"
            "\n"
            " Do you want to proceed?\n"
            " ❯ 1. Yes\n"
            "   2. No\n"
        )
        assert self.det.detect(text) is not None

    def test_subagent_spawn_then_permission(self):
        """Parent agent spawns subagent; subagent then asks for permission."""
        text = (
            "[Agent] Spawning subagent: code-reviewer\n"
            "[Agent] Task: review src/main.py\n"
            "\n"
            " Bash command\n"
            "   cat src/main.py\n"
            "\n"
            " Do you want to proceed?\n"
            " ❯ 1. Yes\n"
            "   2. No\n"
        )
        assert self.det.detect(text) is not None

    def test_multi_subagent_output_prompt_at_bottom(self):
        """Long buffer with output from multiple agents; prompt at bottom."""
        text = (
            "[Agent-1] Reading file: README.md\n"
            "[Agent-1] Analyzing...\n"
            "[Agent-2] Reading file: src/utils.py\n"
            "[Agent-2] Analyzing...\n"
            "[Agent-3] Running tests...\n"
            "  Test suite: 45/50 passed\n"
            "\n"
            " Bash command\n"
            "   pytest tests/ --tb=short 2>&1 | head -100\n"
            "\n"
            " ❯ 1. Yes\n"
            "   2. No\n"
        )
        assert self.det.detect(text) is not None

    def test_allow_menu_after_long_output(self):
        """Tool-permission allow-menu after many lines of agent output."""
        text = (
            "Iteration 1: running analysis...\n"
            "Iteration 2: applying fixes...\n"
            "Iteration 3: verifying results...\n"
            "Iteration 4: generating report...\n"
            "\n"
            " ❯ Allow once\n"
            "   Allow for this session\n"
            "   Always allow\n"
            "   No\n"
        )
        assert self.det.detect(text) is not None

    def test_numbered_allow_menu_after_long_output(self):
        """Numbered allow-menu (newer Claude Code format) after long output."""
        text = (
            "Running overnight analysis loop — iteration 47\n"
            "Processing batch 12/20...\n"
            "\n"
            " Claude wants to run a bash command:\n"
            "   git commit -am 'auto-fix iteration 47'\n"
            "\n"
            " ❯ 1. Allow once\n"
            "   2. Allow for this session\n"
            "   3. Always allow for this project\n"
            "   4. No\n"
        )
        assert self.det.detect(text) is not None

    def test_rapid_second_prompt_fresh_buffer(self):
        """After buffer clear on first response, second prompt is detected."""
        # In production the runner clears the buffer after each match.
        # Simulate that by checking a fresh buffer with the second prompt.
        text2 = (
            " Bash command\n"
            "   make test\n"
            "\n"
            " ❯ 1. Yes\n"
            "   2. No\n"
        )
        assert self.det.detect(text2) is not None

    def test_trust_folder_on_subagent_init(self):
        """Trust-folder prompt when subagent starts in a new directory."""
        text = (
            "Starting agent in /workspace/subproject...\n"
            "\n"
            "❯ 1. Yes, I trust this folder\n"
            "  2. No\n"
        )
        assert self.det.detect(text) is not None

    def test_crlf_wrapped_permission_dialog(self):
        """Permission dialog with CRLF line endings (raw PTY mode)."""
        text = (
            " Do you want to proceed?\r\n"
            " ❯ 1. Yes\r\n"
            "   2. No\r\n"
        )
        assert self.det.detect(text) is not None

    def test_ansi_colored_allow_menu(self):
        """Allow menu items rendered with ANSI colors (real terminal output)."""
        text = (
            "\x1b[1m Claude wants to use Bash\x1b[0m\n"
            "\n"
            "\x1b[32m ❯ Allow once\x1b[0m\n"
            "   Allow for this session\n"
        )
        assert self.det.detect(text) is not None

    def test_do_want_to_proceed_standalone(self):
        """Standalone 'Do you want to proceed?' (not always followed by menu)."""
        assert self.det.detect("Do you want to proceed?") is not None


class TestCursorAdvancedScenarios:
    """Advanced Cursor Agent CLI scenarios including parallel ops and subagents."""

    def setup_method(self):
        self.det = PromptDetector(categories=["generic", "cursor"])

    # --- all action-type variants via (y) ---

    def test_read_file_action(self):
        assert self.det.detect("→ Read (y)") is not None

    def test_search_action(self):
        assert self.det.detect("→ Search (y)") is not None

    def test_move_file_action(self):
        assert self.det.detect("→ Move (y)") is not None

    def test_apply_patch_action(self):
        assert self.det.detect("→ Apply (y)") is not None

    def test_batch_run_action(self):
        assert self.det.detect("→ Run all (y)") is not None

    def test_filepath_with_spaces(self):
        """File path with spaces still caught by (y) pattern."""
        assert self.det.detect("→ Edit src/components/my component.tsx (y)") is not None

    # --- nested / box-drawing dialogs ---

    def test_nested_subagent_write_dialog(self):
        """Subagent approval inside box-drawing frame."""
        text = (
            " ┌──────────────────────────────────────┐\n"
            " │ Subagent: code-reviewer               │\n"
            " │ Action: Write report.md               │\n"
            " ├──────────────────────────────────────┤\n"
            " │  → Write (y)                          │\n"
            " │    Skip (n)                           │\n"
            " └──────────────────────────────────────┘\n"
        )
        assert self.det.detect(text) is not None

    def test_parallel_ops_multiple_y_in_buffer(self):
        """Multiple (y) prompts visible in buffer; bottom-up scan finds last."""
        text = (
            " │  → Edit file1.py (y)  │\n"
            " │  → Edit file2.py (y)  │\n"
            " │  → Edit file3.py (y)  │\n"
        )
        assert self.det.detect(text) is not None

    def test_agent_long_output_then_approval(self):
        """Long agent output followed by approval prompt."""
        text = (
            "Analyzing codebase...\n"
            "Found 47 functions to refactor.\n"
            "Generating patches...\n"
            "\n"
            " │  → Write refactored_module.py (y)  │\n"
        )
        assert self.det.detect(text) is not None

    def test_crlf_approval_inside_box(self):
        """CRLF inside box-drawing TUI (raw PTY output)."""
        text = (
            " │  → Run tests (y)\r\n"
            " │    Skip (n)\r\n"
        )
        assert self.det.detect(text) is not None

    def test_ansi_colored_approval(self):
        """Approval line with ANSI color codes."""
        text = "\x1b[36m │  → Write output.txt (y)\x1b[0m"
        assert self.det.detect(text) is not None

    # --- no false positives ---

    def test_no_match_parenthesized_full_word(self):
        det = PromptDetector(categories=["cursor"])
        assert det.detect("see docs (you should read this)") is None

    def test_no_match_paren_yes(self):
        """(yes) is not (y)."""
        det = PromptDetector(categories=["cursor"])
        assert det.detect("Confirm? (yes)") is None

    def test_no_match_keep_n_in_cursor_only(self):
        det = PromptDetector(categories=["cursor"])
        assert det.detect("    Keep (n)") is None


class TestMixedClaudeCursorScenarios:
    """Verify combined claude+cursor detector handles interleaved tool output.

    Real overnight scenarios often run both tools in the same shell session.
    """

    def setup_method(self):
        self.det = PromptDetector(categories=["generic", "claude", "cursor"])

    def test_claude_prompt_detected_in_mixed(self):
        assert self.det.detect("❯ 1. Yes") is not None

    def test_cursor_prompt_detected_in_mixed(self):
        assert self.det.detect("→ Write file.py (y)") is not None

    def test_cursor_trust_workspace_in_mixed(self):
        r = self.det.detect("▶ [a] Trust this workspace")
        assert r is not None
        assert r.suggested_response == "a"

    def test_generic_ssh_in_mixed(self):
        r = self.det.detect(
            "Are you sure you want to continue connecting (yes/no/[fingerprint])?"
        )
        assert r is not None
        assert r.suggested_response == "yes"

    def test_interleaved_claude_cursor_buffer(self):
        """Buffer with both Claude and Cursor output; Claude prompt at bottom."""
        text = (
            "[cursor-agent] Writing tests...\n"
            " │  → Write tests/test_new.py (y)  │\n"
            "    Written.\n"
            "\n"
            "[claude] Reviewing changes...\n"
            " Bash command\n"
            "   git diff HEAD\n"
            "\n"
            " ❯ 1. Yes\n"
            "   2. No\n"
        )
        assert self.det.detect(text) is not None

    def test_overnight_loop_full_cycle(self):
        """Simulates a complete overnight loop iteration buffer."""
        text = (
            "=== Loop iteration 23 of 100 ===\n"
            "Running: claude 'analyze and fix performance issues'\n"
            "\n"
            "[Agent] Spawning 3 subagents...\n"
            "[Subagent-1] Profiling code...\n"
            "[Subagent-2] Analyzing bottlenecks...\n"
            "[Subagent-3] Generating fixes...\n"
            "\n"
            " Bash command\n"
            "   python -m cProfile -o profile.out main.py\n"
            "\n"
            " Do you want to proceed?\n"
            " ❯ 1. Yes\n"
            "   2. No\n"
        )
        assert self.det.detect(text) is not None
