"""Tests for compliance mapping and finding enrichment."""

from __future__ import annotations

from pathlib import Path

import pytest

from sanicode.compliance.enrichment import EnrichedFinding, enrich_finding, enrich_findings
from sanicode.compliance.mapper import (
    ComplianceMapper,
    ComplianceMapping,
    derive_severity,
)
from sanicode.scanner.patterns import Finding

# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


@pytest.fixture(scope="module")
def mapper() -> ComplianceMapper:
    """Shared ComplianceMapper loaded once per test module."""
    return ComplianceMapper()


def _finding(cwe_id: int, rule_id: str = "SC001") -> Finding:
    """Build a minimal Finding for a given CWE ID."""
    return Finding(
        file=Path("example.py"),
        line=10,
        column=4,
        rule_id=rule_id,
        message="test finding",
        severity="high",
        cwe_id=cwe_id,
    )


# ---------------------------------------------------------------------------
# ComplianceMapper.map_cwe — known CWEs
# ---------------------------------------------------------------------------

ALL_KNOWN_CWES = [20, 77, 78, 79, 89, 90, 94, 116, 120, 352, 502, 917, 918, 1333]


@pytest.mark.parametrize("cwe_id", ALL_KNOWN_CWES)
def test_map_cwe_returns_nonempty_asvs(mapper: ComplianceMapper, cwe_id: int) -> None:
    m = mapper.map_cwe(cwe_id)
    assert m.owasp_asvs, f"CWE-{cwe_id} should have at least one ASVS entry"


@pytest.mark.parametrize("cwe_id", ALL_KNOWN_CWES)
def test_map_cwe_returns_nonempty_nist(mapper: ComplianceMapper, cwe_id: int) -> None:
    m = mapper.map_cwe(cwe_id)
    assert m.nist_800_53, f"CWE-{cwe_id} should have at least one NIST 800-53 control"


@pytest.mark.parametrize("cwe_id", ALL_KNOWN_CWES)
def test_map_cwe_populates_cwe_name(mapper: ComplianceMapper, cwe_id: int) -> None:
    m = mapper.map_cwe(cwe_id)
    assert m.cwe_name, f"CWE-{cwe_id} should have a non-empty cwe_name"


@pytest.mark.parametrize("cwe_id", ALL_KNOWN_CWES)
def test_map_cwe_populates_asvs_level(mapper: ComplianceMapper, cwe_id: int) -> None:
    m = mapper.map_cwe(cwe_id)
    assert m.asvs_level in ("L1", "L2", "L3"), (
        f"CWE-{cwe_id} asvs_level should be L1/L2/L3, got '{m.asvs_level}'"
    )


# STIG category populated only for CWEs that have STIG entries
_CWES_WITH_STIG = [20, 77, 78, 79, 89, 90, 94, 116, 120, 352, 502, 917]


@pytest.mark.parametrize("cwe_id", _CWES_WITH_STIG)
def test_map_cwe_populates_stig_category(mapper: ComplianceMapper, cwe_id: int) -> None:
    m = mapper.map_cwe(cwe_id)
    assert m.stig_category in ("CAT I", "CAT II", "CAT III"), (
        f"CWE-{cwe_id} stig_category should be CAT I/II/III, got '{m.stig_category}'"
    )


def test_map_cwe_unknown_returns_empty(mapper: ComplianceMapper) -> None:
    m = mapper.map_cwe(99999)
    assert m.cwe_id == 99999
    assert m.owasp_asvs == []
    assert m.nist_800_53 == []
    assert m.asd_stig == []
    assert m.pci_dss == []
    assert m.cwe_name == ""


def test_map_cwe_asvs_rich_format(mapper: ComplianceMapper) -> None:
    """ASVS entries should be dicts with id, title, and level keys."""
    m = mapper.map_cwe(78)
    for entry in m.owasp_asvs:
        assert "id" in entry, f"ASVS entry missing 'id': {entry}"
        assert "level" in entry, f"ASVS entry missing 'level': {entry}"


def test_map_cwe_stig_rich_format(mapper: ComplianceMapper) -> None:
    """STIG entries should be dicts with id, cat, and title keys."""
    m = mapper.map_cwe(78)
    for entry in m.asd_stig:
        assert "id" in entry, f"STIG entry missing 'id': {entry}"
        assert "cat" in entry, f"STIG entry missing 'cat': {entry}"


# ---------------------------------------------------------------------------
# derive_severity
# ---------------------------------------------------------------------------


@pytest.mark.parametrize(
    "stig_cat, expected_severity",
    [
        ("CAT I", "critical"),
        ("CAT II", "high"),
        ("CAT III", "medium"),
        ("", "medium"),
    ],
)
def test_derive_severity_from_stig_category(
    stig_cat: str, expected_severity: str
) -> None:
    mapping = ComplianceMapping(cwe_id=0, stig_category=stig_cat)
    assert derive_severity(mapping) == expected_severity


def test_derive_severity_no_stig_data() -> None:
    mapping = ComplianceMapping(cwe_id=1333)  # no STIG entries
    assert derive_severity(mapping) == "medium"


def test_derive_severity_reads_stig_entries_directly() -> None:
    """derive_severity falls through to entry-level cat when stig_category is empty."""
    mapping = ComplianceMapping(
        cwe_id=0,
        stig_category="",
        asd_stig=[{"id": "APSC-DV-002510", "cat": "I", "title": ""}],
    )
    assert derive_severity(mapping) == "critical"


# ---------------------------------------------------------------------------
# ComplianceMapper.map_control — reverse lookup
# ---------------------------------------------------------------------------


def test_map_control_nist_si10_returns_many(mapper: ComplianceMapper) -> None:
    """SI-10 is referenced by nearly every sanitization CWE."""
    results = mapper.map_control("nist_800_53", "SI-10")
    cwe_ids = {m.cwe_id for m in results}
    # At minimum the canonical set should be present
    assert {20, 78, 79, 89, 94}.issubset(cwe_ids), (
        f"Expected core CWEs in SI-10 reverse lookup, got {cwe_ids}"
    )


@pytest.mark.parametrize("expected_cwe", [77, 78, 94])
def test_map_control_stig_apsc_dv_002510(
    mapper: ComplianceMapper, expected_cwe: int
) -> None:
    """APSC-DV-002510 (CAT I injection) maps to the command/code injection family."""
    results = mapper.map_control("asd_stig", "APSC-DV-002510")
    cwe_ids = {m.cwe_id for m in results}
    assert expected_cwe in cwe_ids, (
        f"Expected CWE-{expected_cwe} in APSC-DV-002510 reverse lookup, got {cwe_ids}"
    )


def test_map_control_nonexistent_returns_empty(mapper: ComplianceMapper) -> None:
    results = mapper.map_control("nist_800_53", "NONEXISTENT-CONTROL")
    assert results == []


def test_map_control_owasp_asvs_lookup(mapper: ComplianceMapper) -> None:
    """Reverse lookup by ASVS requirement ID should work for rich-format entries."""
    results = mapper.map_control("owasp_asvs", "v5.0.0-V1-5.3.4")
    cwe_ids = {m.cwe_id for m in results}
    assert 89 in cwe_ids, f"Expected CWE-89 for ASVS v5.0.0-V1-5.3.4, got {cwe_ids}"


def test_map_control_pci_dss_lookup(mapper: ComplianceMapper) -> None:
    results = mapper.map_control("pci_dss", "6.2.4")
    cwe_ids = {m.cwe_id for m in results}
    assert {20, 78, 79, 89}.issubset(cwe_ids), (
        f"Expected injection CWEs in PCI 6.2.4 reverse lookup, got {cwe_ids}"
    )


# ---------------------------------------------------------------------------
# enrich_finding
# ---------------------------------------------------------------------------


def test_enrich_finding_known_cwe(mapper: ComplianceMapper) -> None:
    f = _finding(cwe_id=78)
    ef = enrich_finding(f, mapper)

    assert isinstance(ef, EnrichedFinding)
    assert ef.cwe_id == 78
    assert ef.cwe_name  # non-empty
    assert ef.compliance is not None
    assert ef.compliance.owasp_asvs  # has at least one ASVS entry
    assert ef.derived_severity == "critical"  # CWE-78 maps to CAT I
    assert ef.remediation  # non-empty


def test_enrich_finding_preserves_original_fields(mapper: ComplianceMapper) -> None:
    f = _finding(cwe_id=89, rule_id="SC006")
    ef = enrich_finding(f, mapper)

    assert ef.file == f.file
    assert ef.line == f.line
    assert ef.column == f.column
    assert ef.rule_id == f.rule_id
    assert ef.message == f.message
    assert ef.severity == f.severity


def test_enrich_finding_unknown_cwe(mapper: ComplianceMapper) -> None:
    f = _finding(cwe_id=99999)
    ef = enrich_finding(f, mapper)

    assert ef.cwe_id == 99999
    assert ef.cwe_name == ""
    assert ef.compliance is not None
    assert ef.compliance.owasp_asvs == []
    assert ef.derived_severity == "medium"  # fallback
    assert ef.remediation == ""


# ---------------------------------------------------------------------------
# enrich_findings
# ---------------------------------------------------------------------------


def test_enrich_findings_returns_correct_count(mapper: ComplianceMapper) -> None:
    findings = [_finding(cwe_id=cid) for cid in [78, 89, 502]]
    enriched = enrich_findings(findings, mapper)
    assert len(enriched) == 3


def test_enrich_findings_preserves_order(mapper: ComplianceMapper) -> None:
    cwe_ids = [79, 89, 1333, 20]
    findings = [_finding(cwe_id=cid) for cid in cwe_ids]
    enriched = enrich_findings(findings, mapper)
    assert [ef.cwe_id for ef in enriched] == cwe_ids


def test_enrich_findings_default_mapper() -> None:
    """enrich_findings creates its own mapper when none is provided."""
    findings = [_finding(cwe_id=78)]
    enriched = enrich_findings(findings)
    assert len(enriched) == 1
    assert enriched[0].cwe_name  # populated from the bundled DB


def test_enrich_findings_empty_list(mapper: ComplianceMapper) -> None:
    assert enrich_findings([], mapper) == []
