"""Tests for third-scan findings fixes (Items 1-18).

Covers: Semgrep timeout, path traversal, cli_auth hardening, dev token,
debug_log fixes, SQL masking, scan tool improvements.
"""

from __future__ import annotations

import inspect
import re

import pytest


# ---------------------------------------------------------------------------
# Item 1-2: Semgrep timeout and fallback scoping
# ---------------------------------------------------------------------------

class TestSemgrepTimeout:
    """Verify Semgrep timeout defaults are increased."""

    def test_default_timeout_at_least_120(self):
        """Default subprocess timeout should be >= 120s, not 45s."""
        from autodebug.agent import tools
        source = inspect.getsource(tools)
        # Find the default timeout assignment
        match = re.search(r'_sem_cfg\.get\("timeout_seconds",\s*(\d+)\)', source)
        assert match, "timeout_seconds default not found"
        assert int(match.group(1)) >= 120

    def test_default_executor_timeout_at_least_180(self):
        from autodebug.agent import tools
        source = inspect.getsource(tools)
        match = re.search(r'_sem_cfg\.get\("executor_timeout_seconds",\s*(\d+)\)', source)
        assert match, "executor_timeout_seconds default not found"
        assert int(match.group(1)) >= 180

    def test_fallback_retry_on_timeout(self):
        """If full-repo scan times out, should retry per-directory."""
        from autodebug.agent import tools
        source = inspect.getsource(tools)
        assert "full-repo scan timed out, retrying per-directory" in source


# ---------------------------------------------------------------------------
# Item 3+5: Path traversal and file descriptor leaks in apply_patch
# ---------------------------------------------------------------------------

class TestApplyPatchSecurity:
    """Verify apply_patch validates paths and uses context managers."""

    def test_rejects_absolute_paths(self):
        """apply_patch should reject absolute paths in patch content."""
        from autodebug.agent import tools
        source = inspect.getsource(tools)
        assert "os.path.isabs(filepath)" in source
        assert "REJECTED" in source

    def test_rejects_path_traversal(self):
        """apply_patch should reject '..' in path components."""
        from autodebug.agent import tools
        source = inspect.getsource(tools)
        assert '".."' in source or "'..' in" in source

    def test_validates_resolved_path(self):
        """apply_patch should use realpath to catch symlink escapes."""
        from autodebug.agent import tools
        source = inspect.getsource(tools)
        assert "os.path.realpath" in source
        assert "escapes project root" in source

    def test_uses_context_managers_for_io(self):
        """File I/O should use 'with' statements."""
        from autodebug.agent import tools
        source = inspect.getsource(tools)
        # The apply_patch section should use 'with open'
        patch_section_start = source.index("Simple find-and-replace")
        patch_section = source[patch_section_start:patch_section_start + 500]
        assert "with open(" in patch_section


# ---------------------------------------------------------------------------
# Item 4: Error detail leak in PKCE response
# ---------------------------------------------------------------------------

class TestPkceErrorLeak:
    """Verify PKCE token exchange doesn't leak internal error details."""

    def test_no_exception_in_error_response(self):
        from api.cli_auth import pkce_token
        source = inspect.getsource(pkce_token)
        # Should NOT have f-string with {e} in the HTTPException detail
        assert 'detail=f"Failed to decode authorization code: {e}"' not in source
        # Should have a generic message
        assert 'detail="Failed to decode authorization code"' in source


# ---------------------------------------------------------------------------
# Item 7: Email PII removed from CLI JWT
# ---------------------------------------------------------------------------

class TestCliJwtNoPii:
    """Verify CLI JWT doesn't contain email."""

    def test_no_email_in_jwt_payload(self):
        from api.cli_auth import _generate_cli_access_token
        source = inspect.getsource(_generate_cli_access_token)
        # Should not have "email" as a payload key
        assert '"email"' not in source or "intentionally excluded" in source


# ---------------------------------------------------------------------------
# Item 11: client_id validation
# ---------------------------------------------------------------------------

class TestClientIdValidation:
    """Verify client_id is validated in device_authorize."""

    def test_client_id_validated(self):
        from api.cli_auth import device_authorize
        source = inspect.getsource(device_authorize)
        assert "client_id" in source
        assert "is required" in source or "required" in source


# ---------------------------------------------------------------------------
# Item 6: Dev token constant-time comparison
# ---------------------------------------------------------------------------

class TestDevTokenComparison:
    """Verify dev token uses hmac.compare_digest."""

    def test_uses_hmac_compare_digest(self):
        from api.auth import verify_authorization
        source = inspect.getsource(verify_authorization)
        assert "hmac.compare_digest" in source

    def test_hmac_imported(self):
        import api.auth
        import hmac
        assert hasattr(api.auth, "hmac") or "hmac" in dir(api.auth) or hasattr(hmac, "compare_digest")


# ---------------------------------------------------------------------------
# Item 8: Email regex fix
# ---------------------------------------------------------------------------

class TestEmailRegexFix:
    """Verify email regex doesn't match literal pipe character."""

    def test_no_pipe_in_character_class(self):
        from autodebug.debug_log import _PII_PATTERNS
        email_pattern = _PII_PATTERNS[0][0]
        # The TLD part should be [A-Za-z] not [A-Z|a-z]
        assert "|" not in email_pattern

    def test_email_detected(self):
        from autodebug.debug_log import _scrub_pii
        assert "[EMAIL]" in _scrub_pii("user@example.com")

    def test_pipe_not_matched_as_tld(self):
        from autodebug.debug_log import _scrub_pii
        # "user@example.|om" should NOT match as email
        result = _scrub_pii("user@example.|om")
        assert result == "user@example.|om"


# ---------------------------------------------------------------------------
# Item 9: Scrubber key-name detection
# ---------------------------------------------------------------------------

class TestScrubberKeyNames:
    """Verify scrubber detects sensitive key names."""

    def test_password_key_redacted(self):
        from autodebug.debug_log import _scrub_payload
        result = _scrub_payload({"password": "hunter2"})
        assert result["password"] == "[REDACTED]"

    def test_api_key_redacted(self):
        from autodebug.debug_log import _scrub_payload
        result = _scrub_payload({"api_key": "sk-12345"})
        assert result["api_key"] == "[REDACTED]"

    def test_secret_key_redacted(self):
        from autodebug.debug_log import _scrub_payload
        result = _scrub_payload({"client_secret": "abc123"})
        assert result["client_secret"] == "[REDACTED]"

    def test_normal_key_not_redacted(self):
        from autodebug.debug_log import _scrub_payload
        result = _scrub_payload({"username": "alice"})
        assert result["username"] == "alice"


# ---------------------------------------------------------------------------
# Item 10: ext sanitization in write_session_artifact
# ---------------------------------------------------------------------------

class TestExtSanitization:
    """Verify ext parameter is sanitized."""

    def test_ext_rejects_path_traversal(self):
        from autodebug.debug_log import write_session_artifact
        source = inspect.getsource(write_session_artifact)
        assert ".." in source  # checks for ..
        assert "isalnum" in source  # sanitizes chars


# ---------------------------------------------------------------------------
# Item 12: asyncio.get_running_loop
# ---------------------------------------------------------------------------

class TestAsyncioRunningLoop:
    """Verify get_event_loop replaced with get_running_loop."""

    def test_uses_get_running_loop(self):
        from autodebug.agent.tools import _run_in_executor_with_timeout
        source = inspect.getsource(_run_in_executor_with_timeout)
        assert "get_running_loop" in source
        assert "get_event_loop" not in source


# ---------------------------------------------------------------------------
# Item 13: SQL escaped quotes handling
# ---------------------------------------------------------------------------

class TestSqlEscapedQuotes:
    """Verify SQL normalization handles escaped quotes."""

    def test_escaped_single_quotes(self):
        from autodebug.diff import _normalize_sql_query
        result = _normalize_sql_query("SELECT * FROM t WHERE name = 'it''s a test'")
        assert "'?'" in result
        assert "it" not in result

    def test_normal_quotes_still_work(self):
        from autodebug.diff import _normalize_sql_query
        result = _normalize_sql_query("SELECT * FROM t WHERE name = 'hello'")
        assert "'?'" in result
        assert "hello" not in result

    def test_escaped_double_quotes(self):
        from autodebug.diff import _normalize_sql_query
        result = _normalize_sql_query('SELECT * FROM t WHERE name = "he""llo"')
        assert '"?"' in result


# ---------------------------------------------------------------------------
# Item 14: Payload breadth limits
# ---------------------------------------------------------------------------

class TestPayloadBreadthLimits:
    """Verify non-string payloads are bounded."""

    def test_large_dict_truncated(self):
        from autodebug.debug_log import _scrub_payload
        big_dict = {f"key_{i}": f"val_{i}" for i in range(500)}
        result = _scrub_payload(big_dict)
        assert "__truncated__" in result or len(result) < 500

    def test_large_list_truncated(self):
        from autodebug.debug_log import _scrub_payload
        big_list = [f"item_{i}" for i in range(500)]
        result = _scrub_payload(big_list)
        assert any("TRUNCATED" in str(item) for item in result)
        assert len(result) < 500


# ---------------------------------------------------------------------------
# Item 15: Worker ERROR severity threshold
# ---------------------------------------------------------------------------

class TestWorkerSeverityThreshold:
    """Verify worker prompt has stricter ERROR criteria."""

    def test_error_requires_exploit_scenario(self):
        from autodebug.agent.system_prompt import REVIEW_WORKER_PROMPT
        assert "attack" in REVIEW_WORKER_PROMPT.lower() or "exploit" in REVIEW_WORKER_PROMPT.lower()

    def test_when_in_doubt_use_medium(self):
        from autodebug.agent.system_prompt import REVIEW_WORKER_PROMPT
        assert "When in doubt" in REVIEW_WORKER_PROMPT


# ---------------------------------------------------------------------------
# Item 16: Cross-file context for workers
# ---------------------------------------------------------------------------

class TestCrossFileContext:
    """Verify workers receive guard/validation function context."""

    def test_review_extracts_guard_functions(self):
        from autodebug.agent import tools
        source = inspect.getsource(tools)
        assert "validate" in source and "guard_fns" in source
        assert "crossfile_note" in source


# ---------------------------------------------------------------------------
# Item 17: Scan delta mode
# ---------------------------------------------------------------------------

class TestScanDeltaMode:
    """Verify scan results are saved and compared across runs."""

    def test_load_save_last_scan(self):
        from autodebug.agent.tools import _load_last_scan, _save_last_scan
        assert callable(_load_last_scan)
        assert callable(_save_last_scan)

    def test_delta_labels_in_report(self):
        from autodebug.agent import tools
        source = inspect.getsource(tools)
        assert "[NEW]" in source
        assert "fixed" in source.lower()

    def test_load_returns_empty_for_missing_file(self):
        from autodebug.agent.tools import _load_last_scan
        result = _load_last_scan("/nonexistent/path")
        assert result == set()


# ---------------------------------------------------------------------------
# Item 18: Incremental deep scan
# ---------------------------------------------------------------------------

class TestIncrementalDeepScan:
    """Verify deep scan focuses on changed directories."""

    def test_deep_scan_focuses_on_changed_dirs(self):
        from autodebug.agent import tools
        source = inspect.getsource(tools)
        assert "changed_dirs" in source
        assert "focused_targets" in source
