"""Tests for Rule Coverage Analyzer — OWASP/CWE/MITRE coverage analysis."""

from __future__ import annotations

import json

import pytest

from mcpaudit.rule_coverage import (
    CoverageGap,
    CoverageMatrix,
    analyze_cwe_coverage,
    analyze_mitre_coverage,
    analyze_owasp_coverage,
    format_coverage_json,
    format_coverage_text,
    generate_coverage_html,
    suggest_new_rules,
    _OWASP_AGENTIC_TOP10,
    _CWE_CATEGORIES,
    _MITRE_ATTACK_AI,
    _analyze_framework,
    _get_all_rule_ids,
)


# ---------------------------------------------------------------------------
# CoverageGap dataclass
# ---------------------------------------------------------------------------

class TestCoverageGap:
    """Tests for CoverageGap dataclass."""

    def test_to_dict(self):
        gap = CoverageGap(
            category_id="CWE-78",
            category_name="Command Injection",
            description="OS command injection",
            priority="HIGH",
        )
        d = gap.to_dict()
        assert d["category_id"] == "CWE-78"
        assert d["category_name"] == "Command Injection"
        assert d["description"] == "OS command injection"
        assert d["priority"] == "HIGH"

    def test_all_fields_present(self):
        gap = CoverageGap(
            category_id="X",
            category_name="Y",
            description="Z",
            priority="LOW",
        )
        d = gap.to_dict()
        assert len(d) == 4


# ---------------------------------------------------------------------------
# CoverageMatrix dataclass
# ---------------------------------------------------------------------------

class TestCoverageMatrix:
    """Tests for CoverageMatrix dataclass."""

    def test_to_dict(self):
        matrix = CoverageMatrix(
            framework_name="Test Framework",
            total_categories=10,
            covered_categories=7,
            coverage_pct=70.0,
            gaps=[
                CoverageGap("G1", "Gap1", "desc1", "HIGH"),
            ],
            covered=[
                {"category_id": "C1", "category_name": "Cat1", "rules_matched": ["R1"]},
            ],
        )
        d = matrix.to_dict()
        assert d["framework_name"] == "Test Framework"
        assert d["total_categories"] == 10
        assert d["covered_categories"] == 7
        assert d["coverage_pct"] == 70.0
        assert len(d["gaps"]) == 1
        assert len(d["covered"]) == 1

    def test_coverage_pct_rounding(self):
        matrix = CoverageMatrix(
            framework_name="Test",
            total_categories=3,
            covered_categories=1,
            coverage_pct=33.333,
        )
        d = matrix.to_dict()
        assert d["coverage_pct"] == 33.3

    def test_empty_matrix(self):
        matrix = CoverageMatrix(
            framework_name="Empty",
            total_categories=0,
            covered_categories=0,
            coverage_pct=0.0,
        )
        d = matrix.to_dict()
        assert d["gaps"] == []
        assert d["covered"] == []


# ---------------------------------------------------------------------------
# Framework definitions
# ---------------------------------------------------------------------------

class TestFrameworkDefinitions:
    """Tests for built-in framework category definitions."""

    def test_owasp_has_10_categories(self):
        assert len(_OWASP_AGENTIC_TOP10) == 10

    def test_owasp_category_fields(self):
        for cat in _OWASP_AGENTIC_TOP10:
            assert "id" in cat
            assert "name" in cat
            assert "description" in cat
            assert "rule_ids" in cat
            assert isinstance(cat["rule_ids"], list)

    def test_owasp_unique_ids(self):
        ids = [cat["id"] for cat in _OWASP_AGENTIC_TOP10]
        assert len(ids) == len(set(ids))

    def test_cwe_has_categories(self):
        assert len(_CWE_CATEGORIES) >= 15

    def test_cwe_category_fields(self):
        for cat in _CWE_CATEGORIES:
            assert "id" in cat
            assert "name" in cat
            assert "description" in cat
            assert "rule_ids" in cat

    def test_cwe_unique_ids(self):
        ids = [cat["id"] for cat in _CWE_CATEGORIES]
        assert len(ids) == len(set(ids))

    def test_mitre_has_categories(self):
        assert len(_MITRE_ATTACK_AI) >= 6

    def test_mitre_category_fields(self):
        for cat in _MITRE_ATTACK_AI:
            assert "id" in cat
            assert "name" in cat
            assert "description" in cat
            assert "rule_ids" in cat

    def test_mitre_unique_ids(self):
        ids = [cat["id"] for cat in _MITRE_ATTACK_AI]
        assert len(ids) == len(set(ids))


# ---------------------------------------------------------------------------
# _get_all_rule_ids
# ---------------------------------------------------------------------------

class TestGetAllRuleIds:
    """Tests for collecting all rule IDs."""

    def test_returns_set(self):
        ids = _get_all_rule_ids()
        assert isinstance(ids, set)

    def test_has_python_rules(self):
        ids = _get_all_rule_ids()
        # Should have at least some known Python rules
        assert "CMD-001" in ids or "PATH-001" in ids or len(ids) > 0

    def test_has_rules(self):
        ids = _get_all_rule_ids()
        assert len(ids) > 10  # Should have many rules


# ---------------------------------------------------------------------------
# analyze_owasp_coverage
# ---------------------------------------------------------------------------

class TestAnalyzeOwaspCoverage:
    """Tests for OWASP Agentic Top 10 coverage analysis."""

    def test_returns_coverage_matrix(self):
        matrix = analyze_owasp_coverage()
        assert isinstance(matrix, CoverageMatrix)

    def test_framework_name(self):
        matrix = analyze_owasp_coverage()
        assert matrix.framework_name == "OWASP Agentic Top 10"

    def test_total_is_10(self):
        matrix = analyze_owasp_coverage()
        assert matrix.total_categories == 10

    def test_coverage_pct_range(self):
        matrix = analyze_owasp_coverage()
        assert 0 <= matrix.coverage_pct <= 100

    def test_covered_plus_gaps_equals_total(self):
        matrix = analyze_owasp_coverage()
        assert matrix.covered_categories + len(matrix.gaps) == matrix.total_categories

    def test_has_some_coverage(self):
        matrix = analyze_owasp_coverage()
        assert matrix.covered_categories > 0

    def test_covered_have_rules(self):
        matrix = analyze_owasp_coverage()
        for c in matrix.covered:
            assert len(c["rules_matched"]) > 0


# ---------------------------------------------------------------------------
# analyze_cwe_coverage
# ---------------------------------------------------------------------------

class TestAnalyzeCweCoverage:
    """Tests for CWE coverage analysis."""

    def test_returns_coverage_matrix(self):
        matrix = analyze_cwe_coverage()
        assert isinstance(matrix, CoverageMatrix)

    def test_framework_name(self):
        matrix = analyze_cwe_coverage()
        assert matrix.framework_name == "CWE Categories"

    def test_total_categories(self):
        matrix = analyze_cwe_coverage()
        assert matrix.total_categories >= 15

    def test_coverage_pct_range(self):
        matrix = analyze_cwe_coverage()
        assert 0 <= matrix.coverage_pct <= 100

    def test_has_some_coverage(self):
        matrix = analyze_cwe_coverage()
        assert matrix.covered_categories > 0

    def test_gaps_have_priority(self):
        matrix = analyze_cwe_coverage()
        for gap in matrix.gaps:
            assert gap.priority in ("HIGH", "MEDIUM", "LOW")


# ---------------------------------------------------------------------------
# analyze_mitre_coverage
# ---------------------------------------------------------------------------

class TestAnalyzeMitreCoverage:
    """Tests for MITRE ATT&CK for AI coverage analysis."""

    def test_returns_coverage_matrix(self):
        matrix = analyze_mitre_coverage()
        assert isinstance(matrix, CoverageMatrix)

    def test_framework_name(self):
        matrix = analyze_mitre_coverage()
        assert matrix.framework_name == "MITRE ATT&CK for AI"

    def test_total_categories(self):
        matrix = analyze_mitre_coverage()
        assert matrix.total_categories >= 6

    def test_coverage_pct_range(self):
        matrix = analyze_mitre_coverage()
        assert 0 <= matrix.coverage_pct <= 100

    def test_has_gaps(self):
        # MITRE has some categories without rules
        matrix = analyze_mitre_coverage()
        assert len(matrix.gaps) >= 1


# ---------------------------------------------------------------------------
# _analyze_framework (generic)
# ---------------------------------------------------------------------------

class TestAnalyzeFramework:
    """Tests for generic framework analysis."""

    def test_empty_categories(self):
        matrix = _analyze_framework("Empty", [])
        assert matrix.total_categories == 0
        assert matrix.covered_categories == 0
        assert matrix.coverage_pct == 0.0

    def test_all_covered(self):
        # Use categories that map to known rules
        categories = [
            {"id": "T1", "name": "Test1", "description": "d1", "rule_ids": ["CMD-001"]},
        ]
        matrix = _analyze_framework("Test", categories)
        # CMD-001 should exist
        assert matrix.total_categories == 1

    def test_none_covered(self):
        categories = [
            {"id": "T1", "name": "Test1", "description": "d1", "rule_ids": ["NONEXISTENT-999"]},
        ]
        matrix = _analyze_framework("Test", categories)
        assert matrix.covered_categories == 0
        assert len(matrix.gaps) == 1


# ---------------------------------------------------------------------------
# suggest_new_rules
# ---------------------------------------------------------------------------

class TestSuggestNewRules:
    """Tests for rule suggestion generation."""

    def test_empty_gaps(self):
        suggestions = suggest_new_rules([])
        assert suggestions == []

    def test_suggestion_fields(self):
        gaps = [
            CoverageGap("CWE-99", "Test Gap", "A test gap", "HIGH"),
        ]
        suggestions = suggest_new_rules(gaps)
        assert len(suggestions) == 1
        s = suggestions[0]
        assert s["category_id"] == "CWE-99"
        assert s["category_name"] == "Test Gap"
        assert s["priority"] == "HIGH"
        assert "suggested_rule_id" in s
        assert "description" in s

    def test_effort_mapping(self):
        gaps = [
            CoverageGap("A", "High Priority", "d", "HIGH"),
            CoverageGap("B", "Low Priority", "d", "LOW"),
        ]
        suggestions = suggest_new_rules(gaps)
        high_suggestion = [s for s in suggestions if s["category_id"] == "A"][0]
        low_suggestion = [s for s in suggestions if s["category_id"] == "B"][0]
        assert high_suggestion["effort"] == "MEDIUM"
        assert low_suggestion["effort"] == "LOW"

    def test_multiple_gaps(self):
        gaps = [
            CoverageGap("A", "Gap A", "d", "HIGH"),
            CoverageGap("B", "Gap B", "d", "MEDIUM"),
            CoverageGap("C", "Gap C", "d", "LOW"),
        ]
        suggestions = suggest_new_rules(gaps)
        assert len(suggestions) == 3


# ---------------------------------------------------------------------------
# generate_coverage_html
# ---------------------------------------------------------------------------

class TestGenerateCoverageHtml:
    """Tests for HTML heatmap dashboard generation."""

    def test_valid_html(self):
        matrices = [analyze_owasp_coverage()]
        html = generate_coverage_html(matrices)
        assert "<!DOCTYPE html>" in html
        assert "mcpaudit" in html
        assert "Rule Coverage Analysis" in html

    def test_contains_framework_name(self):
        matrices = [analyze_owasp_coverage()]
        html = generate_coverage_html(matrices)
        assert "OWASP Agentic Top 10" in html

    def test_multiple_frameworks(self):
        matrices = [
            analyze_owasp_coverage(),
            analyze_cwe_coverage(),
            analyze_mitre_coverage(),
        ]
        html = generate_coverage_html(matrices)
        assert "OWASP" in html
        assert "CWE" in html
        assert "MITRE" in html

    def test_coverage_percentage_shown(self):
        matrices = [analyze_owasp_coverage()]
        html = generate_coverage_html(matrices)
        assert "%" in html

    def test_empty_matrices(self):
        html = generate_coverage_html([])
        assert "<!DOCTYPE html>" in html


# ---------------------------------------------------------------------------
# format_coverage_text
# ---------------------------------------------------------------------------

class TestFormatCoverageText:
    """Tests for text coverage formatting."""

    def test_header(self):
        matrices = [analyze_owasp_coverage()]
        text = format_coverage_text(matrices)
        assert "Rule Coverage Analysis" in text

    def test_coverage_bar(self):
        matrices = [analyze_owasp_coverage()]
        text = format_coverage_text(matrices)
        assert "Coverage:" in text
        assert "%" in text

    def test_covered_items(self):
        matrices = [analyze_owasp_coverage()]
        text = format_coverage_text(matrices)
        # Should show covered items with [+]
        assert "[+]" in text or "Gaps:" in text

    def test_gap_items(self):
        matrices = [analyze_mitre_coverage()]
        text = format_coverage_text(matrices)
        # MITRE should have gaps
        if analyze_mitre_coverage().gaps:
            assert "[-]" in text

    def test_multiple_frameworks(self):
        matrices = [
            analyze_owasp_coverage(),
            analyze_cwe_coverage(),
        ]
        text = format_coverage_text(matrices)
        assert "OWASP" in text
        assert "CWE" in text


# ---------------------------------------------------------------------------
# format_coverage_json
# ---------------------------------------------------------------------------

class TestFormatCoverageJson:
    """Tests for JSON coverage formatting."""

    def test_valid_json(self):
        matrices = [analyze_owasp_coverage()]
        output = format_coverage_json(matrices)
        data = json.loads(output)
        assert "coverage_analysis" in data

    def test_framework_data(self):
        matrices = [analyze_owasp_coverage()]
        output = format_coverage_json(matrices)
        data = json.loads(output)
        assert len(data["coverage_analysis"]) == 1
        assert data["coverage_analysis"][0]["framework_name"] == "OWASP Agentic Top 10"

    def test_multiple_frameworks_json(self):
        matrices = [
            analyze_owasp_coverage(),
            analyze_cwe_coverage(),
            analyze_mitre_coverage(),
        ]
        output = format_coverage_json(matrices)
        data = json.loads(output)
        assert len(data["coverage_analysis"]) == 3

    def test_empty_json(self):
        output = format_coverage_json([])
        data = json.loads(output)
        assert data["coverage_analysis"] == []


# ---------------------------------------------------------------------------
# CLI integration
# ---------------------------------------------------------------------------

class TestCoverageCLI:
    """Tests for coverage CLI subcommand."""

    def test_cli_text_format(self, capsys):
        from mcpaudit.__main__ import main
        exit_code = main(["coverage"])
        captured = capsys.readouterr()
        assert "Coverage" in captured.out
        assert exit_code == 0

    def test_cli_json_format(self, capsys):
        from mcpaudit.__main__ import main
        exit_code = main(["coverage", "--format", "json"])
        captured = capsys.readouterr()
        data = json.loads(captured.out)
        assert "coverage_analysis" in data
        assert exit_code == 0

    def test_cli_html_format(self, capsys):
        from mcpaudit.__main__ import main
        exit_code = main(["coverage", "--format", "html"])
        captured = capsys.readouterr()
        assert "<!DOCTYPE html>" in captured.out
        assert exit_code == 0

    def test_cli_output_file(self, tmp_path):
        out = tmp_path / "coverage.html"
        from mcpaudit.__main__ import main
        exit_code = main(["coverage", "--format", "html", "--output", str(out)])
        assert out.exists()
        assert exit_code == 0
        content = out.read_text()
        assert "<!DOCTYPE html>" in content



# ---------------------------------------------------------------------------
# Additional CoverageGap tests
# ---------------------------------------------------------------------------

class TestCoverageGapAdditional:
    """Additional CoverageGap tests."""

    def test_gap_priority_values(self):
        for priority in ("HIGH", "MEDIUM", "LOW"):
            gap = CoverageGap("id", "name", "desc", priority)
            assert gap.priority == priority

    def test_gap_to_dict_keys(self):
        gap = CoverageGap("CWE-99", "Test", "Description", "MEDIUM")
        d = gap.to_dict()
        assert set(d.keys()) == {"category_id", "category_name", "description", "priority"}


# ---------------------------------------------------------------------------
# Additional CoverageMatrix tests
# ---------------------------------------------------------------------------

class TestCoverageMatrixAdditional:
    """Additional CoverageMatrix tests."""

    def test_full_coverage(self):
        matrix = CoverageMatrix(
            framework_name="Full",
            total_categories=5,
            covered_categories=5,
            coverage_pct=100.0,
        )
        assert matrix.coverage_pct == 100.0
        assert len(matrix.gaps) == 0

    def test_zero_coverage(self):
        matrix = CoverageMatrix(
            framework_name="Empty",
            total_categories=5,
            covered_categories=0,
            coverage_pct=0.0,
            gaps=[
                CoverageGap("A", "Gap A", "d", "HIGH"),
                CoverageGap("B", "Gap B", "d", "HIGH"),
            ],
        )
        assert matrix.coverage_pct == 0.0
        assert len(matrix.gaps) == 2

    def test_matrix_to_dict_gaps_serialized(self):
        matrix = CoverageMatrix(
            framework_name="Test",
            total_categories=2,
            covered_categories=1,
            coverage_pct=50.0,
            gaps=[CoverageGap("G1", "Gap", "desc", "MEDIUM")],
            covered=[{"category_id": "C1", "category_name": "Covered", "rules_matched": ["R1"]}],
        )
        d = matrix.to_dict()
        assert d["gaps"][0]["category_id"] == "G1"
        assert d["covered"][0]["rules_matched"] == ["R1"]

    def test_default_empty_lists(self):
        matrix = CoverageMatrix(
            framework_name="Defaults",
            total_categories=0,
            covered_categories=0,
            coverage_pct=0.0,
        )
        assert matrix.gaps == []
        assert matrix.covered == []


# ---------------------------------------------------------------------------
# Additional framework definition tests
# ---------------------------------------------------------------------------

class TestFrameworkDefinitionsAdditional:
    """Additional framework definition structure tests."""

    def test_owasp_ids_start_with_owasp(self):
        for cat in _OWASP_AGENTIC_TOP10:
            assert cat["id"].startswith("OWASP-A")

    def test_owasp_all_have_rule_ids(self):
        for cat in _OWASP_AGENTIC_TOP10:
            assert len(cat["rule_ids"]) > 0

    def test_cwe_ids_start_with_cwe(self):
        for cat in _CWE_CATEGORIES:
            assert cat["id"].startswith("CWE-")

    def test_cwe_all_have_rule_ids(self):
        for cat in _CWE_CATEGORIES:
            assert len(cat["rule_ids"]) > 0

    def test_mitre_ids_start_with_aml(self):
        for cat in _MITRE_ATTACK_AI:
            assert cat["id"].startswith("AML.")

    def test_mitre_some_have_no_rules(self):
        # Categories like Model Evasion and Supply Chain should have no rules
        empty_cats = [c for c in _MITRE_ATTACK_AI if len(c["rule_ids"]) == 0]
        assert len(empty_cats) >= 1

    def test_owasp_descriptions_non_empty(self):
        for cat in _OWASP_AGENTIC_TOP10:
            assert len(cat["description"]) > 0

    def test_cwe_descriptions_non_empty(self):
        for cat in _CWE_CATEGORIES:
            assert len(cat["description"]) > 0


# ---------------------------------------------------------------------------
# Additional _get_all_rule_ids tests
# ---------------------------------------------------------------------------

class TestGetAllRuleIdsAdditional:
    """Additional tests for rule ID collection."""

    def test_includes_ts_rules(self):
        ids = _get_all_rule_ids()
        ts_rules = [r for r in ids if r.startswith("TS-")]
        assert len(ts_rules) > 0

    def test_includes_go_rules(self):
        ids = _get_all_rule_ids()
        go_rules = [r for r in ids if r.startswith("GO-")]
        assert len(go_rules) > 0

    def test_no_empty_rule_ids(self):
        ids = _get_all_rule_ids()
        assert "" not in ids

    def test_all_rule_ids_are_strings(self):
        ids = _get_all_rule_ids()
        for rule_id in ids:
            assert isinstance(rule_id, str)


# ---------------------------------------------------------------------------
# Additional _analyze_framework tests
# ---------------------------------------------------------------------------

class TestAnalyzeFrameworkAdditional:
    """Additional tests for generic framework analysis."""

    def test_partial_rule_match(self):
        # Category has multiple expected rules, only some exist
        categories = [
            {
                "id": "T1", "name": "Test",
                "description": "desc",
                "rule_ids": ["CMD-001", "NONEXISTENT-001"],
            },
        ]
        matrix = _analyze_framework("Test", categories)
        # Should still be covered if at least one rule matches
        assert matrix.covered_categories == 1

    def test_gap_priority_high_for_empty_rules(self):
        categories = [
            {"id": "T1", "name": "NoRules", "description": "d", "rule_ids": []},
        ]
        matrix = _analyze_framework("Test", categories)
        assert len(matrix.gaps) == 1
        assert matrix.gaps[0].priority == "HIGH"

    def test_gap_priority_medium_for_single_rule(self):
        categories = [
            {"id": "T1", "name": "OneRule", "description": "d", "rule_ids": ["NONEXISTENT-001"]},
        ]
        matrix = _analyze_framework("Test", categories)
        assert len(matrix.gaps) == 1
        assert matrix.gaps[0].priority == "MEDIUM"

    def test_gap_priority_high_for_multiple_rules(self):
        categories = [
            {"id": "T1", "name": "TwoRules", "description": "d", "rule_ids": ["NOPE-1", "NOPE-2"]},
        ]
        matrix = _analyze_framework("Test", categories)
        assert len(matrix.gaps) == 1
        assert matrix.gaps[0].priority == "HIGH"

    def test_coverage_pct_calculation(self):
        categories = [
            {"id": "T1", "name": "A", "description": "d", "rule_ids": ["CMD-001"]},
            {"id": "T2", "name": "B", "description": "d", "rule_ids": ["NOPE-001"]},
        ]
        matrix = _analyze_framework("Test", categories)
        # CMD-001 exists, NOPE-001 does not = 50%
        assert matrix.coverage_pct == 50.0

    def test_covered_list_has_rules_matched(self):
        categories = [
            {"id": "T1", "name": "A", "description": "d", "rule_ids": ["CMD-001", "CMD-002"]},
        ]
        matrix = _analyze_framework("Test", categories)
        if matrix.covered:
            assert "rules_matched" in matrix.covered[0]
            assert len(matrix.covered[0]["rules_matched"]) >= 1


# ---------------------------------------------------------------------------
# Additional suggest_new_rules tests
# ---------------------------------------------------------------------------

class TestSuggestNewRulesAdditional:
    """Additional suggestion tests."""

    def test_suggested_rule_id_format(self):
        gaps = [CoverageGap("CWE-78", "CMD Injection", "desc", "HIGH")]
        suggestions = suggest_new_rules(gaps)
        assert suggestions[0]["suggested_rule_id"].startswith("NEW-")

    def test_suggestion_description_includes_gap(self):
        gaps = [CoverageGap("X", "TestGap", "some description", "MEDIUM")]
        suggestions = suggest_new_rules(gaps)
        assert "some description" in suggestions[0]["description"]

    def test_effort_medium_for_medium_priority(self):
        gaps = [CoverageGap("X", "Y", "d", "MEDIUM")]
        suggestions = suggest_new_rules(gaps)
        assert suggestions[0]["effort"] == "LOW"


# ---------------------------------------------------------------------------
# Additional format tests
# ---------------------------------------------------------------------------

class TestFormatAdditional:
    """Additional formatting tests."""

    def test_text_format_separator(self):
        matrices = [analyze_owasp_coverage()]
        text = format_coverage_text(matrices)
        assert "=" * 60 in text

    def test_text_format_progress_bar(self):
        matrices = [analyze_owasp_coverage()]
        text = format_coverage_text(matrices)
        # Should have a progress bar with # and .
        assert "[" in text and "]" in text

    def test_html_high_coverage_green(self):
        matrix = CoverageMatrix(
            framework_name="Green",
            total_categories=10,
            covered_categories=9,
            coverage_pct=90.0,
            covered=[{"category_id": "C1", "category_name": "Cat", "rules_matched": ["R1"]}],
        )
        html = generate_coverage_html([matrix])
        assert "#2ecc71" in html  # green color

    def test_html_medium_coverage_orange(self):
        matrix = CoverageMatrix(
            framework_name="Orange",
            total_categories=10,
            covered_categories=7,
            coverage_pct=70.0,
            covered=[{"category_id": "C1", "category_name": "Cat", "rules_matched": ["R1"]}],
        )
        html = generate_coverage_html([matrix])
        assert "#f39c12" in html  # orange color

    def test_html_low_coverage_red(self):
        matrix = CoverageMatrix(
            framework_name="Red",
            total_categories=10,
            covered_categories=4,
            coverage_pct=40.0,
            gaps=[CoverageGap("G1", "Gap", "d", "HIGH")],
        )
        html = generate_coverage_html([matrix])
        assert "#e74c3c" in html  # red color

    def test_html_no_gaps_message(self):
        matrix = CoverageMatrix(
            framework_name="Perfect",
            total_categories=1,
            covered_categories=1,
            coverage_pct=100.0,
            covered=[{"category_id": "C1", "category_name": "Cat", "rules_matched": ["R1"]}],
        )
        html = generate_coverage_html([matrix])
        assert "No gaps found" in html

    def test_json_format_coverage_pct(self):
        matrices = [analyze_owasp_coverage()]
        output = format_coverage_json(matrices)
        data = json.loads(output)
        pct = data["coverage_analysis"][0]["coverage_pct"]
        assert isinstance(pct, float)

    def test_json_format_has_gaps_array(self):
        matrices = [analyze_mitre_coverage()]
        output = format_coverage_json(matrices)
        data = json.loads(output)
        assert "gaps" in data["coverage_analysis"][0]
