"""Tests for scan tool improvements: noise reduction, suppression, context."""

from __future__ import annotations

import os
import json

import pytest

from autodebug.agent.tools import (
    _SEMGREP_EXCLUDED_RULES,
    _SEVERITY_ORDER,
    _load_scan_ignore,
    _build_review_context,
)


class TestSemgrepExcludedRules:
    """Part 1: Verify excluded rules constant and filtering logic."""

    def test_excluded_rules_contains_string_concat(self):
        # dict-key-access-without-check moved to _FALLBACK_AGENT_CONFIG; merged at scan time
        assert "string-concat-in-loop" in _SEMGREP_EXCLUDED_RULES

    def test_excluded_rules_is_frozenset(self):
        assert isinstance(_SEMGREP_EXCLUDED_RULES, frozenset)

    def test_severity_order_error_first(self):
        assert _SEVERITY_ORDER["ERROR"] < _SEVERITY_ORDER["WARNING"]
        assert _SEVERITY_ORDER["WARNING"] < _SEVERITY_ORDER["INFO"]

    def test_excluded_rules_filter_logic(self):
        """Simulate the filtering logic used in _semgrep_scan."""
        findings = [
            {"check_id": "python.perf.string-concat-in-loop", "path": "a.py"},
            {"check_id": "security.hardcoded-token", "path": "b.py"},
            {"check_id": "python.perf.string-concat-in-loop", "path": "c.py"},
            {"check_id": "security.sql-injection", "path": "d.py"},
        ]
        filtered = [
            f for f in findings
            if not any(excl in f.get("check_id", "") for excl in _SEMGREP_EXCLUDED_RULES)
        ]
        assert len(filtered) == 2
        assert filtered[0]["check_id"] == "security.hardcoded-token"
        assert filtered[1]["check_id"] == "security.sql-injection"

    def test_severity_sorting(self):
        """Verify severity-based sorting of findings."""
        findings = [
            {"extra": {"severity": "WARNING"}, "path": "b.py", "start": {"line": 1}},
            {"extra": {"severity": "ERROR"}, "path": "a.py", "start": {"line": 5}},
            {"extra": {"severity": "INFO"}, "path": "c.py", "start": {"line": 1}},
            {"extra": {"severity": "ERROR"}, "path": "a.py", "start": {"line": 1}},
        ]
        findings.sort(
            key=lambda f: (
                _SEVERITY_ORDER.get(f.get("extra", {}).get("severity", "WARNING").upper(), 9),
                f.get("path", ""),
                f.get("start", {}).get("line", 0),
            )
        )
        severities = [f["extra"]["severity"] for f in findings]
        assert severities == ["ERROR", "ERROR", "WARNING", "INFO"]
        # ERROR findings should be sorted by path+line
        assert findings[0]["start"]["line"] == 1
        assert findings[1]["start"]["line"] == 5


class TestLoadScanIgnore:
    """Part 3: Verify scan-ignore.yaml loading."""

    def test_returns_empty_when_no_file(self, tmp_path):
        result = _load_scan_ignore(str(tmp_path))
        assert result == {}

    def test_returns_empty_when_invalid_yaml(self, tmp_path):
        hikaflow_dir = tmp_path / ".hikaflow"
        hikaflow_dir.mkdir()
        (hikaflow_dir / "scan-ignore.yaml").write_text("not: [valid yaml: {{", encoding="utf-8")
        result = _load_scan_ignore(str(tmp_path))
        # yaml.safe_load may parse this or fail — either way should not raise
        assert isinstance(result, dict)

    def test_loads_semgrep_suppressions(self, tmp_path):
        hikaflow_dir = tmp_path / ".hikaflow"
        hikaflow_dir.mkdir()
        (hikaflow_dir / "scan-ignore.yaml").write_text(
            "semgrep:\n"
            '  - rule: "catch-and-pass"\n'
            "    paths: [\"api/db/core.py\"]\n"
            '    reason: "Intentional"\n',
            encoding="utf-8",
        )
        result = _load_scan_ignore(str(tmp_path))
        assert "semgrep" in result
        assert len(result["semgrep"]) == 1
        assert result["semgrep"][0]["rule"] == "catch-and-pass"

    def test_loads_review_suppressions(self, tmp_path):
        hikaflow_dir = tmp_path / ".hikaflow"
        hikaflow_dir.mkdir()
        (hikaflow_dir / "scan-ignore.yaml").write_text(
            "review:\n"
            '  - pattern: "service-role key"\n'
            '    paths: ["api/db/"]\n'
            '    reason: "Server-side auth"\n',
            encoding="utf-8",
        )
        result = _load_scan_ignore(str(tmp_path))
        assert "review" in result
        assert result["review"][0]["pattern"] == "service-role key"

    def test_returns_empty_when_yaml_is_list(self, tmp_path):
        hikaflow_dir = tmp_path / ".hikaflow"
        hikaflow_dir.mkdir()
        (hikaflow_dir / "scan-ignore.yaml").write_text("- item1\n- item2\n", encoding="utf-8")
        result = _load_scan_ignore(str(tmp_path))
        assert result == {}

    def test_semgrep_suppression_filtering(self, tmp_path):
        """Simulate the suppression filtering logic from _semgrep_scan."""
        suppressions = [
            {"rule": "catch-and-pass", "paths": ["api/db/"]},
        ]
        findings = [
            {"check_id": "python.lang.catch-and-pass", "path": os.path.join(str(tmp_path), "api/db/core.py")},
            {"check_id": "python.lang.catch-and-pass", "path": os.path.join(str(tmp_path), "worker/queue.py")},
            {"check_id": "security.sql-injection", "path": os.path.join(str(tmp_path), "api/db/core.py")},
        ]

        def _is_suppressed(finding):
            check_id = finding.get("check_id", "")
            fpath = os.path.relpath(finding.get("path", ""), str(tmp_path))
            for sup in suppressions:
                if sup.get("rule", "") in check_id:
                    for pat in sup.get("paths", []):
                        if fpath.startswith(pat) or fpath == pat:
                            return True
            return False

        filtered = [f for f in findings if not _is_suppressed(f)]
        # catch-and-pass in api/db/ is suppressed, but worker/ and sql-injection are kept
        assert len(filtered) == 2
        assert "queue.py" in filtered[0]["path"]
        assert "sql-injection" in filtered[1]["check_id"]


class TestBuildReviewContext:
    """Part 2: Verify project context auto-detection."""

    def test_returns_none_detected_for_empty_project(self, tmp_path):
        result = _build_review_context(str(tmp_path))
        assert result == "None detected."

    def test_detects_dead_db_module(self, tmp_path):
        api_dir = tmp_path / "api"
        api_dir.mkdir()
        (api_dir / "db.py").write_text("# old module", encoding="utf-8")
        db_pkg = api_dir / "db"
        db_pkg.mkdir()
        (db_pkg / "__init__.py").write_text("# package", encoding="utf-8")

        result = _build_review_context(str(tmp_path))
        assert "dead code" in result.lower()
        assert "api/db.py" in result

    def test_detects_structured_logger(self, tmp_path):
        api_dir = tmp_path / "api"
        api_dir.mkdir()
        (api_dir / "logging_config.py").write_text(
            "class StructuredLogger:\n    def warning(self, msg, **kwargs):\n        pass\n",
            encoding="utf-8",
        )

        result = _build_review_context(str(tmp_path))
        assert "StructuredLogger" in result
        assert "**kwargs" in result

    def test_reads_user_context_file(self, tmp_path):
        hikaflow_dir = tmp_path / ".hikaflow"
        hikaflow_dir.mkdir()
        (hikaflow_dir / "scan-context.md").write_text(
            "- Custom pattern: our API uses gRPC not REST\n",
            encoding="utf-8",
        )

        result = _build_review_context(str(tmp_path))
        assert "gRPC" in result

    def test_user_context_truncated_at_1000_chars(self, tmp_path):
        hikaflow_dir = tmp_path / ".hikaflow"
        hikaflow_dir.mkdir()
        (hikaflow_dir / "scan-context.md").write_text("x" * 2000, encoding="utf-8")

        result = _build_review_context(str(tmp_path))
        assert len(result) <= 1000

    def test_no_db_package_means_no_dead_code_note(self, tmp_path):
        api_dir = tmp_path / "api"
        api_dir.mkdir()
        (api_dir / "db.py").write_text("# module", encoding="utf-8")
        # No api/db/ package

        result = _build_review_context(str(tmp_path))
        assert "dead code" not in result.lower()

    def test_reads_review_rules_file(self, tmp_path):
        hikaflow_dir = tmp_path / ".hikaflow"
        hikaflow_dir.mkdir()
        (hikaflow_dir / "review-rules.md").write_text(
            "- Always check for HIPAA compliance\n- Flag any PII in logs\n",
            encoding="utf-8",
        )

        result = _build_review_context(str(tmp_path))
        assert "HIPAA" in result
        assert "Project Review Rules" in result

    def test_review_rules_truncated_at_3000_chars(self, tmp_path):
        hikaflow_dir = tmp_path / ".hikaflow"
        hikaflow_dir.mkdir()
        (hikaflow_dir / "review-rules.md").write_text("r" * 5000, encoding="utf-8")

        result = _build_review_context(str(tmp_path))
        # Rules section should be present but truncated
        assert "Project Review Rules" in result
        # Total content should not include the full 5000 chars
        rules_section = result.split("Project Review Rules")[1]
        assert len(rules_section.strip()) <= 3000

    def test_review_rules_and_scan_context_both_loaded(self, tmp_path):
        hikaflow_dir = tmp_path / ".hikaflow"
        hikaflow_dir.mkdir()
        (hikaflow_dir / "scan-context.md").write_text("- Uses gRPC\n", encoding="utf-8")
        (hikaflow_dir / "review-rules.md").write_text("- Check PCI-DSS\n", encoding="utf-8")

        result = _build_review_context(str(tmp_path))
        assert "gRPC" in result
        assert "PCI-DSS" in result

    def test_missing_review_rules_file_is_fine(self, tmp_path):
        # No .hikaflow dir at all
        result = _build_review_context(str(tmp_path))
        assert "Project Review Rules" not in result


class TestReviewWorkerPrompt:
    """Part 2: Verify REVIEW_WORKER_PROMPT has the expected structure."""

    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_mentions_structured_logger(self):
        # StructuredLogger guidance moved to specialized worker sub-prompts
        import inspect
        from autodebug.agent import system_prompt
        full_source = inspect.getsource(system_prompt)
        assert "StructuredLogger" in full_source

    def test_prompt_mentions_service_role_key(self):
        # service-role suppression moved to scan-ignore.yaml review section
        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_mentions_dead_code(self):
        from autodebug.agent.system_prompt import REVIEW_WORKER_PROMPT
        assert "dead code" in REVIEW_WORKER_PROMPT

    def test_prompt_can_be_formatted(self):
        from autodebug.agent.system_prompt import REVIEW_WORKER_PROMPT
        formatted = REVIEW_WORKER_PROMPT.format(project_context="- Test context")
        assert "- Test context" in formatted
        assert "{project_context}" not in formatted


class TestFallbackAgentConfig:
    """Part 1e: Verify excluded_rules in fallback config."""

    def test_fallback_config_has_excluded_rules(self):
        from autodebug.agent.core import _FALLBACK_AGENT_CONFIG
        semgrep = _FALLBACK_AGENT_CONFIG.get("semgrep", {})
        assert "excluded_rules" in semgrep
        # Rules like dict-key-access-without-check are in the fallback config
        assert len(semgrep["excluded_rules"]) >= 1


class TestSemgrepScanMeta:
    """Part 1f/4a: Verify SemgrepScanMeta supports new fields."""

    def test_meta_accepts_findings_by_severity(self):
        from autodebug.agent.tools import SemgrepScanMeta
        meta: SemgrepScanMeta = {
            "ok": True,
            "status": "findings",
            "findings": 5,
            "findings_by_severity": {"ERROR": 2, "WARNING": 3},
            "filtered": 10,
            "files": ["a.py"],
            "error": None,
        }
        assert meta["findings_by_severity"]["ERROR"] == 2
        assert meta["filtered"] == 10

    def test_meta_works_without_new_fields(self):
        """Backward compat: old-style meta without new fields should work."""
        from autodebug.agent.tools import SemgrepScanMeta
        meta: SemgrepScanMeta = {
            "ok": True,
            "status": "findings",
            "findings": 5,
            "files": ["a.py"],
            "error": None,
        }
        assert meta.get("filtered", 0) == 0
        assert meta.get("findings_by_severity", {}) == {}
