# ruff: noqa: RUF001, RUF002, RUF003
"""Stress tests for auto_yes — performance, encoding robustness, false-positive
prevention, buffer management, thread-safety, pattern completeness, and
overnight-loop simulation.

All tests are self-contained and require no external fixtures.
"""

import threading
import time
from typing import ClassVar

from auto_yes.detector import PromptDetector
from auto_yes.patterns import REGISTRY

# ==================================================================
# Class 1: TestDetectionPerformance
# ==================================================================


class TestDetectionPerformance:
    """Performance benchmarks — verify the detector is fast enough for
    high-frequency overnight-loop usage."""

    def test_1000_rapid_prompts(self):
        """1000 rapid detections of a Claude prompt must all succeed within 3s."""
        det = PromptDetector(categories=["generic", "claude"])
        text = " Do you want to proceed?\n ❯ 1. Yes\n   2. No\n"

        start = time.monotonic()
        results = [det.detect(text) for _ in range(1000)]
        elapsed = time.monotonic() - start

        assert all(r is not None for r in results), "Some detections returned None"
        assert elapsed < 3.0, f"1000 detections took {elapsed:.2f}s (limit 3s)"

    def test_huge_buffer_1mb(self):
        """Detection in a 1 MB buffer (fake output + prompt) must succeed."""
        det = PromptDetector(categories=["generic", "claude"])
        filler = ("x" * 79 + "\n") * (1024 * 1024 // 80)  # ~1 MB of 80-byte lines
        prompt = " ❯ 1. Yes\n   2. No\n"
        text = filler + prompt

        assert len(text) >= 1024 * 1024, "Precondition: buffer is at least 1 MB"
        result = det.detect(text)
        assert result is not None, "Prompt not detected in 1 MB buffer"

    def test_long_output_before_prompt(self):
        """10 000 lines of normal output then a Claude prompt; detected in < 0.5s."""
        det = PromptDetector(categories=["generic", "claude"])
        filler = "\n".join(f"[INFO] Processing item {i}" for i in range(10_000))
        prompt = "\n ❯ 1. Yes\n   2. No\n"
        text = filler + prompt

        start = time.monotonic()
        result = det.detect(text)
        elapsed = time.monotonic() - start

        assert result is not None, "Prompt not detected after 10 000 output lines"
        assert elapsed < 0.5, f"Detection took {elapsed:.3f}s (limit 0.5s)"


# ==================================================================
# Class 2: TestEncodingEdgeCases
# ==================================================================


class TestEncodingEdgeCases:
    """Verify the detector handles unusual / hostile byte sequences gracefully."""

    def test_null_bytes_ignored(self):
        """Null bytes (\x00) mixed into the buffer must not break detection."""
        det = PromptDetector(categories=["generic", "claude"])
        # clean_text strips control chars (range 0x00-0x08) via strip_control,
        # so \x00 disappears before pattern matching.
        text = "some\x00 output\x00\n ❯\x00 1.\x00 Yes\n"
        result = det.detect(text)
        assert result is not None, "Null bytes prevented detection"

    def test_unicode_in_prompt(self):
        """Chinese, Japanese, and emoji characters before a prompt."""
        det = PromptDetector(categories=["generic", "claude"])
        text = (
            "分析完了\u3002\U0001f916 処理中\u2026\n"
            " ❯ 1. Yes\n"
            "   2. No\n"
        )
        result = det.detect(text)
        assert result is not None, "Unicode preamble broke detection"

    def test_latin1_decoded_as_utf8(self):
        """Partial/invalid UTF-8 bytes decoded with errors='replace' don't crash."""
        det = PromptDetector(categories=["generic", "claude"])
        # Simulate what happens when raw bytes arrive and get decoded leniently.
        raw = b"\xff\xfe invalid bytes \xc0\n \xe2\x9d\xaf 1. Yes\n"
        # The ❯ in the bytes above is encoded as \xe2\x9d\xaf but the invalid
        # leading bytes will become replacement chars; we just need no crash.
        # Use a simpler prompt guaranteed to survive mangling.
        text2 = raw.decode("utf-8", errors="replace") + "\nContinue? [y/n]\n"
        result = det.detect(text2)
        assert result is not None, "errors='replace' decoding broke detection"

    def test_ansi_bomb(self):
        """10 000 ANSI color-reset codes followed by a prompt are still detected."""
        det = PromptDetector(categories=["generic", "claude"])
        bomb = "\x1b[0m" * 10_000
        text = bomb + " ❯ 1. Yes\n   2. No\n"
        result = det.detect(text)
        assert result is not None, "ANSI bomb prevented detection"

    def test_mixed_line_endings(self):
        """Mix of \\r\\n, \\n, and \\r\\r\\n endings with a prompt."""
        det = PromptDetector(categories=["generic", "claude"])
        text = (
            "Header line\r\n"
            "Second line\n"
            "Third line\r\r\n"          # Claude Code-style \\r\\r\\n
            " Do you want to proceed?\r\n"
            " ❯ 1. Yes\r\n"
            "   2. No\r\n"
        )
        result = det.detect(text)
        assert result is not None, "Mixed line endings prevented detection"

    def test_very_long_line(self):
        """Single 100 KB line ending with prompt text is detected."""
        det = PromptDetector(categories=["generic"])
        long_line = "a" * (100 * 1024) + " Continue? [y/n]"
        result = det.detect(long_line)
        assert result is not None, "Very long line (100 KB) prevented detection"

    def test_unicode_heavy_output(self):
        """1000 lines of Chinese/emoji output followed by a prompt."""
        det = PromptDetector(categories=["generic", "claude"])
        lines = [f"\u4e2d\u6587\u884c {i} \U0001f680\U0001f4a1" for i in range(1000)]
        text = "\n".join(lines) + "\n ❯ 1. Yes\n   2. No\n"
        result = det.detect(text)
        assert result is not None, "Unicode-heavy output prevented detection"


# ==================================================================
# Class 3: TestFalsePositiveGauntlet
# ==================================================================


class TestFalsePositiveGauntlet:
    """Verify that ordinary program output does NOT trigger the detector."""

    def test_no_match_on_normal_code_output(self):
        """Lines like 'yes, this works' or 'Yes, I fixed it' are not prompts."""
        det = PromptDetector(categories=["generic", "claude", "cursor"])
        non_prompts = [
            "yes, this works fine",
            "Yes, I fixed it",
            "yes — all tests pass",
            "YES! build succeeded",
        ]
        for text in non_prompts:
            assert det.detect(text) is None, f"False positive on: {text!r}"

    def test_no_match_on_log_lines(self):
        """Structured log output containing 'yes' is not a prompt."""
        det = PromptDetector(categories=["generic", "claude", "cursor"])
        log_lines = [
            "[INFO] yes: operation completed",
            "ERROR: yes value unexpected",
            "DEBUG yes=True flag set",
            "2024-01-01 00:00:00 INFO yes",
        ]
        for line in log_lines:
            assert det.detect(line) is None, f"False positive on log line: {line!r}"

    def test_match_requires_prompt_context(self):
        """'❯ 1. Yes' triggers detection; bare 'yes' does not."""
        det = PromptDetector(categories=["generic", "claude", "cursor"])
        assert det.detect("yes") is None, "Bare 'yes' should not be detected"
        assert det.detect("❯ 1. Yes") is not None, "Prompted 'yes' should be detected"

    def test_no_match_empty_buffer(self):
        """Empty string returns None."""
        det = PromptDetector(categories=["generic", "claude", "cursor"])
        assert det.detect("") is None

    def test_no_match_whitespace_only(self):
        """Whitespace-only buffer returns None."""
        det = PromptDetector(categories=["generic", "claude", "cursor"])
        assert det.detect("   \n  \t  \n\r\n  ") is None

    def test_no_match_on_yes_in_code(self):
        """Python code snippet with 'yes = True' is not a prompt."""
        det = PromptDetector(categories=["generic", "claude", "cursor"])
        code = (
            "def check_flag():\n"
            "    yes = True\n"
            "    return yes\n"
        )
        assert det.detect(code) is None, "Python code with 'yes' triggered detection"


# ==================================================================
# Class 4: TestBufferManagement
# ==================================================================


class TestBufferManagement:
    """Edge cases around buffer content, partial sequences, and trimming."""

    def test_prompt_at_very_end(self):
        """Prompt in the last bytes of an 8192-byte buffer."""
        det = PromptDetector(categories=["generic", "claude"])
        prompt = " ❯ 1. Yes\n   2. No\n"
        # Pad so total length is exactly 8192 bytes.
        pad_len = 8192 - len(prompt.encode("utf-8"))
        filler = ("z" * 79 + "\n") * (pad_len // 80) + "z" * (pad_len % 80)
        text = filler + prompt
        result = det.detect(text)
        assert result is not None, "Prompt at end of 8192-byte buffer not detected"

    def test_prompt_split_across_reads(self):
        """Simulate an ANSI escape sequence split mid-sequence across two reads.

        In practice the runner accumulates chunks; we verify that once the
        full sequence is assembled the detector works correctly.
        """
        det = PromptDetector(categories=["generic", "claude"])
        chunk1 = "output line\n\x1b[1m"          # bold-start, split here
        chunk2 = " ❯ 1. Yes\x1b[0m\n   2. No\n"  # rest of escape + prompt
        combined = chunk1 + chunk2
        result = det.detect(combined)
        assert result is not None, "Detection failed after simulated split read"

    def test_partial_escape_sequence(self):
        """Buffer ending mid-ANSI sequence (e.g. '\\x1b[3') must not crash."""
        det = PromptDetector(categories=["generic", "claude"])
        # Truncated CSI — clean_text must survive this without exception.
        text = "some output\nContinue? [y/n]\x1b[3"
        result = det.detect(text)
        # The prompt is present; the partial escape is harmless.
        assert result is not None, "Partial escape sequence caused detection failure"

    def test_buffer_accumulation(self):
        """Multiple chunks accumulated before detection still works."""
        det = PromptDetector(categories=["generic", "claude"])
        chunks = [
            "Starting task...\n",
            "Processing file 1/3\n",
            "Processing file 2/3\n",
            "Processing file 3/3\n",
            " ❯ 1. Yes\n",
            "   2. No\n",
        ]
        accumulated = "".join(chunks)
        result = det.detect(accumulated)
        assert result is not None, "Detection failed on accumulated multi-chunk buffer"

    def test_buffer_trim_still_detects(self):
        """After trimming a large buffer to its tail, the prompt is still detected.

        Mimics what a runner might do when the buffer grows beyond BUFFER_LIMIT:
        discard everything except the last N bytes and re-run detection.
        """
        det = PromptDetector(categories=["generic", "claude"])
        BUFFER_LIMIT = 64 * 1024  # 64 KB
        KEEP = 8 * 1024           # keep last 8 KB
        filler = ("y" * 79 + "\n") * (BUFFER_LIMIT // 80 + 1)
        prompt = " ❯ 1. Yes\n   2. No\n"
        full_buffer = filler + prompt

        # Trim: keep only the tail.
        tail = full_buffer[-KEEP:]
        result = det.detect(tail)
        assert result is not None, "Prompt lost after buffer trim"


# ==================================================================
# Class 5: TestConcurrentDetection
# ==================================================================


class TestConcurrentDetection:
    """Thread-safety baseline — PromptDetector must be safe for concurrent use."""

    def test_parallel_detector_instances(self):
        """10 threads, each with their own PromptDetector, 100 detections each."""
        text = " ❯ 1. Yes\n   2. No\n"
        errors = []

        def worker():
            try:
                det = PromptDetector(categories=["generic", "claude"])
                for _ in range(100):
                    result = det.detect(text)
                    if result is None:
                        errors.append("None result in thread")
            except Exception as exc:
                errors.append(str(exc))

        threads = [threading.Thread(target=worker) for _ in range(10)]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        assert not errors, f"Errors in threads: {errors}"

    def test_shared_detector_read_only(self):
        """Single detector shared by 20 threads calling detect() simultaneously."""
        det = PromptDetector(categories=["generic", "claude", "cursor"])
        text = " ❯ 1. Yes\n   2. No\n"
        results = []
        lock = threading.Lock()
        errors = []

        def worker():
            try:
                for _ in range(50):
                    r = det.detect(text)
                    with lock:
                        results.append(r)
            except Exception as exc:
                with lock:
                    errors.append(str(exc))

        threads = [threading.Thread(target=worker) for _ in range(20)]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        assert not errors, f"Errors in threads: {errors}"
        assert all(r is not None for r in results), "Some shared-detector detections failed"

    def test_rapid_fire_mixed_patterns(self):
        """500 iterations alternating between claude/cursor/generic prompts."""
        det = PromptDetector(categories=["generic", "claude", "cursor"])
        prompts = [
            (" ❯ 1. Yes\n   2. No\n", True),
            ("→ Write output.py (y)\n", True),
            ("Continue? [y/n]\n", True),
            ("Are you sure?\n", True),
            ("Compiling main.py...\n", False),
            ("Build succeeded.\n", False),
        ]
        errors = []
        for i in range(500):
            text, should_match = prompts[i % len(prompts)]
            result = det.detect(text)
            if should_match and result is None:
                errors.append(f"iter {i}: expected match for {text!r}")
            elif not should_match and result is not None:
                errors.append(f"iter {i}: unexpected match for {text!r}")

        assert not errors, "Rapid-fire failures:\n" + "\n".join(errors[:10])


# ==================================================================
# Class 6: TestPatternCompleteness
# ==================================================================


class TestPatternCompleteness:
    """Verify every registered pattern can actually fire."""

    # Canonical trigger texts for each pattern in the claude category.
    _CLAUDE_TRIGGERS: ClassVar[dict[str, str]] = {
        r"[>❯]\s*1\.\s*Yes,?\s+I trust this folder": "❯ 1. Yes, I trust this folder",
        r"Do you want to use this API key\s*\?": "Do you want to use this API key?",
        r"[>❯]\s*\d+\.\s*Yes": "❯ 1. Yes",
        r"Press Enter to continue": "Press Enter to continue",
        r"[>❯]\s*1\.\s*Dark mode": "❯ 1. Dark mode",
        r"[>❯]?\s*Yes,?\s+allow": "❯ Yes, allow this action",
        r"[>❯]\s*(?:\d+\.\s*)?(?:Allow\s+(?:once|always|for)|Always\s+allow)\b": (
            "❯ Allow once"
        ),
        r"[>❯]\s*\d+\.\s*Allow\b": "❯ 1. Allow all",
        r"Do you want to proceed\s*\?": "Do you want to proceed?",
    }

    # Canonical trigger texts for each pattern in the cursor category.
    _CURSOR_TRIGGERS: ClassVar[dict[str, str]] = {
        r"\(y\)": "→ Run (once) (y)",
        r"▶\s*\[a\]\s*Trust this workspace": "▶ [a] Trust this workspace",
    }

    def test_all_claude_patterns_fire(self):
        """Each Claude pattern, tested with a canonical trigger text, must fire."""
        det = PromptDetector(categories=["claude"])
        for pat_src, _response in REGISTRY["claude"]["patterns"]:
            trigger = self._CLAUDE_TRIGGERS.get(pat_src)
            assert trigger is not None, (
                f"No canonical trigger defined for claude pattern: {pat_src!r}"
            )
            result = det.detect(trigger)
            assert result is not None, (
                f"Claude pattern {pat_src!r} did not match trigger {trigger!r}"
            )

    def test_all_cursor_patterns_fire(self):
        """Each Cursor pattern, tested with a canonical trigger text, must fire."""
        det = PromptDetector(categories=["cursor"])
        for pat_src, _response in REGISTRY["cursor"]["patterns"]:
            trigger = self._CURSOR_TRIGGERS.get(pat_src)
            assert trigger is not None, (
                f"No canonical trigger defined for cursor pattern: {pat_src!r}"
            )
            result = det.detect(trigger)
            assert result is not None, (
                f"Cursor pattern {pat_src!r} did not match trigger {trigger!r}"
            )

    def test_all_generic_patterns_fire(self):
        """Each generic pattern must match its own regex source when embedded in text.

        Rather than maintaining a hand-crafted trigger table, we compile each
        pattern individually and confirm the detector (loaded with that one
        pattern via extra_patterns) hits it with a known-good example from the
        existing test suite.
        """
        generic_triggers = [
            # [y/n] family
            ("Continue? [y/n]",          r"\[y/n\]"),
            ("Proceed? [Y/n]",           r"\[Y/n\]"),
            ("Delete? [y/N]",            r"\[y/N\]"),
            ("Remove? [Y/N]",            r"\[Y/N\]"),
            ("Remove? (y/n)",            r"\(y/n\)"),
            ("Remove? (Y/n)",            r"\(Y/n\)"),
            ("Remove? (y/N)",            r"\(y/N\)"),
            ("Remove? (Y/N)",            r"\(Y/N\)"),
            # [yes/no] family
            ("Overwrite? [yes/no]",      r"\[yes/no\]"),
            ("Overwrite? (yes/no)",      r"\(yes/no\)"),
            ("Overwrite? [Yes/No]",      r"\[Yes/No\]"),
            # conda-style
            ("Proceed ([y]/n)?",         r"\(\[y\]/n\)"),
            ("Proceed (y/[n])?",         r"\(y/\[n\]\)"),
            # standalone (y)
            ("Ok to proceed? (y)",       r"\?\s*\(y\)\s*$"),
            # bare y/n
            ("Continue? y/n",            r"\?\s*y/n\s*:?\s*$"),
            # [Y]es/[N]o style
            ("[Y]es / [N]o",             r"\[Y\]es\s*/\s*\[N\]o"),
            ("[Y]es / [n]o",             r"\[Y\]es\s*/\s*\[n\]o"),
            # SSH
            ("continue connecting (yes/no)?",
             r"continue connecting\s*\(yes/no(?:/\[fingerprint\])?\)"),
            # common sentences
            ("Do you want to continue?", r"Do you want to continue\s*\?"),
            ("Do you wish to proceed?",  r"Do you wish to proceed\s*\?"),
            ("Are you sure?",            r"Are you sure\s*\?"),
            ("Continue?",               r"Continue\s*\?"),
            ("Proceed?",                r"Proceed\s*\?"),
            ("Confirm?",                r"Confirm\s*\?"),
            ("Would you like to help?",  r"[Ww]ould you like to\b.*\?"),
            ("Do you agree to terms?",   r"Do you (?:agree|accept)\b.*\?"),
            # action verbs
            ("Allow access?",            r"[Aa]llow\b.*\?"),
            ("Approve this?",            r"[Aa]pprove\b.*\?"),
            ("Accept incoming?",         r"[Aa]ccept\b.*\?"),
            ("Download package?",        r"[Dd]ownload\b.*\?"),
            ("Enable feature?",          r"[Ee]nable\b.*\?"),
            ("Create directory?",        r"[Cc]reate\b.*\?"),
            # license
            ("Accept the license agreement", r"[Aa]ccept\s+(?:the\s+)?license\b"),
            ("Agree to the terms",       r"[Aa]gree\s+to\s+(?:the\s+)?terms\b"),
            # package manager
            ("Is this ok [y/d/N]:",      r"Is this ok\s*\["),
            # need/require
            ("Need to download?",        r"\b[Nn]eed\s+to\b.*\?"),
            ("Required?",               r"\b[Rr]equire[ds]?\b.*\?"),
            ("Is this necessary?",       r"\b[Nn]ecessary\b.*\?"),
            # type yes / enter yes
            ("Type 'yes' to continue",   r"[Tt]ype\s+'?(?:yes|YES)'?\s+to\s+(?:continue|confirm|proceed)"),
            ("Enter 'yes' to confirm",   r"[Ee]nter\s+'?(?:yes|YES)'?\s+to\s+(?:continue|confirm|proceed)"),
            # terraform
            ("Only 'yes' will be accepted", r"[Oo]nly\s+'?yes'?\s+will be accepted"),
            # press enter
            ("Press Enter to continue",  r"[Pp]ress\s+(?:[Ee]nter|[Rr]eturn)\s+to\s+continue"),
            ("Press any key to continue", r"[Pp]ress\s+any\s+key\s+to\s+continue"),
            # default Y
            ("Use cache? (default: Y)",  r"\?\s*\(default:\s*[Yy](?:es)?\)"),
            ("Use cache? [default=yes]", r"\?\s*\[default[=:]\s*[Yy](?:es)?\]"),
        ]

        det = PromptDetector(categories=["generic"])
        pat_sources = {src for src, _ in REGISTRY["generic"]["patterns"]}

        for trigger, pat_src in generic_triggers:
            assert pat_src in pat_sources, (
                f"Pattern {pat_src!r} is not in the generic registry — "
                "update the trigger table to match the current registry."
            )
            result = det.detect(trigger)
            assert result is not None, (
                f"Generic pattern {pat_src!r} did not fire for trigger {trigger!r}"
            )

    def test_no_category_means_generic_only(self):
        """Detector built with no categories argument only matches generic patterns."""
        det = PromptDetector()  # defaults to ["generic"]
        # Generic prompt — must match.
        assert det.detect("Continue? [y/n]") is not None
        # Claude-specific prompt — must NOT match.
        assert det.detect("❯ 1. Yes, I trust this folder") is None
        # Cursor-specific prompt — must NOT match (bare (y) is cursor-only).
        det_claude_only = PromptDetector(categories=["claude"])
        assert det_claude_only.detect("→ Run (once) (y)") is None

    def test_extra_patterns_override(self):
        """Custom regex added via extra_patterns is recognised."""
        det = PromptDetector(
            categories=["generic"],
            extra_patterns=[r"custom_deploy_confirm\?"],
        )
        assert det.detect("custom_deploy_confirm?") is not None
        assert r"custom_deploy_confirm\?" in det.pattern_strings


# ==================================================================
# Class 7: TestOvernightLoopStress
# ==================================================================


class TestOvernightLoopStress:
    """Simulate realistic overnight-loop scenarios at scale."""

    def test_100_iteration_loop(self):
        """100 iterations of: generate output + prompt, detect, verify detected."""
        det = PromptDetector(categories=["generic", "claude"])
        failures = []

        for i in range(100):
            output = "\n".join(
                f"[Iteration {i}] step {j}: processing..." for j in range(20)
            )
            prompt = "\n Do you want to proceed?\n ❯ 1. Yes\n   2. No\n"
            text = output + prompt

            result = det.detect(text)
            if result is None:
                failures.append(i)

        assert not failures, f"Detection failed in iterations: {failures}"

    def test_alternating_tool_prompts(self):
        """200 prompts alternating between Claude and Cursor — all detected."""
        det = PromptDetector(categories=["generic", "claude", "cursor"])
        claude_prompt = " ❯ 1. Yes\n   2. No\n"
        cursor_prompt = " │  → Write output.py (y)  │\n"
        failures = []

        for i in range(200):
            text = claude_prompt if i % 2 == 0 else cursor_prompt
            result = det.detect(text)
            if result is None:
                tool = "claude" if i % 2 == 0 else "cursor"
                failures.append(f"iteration {i} ({tool})")

        assert not failures, f"Detection failed: {failures[:10]}"

    def test_prompt_after_no_response(self):
        """Prompt detected even after 500 lines of output with no earlier prompts."""
        det = PromptDetector(categories=["generic", "claude"])
        long_output = "\n".join(
            f"[{i:04d}] Analyzing function_{i}... done" for i in range(500)
        )
        prompt = "\n Do you want to proceed?\n ❯ 1. Yes\n   2. No\n"
        text = long_output + prompt

        result = det.detect(text)
        assert result is not None, (
            "Prompt not detected after 500 lines of prompt-free output"
        )
