from __future__ import annotations

import subprocess
from types import SimpleNamespace

import pytest
import httpx

from autodebug.agent.core import HikaflowDeps
from autodebug.agent.tools import (
    ScanClassification,
    _apply_evidence_guard,
    _parse_scan_findings,
    register_file_tools,
    register_hikaflow_tools,
)
from api.metrics import reset as reset_metrics
from api.metrics import snapshot as snapshot_metrics


class _DummyAgent:
    def __init__(self):
        self.tools = {}

    def tool(self, fn):
        self.tools[fn.__name__] = fn
        return fn


class _FakeAsyncClient:
    def __init__(self, responses):
        self._responses = responses

    async def __aenter__(self):
        return self

    async def __aexit__(self, exc_type, exc, tb):
        return False

    async def get(self, url, headers=None, params=None, timeout=None):
        for needle, response in self._responses:
            if needle in url:
                return response(url)
        req = httpx.Request("GET", url, params=params)
        return httpx.Response(200, request=req, json={})


def _make_ctx(tmp_path):
    deps = HikaflowDeps(
        api_base="https://api.example.com",
        token="test-token",
        project_path=str(tmp_path),
    )
    return SimpleNamespace(deps=deps)


def _register_scan_tool():
    agent = _DummyAgent()
    register_hikaflow_tools(agent)
    return agent.tools["scan_codebase"]


def _section_headers(report: str) -> list[str]:
    headers: list[str] = []
    for line in report.splitlines():
        if line.startswith("### "):
            headers.append(line[4:].strip())
    return headers


@pytest.mark.asyncio
async def test_scan_codebase_reports_real_root_causes_with_semgrep_recovery(monkeypatch, tmp_path):
    reset_metrics()
    (tmp_path / "foo.py").write_text("print('hi')\n", encoding="utf-8")

    monkeypatch.setattr("autodebug.auth.get_credentials", lambda: {"user_id": "user_1"})
    # _get_project_id reads from config.load_credentials — ensure no project_id
    monkeypatch.setattr("autodebug.config.load_credentials", lambda: {"user_id": "user_1"})

    checks = [
        SimpleNamespace(passed=True, warning=False, message="ok"),
        SimpleNamespace(passed=False, warning=False, message="Docker not running"),
    ]
    monkeypatch.setattr("autodebug.doctor.run_doctor", lambda: {"checks": checks})

    class _Store:
        def get_stats(self):
            return {"total_patterns": 0, "overall_success_rate": 0.0, "total_uses": 0}

    monkeypatch.setattr("autodebug.patterns.get_pattern_store", lambda: _Store())
    monkeypatch.setattr("autodebug.flaky_quarantine.list_quarantined_tests", lambda _p: [])
    monkeypatch.setattr(
        "autodebug.healing.detector.detect_in_changed_files",
        lambda _p: SimpleNamespace(files_scanned=0),
    )

    def _subprocess_run(cmd, *args, **kwargs):
        if cmd[:4] == ["git", "remote", "get-url", "origin"]:
            return subprocess.CompletedProcess(cmd, 1, stdout="", stderr="")
        if cmd[:4] == ["git", "remote", "get-url", "upstream"]:
            return subprocess.CompletedProcess(cmd, 1, stdout="", stderr="")
        if cmd[:2] == ["git", "merge-base"]:
            return subprocess.CompletedProcess(cmd, 1, stdout="", stderr="")
        if cmd[:2] == ["git", "diff"]:
            return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="")
        if cmd[:3] == ["git", "status", "--porcelain"]:
            return subprocess.CompletedProcess(cmd, 0, stdout=" M web/tsconfig.tsbuildinfo\n", stderr="")
        if cmd[:3] == ["semgrep", "scan", "--help"]:
            return subprocess.CompletedProcess(cmd, 0, stdout="--cache\n", stderr="")
        if len(cmd) >= 2 and cmd[0] == "semgrep" and cmd[1] == "scan":
            stdout = (
                "INFO: semgrep bootstrap log\n"
                '{"results": ['
                '{"path": "' + str(tmp_path / "foo.py") + '", '
                '"start": {"line": 12}, '
                '"check_id": "security.rule", '
                '"extra": {"severity": "ERROR", "message": "Dangerous pattern"}'
                "}]}"
            )
            return subprocess.CompletedProcess(cmd, 2, stdout=stdout, stderr="")
        return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="")

    monkeypatch.setattr("autodebug.agent.tools.subprocess.run", _subprocess_run)

    monkeypatch.setattr("autodebug.agent.tools.httpx.AsyncClient", lambda: _FakeAsyncClient([]))

    scan_codebase = _register_scan_tool()
    report = await scan_codebase(_make_ctx(tmp_path))

    assert "Project: `Not configured` (No project configured (scan is read-only; run setup/link explicitly))" in report
    assert "Skipped: No project linked. Run `hikaflow init --project-id <id>` first." in report

    assert "## Critical Blockers" in report
    assert "## Scan Fidelity" in report
    assert "status: **DEGRADED**" in report
    assert "analysis confidence: **LOW**" in report
    assert "strict_evidence_mode: **ON**" in report
    assert "degraded_scan_guard: **ON**" in report
    assert "## Evidence" in report
    assert "source=`Project`" in report
    assert "## Finding Classification" in report
    assert "### Derived Symptoms" in report
    assert "Project is not configured in local credentials." in report
    assert "lookup skipped until project is linked" in report

    assert "**1 finding(s)" in report  # pagination suffix may follow
    assert "foo.py:12" in report
    assert "Deep review Semgrep findings" in report
    assert "Link project explicitly first" in report

    metrics = snapshot_metrics()
    assert metrics.get("scan.codebase.runs", 0) >= 1
    assert metrics.get("scan.api.prod_errors.failed", 0) == 0
    assert metrics.get("scan.api.issue_groups.failed", 0) == 0
    assert metrics.get("scan.api.sessions.failed", 0) == 0
    assert metrics.get("scan.semgrep.findings", 0) >= 1


@pytest.mark.asyncio
async def test_scan_codebase_caches_after_first_run(monkeypatch, tmp_path):
    reset_metrics()
    monkeypatch.setattr("autodebug.auth.get_credentials", lambda: {"project_id": "proj_1"})
    monkeypatch.setattr("autodebug.doctor.run_doctor", lambda: {"checks": []})
    monkeypatch.setattr("autodebug.patterns.get_pattern_store", lambda: SimpleNamespace(get_stats=lambda: {"total_patterns": 0}))
    monkeypatch.setattr("autodebug.flaky_quarantine.list_quarantined_tests", lambda _p: [])
    monkeypatch.setattr(
        "autodebug.healing.detector.detect_in_changed_files",
        lambda _p: SimpleNamespace(files_scanned=0),
    )

    def _subprocess_run(cmd, *args, **kwargs):
        if cmd[:3] == ["git", "status", "--porcelain"]:
            return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="")
        if cmd[:3] == ["semgrep", "scan", "--help"]:
            return subprocess.CompletedProcess(cmd, 0, stdout="--cache\n", stderr="")
        if len(cmd) >= 2 and cmd[0] == "semgrep" and cmd[1] == "scan":
            req = '{"results": []}'
            return subprocess.CompletedProcess(cmd, 0, stdout=req, stderr="")
        return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="")

    monkeypatch.setattr("autodebug.agent.tools.subprocess.run", _subprocess_run)
    monkeypatch.setattr("autodebug.agent.tools.httpx.AsyncClient", lambda: _FakeAsyncClient([]))

    scan_codebase = _register_scan_tool()
    ctx = _make_ctx(tmp_path)

    first = await scan_codebase(ctx)
    second = await scan_codebase(ctx)

    assert "# Codebase Scan Report" in first
    # Cache now returns the full report (from dict cache), not "Already scanned."
    assert "# Codebase Scan Report" in second

    metrics = snapshot_metrics()
    # Called twice, but second call is cache hit.
    assert metrics.get("scan.codebase.runs", 0) >= 2


@pytest.mark.asyncio
async def test_scan_codebase_semgrep_fast_success_with_deep_partial_error(monkeypatch, tmp_path):
    reset_metrics()
    (tmp_path / "foo.py").write_text("print('hi')\n", encoding="utf-8")

    monkeypatch.setattr("autodebug.auth.get_credentials", lambda: {"project_id": "proj_1", "user_id": "user_1"})
    monkeypatch.setattr("autodebug.config.load_credentials", lambda: {"project_id": "proj_1"})
    monkeypatch.setattr("autodebug.doctor.run_doctor", lambda: {"checks": []})
    monkeypatch.setattr(
        "autodebug.patterns.get_pattern_store",
        lambda: SimpleNamespace(get_stats=lambda: {"total_patterns": 0, "overall_success_rate": 0.0, "total_uses": 0}),
    )
    monkeypatch.setattr("autodebug.flaky_quarantine.list_quarantined_tests", lambda _p: [])
    monkeypatch.setattr(
        "autodebug.healing.detector.detect_in_changed_files",
        lambda _p: SimpleNamespace(files_scanned=0),
    )

    def _subprocess_run(cmd, *args, **kwargs):
        if cmd[:2] == ["git", "merge-base"]:
            return subprocess.CompletedProcess(cmd, 1, stdout="", stderr="")
        if cmd[:2] == ["git", "diff"]:
            return subprocess.CompletedProcess(cmd, 0, stdout="foo.py\n", stderr="")
        if cmd[:3] == ["git", "status", "--porcelain"]:
            return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="")
        if cmd[:3] == ["semgrep", "scan", "--help"]:
            return subprocess.CompletedProcess(cmd, 0, stdout="--cache\n", stderr="")
        if len(cmd) >= 2 and cmd[0] == "semgrep" and cmd[1] == "scan":
            target = cmd[-1]
            if target == "foo.py":
                stdout = (
                    '{"results": ['
                    '{"path": "' + str(tmp_path / "foo.py") + '", '
                    '"start": {"line": 7}, '
                    '"check_id": "quality.rule", '
                    '"extra": {"severity": "WARNING", "message": "Potential issue"}'
                    "}]}",
                )[0]
                return subprocess.CompletedProcess(cmd, 1, stdout=stdout, stderr="")
            # deep-tier target path -> parse failure
            return subprocess.CompletedProcess(cmd, 0, stdout="not-json-output", stderr="")
        return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="")

    monkeypatch.setattr("autodebug.agent.tools.subprocess.run", _subprocess_run)
    monkeypatch.setattr("autodebug.agent.tools.httpx.AsyncClient", lambda: _FakeAsyncClient([]))

    scan_codebase = _register_scan_tool()
    report = await scan_codebase(_make_ctx(tmp_path))

    assert "**1 finding(s)" in report  # pagination suffix may follow
    assert "foo.py:7" in report
    assert "partial Semgrep errors encountered" in report
    assert "Semgrep returned partial results with recoverable parser/execution issues." in report

    metrics = snapshot_metrics()
    assert metrics.get("scan.semgrep.findings", 0) >= 1
    assert metrics.get("scan.semgrep.partial_error", 0) >= 1


def test_apply_evidence_guard_demotes_when_missing():
    classification = ScanClassification(
        root_causes=["Production errors API request failed."],
        symptoms=[],
        notes=[],
    )
    updated, applied = _apply_evidence_guard(classification, evidence_lines=[], strict_evidence_mode=True)
    assert applied is True
    assert updated.root_causes == []
    assert any("Evidence guard" in n for n in updated.notes)


@pytest.mark.asyncio
async def test_scan_codebase_retries_transient_api_timeout_then_recovers(monkeypatch, tmp_path):
    reset_metrics()
    (tmp_path / "foo.py").write_text("print('hi')\n", encoding="utf-8")

    monkeypatch.setattr("autodebug.auth.get_credentials", lambda: {"project_id": "proj_1", "user_id": "user_1"})
    monkeypatch.setattr("autodebug.config.load_credentials", lambda: {"project_id": "proj_1"})
    monkeypatch.setattr("autodebug.doctor.run_doctor", lambda: {"checks": []})
    monkeypatch.setattr(
        "autodebug.patterns.get_pattern_store",
        lambda: SimpleNamespace(get_stats=lambda: {"total_patterns": 0, "overall_success_rate": 0.0, "total_uses": 0}),
    )
    monkeypatch.setattr("autodebug.flaky_quarantine.list_quarantined_tests", lambda _p: [])
    monkeypatch.setattr(
        "autodebug.healing.detector.detect_in_changed_files",
        lambda _p: SimpleNamespace(files_scanned=0),
    )

    def _subprocess_run(cmd, *args, **kwargs):
        if cmd[:2] == ["git", "merge-base"]:
            return subprocess.CompletedProcess(cmd, 1, stdout="", stderr="")
        if cmd[:2] == ["git", "diff"]:
            return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="")
        if cmd[:3] == ["git", "status", "--porcelain"]:
            return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="")
        if cmd[:3] == ["semgrep", "scan", "--help"]:
            return subprocess.CompletedProcess(cmd, 0, stdout="--cache\n", stderr="")
        if len(cmd) >= 2 and cmd[0] == "semgrep" and cmd[1] == "scan":
            return subprocess.CompletedProcess(cmd, 0, stdout='{"results": []}', stderr="")
        return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="")

    monkeypatch.setattr("autodebug.agent.tools.subprocess.run", _subprocess_run)

    calls = {"prod": 0}

    class _RetryingClient:
        async def __aenter__(self):
            return self

        async def __aexit__(self, exc_type, exc, tb):
            return False

        async def get(self, url, headers=None, params=None, timeout=None):
            req = httpx.Request("GET", url, params=params)
            if "/v1/prod-errors" in url:
                calls["prod"] += 1
                if calls["prod"] < 3:
                    raise httpx.ReadTimeout("timed out", request=req)
                return httpx.Response(200, request=req, json={"errors": [], "total": 0})
            if "/v1/issue-groups" in url:
                return httpx.Response(200, request=req, json={"groups": []})
            if "/v1/sessions" in url:
                return httpx.Response(200, request=req, json={"sessions": []})
            return httpx.Response(200, request=req, json={})

    monkeypatch.setattr("autodebug.agent.tools.httpx.AsyncClient", lambda: _RetryingClient())

    scan_codebase = _register_scan_tool()
    report = await scan_codebase(_make_ctx(tmp_path))

    assert calls["prod"] == 3
    assert "### Production Errors\nNo production errors found." in report
    assert "Could not fetch: connect timed out" not in report


@pytest.mark.asyncio
async def test_scan_codebase_reports_timeout_after_retry_budget(monkeypatch, tmp_path):
    reset_metrics()
    (tmp_path / "foo.py").write_text("print('hi')\n", encoding="utf-8")

    monkeypatch.setattr("autodebug.auth.get_credentials", lambda: {"project_id": "proj_1", "user_id": "user_1"})
    monkeypatch.setattr("autodebug.config.load_credentials", lambda: {"project_id": "proj_1"})
    monkeypatch.setattr("autodebug.doctor.run_doctor", lambda: {"checks": []})
    monkeypatch.setattr(
        "autodebug.patterns.get_pattern_store",
        lambda: SimpleNamespace(get_stats=lambda: {"total_patterns": 0, "overall_success_rate": 0.0, "total_uses": 0}),
    )
    monkeypatch.setattr("autodebug.flaky_quarantine.list_quarantined_tests", lambda _p: [])
    monkeypatch.setattr(
        "autodebug.healing.detector.detect_in_changed_files",
        lambda _p: SimpleNamespace(files_scanned=0),
    )

    def _subprocess_run(cmd, *args, **kwargs):
        if cmd[:2] == ["git", "merge-base"]:
            return subprocess.CompletedProcess(cmd, 1, stdout="", stderr="")
        if cmd[:2] == ["git", "diff"]:
            return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="")
        if cmd[:3] == ["git", "status", "--porcelain"]:
            return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="")
        if cmd[:3] == ["semgrep", "scan", "--help"]:
            return subprocess.CompletedProcess(cmd, 0, stdout="--cache\n", stderr="")
        if len(cmd) >= 2 and cmd[0] == "semgrep" and cmd[1] == "scan":
            return subprocess.CompletedProcess(cmd, 0, stdout='{"results": []}', stderr="")
        return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="")

    monkeypatch.setattr("autodebug.agent.tools.subprocess.run", _subprocess_run)

    class _TimeoutClient:
        async def __aenter__(self):
            return self

        async def __aexit__(self, exc_type, exc, tb):
            return False

        async def get(self, url, headers=None, params=None, timeout=None):
            req = httpx.Request("GET", url, params=params)
            if "/v1/prod-errors" in url:
                raise httpx.ConnectTimeout("connect timed out", request=req)
            if "/v1/issue-groups" in url:
                return httpx.Response(200, request=req, json={"groups": []})
            if "/v1/sessions" in url:
                return httpx.Response(200, request=req, json={"sessions": []})
            return httpx.Response(200, request=req, json={})

    monkeypatch.setattr("autodebug.agent.tools.httpx.AsyncClient", lambda: _TimeoutClient())

    scan_codebase = _register_scan_tool()
    report = await scan_codebase(_make_ctx(tmp_path))

    assert "Production errors API request failed." in report
    assert "timed out" in report
    assert "status: **DEGRADED**" in report


@pytest.mark.asyncio
async def test_scan_codebase_report_section_contract(monkeypatch, tmp_path):
    """Contract test: scan report keeps deterministic section schema/order."""
    (tmp_path / "foo.py").write_text("print('hi')\n", encoding="utf-8")
    monkeypatch.setattr("autodebug.auth.get_credentials", lambda: {"project_id": "proj_1", "user_id": "user_1"})
    monkeypatch.setattr("autodebug.config.load_credentials", lambda: {"project_id": "proj_1"})
    monkeypatch.setattr("autodebug.doctor.run_doctor", lambda: {"checks": []})
    monkeypatch.setattr(
        "autodebug.patterns.get_pattern_store",
        lambda: SimpleNamespace(get_stats=lambda: {"total_patterns": 0, "overall_success_rate": 0.0, "total_uses": 0}),
    )
    monkeypatch.setattr("autodebug.flaky_quarantine.list_quarantined_tests", lambda _p: [])
    monkeypatch.setattr("autodebug.healing.detector.detect_in_changed_files", lambda _p: SimpleNamespace(files_scanned=0))

    def _subprocess_run(cmd, *args, **kwargs):
        if cmd[:2] == ["git", "merge-base"]:
            return subprocess.CompletedProcess(cmd, 1, stdout="", stderr="")
        if cmd[:2] == ["git", "diff"]:
            return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="")
        if cmd[:3] == ["git", "status", "--porcelain"]:
            return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="")
        if cmd[:3] == ["semgrep", "scan", "--help"]:
            return subprocess.CompletedProcess(cmd, 0, stdout="--cache\n", stderr="")
        if len(cmd) >= 2 and cmd[0] == "semgrep" and cmd[1] == "scan":
            return subprocess.CompletedProcess(cmd, 0, stdout='{"results": []}', stderr="")
        return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="")

    monkeypatch.setattr("autodebug.agent.tools.subprocess.run", _subprocess_run)
    monkeypatch.setattr("autodebug.agent.tools.httpx.AsyncClient", lambda: _FakeAsyncClient([]))

    report = await _register_scan_tool()(_make_ctx(tmp_path))
    headers = _section_headers(report)

    assert headers[:10] == [
        "Project",
        "Production Errors",
        "Issue Groups",
        "Flaky Patterns",
        "Environment",
        "Pattern Database",
        "Quarantine",
        "Sessions",
        "Repo Hygiene",
        "Semgrep",
    ]
    assert "## Scan Fidelity" in report
    assert "---\n### Recommended Next Steps" in report


@pytest.mark.asyncio
async def test_scan_codebase_invalid_mode_returns_error(monkeypatch, tmp_path):
    """Invalid mode parameter should return helpful error message."""
    reset_metrics()
    monkeypatch.setattr("autodebug.auth.get_credentials", lambda: {"project_id": "proj_1"})
    monkeypatch.setattr("autodebug.config.load_credentials", lambda: {"project_id": "proj_1"})

    scan_codebase = _register_scan_tool()
    ctx = _make_ctx(tmp_path)

    result = await scan_codebase(ctx, mode="invalid-mode")

    assert "Error: Invalid mode" in result
    assert "full" in result  # valid mode should be listed
    assert "semgrep-only" in result


@pytest.mark.asyncio
async def test_scan_codebase_semgrep_only_mode_filters_checks(monkeypatch, tmp_path):
    """semgrep-only mode should only run Project and Semgrep checks."""
    reset_metrics()
    (tmp_path / "foo.py").write_text("print('hi')\n", encoding="utf-8")

    monkeypatch.setattr("autodebug.auth.get_credentials", lambda: {"project_id": "proj_1", "user_id": "user_1"})
    monkeypatch.setattr("autodebug.config.load_credentials", lambda: {"project_id": "proj_1"})

    checks_run = []

    def _subprocess_run(cmd, *args, **kwargs):
        checks_run.append(" ".join(cmd[:3]))
        if cmd[:3] == ["git", "status", "--porcelain"]:
            return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="")
        if cmd[:3] == ["semgrep", "scan", "--help"]:
            return subprocess.CompletedProcess(cmd, 0, stdout="--cache\n", stderr="")
        if len(cmd) >= 2 and cmd[0] == "semgrep" and cmd[1] == "scan":
            return subprocess.CompletedProcess(cmd, 0, stdout='{"results": []}', stderr="")
        return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="")

    monkeypatch.setattr("autodebug.agent.tools.subprocess.run", _subprocess_run)
    monkeypatch.setattr("autodebug.agent.tools.httpx.AsyncClient", lambda: _FakeAsyncClient([]))

    scan_codebase = _register_scan_tool()
    report = await scan_codebase(_make_ctx(tmp_path), mode="semgrep-only")

    # Should only have git commands (Project check) and semgrep commands, not API calls
    all_checks = " ".join(checks_run)
    assert "git" in all_checks  # Project check runs git commands
    assert "semgrep scan" in all_checks  # Semgrep check
    # Should not make API calls for production errors, issue groups, etc.
    assert "# Codebase Scan Report" in report


@pytest.mark.asyncio
async def test_scan_codebase_api_only_mode_skips_semgrep(monkeypatch, tmp_path):
    """api-only mode should not run semgrep, only API-connected checks."""
    reset_metrics()
    (tmp_path / "foo.py").write_text("print('hi')\n", encoding="utf-8")

    monkeypatch.setattr("autodebug.auth.get_credentials", lambda: {"project_id": "proj_1", "user_id": "user_1"})
    monkeypatch.setattr("autodebug.config.load_credentials", lambda: {"project_id": "proj_1"})

    semgrep_called = False

    def _subprocess_run(cmd, *args, **kwargs):
        nonlocal semgrep_called
        if len(cmd) >= 2 and cmd[0] == "semgrep" and cmd[1] == "scan":
            semgrep_called = True
        if cmd[:3] == ["git", "status", "--porcelain"]:
            return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="")
        return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="")

    monkeypatch.setattr("autodebug.agent.tools.subprocess.run", _subprocess_run)

    class _ApiClient:
        async def __aenter__(self):
            return self
        async def __aexit__(self, *args):
            return False
        async def get(self, url, **kwargs):
            req = httpx.Request("GET", url)
            if "/v1/prod-errors" in url:
                return httpx.Response(200, request=req, json={"errors": [], "total": 0})
            if "/v1/issue-groups" in url:
                return httpx.Response(200, request=req, json={"groups": []})
            if "/v1/sessions" in url:
                return httpx.Response(200, request=req, json={"sessions": []})
            return httpx.Response(200, request=req, json={})

    monkeypatch.setattr("autodebug.agent.tools.httpx.AsyncClient", lambda: _ApiClient())

    scan_codebase = _register_scan_tool()
    report = await scan_codebase(_make_ctx(tmp_path), mode="api-only")

    assert not semgrep_called, "Semgrep should not be called in api-only mode"
    assert "# Codebase Scan Report" in report
    assert "### Production Errors" in report


def test_quality_gate_demotes_root_causes_when_evidence_missing():
    """CI guard: strong root-cause claims require evidence artifacts."""
    classification = ScanClassification(
        root_causes=["Semgrep scan failed."],
        symptoms=[],
        notes=[],
    )
    updated, applied = _apply_evidence_guard(
        classification,
        evidence_lines=[],
        strict_evidence_mode=True,
    )
    assert applied is True
    assert updated.root_causes == []
    assert any("Evidence guard: root-cause claims were demoted" in n for n in updated.notes)


# ---------------------------------------------------------------------------
# Gap 1: Taint Failure Classification
# ---------------------------------------------------------------------------
class TestTaintFailureClassification:
    """Verify taint analysis uses spec-compliant status values."""

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

    def test_ok_no_flows_status_exists(self):
        src = self._get_taint_source()
        assert '"ok_no_flows"' in src, "Expected ok_no_flows status in taint analysis"

    def test_ok_with_flows_status_exists(self):
        src = self._get_taint_source()
        assert '"ok_with_flows"' in src, "Expected ok_with_flows status in taint analysis"

    def test_execution_failed_status_exists(self):
        src = self._get_taint_source()
        assert '"execution_failed"' in src, "Expected execution_failed status in taint analysis"

    def test_parse_failed_status_exists(self):
        src = self._get_taint_source()
        assert '"parse_failed"' in src, "Expected parse_failed status in taint analysis"

    def test_old_taint_status_values_removed(self):
        """Verify the taint-specific code uses new status names, not old ones."""
        import inspect
        from autodebug.agent import tools
        # Get the source of the entire module and extract just the _taint_analysis section
        full_src = inspect.getsource(tools)
        # Find the taint analysis function boundaries
        taint_start = full_src.find("async def _taint_analysis()")
        taint_end = full_src.find("# Fire selected checks in parallel", taint_start)
        taint_src = full_src[taint_start:taint_end] if taint_start >= 0 and taint_end >= 0 else ""
        assert taint_src, "Could not locate _taint_analysis function in source"
        assert '"no_findings"' not in taint_src, "Old no_findings status should be renamed to ok_no_flows in taint"
        assert 'status": "findings"' not in taint_src, "Old findings status should be renamed to ok_with_flows in taint"


# ---------------------------------------------------------------------------
# Gap 2: Continuation Token Resume
# ---------------------------------------------------------------------------
class TestContinuationTokenResume:
    """Verify continuation_token is decoded and drives pagination."""

    @pytest.fixture(autouse=True)
    def _setup(self, monkeypatch, tmp_path):
        self.tmp_path = tmp_path
        (tmp_path / "foo.py").write_text("print('hi')\n", encoding="utf-8")
        monkeypatch.setattr("autodebug.auth.get_credentials", lambda: {"user_id": "u1"})
        monkeypatch.setattr("autodebug.config.load_credentials", lambda: {"user_id": "u1"})

    def _get_codebase_scan(self):
        agent = _DummyAgent()
        register_hikaflow_tools(agent)
        return agent.tools["codebase_scan"]

    @pytest.mark.asyncio
    async def test_valid_token_overrides_page(self):
        import json
        scan = self._get_codebase_scan()
        ctx = _make_ctx(self.tmp_path)
        result = await scan(ctx, continuation_token="scan:incremental:3:50")
        data = json.loads(result)
        assert data["data"]["scan_meta"]["page"] == 3

    @pytest.mark.asyncio
    async def test_valid_token_overrides_scope(self):
        import json
        scan = self._get_codebase_scan()
        ctx = _make_ctx(self.tmp_path)
        result = await scan(ctx, scope="incremental", continuation_token="scan:full:1:100")
        data = json.loads(result)
        assert data["data"]["scan_meta"]["scope"] == "full"

    @pytest.mark.asyncio
    async def test_invalid_token_returns_error(self):
        import json
        scan = self._get_codebase_scan()
        ctx = _make_ctx(self.tmp_path)
        result = await scan(ctx, continuation_token="invalid_token")
        data = json.loads(result)
        assert data["status"] == "error"
        assert "Invalid continuation_token" in data["summary"]

    @pytest.mark.asyncio
    async def test_malformed_page_returns_error(self):
        import json
        scan = self._get_codebase_scan()
        ctx = _make_ctx(self.tmp_path)
        result = await scan(ctx, continuation_token="scan:incremental:abc:50")
        data = json.loads(result)
        assert data["status"] == "error"
        assert "Malformed" in data["summary"]

    @pytest.mark.asyncio
    async def test_empty_token_ignored(self):
        import json
        scan = self._get_codebase_scan()
        ctx = _make_ctx(self.tmp_path)
        result = await scan(ctx, continuation_token="")
        data = json.loads(result)
        # Should work normally (no error)
        assert data["status"] in ("ok", "partial")

    @pytest.mark.asyncio
    async def test_manual_page_still_works(self):
        import json
        scan = self._get_codebase_scan()
        ctx = _make_ctx(self.tmp_path)
        result = await scan(ctx, page=3)
        data = json.loads(result)
        assert data["data"]["scan_meta"]["page"] == 3


# ---------------------------------------------------------------------------
# Gap 3: Structured Findings Parsing
# ---------------------------------------------------------------------------
class TestParseScanFindings:
    """Verify _parse_scan_findings extracts structured findings from raw reports."""

    def test_extracts_structured_data(self):
        raw = (
            "**3 finding(s):**\n"
            "  - `api/main.py:12` [ERROR|HIGH|0.85] security.rule: Dangerous pattern\n"
            "  - `api/db.py:45` [WARNING|MED|0.65] code-quality.check: Unused import\n"
            "  - `api/db.py:80` [WARNING|LOW|0.40] style.rule: Line too long\n"
        )
        findings, counts = _parse_scan_findings(raw)
        assert len(findings) == 3
        assert findings[0]["file"] == "api/main.py"
        assert findings[0]["line"] == 12
        assert findings[0]["severity"] == "ERROR"
        assert findings[0]["check"] == "security.rule"
        assert findings[0]["message"] == "Dangerous pattern"
        assert findings[0]["score"] == 0.85

    def test_respects_max_findings(self):
        raw = (
            "  - `a.py:1` [ERROR|HIGH|0.9] r1: msg1\n"
            "  - `b.py:2` [ERROR|HIGH|0.8] r2: msg2\n"
            "  - `c.py:3` [ERROR|HIGH|0.7] r3: msg3\n"
        )
        findings, counts = _parse_scan_findings(raw, max_findings=2)
        assert len(findings) == 2
        # Severity counts should still reflect ALL matches, not just returned ones
        assert counts["ERROR"] == 3

    def test_handles_empty_report(self):
        findings, counts = _parse_scan_findings("")
        assert findings == []
        assert counts == {}

    def test_counts_severities(self):
        raw = (
            "  - `a.py:1` [ERROR|HIGH|0.9] r1: msg1\n"
            "  - `b.py:2` [WARNING|MED|0.6] r2: msg2\n"
            "  - `c.py:3` [ERROR|HIGH|0.8] r3: msg3\n"
            "  - `d.py:4` [INFO|LOW|0.3] r4: msg4\n"
        )
        _, counts = _parse_scan_findings(raw)
        assert counts == {"ERROR": 2, "WARNING": 1, "INFO": 1}

    def test_handles_new_tag(self):
        raw = "  - `a.py:10` [ERROR|HIGH|0.9] [NEW] rule.name: New finding\n"
        findings, _ = _parse_scan_findings(raw)
        assert len(findings) == 1
        assert findings[0]["message"] == "New finding"

    def test_ignores_non_finding_lines(self):
        raw = (
            "# Codebase Scan Report\n"
            "Some description text\n"
            "  - `a.py:1` [ERROR|HIGH|0.9] r1: msg1\n"
            "More text\n"
        )
        findings, _ = _parse_scan_findings(raw)
        assert len(findings) == 1


# ---------------------------------------------------------------------------
# Scan mode aliases
# ---------------------------------------------------------------------------
class TestScanModeAliases:
    """Verify spec mode names (quick, security, async, deep) work as aliases."""

    @pytest.fixture(autouse=True)
    def _setup(self, monkeypatch, tmp_path):
        self.tmp_path = tmp_path
        (tmp_path / "foo.py").write_text("print('hi')\n", encoding="utf-8")
        monkeypatch.setattr("autodebug.auth.get_credentials", lambda: {"user_id": "u1"})
        monkeypatch.setattr("autodebug.config.load_credentials", lambda: {"user_id": "u1"})

    def _get_scan(self):
        agent = _DummyAgent()
        register_hikaflow_tools(agent)
        return agent.tools["scan_codebase"]

    @pytest.mark.asyncio
    async def test_quick_mode_alias(self):
        scan = self._get_scan()
        ctx = _make_ctx(self.tmp_path)
        result = await scan(ctx, mode="quick")
        assert "Error: Invalid mode" not in result

    @pytest.mark.asyncio
    async def test_security_mode_alias(self):
        scan = self._get_scan()
        ctx = _make_ctx(self.tmp_path)
        result = await scan(ctx, mode="security")
        assert "Error: Invalid mode" not in result

    @pytest.mark.asyncio
    async def test_deep_mode_alias(self):
        scan = self._get_scan()
        ctx = _make_ctx(self.tmp_path)
        result = await scan(ctx, mode="deep")
        assert "Error: Invalid mode" not in result

    @pytest.mark.asyncio
    async def test_async_mode_alias(self):
        scan = self._get_scan()
        ctx = _make_ctx(self.tmp_path)
        result = await scan(ctx, mode="async")
        assert "Error: Invalid mode" not in result

    @pytest.mark.asyncio
    async def test_invalid_mode_still_errors(self):
        scan = self._get_scan()
        ctx = _make_ctx(self.tmp_path)
        result = await scan(ctx, mode="nonexistent")
        assert "Error: Invalid mode" in result


class TestSectionFailureSurfacing:
    """V-13: Verify that section failures are surfaced in compact codebase_scan output."""

    def test_failed_section_regex_extracts_errors(self):
        """Section errors in raw report should be parsed into failed_sections."""
        import re
        raw = (
            "### Project\nUser: test\n\n"
            "### Production Errors\nCould not fetch: 401 Unauthorized\n\n"
            "### Semgrep\nNo findings.\n\n"
            "### Taint Analysis\nError: subprocess timed out\n"
        )
        failed = []
        for m in re.finditer(
            r"###\s+(.+?)\n(?:Error:|Could not fetch:)\s*(.+?)(?:\n|$)", raw
        ):
            failed.append({"section": m.group(1).strip(), "error": m.group(2).strip()[:200]})
        assert len(failed) == 2
        assert failed[0]["section"] == "Production Errors"
        assert "401" in failed[0]["error"]
        assert failed[1]["section"] == "Taint Analysis"
        assert "timed out" in failed[1]["error"]

    def test_no_failures_gives_empty_list(self):
        import re
        raw = "### Project\nUser: test\n\n### Semgrep\nNo findings.\n"
        failed = []
        for m in re.finditer(
            r"###\s+(.+?)\n(?:Error:|Could not fetch:)\s*(.+?)(?:\n|$)", raw
        ):
            failed.append({"section": m.group(1).strip()})
        assert failed == []


class TestReadFileGuard:
    """V-14: Verify read_file rejects >10MB and warns >1MB files."""

    @pytest.fixture(autouse=True)
    def _setup(self, tmp_path, monkeypatch):
        self.tmp_path = tmp_path
        (tmp_path / "small.py").write_text("x = 1\n", encoding="utf-8")
        monkeypatch.setattr("autodebug.auth.get_credentials", lambda: {"user_id": "u1"})
        monkeypatch.setattr("autodebug.config.load_credentials", lambda: {"user_id": "u1"})

    def _get_read_file(self):
        agent = _DummyAgent()
        register_file_tools(agent)
        return agent.tools["read_file"]

    @pytest.mark.asyncio
    async def test_refuses_files_over_10mb(self):
        big = self.tmp_path / "huge.bin"
        big.write_bytes(b"x" * (10_000_001))
        read_file = self._get_read_file()
        ctx = _make_ctx(self.tmp_path)
        result = await read_file(ctx, path="huge.bin")
        assert "too large" in result.lower()
        assert "10" in result

    @pytest.mark.asyncio
    async def test_warns_for_files_over_1mb(self):
        medium = self.tmp_path / "medium.txt"
        medium.write_text("a" * 1_500_000, encoding="utf-8")
        read_file = self._get_read_file()
        ctx = _make_ctx(self.tmp_path)
        result = await read_file(ctx, path="medium.txt")
        assert "WARNING" in result
        assert "truncated" in result.lower()

    @pytest.mark.asyncio
    async def test_small_file_reads_normally(self):
        read_file = self._get_read_file()
        ctx = _make_ctx(self.tmp_path)
        result = await read_file(ctx, path="small.py")
        assert "x = 1" in result
