"""Tests for Security Training Generator v2 (mcpaudit.training_v2).

Tests training material generation, quiz generation, curriculum planning,
content builders, and format helpers.
"""

from __future__ import annotations

import json

import pytest

from mcpaudit.training_v2 import (
    Answer,
    Curriculum,
    CurriculumModule,
    Question,
    Quiz,
    TrainingLevel,
    TrainingMaterial,
    TrainingModule,
    TrainingModuleType,
    TrainingSection,
    format_curriculum_json,
    format_curriculum_text,
    format_quiz_json,
    format_quiz_text,
    format_training_html,
    format_training_json,
    format_training_text,
    generate_curriculum,
    generate_quiz,
    generate_training,
    get_module_types,
    _classify_finding_module,
    _severity_to_level,
    _build_awareness_sections,
    _build_secure_coding_sections,
    _build_mcp_security_sections,
    _build_owasp_sections,
    _build_tool_safety_sections,
    _build_module,
    _build_finding_question,
    _question_id,
)


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _make_finding(
    rule_id: str = "CMD-001",
    severity: str = "HIGH",
    title: str = "Test finding",
    description: str = "Test description",
    recommendation: str = "Fix it",
) -> dict:
    return {
        "rule_id": rule_id,
        "severity": severity,
        "title": title,
        "description": description,
        "file": "server.py",
        "line": 10,
        "code_snippet": "dangerous_code()",
        "recommendation": recommendation,
    }


# ===========================================================================
# TrainingLevel Enum
# ===========================================================================

class TestTrainingLevel:
    def test_all_values(self) -> None:
        assert len(TrainingLevel) == 3

    def test_from_string_beginner(self) -> None:
        assert TrainingLevel.from_string("beginner") == TrainingLevel.BEGINNER

    def test_from_string_case_insensitive(self) -> None:
        assert TrainingLevel.from_string("INTERMEDIATE") == TrainingLevel.INTERMEDIATE

    def test_from_string_advanced(self) -> None:
        assert TrainingLevel.from_string("advanced") == TrainingLevel.ADVANCED

    def test_from_string_invalid(self) -> None:
        with pytest.raises(ValueError, match="Unknown training level"):
            TrainingLevel.from_string("expert")

    def test_label(self) -> None:
        assert TrainingLevel.BEGINNER.label == "Beginner"
        assert TrainingLevel.INTERMEDIATE.label == "Intermediate"
        assert TrainingLevel.ADVANCED.label == "Advanced"

    def test_depth(self) -> None:
        assert TrainingLevel.BEGINNER.depth == 1
        assert TrainingLevel.INTERMEDIATE.depth == 2
        assert TrainingLevel.ADVANCED.depth == 3


# ===========================================================================
# TrainingModuleType Enum
# ===========================================================================

class TestTrainingModuleType:
    def test_all_values(self) -> None:
        assert len(TrainingModuleType) == 5

    def test_from_string_basic(self) -> None:
        assert TrainingModuleType.from_string("awareness") == TrainingModuleType.AWARENESS

    def test_from_string_with_hyphens(self) -> None:
        assert TrainingModuleType.from_string("secure-coding") == TrainingModuleType.SECURE_CODING

    def test_from_string_with_spaces(self) -> None:
        assert TrainingModuleType.from_string("mcp security") == TrainingModuleType.MCP_SECURITY

    def test_from_string_invalid(self) -> None:
        with pytest.raises(ValueError, match="Unknown module type"):
            TrainingModuleType.from_string("nonexistent")

    def test_label(self) -> None:
        assert TrainingModuleType.AWARENESS.label == "Security Awareness"
        assert TrainingModuleType.SECURE_CODING.label == "Secure Coding Practices"

    def test_estimated_minutes(self) -> None:
        assert TrainingModuleType.AWARENESS.estimated_minutes == 30
        assert TrainingModuleType.SECURE_CODING.estimated_minutes == 60

    def test_get_module_types(self) -> None:
        types = get_module_types()
        assert len(types) == 5
        assert "awareness" in types


# ===========================================================================
# TrainingSection
# ===========================================================================

class TestTrainingSection:
    def test_creation(self) -> None:
        sec = TrainingSection(title="Test", content="Content")
        assert sec.title == "Test"
        assert sec.code_example == ""

    def test_to_dict(self) -> None:
        sec = TrainingSection(
            title="Test",
            content="Content",
            bad_example="bad code",
            good_example="good code",
            key_points=["Point 1"],
        )
        d = sec.to_dict()
        assert d["title"] == "Test"
        assert d["bad_example"] == "bad code"
        assert len(d["key_points"]) == 1


# ===========================================================================
# TrainingModule
# ===========================================================================

class TestTrainingModule:
    def test_creation(self) -> None:
        mod = TrainingModule(
            module_type=TrainingModuleType.AWARENESS,
            title="Test Module",
            description="Desc",
        )
        assert mod.level == TrainingLevel.BEGINNER
        assert mod.estimated_minutes == 30

    def test_to_dict(self) -> None:
        mod = TrainingModule(
            module_type=TrainingModuleType.SECURE_CODING,
            title="Secure Coding",
            description="Learn secure coding",
            sections=[TrainingSection("S1", "C1")],
            objectives=["O1", "O2"],
        )
        d = mod.to_dict()
        assert d["module_type"] == "secure_coding"
        assert d["total_sections"] == 1
        assert len(d["objectives"]) == 2


# ===========================================================================
# Answer & Question
# ===========================================================================

class TestAnswer:
    def test_creation(self) -> None:
        a = Answer(text="Option A", is_correct=True, explanation="Because")
        assert a.is_correct
        assert a.explanation == "Because"

    def test_to_dict(self) -> None:
        a = Answer(text="X", is_correct=False)
        d = a.to_dict()
        assert d["text"] == "X"
        assert d["is_correct"] is False


class TestQuestion:
    def test_creation(self) -> None:
        q = Question(question_id="Q1", text="What?")
        assert q.question_id == "Q1"
        assert q.answers == []

    def test_correct_answer(self) -> None:
        q = Question(
            question_id="Q1",
            text="What?",
            answers=[
                Answer("A", False),
                Answer("B", True),
                Answer("C", False),
            ],
        )
        correct = q.correct_answer
        assert correct is not None
        assert correct.text == "B"

    def test_no_correct_answer(self) -> None:
        q = Question(
            question_id="Q1",
            text="What?",
            answers=[Answer("A", False)],
        )
        assert q.correct_answer is None

    def test_to_dict(self) -> None:
        q = Question(
            question_id="Q1",
            text="Test?",
            difficulty=TrainingLevel.ADVANCED,
            category="test",
            related_rule_id="CMD-001",
        )
        d = q.to_dict()
        assert d["difficulty"] == "advanced"
        assert d["related_rule_id"] == "CMD-001"


# ===========================================================================
# Quiz
# ===========================================================================

class TestQuiz:
    def test_creation(self) -> None:
        quiz = Quiz(title="Test Quiz")
        assert quiz.total_questions == 0
        assert quiz.passing_score == 0.7
        assert quiz.created_at  # auto-set

    def test_to_dict(self) -> None:
        quiz = Quiz(
            title="Test",
            questions=[
                Question("Q1", "Q?", [Answer("A", True)]),
                Question("Q2", "Q?", [Answer("B", True)]),
            ],
            level=TrainingLevel.INTERMEDIATE,
        )
        d = quiz.to_dict()
        assert d["total_questions"] == 2
        assert d["level"] == "intermediate"


# ===========================================================================
# CurriculumModule & Curriculum
# ===========================================================================

class TestCurriculumModule:
    def test_creation(self) -> None:
        cm = CurriculumModule(
            module_type=TrainingModuleType.AWARENESS,
            title="Awareness",
            level=TrainingLevel.BEGINNER,
            estimated_minutes=30,
            order=1,
        )
        assert cm.is_required

    def test_to_dict(self) -> None:
        cm = CurriculumModule(
            module_type=TrainingModuleType.OWASP,
            title="OWASP",
            level=TrainingLevel.ADVANCED,
            estimated_minutes=60,
            order=3,
            is_required=False,
        )
        d = cm.to_dict()
        assert d["module_type"] == "owasp"
        assert d["is_required"] is False


class TestCurriculum:
    def test_creation(self) -> None:
        cur = Curriculum(title="Test", level=TrainingLevel.BEGINNER)
        assert cur.total_hours == 0.0
        assert cur.created_at

    def test_total_hours(self) -> None:
        modules = [
            CurriculumModule(TrainingModuleType.AWARENESS, "A",
                             TrainingLevel.BEGINNER, 60, 1),
            CurriculumModule(TrainingModuleType.SECURE_CODING, "B",
                             TrainingLevel.BEGINNER, 120, 2),
        ]
        cur = Curriculum(title="Test", level=TrainingLevel.BEGINNER,
                         modules=modules)
        assert cur.total_hours == 3.0

    def test_required_modules(self) -> None:
        modules = [
            CurriculumModule(TrainingModuleType.AWARENESS, "A",
                             TrainingLevel.BEGINNER, 30, 1, is_required=True),
            CurriculumModule(TrainingModuleType.OWASP, "O",
                             TrainingLevel.BEGINNER, 30, 2, is_required=False),
        ]
        cur = Curriculum(title="Test", level=TrainingLevel.BEGINNER,
                         modules=modules)
        assert len(cur.required_modules) == 1

    def test_to_dict(self) -> None:
        cur = Curriculum(
            title="Test",
            level=TrainingLevel.INTERMEDIATE,
            description="Test curriculum",
        )
        d = cur.to_dict()
        assert d["level"] == "intermediate"
        assert "total_hours" in d


# ===========================================================================
# TrainingMaterial
# ===========================================================================

class TestTrainingMaterial:
    def test_creation(self) -> None:
        mat = TrainingMaterial(title="Test Material")
        assert mat.total_sections == 0
        assert mat.estimated_minutes == 0
        assert mat.created_at

    def test_total_sections(self) -> None:
        mod = TrainingModule(
            module_type=TrainingModuleType.AWARENESS,
            title="A",
            description="D",
            sections=[
                TrainingSection("S1", "C1"),
                TrainingSection("S2", "C2"),
            ],
        )
        mat = TrainingMaterial(title="Test", modules=[mod])
        assert mat.total_sections == 2

    def test_to_dict(self) -> None:
        mat = TrainingMaterial(
            title="Test",
            level=TrainingLevel.ADVANCED,
            findings_count=5,
        )
        d = mat.to_dict()
        assert d["level"] == "advanced"
        assert d["findings_count"] == 5


# ===========================================================================
# _classify_finding_module
# ===========================================================================

class TestClassifyFindingModule:
    def test_cmd_prefix(self) -> None:
        assert _classify_finding_module("CMD-001") == TrainingModuleType.SECURE_CODING

    def test_leak_prefix(self) -> None:
        assert _classify_finding_module("LEAK-001") == TrainingModuleType.AWARENESS

    def test_mcp_prefix(self) -> None:
        assert _classify_finding_module("MCP-001") == TrainingModuleType.MCP_SECURITY

    def test_tool_prefix(self) -> None:
        assert _classify_finding_module("TOOL-001") == TrainingModuleType.TOOL_SAFETY

    def test_owasp_prefix(self) -> None:
        assert _classify_finding_module("OWASP-001") == TrainingModuleType.OWASP

    def test_dep_prefix(self) -> None:
        assert _classify_finding_module("DEP-001") == TrainingModuleType.OWASP

    def test_unknown_prefix(self) -> None:
        result = _classify_finding_module("UNKNOWN-001")
        assert result == TrainingModuleType.AWARENESS


# ===========================================================================
# _severity_to_level
# ===========================================================================

class TestSeverityToLevel:
    def test_critical(self) -> None:
        assert _severity_to_level("CRITICAL") == TrainingLevel.ADVANCED

    def test_high(self) -> None:
        assert _severity_to_level("HIGH") == TrainingLevel.ADVANCED

    def test_medium(self) -> None:
        assert _severity_to_level("MEDIUM") == TrainingLevel.INTERMEDIATE

    def test_low(self) -> None:
        assert _severity_to_level("LOW") == TrainingLevel.BEGINNER

    def test_info(self) -> None:
        assert _severity_to_level("INFO") == TrainingLevel.BEGINNER


# ===========================================================================
# Section Builders
# ===========================================================================

class TestSectionBuilders:
    def test_awareness_beginner(self) -> None:
        sections = _build_awareness_sections([], TrainingLevel.BEGINNER)
        assert len(sections) >= 2
        assert any("Credential" in s.title for s in sections)

    def test_awareness_advanced(self) -> None:
        sections = _build_awareness_sections([], TrainingLevel.ADVANCED)
        assert len(sections) >= 4
        assert any("Threat" in s.title for s in sections)

    def test_awareness_with_findings(self) -> None:
        findings = [_make_finding("LEAK-001")]
        sections = _build_awareness_sections(findings, TrainingLevel.BEGINNER)
        assert any("Finding" in s.title for s in sections)

    def test_secure_coding_beginner(self) -> None:
        sections = _build_secure_coding_sections([], TrainingLevel.BEGINNER)
        assert len(sections) >= 2
        assert any("Input" in s.title for s in sections)

    def test_secure_coding_advanced(self) -> None:
        sections = _build_secure_coding_sections([], TrainingLevel.ADVANCED)
        assert len(sections) >= 4

    def test_secure_coding_with_findings(self) -> None:
        findings = [_make_finding("CMD-001")]
        sections = _build_secure_coding_sections(findings, TrainingLevel.BEGINNER)
        assert any("Finding" in s.title for s in sections)

    def test_mcp_security_beginner(self) -> None:
        sections = _build_mcp_security_sections([], TrainingLevel.BEGINNER)
        assert len(sections) >= 2

    def test_mcp_security_advanced(self) -> None:
        sections = _build_mcp_security_sections([], TrainingLevel.ADVANCED)
        assert len(sections) >= 4

    def test_owasp_beginner(self) -> None:
        sections = _build_owasp_sections([], TrainingLevel.BEGINNER)
        assert len(sections) >= 1

    def test_owasp_advanced(self) -> None:
        sections = _build_owasp_sections([], TrainingLevel.ADVANCED)
        assert len(sections) >= 3

    def test_tool_safety_beginner(self) -> None:
        sections = _build_tool_safety_sections([], TrainingLevel.BEGINNER)
        assert len(sections) >= 2

    def test_tool_safety_intermediate(self) -> None:
        sections = _build_tool_safety_sections([], TrainingLevel.INTERMEDIATE)
        assert len(sections) >= 3


# ===========================================================================
# _build_module
# ===========================================================================

class TestBuildModule:
    def test_builds_awareness(self) -> None:
        mod = _build_module(TrainingModuleType.AWARENESS, [], TrainingLevel.BEGINNER)
        assert mod.module_type == TrainingModuleType.AWARENESS
        assert len(mod.sections) > 0

    def test_builds_secure_coding(self) -> None:
        mod = _build_module(TrainingModuleType.SECURE_CODING, [], TrainingLevel.INTERMEDIATE)
        assert mod.estimated_minutes > 60  # 60 * 1.3

    def test_builds_all_types(self) -> None:
        for mt in TrainingModuleType:
            mod = _build_module(mt, [], TrainingLevel.BEGINNER)
            assert mod.module_type == mt
            assert len(mod.sections) > 0

    def test_objectives_populated(self) -> None:
        mod = _build_module(TrainingModuleType.OWASP, [], TrainingLevel.BEGINNER)
        assert len(mod.objectives) > 0


# ===========================================================================
# generate_training
# ===========================================================================

class TestGenerateTraining:
    def test_basic_generation(self) -> None:
        findings = [_make_finding("CMD-001")]
        mat = generate_training(findings)
        assert mat.title
        assert len(mat.modules) > 0
        assert mat.findings_count == 1

    def test_with_string_level(self) -> None:
        findings = [_make_finding("CMD-001")]
        mat = generate_training(findings, level="advanced")
        assert mat.level == TrainingLevel.ADVANCED

    def test_auto_detect_modules(self) -> None:
        findings = [
            _make_finding("CMD-001"),
            _make_finding("LEAK-001"),
            _make_finding("MCP-001"),
        ]
        mat = generate_training(findings)
        types = {m.module_type for m in mat.modules}
        assert TrainingModuleType.SECURE_CODING in types
        assert TrainingModuleType.AWARENESS in types
        assert TrainingModuleType.MCP_SECURITY in types

    def test_explicit_module_types(self) -> None:
        findings = [_make_finding("CMD-001")]
        mat = generate_training(
            findings,
            module_types=[TrainingModuleType.OWASP],
        )
        assert len(mat.modules) == 1
        assert mat.modules[0].module_type == TrainingModuleType.OWASP

    def test_empty_findings(self) -> None:
        mat = generate_training([])
        assert len(mat.modules) >= 2  # defaults to awareness + mcp_security

    def test_estimated_minutes(self) -> None:
        findings = [_make_finding("CMD-001")]
        mat = generate_training(findings, level=TrainingLevel.BEGINNER)
        assert mat.estimated_minutes > 0


# ===========================================================================
# generate_quiz
# ===========================================================================

class TestGenerateQuiz:
    def test_basic_quiz(self) -> None:
        findings = [_make_finding("CMD-001")]
        quiz = generate_quiz(findings)
        assert quiz.total_questions > 0
        assert quiz.level == TrainingLevel.BEGINNER

    def test_num_questions_limit(self) -> None:
        findings = [_make_finding(f"CMD-{i:03d}") for i in range(20)]
        quiz = generate_quiz(findings, num_questions=5)
        assert quiz.total_questions <= 5

    def test_with_string_level(self) -> None:
        findings = [_make_finding("CMD-001")]
        quiz = generate_quiz(findings, level="advanced")
        assert quiz.level == TrainingLevel.ADVANCED

    def test_questions_have_answers(self) -> None:
        findings = [_make_finding("CMD-001")]
        quiz = generate_quiz(findings)
        for q in quiz.questions:
            assert len(q.answers) > 0

    def test_each_question_has_correct_answer(self) -> None:
        findings = [_make_finding("CMD-001")]
        quiz = generate_quiz(findings, num_questions=5)
        for q in quiz.questions:
            assert q.correct_answer is not None

    def test_finding_specific_questions(self) -> None:
        findings = [
            _make_finding("CMD-001", title="Command Injection"),
            _make_finding("LEAK-001", title="Data Leak"),
        ]
        quiz = generate_quiz(findings, num_questions=10)
        texts = [q.text for q in quiz.questions]
        assert any("Command Injection" in t for t in texts)

    def test_empty_findings(self) -> None:
        quiz = generate_quiz([])
        assert quiz.total_questions > 0  # still has template questions

    def test_passing_score(self) -> None:
        findings = [_make_finding("CMD-001")]
        quiz = generate_quiz(findings)
        assert quiz.passing_score == 0.7


# ===========================================================================
# _build_finding_question
# ===========================================================================

class TestBuildFindingQuestion:
    def test_basic(self) -> None:
        f = _make_finding("CMD-001", title="Cmd Injection")
        q = _build_finding_question(f, 1, TrainingLevel.BEGINNER)
        assert "CMD-001" in q.text
        assert q.related_rule_id == "CMD-001"
        assert len(q.answers) == 4

    def test_correct_answer_matches_recommendation(self) -> None:
        f = _make_finding("CMD-001", recommendation="Use parameterized commands")
        q = _build_finding_question(f, 1, TrainingLevel.BEGINNER)
        correct = q.correct_answer
        assert correct is not None
        assert "parameterized" in correct.text


# ===========================================================================
# _question_id
# ===========================================================================

class TestQuestionId:
    def test_format(self) -> None:
        qid = _question_id("test question", 1)
        assert qid.startswith("Q001_")

    def test_deterministic(self) -> None:
        a = _question_id("test", 5)
        b = _question_id("test", 5)
        assert a == b

    def test_different_texts(self) -> None:
        a = _question_id("question A", 1)
        b = _question_id("question B", 1)
        assert a != b


# ===========================================================================
# generate_curriculum
# ===========================================================================

class TestGenerateCurriculum:
    def test_basic_curriculum(self) -> None:
        findings = [_make_finding("CMD-001")]
        cur = generate_curriculum(findings)
        assert cur.title
        assert len(cur.modules) == 5  # all module types
        assert cur.total_estimated_minutes > 0

    def test_with_string_level(self) -> None:
        findings = [_make_finding("CMD-001")]
        cur = generate_curriculum(findings, level="advanced")
        assert cur.level == TrainingLevel.ADVANCED

    def test_required_modules_from_findings(self) -> None:
        findings = [_make_finding("CMD-001")]
        cur = generate_curriculum(findings)
        required = cur.required_modules
        types = {m.module_type for m in required}
        assert TrainingModuleType.SECURE_CODING in types

    def test_empty_findings(self) -> None:
        cur = generate_curriculum([])
        assert len(cur.modules) == 5
        assert len(cur.required_modules) >= 2

    def test_total_hours(self) -> None:
        findings = [_make_finding("CMD-001")]
        cur = generate_curriculum(findings)
        assert cur.total_hours > 0

    def test_description(self) -> None:
        findings = [_make_finding("CMD-001")]
        cur = generate_curriculum(findings)
        assert "modules" in cur.description

    def test_module_ordering(self) -> None:
        findings = [_make_finding("CMD-001")]
        cur = generate_curriculum(findings)
        orders = [m.order for m in cur.modules]
        assert orders == sorted(orders)


# ===========================================================================
# format_training_json
# ===========================================================================

class TestFormatTrainingJson:
    def test_valid_json(self) -> None:
        mat = generate_training([_make_finding("CMD-001")])
        j = format_training_json(mat)
        data = json.loads(j)
        assert "modules" in data
        assert data["level"] == "beginner"

    def test_round_trip(self) -> None:
        mat = generate_training([_make_finding("CMD-001")])
        j = format_training_json(mat)
        data = json.loads(j)
        assert data["title"] == mat.title


# ===========================================================================
# format_quiz_json
# ===========================================================================

class TestFormatQuizJson:
    def test_valid_json(self) -> None:
        quiz = generate_quiz([_make_finding("CMD-001")])
        j = format_quiz_json(quiz)
        data = json.loads(j)
        assert "questions" in data
        assert data["total_questions"] == len(data["questions"])


# ===========================================================================
# format_curriculum_json
# ===========================================================================

class TestFormatCurriculumJson:
    def test_valid_json(self) -> None:
        cur = generate_curriculum([_make_finding("CMD-001")])
        j = format_curriculum_json(cur)
        data = json.loads(j)
        assert "modules" in data
        assert "total_hours" in data


# ===========================================================================
# format_training_text
# ===========================================================================

class TestFormatTrainingText:
    def test_contains_title(self) -> None:
        mat = generate_training([_make_finding("CMD-001")])
        text = format_training_text(mat)
        assert "MCP Security Training" in text

    def test_contains_level(self) -> None:
        mat = generate_training([_make_finding("CMD-001")], level="advanced")
        text = format_training_text(mat)
        assert "Advanced" in text

    def test_contains_sections(self) -> None:
        mat = generate_training([_make_finding("CMD-001")])
        text = format_training_text(mat)
        assert "##" in text  # section headers

    def test_contains_objectives(self) -> None:
        mat = generate_training([_make_finding("CMD-001")])
        text = format_training_text(mat)
        assert "Objectives" in text


# ===========================================================================
# format_quiz_text
# ===========================================================================

class TestFormatQuizText:
    def test_contains_title(self) -> None:
        quiz = generate_quiz([_make_finding("CMD-001")])
        text = format_quiz_text(quiz)
        assert "Quiz" in text

    def test_contains_questions(self) -> None:
        quiz = generate_quiz([_make_finding("CMD-001")])
        text = format_quiz_text(quiz)
        assert "Q1." in text

    def test_contains_answer_options(self) -> None:
        quiz = generate_quiz([_make_finding("CMD-001")])
        text = format_quiz_text(quiz)
        assert "A)" in text


# ===========================================================================
# format_curriculum_text
# ===========================================================================

class TestFormatCurriculumText:
    def test_contains_title(self) -> None:
        cur = generate_curriculum([_make_finding("CMD-001")])
        text = format_curriculum_text(cur)
        assert "Curriculum" in text

    def test_contains_modules(self) -> None:
        cur = generate_curriculum([_make_finding("CMD-001")])
        text = format_curriculum_text(cur)
        assert "REQUIRED" in text or "OPTIONAL" in text

    def test_contains_total_hours(self) -> None:
        cur = generate_curriculum([_make_finding("CMD-001")])
        text = format_curriculum_text(cur)
        assert "min" in text


# ===========================================================================
# format_training_html
# ===========================================================================

class TestFormatTrainingHtml:
    def test_valid_html(self) -> None:
        mat = generate_training([_make_finding("CMD-001")])
        html = format_training_html(mat)
        assert "<!DOCTYPE html>" in html
        assert "</html>" in html

    def test_contains_title(self) -> None:
        mat = generate_training([_make_finding("CMD-001")])
        html = format_training_html(mat)
        assert "MCP Security Training" in html

    def test_contains_modules(self) -> None:
        mat = generate_training([_make_finding("CMD-001")])
        html = format_training_html(mat)
        assert "module" in html.lower()

    def test_contains_code_examples(self) -> None:
        mat = generate_training(
            [_make_finding("CMD-001")],
            module_types=[TrainingModuleType.SECURE_CODING],
        )
        html = format_training_html(mat)
        assert "code-bad" in html or "code-good" in html

    def test_escapes_html(self) -> None:
        findings = [_make_finding(title="<script>alert(1)</script>")]
        mat = generate_training(findings)
        html = format_training_html(mat)
        assert "<script>" not in html
