"""Tests for scan pipeline improvements (21 items across 6 tiers).

Covers: structured output parsing, semgrep noise reduction, severity grouping,
review worker prompt, suppression loading, /clear cache invalidation,
delta atomicity, truncation awareness, cross-file context, worker dedup,
per-file timeout, report truncation, thread-safety, model switch invalidation,
frontend scanning, target roots, dependency audit, pre-flight checks,
semgrep config validation.
"""

from __future__ import annotations

import inspect
import json
import os
import re
import tempfile
import threading

import pytest


# ===========================================================================
# Tier 1: Scan Accuracy
# ===========================================================================

class TestStructuredOutputParsing:
    """Item 1: Verify _parse_worker_output handles JSON and fallback."""

    def test_parses_valid_json_array(self):
        from autodebug.agent.tools import _parse_worker_output
        raw = json.dumps([
            {"line": 42, "message": "SQL injection risk", "severity": "HIGH", "confidence": "DEFINITE", "category": "security"},
            {"line": 100, "message": "Unused variable", "severity": "LOW", "confidence": "LIKELY", "category": "code-quality"},
        ])
        findings = _parse_worker_output(raw, "api/main.py")
        assert len(findings) == 2
        assert findings[0]["line"] == 42
        assert findings[0]["severity"] == "HIGH"
        assert findings[0]["path"] == "api/main.py"

    def test_parses_json_in_markdown_codeblock(self):
        from autodebug.agent.tools import _parse_worker_output
        raw = '```json\n[{"line": 10, "message": "Bug", "severity": "MEDIUM", "confidence": "LIKELY"}]\n```'
        findings = _parse_worker_output(raw, "test.py")
        assert len(findings) == 1
        assert findings[0]["line"] == 10

    def test_handles_empty_array(self):
        from autodebug.agent.tools import _parse_worker_output
        findings = _parse_worker_output("[]", "test.py")
        assert findings == []

    def test_fallback_line_extraction(self):
        from autodebug.agent.tools import _parse_worker_output
        raw = "Line 15: possible null pointer dereference\nLine 28: unused import"
        findings = _parse_worker_output(raw, "test.py")
        assert len(findings) == 2
        assert findings[0]["line"] == 15
        assert findings[1]["line"] == 28

    def test_normalizes_invalid_severity(self):
        from autodebug.agent.tools import _parse_worker_output
        raw = json.dumps([{"line": 1, "message": "test", "severity": "CRITICAL", "confidence": "MAYBE"}])
        findings = _parse_worker_output(raw, "test.py")
        assert findings[0]["severity"] == "LOW"
        assert findings[0]["confidence"] == "LIKELY"

    def test_skips_entries_without_line(self):
        from autodebug.agent.tools import _parse_worker_output
        raw = json.dumps([
            {"message": "No line number"},
            {"line": 5, "message": "Has line number"},
        ])
        findings = _parse_worker_output(raw, "test.py")
        assert len(findings) == 1

    def test_handles_garbage_input(self):
        from autodebug.agent.tools import _parse_worker_output
        findings = _parse_worker_output("No findings here. Everything looks great!", "test.py")
        assert findings == []


class TestPostDedupWorkerFindings:
    """Item 10: Verify post-processing dedup against Semgrep findings."""

    def test_removes_duplicate_on_same_line(self):
        from autodebug.agent.tools import _post_dedup_worker_findings
        worker = [{"path": "api/main.py", "line": 42, "message": "issue"}]
        semgrep = {("api/main.py", 42)}
        result, _ = _post_dedup_worker_findings(worker, semgrep)
        # Merged findings are tagged, not dropped — check the enriched tag
        assert all(f.get("_semgrep_enriched") for f in result)

    def test_removes_duplicate_within_tolerance(self):
        from autodebug.agent.tools import _post_dedup_worker_findings
        worker = [{"path": "api/main.py", "line": 44, "message": "issue"}]
        semgrep = {("api/main.py", 42)}
        result, merged = _post_dedup_worker_findings(worker, semgrep, line_tolerance=3)
        assert merged >= 1

    def test_keeps_non_duplicate(self):
        from autodebug.agent.tools import _post_dedup_worker_findings
        worker = [{"path": "api/main.py", "line": 100, "message": "issue"}]
        semgrep = {("api/main.py", 42)}
        result, merged = _post_dedup_worker_findings(worker, semgrep)
        assert len(result) == 1
        assert merged == 0

    def test_no_semgrep_locations_passes_all(self):
        from autodebug.agent.tools import _post_dedup_worker_findings
        worker = [{"path": "api/main.py", "line": 42, "message": "issue"}]
        result, merged = _post_dedup_worker_findings(worker, set())
        assert len(result) == 1
        assert merged == 0


class TestSemgrepExcludedRules:
    """Item 2: Verify excluded rules constant exists and has known entries."""

    def test_excluded_rules_is_frozenset(self):
        from autodebug.agent.tools import _SEMGREP_EXCLUDED_RULES
        assert isinstance(_SEMGREP_EXCLUDED_RULES, frozenset)

    def test_has_known_noisy_rules(self):
        from autodebug.agent.tools import _SEMGREP_EXCLUDED_RULES
        # Only string-concat-in-loop remains in the constant; others moved to
        # _FALLBACK_AGENT_CONFIG["semgrep"]["excluded_rules"] and merged at scan time
        assert "string-concat-in-loop" in _SEMGREP_EXCLUDED_RULES

    def test_severity_order_defined(self):
        from autodebug.agent.tools import _SEVERITY_ORDER
        assert _SEVERITY_ORDER["ERROR"] < _SEVERITY_ORDER["WARNING"]
        assert _SEVERITY_ORDER["WARNING"] < _SEVERITY_ORDER["INFO"]


class TestReviewWorkerPrompt:
    """Item 4: Verify worker prompt has structured output instructions."""

    def test_prompt_has_json_output_format(self):
        from autodebug.agent.system_prompt import REVIEW_WORKER_PROMPT
        assert "JSON array" in REVIEW_WORKER_PROMPT
        # After .format(), double braces become single braces with JSON keys
        formatted = REVIEW_WORKER_PROMPT.format(project_context="test")
        assert '"line"' in formatted
        assert '"severity"' in formatted

    def test_prompt_has_project_context_placeholder(self):
        from autodebug.agent.system_prompt import REVIEW_WORKER_PROMPT
        assert "{project_context}" in REVIEW_WORKER_PROMPT

    def test_prompt_has_known_safe_patterns(self):
        # These patterns are now split across specialized worker sub-prompts
        # and scan-ignore.yaml
        import inspect
        from autodebug.agent import system_prompt
        full_source = inspect.getsource(system_prompt)
        assert "StructuredLogger" in full_source
        assert "torch.save" in full_source
        # service-role moved entirely to scan-ignore.yaml
        import yaml
        path = os.path.join(
            os.path.dirname(os.path.dirname(__file__)),
            ".hikaflow", "scan-ignore.yaml",
        )
        with open(path) as f:
            data = yaml.safe_load(f)
        review_entries = data.get("review", [])
        assert any("service-role" in e.get("pattern", "") for e in review_entries)

    def test_prompt_has_severity_definitions(self):
        from autodebug.agent.system_prompt import REVIEW_WORKER_PROMPT
        assert "HIGH" in REVIEW_WORKER_PROMPT
        assert "MEDIUM" in REVIEW_WORKER_PROMPT
        assert "LOW" in REVIEW_WORKER_PROMPT
        assert "DEFINITE" in REVIEW_WORKER_PROMPT
        assert "LIKELY" in REVIEW_WORKER_PROMPT


# ===========================================================================
# Tier 2: Suppression & Caching
# ===========================================================================

class TestScanIgnoreLoading:
    """Item 5: Verify scan-ignore.yaml is loaded by the pipeline."""

    def test_load_scan_ignore_reads_yaml(self):
        from autodebug.agent.tools import _load_scan_ignore
        project_root = os.path.dirname(os.path.dirname(__file__))
        data = _load_scan_ignore(project_root)
        assert isinstance(data, dict)
        assert "semgrep" in data
        assert "review" in data

    def test_load_scan_ignore_returns_empty_on_missing(self):
        from autodebug.agent.tools import _load_scan_ignore
        data = _load_scan_ignore("/nonexistent/path")
        assert data == {}


class TestClearCacheInvalidation:
    """Item 6: Verify /clear resets scan state."""

    def test_reset_scan_state_function_exists(self):
        from autodebug.agent.tools import reset_scan_state
        assert callable(reset_scan_state)

    def test_reset_clears_tool_tracking(self):
        from autodebug.agent.tools import reset_scan_state, _tools_called_this_run
        _tools_called_this_run.add("scan_codebase")
        reset_scan_state()
        assert "scan_codebase" not in _tools_called_this_run

    def test_clear_handler_calls_reset(self):
        from autodebug import cli_v2
        source = inspect.getsource(cli_v2)
        # Both /clear handlers should import and call reset_scan_state
        assert "reset_scan_state" in source


class TestDeltaModeAtomicity:
    """Item 7: Verify _save_last_scan uses atomic write."""

    def test_save_uses_os_replace(self):
        from autodebug.agent.tools import _save_last_scan
        source = inspect.getsource(_save_last_scan)
        assert "os.replace" in source
        assert "tempfile" in source or "mkstemp" in source

    def test_save_and_load_roundtrip(self):
        from autodebug.agent.tools import _save_last_scan, _load_last_scan
        with tempfile.TemporaryDirectory() as tmpdir:
            findings = [
                {"path": os.path.join(tmpdir, "test.py"), "start": {"line": 10}, "check_id": "rule-1"},
                {"path": os.path.join(tmpdir, "main.py"), "start": {"line": 20}, "check_id": "rule-2"},
            ]
            _save_last_scan(tmpdir, findings)
            loaded = _load_last_scan(tmpdir)
            assert len(loaded) == 2


# ===========================================================================
# Tier 3: Review Pipeline Reliability
# ===========================================================================

class TestTruncationAwareness:
    """Item 8: Verify file content truncation is communicated to worker."""

    def test_review_one_mentions_truncation_in_code(self):
        from autodebug.agent import tools
        source = inspect.getsource(tools.register_hikaflow_tools)
        assert "TRUNCATED" in source
        assert "_FILE_CONTENT_CAP" in source


class TestCrossFileContext:
    """Item 9: Verify import context is extracted for workers."""

    def test_build_review_context_returns_string(self):
        from autodebug.agent.tools import _build_review_context
        project_root = os.path.dirname(os.path.dirname(__file__))
        context = _build_review_context(project_root)
        assert isinstance(context, str)
        assert len(context) > 0

    def test_review_one_extracts_imports(self):
        from autodebug.agent import tools
        source = inspect.getsource(tools.register_hikaflow_tools)
        assert "import_note" in source
        assert "imports from" in source


class TestWorkerDedupEnforcement:
    """Item 10: Verify worker findings are deduped against Semgrep."""

    def test_dedup_function_exists(self):
        from autodebug.agent.tools import _post_dedup_worker_findings
        assert callable(_post_dedup_worker_findings)

    def test_review_files_calls_dedup(self):
        from autodebug.agent import tools
        source = inspect.getsource(tools.register_hikaflow_tools)
        assert "_post_dedup_worker_findings" in source


# ===========================================================================
# Tier 4: REPL Quality
# ===========================================================================

class TestPerFileWorkerTimeout:
    """Item 11: Verify per-file timeout in review_files."""

    def test_worker_timeout_defined(self):
        from autodebug.agent import tools
        source = inspect.getsource(tools.register_hikaflow_tools)
        assert "_WORKER_TIMEOUT" in source
        assert "asyncio.wait_for" in source or "wait_for" in source

    def test_timeout_handler_returns_message(self):
        from autodebug.agent import tools
        source = inspect.getsource(tools.register_hikaflow_tools)
        assert "TIMEOUT]" in source


class TestReportTruncationSafety:
    """Item 12: Verify truncated findings show helpful message."""

    def test_truncation_message_mentions_review_files(self):
        from autodebug.agent import tools
        source = inspect.getsource(tools.register_hikaflow_tools)
        assert "review_files" in source
        # Pagination replaces the old truncation message
        assert "scan_codebase(page=" in source or "additional finding(s) omitted" in source


class TestThreadSafety:
    """Item 13: Verify semgrep finding locations use a lock."""

    def test_semgrep_lock_exists(self):
        from autodebug.agent import tools
        source = inspect.getsource(tools.register_hikaflow_tools)
        assert "_semgrep_lock" in source
        assert "with _semgrep_lock:" in source


class TestModelSwitchInvalidation:
    """Item 14: Verify model switch resets scan state."""

    def test_model_switch_calls_reset(self):
        from autodebug import cli_v2
        source = inspect.getsource(cli_v2)
        # After create_agent calls, reset_scan_state should be called
        model_switch_pattern = re.compile(
            r"create_agent\(_new_model.*?reset_scan_state", re.DOTALL
        )
        assert model_switch_pattern.search(source), "Model switch should call reset_scan_state"

    def test_interactive_picker_calls_reset(self):
        from autodebug import cli_v2
        source = inspect.getsource(cli_v2)
        picker_pattern = re.compile(
            r"create_agent\(_selected.*?reset_scan_state", re.DOTALL
        )
        assert picker_pattern.search(source), "Interactive picker should call reset_scan_state"


# ===========================================================================
# Tier 5: Coverage Expansion
# ===========================================================================

class TestFrontendScanning:
    """Item 15: Verify TypeScript/React configs in Semgrep."""

    def test_deep_config_includes_typescript(self):
        from autodebug.agent.core import _FALLBACK_AGENT_CONFIG
        deep = _FALLBACK_AGENT_CONFIG["semgrep"].get("deep_config", [])
        assert "p/typescript" in deep

    def test_deep_config_includes_react(self):
        from autodebug.agent.core import _FALLBACK_AGENT_CONFIG
        deep = _FALLBACK_AGENT_CONFIG["semgrep"].get("deep_config", [])
        assert "p/react" in deep

    def test_tools_default_deep_config_has_typescript(self):
        from autodebug.agent import tools
        source = inspect.getsource(tools.register_hikaflow_tools)
        assert '"p/typescript"' in source
        assert '"p/react"' in source


class TestTargetRootsExpanded:
    """Item 16: Verify worker/scripts/ide in target roots."""

    def test_fallback_config_has_scripts(self):
        from autodebug.agent.core import _FALLBACK_AGENT_CONFIG
        roots = _FALLBACK_AGENT_CONFIG["semgrep"].get("target_roots", [])
        assert "scripts" in roots
        assert "ide" in roots

    def test_tools_default_roots_expanded(self):
        from autodebug.agent import tools
        source = inspect.getsource(tools.register_hikaflow_tools)
        assert '"scripts"' in source
        assert '"ide"' in source


class TestDependencyAudit:
    """Item 17: Verify dependency audit runs in parallel scan."""

    def test_dependency_audit_in_scan_labels(self):
        from autodebug.agent import tools
        source = inspect.getsource(tools.register_hikaflow_tools)
        assert '"Dependency Audit"' in source

    def test_dependency_audit_function_exists(self):
        from autodebug.agent import tools
        source = inspect.getsource(tools.register_hikaflow_tools)
        assert "async def _dependency_audit():" in source

    def test_dependency_audit_checks_pip_audit(self):
        from autodebug.agent import tools
        source = inspect.getsource(tools.register_hikaflow_tools)
        assert "pip-audit" in source

    def test_dependency_audit_checks_npm_audit(self):
        from autodebug.agent import tools
        source = inspect.getsource(tools.register_hikaflow_tools)
        assert "npm audit" in source or "npm\", \"audit" in source


# ===========================================================================
# Tier 6: Advanced
# ===========================================================================

class TestPreflightCheck:
    """Item 20: Verify pre-flight validation before scan."""

    def test_preflight_checks_project_structure(self):
        from autodebug.agent import tools
        source = inspect.getsource(tools.register_hikaflow_tools)
        assert "_preflight_warnings" in source
        assert "No Python or Node.js project structure" in source

    def test_preflight_validates_semgrep_yaml(self):
        from autodebug.agent import tools
        source = inspect.getsource(tools.register_hikaflow_tools)
        assert "--validate" in source
        assert ".semgrep.yaml" in source

    def test_preflight_warnings_in_report(self):
        from autodebug.agent import tools
        source = inspect.getsource(tools.register_hikaflow_tools)
        assert "Pre-flight Warnings" in source


class TestSemgrepConfigValidation:
    """Item 21: Verify .semgrep.yaml is validated before scan."""

    def test_validation_command_in_scan(self):
        from autodebug.agent import tools
        source = inspect.getsource(tools.register_hikaflow_tools)
        assert '"semgrep", "--validate"' in source or '"--validate"' in source


# ===========================================================================
# Integration: System prompt quality
# ===========================================================================

class TestSystemPromptQuality:
    """Verify system prompt has all required sections."""

    def test_has_skill_menu_placeholder(self):
        from autodebug.agent.system_prompt import HIKAFLOW_SYSTEM_PROMPT
        assert "{skill_menu}" in HIKAFLOW_SYSTEM_PROMPT

    def test_has_rules_section(self):
        from autodebug.agent.system_prompt import HIKAFLOW_SYSTEM_PROMPT
        assert "## Shared Rules" in HIKAFLOW_SYSTEM_PROMPT

    def test_has_coverage_section_rule(self):
        from autodebug.agent.system_prompt import HIKAFLOW_SYSTEM_PROMPT
        assert "Coverage" in HIKAFLOW_SYSTEM_PROMPT

    def test_has_scan_tool_instructions(self):
        from autodebug.agent.system_prompt import HIKAFLOW_SCAN_PROMPT
        assert "scan_codebase" in HIKAFLOW_SCAN_PROMPT

    def test_worker_prompt_format_complete(self):
        from autodebug.agent.system_prompt import REVIEW_WORKER_PROMPT
        # Must be formattable with project_context
        formatted = REVIEW_WORKER_PROMPT.format(project_context="Test context")
        assert "Test context" in formatted


# ===========================================================================
# Integration: Agent config quality
# ===========================================================================

class TestAgentConfig:
    """Verify agent config has all required fields."""

    def test_fallback_has_semgrep_section(self):
        from autodebug.agent.core import _FALLBACK_AGENT_CONFIG
        assert "semgrep" in _FALLBACK_AGENT_CONFIG

    def test_fallback_has_excluded_rules(self):
        from autodebug.agent.core import _FALLBACK_AGENT_CONFIG
        rules = _FALLBACK_AGENT_CONFIG["semgrep"]["excluded_rules"]
        assert len(rules) >= 3

    def test_fallback_has_target_roots(self):
        from autodebug.agent.core import _FALLBACK_AGENT_CONFIG
        roots = _FALLBACK_AGENT_CONFIG["semgrep"]["target_roots"]
        assert "api" in roots
        assert "worker" in roots
        assert "web" in roots

    def test_fallback_has_deep_config(self):
        from autodebug.agent.core import _FALLBACK_AGENT_CONFIG
        deep = _FALLBACK_AGENT_CONFIG["semgrep"]["deep_config"]
        assert len(deep) >= 2


# ---------------------------------------------------------------------------
# Gap 4: review_files Worker Budget Limits
# ---------------------------------------------------------------------------
class TestReviewFilesBudget:
    """Verify review_files respects budget parameters."""

    def _get_source(self):
        import inspect
        from autodebug.agent import tools
        return inspect.getsource(tools.register_hikaflow_tools)

    def test_review_files_has_max_files_per_turn_param(self):
        src = self._get_source()
        assert "max_files_per_turn" in src

    def test_review_files_has_max_workers_per_file_param(self):
        src = self._get_source()
        assert "max_workers_per_file" in src

    def test_review_files_has_max_total_worker_invocations_param(self):
        src = self._get_source()
        assert "max_total_worker_invocations" in src

    def test_budget_params_have_defaults(self):
        src = self._get_source()
        assert "max_files_per_turn: int = 15" in src
        assert "max_workers_per_file: int = 3" in src
        assert "max_total_worker_invocations: int = 30" in src

    def test_budget_clamping_exists(self):
        src = self._get_source()
        # Verify clamping logic is applied for each parameter
        assert "min(int(max_files_per_turn), 15)" in src
        assert "min(int(max_workers_per_file), 3)" in src
        assert "min(int(max_total_worker_invocations), 45)" in src

    def test_pending_reviews_in_output(self):
        src = self._get_source()
        assert "pending_reviews" in src
        assert "Budget limit reached" in src
        assert "worker_budget_exhausted" in src

    def test_workers_capped_by_max_workers_per_file(self):
        src = self._get_source()
        assert "[:max_workers_per_file]" in src

    def test_paths_capped_by_max_files_per_turn(self):
        src = self._get_source()
        assert "[:max_files_per_turn]" in src
