"""Tests for the Sanicode API server."""

from __future__ import annotations

import asyncio

import httpx
import pytest

from sanicode.config import SanicodeConfig
from sanicode.server.app import app, configure


@pytest.fixture(autouse=True)
def _reset_server_state():
    """Reset server state before each test."""
    configure(SanicodeConfig())


@pytest.fixture
def client():
    """Create an async test client."""
    transport = httpx.ASGITransport(app=app)
    return httpx.AsyncClient(transport=transport, base_url="http://test")


class TestHealth:
    @pytest.mark.asyncio
    async def test_returns_ok(self, client: httpx.AsyncClient) -> None:
        resp = await client.get("/api/v1/health")
        assert resp.status_code == 200
        data = resp.json()
        assert data["status"] == "ok"
        assert "version" in data
        assert "llm_tiers" in data

    @pytest.mark.asyncio
    async def test_llm_tiers_not_configured(self, client: httpx.AsyncClient) -> None:
        resp = await client.get("/api/v1/health")
        tiers = resp.json()["llm_tiers"]
        for tier in ("fast", "analysis", "reasoning"):
            assert tiers[tier] == "not_configured"


class TestScanSubmit:
    @pytest.mark.asyncio
    async def test_submit_returns_202(self, client: httpx.AsyncClient, tmp_path) -> None:
        target = tmp_path / "code.py"
        target.write_text("x = 1\n", encoding="utf-8")
        resp = await client.post("/api/v1/scan", json={"path": str(tmp_path)})
        assert resp.status_code == 202
        data = resp.json()
        assert "scan_id" in data
        assert data["status"] == "pending"

    @pytest.mark.asyncio
    async def test_nonexistent_path_400(self, client: httpx.AsyncClient) -> None:
        resp = await client.post("/api/v1/scan", json={"path": "/nonexistent/xyz"})
        assert resp.status_code == 400


class TestScanLifecycle:
    @pytest.mark.asyncio
    async def test_full_lifecycle(self, client: httpx.AsyncClient, tmp_path) -> None:
        """Submit a scan, poll until done, retrieve findings and graph."""
        target = tmp_path / "vuln.py"
        target.write_text("eval(input())\n", encoding="utf-8")

        # Submit
        resp = await client.post("/api/v1/scan", json={"path": str(tmp_path)})
        assert resp.status_code == 202
        scan_id = resp.json()["scan_id"]

        # Poll until completed (max 5 seconds)
        for _ in range(50):
            resp = await client.get(f"/api/v1/scan/{scan_id}")
            assert resp.status_code == 200
            if resp.json()["status"] == "completed":
                break
            await asyncio.sleep(0.1)
        else:
            pytest.fail("Scan did not complete within 5 seconds")

        status_data = resp.json()
        assert status_data["summary"] is not None
        assert status_data["summary"]["total_findings"] > 0

        # Get findings
        resp = await client.get(f"/api/v1/scan/{scan_id}/findings")
        assert resp.status_code == 200
        findings_data = resp.json()
        assert findings_data["total"] > 0
        assert any(f["rule_id"] == "SC001" for f in findings_data["findings"])

        # Get SARIF findings
        resp = await client.get(f"/api/v1/scan/{scan_id}/findings?format=sarif")
        assert resp.status_code == 200
        sarif = resp.json()
        assert sarif["version"] == "2.1.0"
        assert "$schema" in sarif

        # Get graph
        resp = await client.get(f"/api/v1/scan/{scan_id}/graph")
        assert resp.status_code == 200
        graph_data = resp.json()
        assert "graph" in graph_data
        assert isinstance(graph_data["nodes"], int)


class TestScanErrors:
    @pytest.mark.asyncio
    async def test_unknown_scan_id_404(self, client: httpx.AsyncClient) -> None:
        resp = await client.get("/api/v1/scan/nonexistent")
        assert resp.status_code == 404

    @pytest.mark.asyncio
    async def test_findings_before_complete_409(
        self, client: httpx.AsyncClient, tmp_path
    ) -> None:
        """Requesting findings on a not-yet-completed scan returns 409."""
        target = tmp_path / "slow.py"
        target.write_text("x = 1\n", encoding="utf-8")
        resp = await client.post("/api/v1/scan", json={"path": str(tmp_path)})
        scan_id = resp.json()["scan_id"]

        # Immediately request findings (scan likely still running)
        # The scan might complete very fast, so we check for either 200 or 409
        resp = await client.get(f"/api/v1/scan/{scan_id}/findings")
        assert resp.status_code in (200, 409)


class TestAnalyze:
    @pytest.mark.asyncio
    async def test_detects_eval(self, client: httpx.AsyncClient) -> None:
        resp = await client.post("/api/v1/analyze", json={"code": "eval(input())"})
        assert resp.status_code == 200
        data = resp.json()
        assert data["total"] > 0
        assert any(f["rule_id"] == "SC001" for f in data["findings"])

    @pytest.mark.asyncio
    async def test_clean_code_no_findings(self, client: httpx.AsyncClient) -> None:
        resp = await client.post("/api/v1/analyze", json={"code": "x = 1 + 2"})
        assert resp.status_code == 200
        assert resp.json()["total"] == 0

    @pytest.mark.asyncio
    async def test_syntax_error_400(self, client: httpx.AsyncClient) -> None:
        resp = await client.post("/api/v1/analyze", json={"code": "def ("})
        assert resp.status_code == 400

    @pytest.mark.asyncio
    async def test_custom_filename(self, client: httpx.AsyncClient) -> None:
        resp = await client.post(
            "/api/v1/analyze", json={"code": "eval(input())", "filename": "test.py"}
        )
        assert resp.status_code == 200
        assert resp.json()["filename"] == "test.py"


class TestComplianceMap:
    @pytest.mark.asyncio
    async def test_forward_cwe_lookup(self, client: httpx.AsyncClient) -> None:
        resp = await client.get("/api/v1/compliance/map?cwe_id=78")
        assert resp.status_code == 200
        data = resp.json()
        assert data["cwe_id"] == 78
        assert len(data["nist_800_53"]) > 0

    @pytest.mark.asyncio
    async def test_reverse_nist_lookup(self, client: httpx.AsyncClient) -> None:
        resp = await client.get(
            "/api/v1/compliance/map?framework=nist_800_53&control_id=SI-10"
        )
        assert resp.status_code == 200
        data = resp.json()
        assert isinstance(data, list)
        assert len(data) > 0

    @pytest.mark.asyncio
    async def test_missing_params_400(self, client: httpx.AsyncClient) -> None:
        resp = await client.get("/api/v1/compliance/map")
        assert resp.status_code == 400


class TestMetrics:
    @pytest.mark.asyncio
    async def test_metrics_accessible(self, client: httpx.AsyncClient) -> None:
        # The metrics sub-app is mounted at /metrics; the ASGI mount redirects
        # the bare path to /metrics/ (trailing slash), so we request that directly.
        resp = await client.get("/metrics/")
        assert resp.status_code == 200
        body = resp.text
        assert "sanicode_scans_total" in body
