"""Tests for Incident Response Playbook Generator (mcpaudit.incident_response).

Tests incident classification, playbook generation, runbook creation,
template steps, and format helpers.
"""

from __future__ import annotations

import json

import pytest

from mcpaudit.incident_response import (
    ChecklistItem,
    IncidentClassification,
    IncidentPhase,
    IncidentPlaybook,
    IncidentType,
    PlaybookStep,
    ResponseAction,
    RunbookEntry,
    classify_incident,
    format_playbook_html,
    format_playbook_json,
    format_playbook_text,
    generate_playbook,
    generate_playbooks_from_findings,
    generate_playbooks_from_scan,
    generate_runbook,
    get_incident_templates,
    get_template_for_type,
    _classify_rule,
    _severity_to_confidence,
    _build_template_steps,
    _build_checklist,
    _data_breach_steps,
    _privilege_escalation_steps,
    _injection_steps,
    _dos_steps,
    _unauthorized_access_steps,
    _supply_chain_steps,
    _config_drift_steps,
    _tool_abuse_steps,
    _generic_steps,
)


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

def _make_finding(
    rule_id: str = "CMD-001",
    severity: str = "HIGH",
    title: str = "Test finding",
    file: str = "server.py",
    line: int = 10,
) -> dict:
    return {
        "rule_id": rule_id,
        "severity": severity,
        "title": title,
        "description": f"Test description for {rule_id}",
        "file": file,
        "line": line,
        "code_snippet": "test code",
        "recommendation": "Fix this issue",
    }


def _make_scan(findings: list[dict] | None = None) -> dict:
    if findings is None:
        findings = [_make_finding()]
    return {
        "summary": {"total_findings": len(findings)},
        "results": [{"file": "server.py", "findings": findings}],
    }


# ===========================================================================
# IncidentType Enum
# ===========================================================================

class TestIncidentType:
    def test_all_values(self) -> None:
        assert len(IncidentType) == 8

    def test_from_string_basic(self) -> None:
        assert IncidentType.from_string("data_breach") == IncidentType.DATA_BREACH

    def test_from_string_with_hyphens(self) -> None:
        assert IncidentType.from_string("data-breach") == IncidentType.DATA_BREACH

    def test_from_string_with_spaces(self) -> None:
        assert IncidentType.from_string("privilege escalation") == IncidentType.PRIVILEGE_ESCALATION

    def test_from_string_partial_match(self) -> None:
        assert IncidentType.from_string("injection") == IncidentType.INJECTION

    def test_from_string_invalid(self) -> None:
        with pytest.raises(ValueError, match="Unknown incident type"):
            IncidentType.from_string("nonexistent_type")

    def test_from_string_denial_of_service(self) -> None:
        assert IncidentType.from_string("denial_of_service") == IncidentType.DOS

    def test_from_string_supply_chain(self) -> None:
        assert IncidentType.from_string("supply_chain") == IncidentType.SUPPLY_CHAIN

    def test_from_string_tool_abuse(self) -> None:
        assert IncidentType.from_string("tool_abuse") == IncidentType.TOOL_ABUSE


# ===========================================================================
# IncidentPhase Enum
# ===========================================================================

class TestIncidentPhase:
    def test_all_phases(self) -> None:
        assert len(IncidentPhase) == 5

    def test_phase_labels(self) -> None:
        assert IncidentPhase.DETECT.label == "Detection & Analysis"
        assert IncidentPhase.CONTAIN.label == "Containment"
        assert IncidentPhase.ERADICATE.label == "Eradication"
        assert IncidentPhase.RECOVER.label == "Recovery"
        assert IncidentPhase.LESSONS_LEARNED.label == "Lessons Learned"

    def test_phase_order(self) -> None:
        assert IncidentPhase.DETECT.order == 1
        assert IncidentPhase.CONTAIN.order == 2
        assert IncidentPhase.ERADICATE.order == 3
        assert IncidentPhase.RECOVER.order == 4
        assert IncidentPhase.LESSONS_LEARNED.order == 5


# ===========================================================================
# ResponseAction Enum
# ===========================================================================

class TestResponseAction:
    def test_all_actions(self) -> None:
        assert len(ResponseAction) == 10

    def test_urgency_ordering(self) -> None:
        assert ResponseAction.ISOLATE.urgency < ResponseAction.BLOCK.urgency
        assert ResponseAction.BLOCK.urgency < ResponseAction.PATCH.urgency
        assert ResponseAction.PATCH.urgency < ResponseAction.DOCUMENT.urgency

    def test_urgency_values(self) -> None:
        assert ResponseAction.ISOLATE.urgency == 1
        assert ResponseAction.DOCUMENT.urgency == 10


# ===========================================================================
# PlaybookStep
# ===========================================================================

class TestPlaybookStep:
    def test_basic_creation(self) -> None:
        step = PlaybookStep(
            phase=IncidentPhase.DETECT,
            step_number=1,
            action=ResponseAction.INVESTIGATE,
            title="Test step",
            description="Test description",
        )
        assert step.phase == IncidentPhase.DETECT
        assert step.step_number == 1
        assert step.title == "Test step"
        assert not step.is_critical

    def test_to_dict(self) -> None:
        step = PlaybookStep(
            phase=IncidentPhase.CONTAIN,
            step_number=2,
            action=ResponseAction.ISOLATE,
            title="Isolate server",
            description="Disconnect from network",
            commands=["systemctl stop mcp"],
            estimated_minutes=10,
            is_critical=True,
        )
        d = step.to_dict()
        assert d["phase"] == "contain"
        assert d["step_number"] == 2
        assert d["action"] == "isolate"
        assert d["is_critical"] is True
        assert "systemctl stop mcp" in d["commands"]

    def test_default_values(self) -> None:
        step = PlaybookStep(
            phase=IncidentPhase.DETECT,
            step_number=1,
            action=ResponseAction.INVESTIGATE,
            title="T",
            description="D",
        )
        assert step.responsible == "Security Team"
        assert step.estimated_minutes == 15
        assert step.commands == []


# ===========================================================================
# ChecklistItem
# ===========================================================================

class TestChecklistItem:
    def test_creation(self) -> None:
        item = ChecklistItem(
            item_id="test_1",
            description="Do something",
            phase=IncidentPhase.DETECT,
        )
        assert item.item_id == "test_1"
        assert item.is_required is True
        assert item.is_completed is False

    def test_to_dict(self) -> None:
        item = ChecklistItem(
            item_id="test_2",
            description="Check something",
            phase=IncidentPhase.CONTAIN,
            is_required=False,
            is_completed=True,
        )
        d = item.to_dict()
        assert d["item_id"] == "test_2"
        assert d["phase"] == "contain"
        assert d["is_required"] is False
        assert d["is_completed"] is True


# ===========================================================================
# RunbookEntry
# ===========================================================================

class TestRunbookEntry:
    def test_creation(self) -> None:
        rb = RunbookEntry(
            title="Test Runbook",
            incident_type=IncidentType.DATA_BREACH,
            estimated_total_minutes=120,
        )
        assert rb.title == "Test Runbook"
        assert rb.incident_type == IncidentType.DATA_BREACH

    def test_to_dict(self) -> None:
        items = [
            ChecklistItem("i1", "Step 1", IncidentPhase.DETECT, is_required=True),
            ChecklistItem("i2", "Step 2", IncidentPhase.CONTAIN, is_required=False),
        ]
        rb = RunbookEntry(
            title="Test",
            incident_type=IncidentType.INJECTION,
            checklist=items,
            estimated_total_minutes=60,
        )
        d = rb.to_dict()
        assert d["total_items"] == 2
        assert d["required_items"] == 1
        assert d["incident_type"] == "injection"


# ===========================================================================
# IncidentClassification
# ===========================================================================

class TestIncidentClassification:
    def test_creation(self) -> None:
        c = IncidentClassification(
            incident_type=IncidentType.DATA_BREACH,
            severity="CRITICAL",
            confidence=0.95,
        )
        assert c.severity == "CRITICAL"
        assert c.confidence == 0.95

    def test_to_dict(self) -> None:
        c = IncidentClassification(
            incident_type=IncidentType.INJECTION,
            severity="HIGH",
            confidence=0.85,
            affected_findings=["CMD-001", "INJ-002"],
            description="Test",
        )
        d = c.to_dict()
        assert d["incident_type"] == "injection"
        assert d["confidence"] == 0.85
        assert len(d["affected_findings"]) == 2


# ===========================================================================
# IncidentPlaybook
# ===========================================================================

class TestIncidentPlaybook:
    def test_creation(self) -> None:
        classification = IncidentClassification(
            incident_type=IncidentType.DATA_BREACH,
            severity="HIGH",
            confidence=0.8,
        )
        pb = IncidentPlaybook(
            title="Test Playbook",
            incident_type=IncidentType.DATA_BREACH,
            classification=classification,
        )
        assert pb.title == "Test Playbook"
        assert pb.created_at  # auto-set

    def test_phases_property(self) -> None:
        classification = IncidentClassification(
            incident_type=IncidentType.INJECTION,
            severity="HIGH",
            confidence=0.8,
        )
        steps = [
            PlaybookStep(IncidentPhase.ERADICATE, 3, ResponseAction.PATCH, "T", "D"),
            PlaybookStep(IncidentPhase.DETECT, 1, ResponseAction.INVESTIGATE, "T", "D"),
            PlaybookStep(IncidentPhase.CONTAIN, 2, ResponseAction.BLOCK, "T", "D"),
        ]
        pb = IncidentPlaybook(
            title="Test",
            incident_type=IncidentType.INJECTION,
            classification=classification,
            steps=steps,
        )
        phases = pb.phases
        assert phases[0] == IncidentPhase.DETECT
        assert phases[1] == IncidentPhase.CONTAIN
        assert phases[2] == IncidentPhase.ERADICATE

    def test_critical_steps(self) -> None:
        classification = IncidentClassification(
            incident_type=IncidentType.DATA_BREACH,
            severity="HIGH",
            confidence=0.8,
        )
        steps = [
            PlaybookStep(IncidentPhase.DETECT, 1, ResponseAction.INVESTIGATE,
                         "T", "D", is_critical=True),
            PlaybookStep(IncidentPhase.CONTAIN, 2, ResponseAction.BLOCK,
                         "T", "D", is_critical=False),
            PlaybookStep(IncidentPhase.ERADICATE, 3, ResponseAction.PATCH,
                         "T", "D", is_critical=True),
        ]
        pb = IncidentPlaybook(
            title="Test",
            incident_type=IncidentType.DATA_BREACH,
            classification=classification,
            steps=steps,
        )
        assert len(pb.critical_steps) == 2

    def test_steps_for_phase(self) -> None:
        classification = IncidentClassification(
            incident_type=IncidentType.DOS,
            severity="MEDIUM",
            confidence=0.7,
        )
        steps = [
            PlaybookStep(IncidentPhase.DETECT, 1, ResponseAction.INVESTIGATE, "T1", "D1"),
            PlaybookStep(IncidentPhase.DETECT, 2, ResponseAction.MONITOR, "T2", "D2"),
            PlaybookStep(IncidentPhase.CONTAIN, 3, ResponseAction.BLOCK, "T3", "D3"),
        ]
        pb = IncidentPlaybook(
            title="Test",
            incident_type=IncidentType.DOS,
            classification=classification,
            steps=steps,
        )
        detect_steps = pb.steps_for_phase(IncidentPhase.DETECT)
        assert len(detect_steps) == 2

    def test_estimated_total_auto(self) -> None:
        classification = IncidentClassification(
            incident_type=IncidentType.DATA_BREACH,
            severity="HIGH",
            confidence=0.8,
        )
        steps = [
            PlaybookStep(IncidentPhase.DETECT, 1, ResponseAction.INVESTIGATE,
                         "T", "D", estimated_minutes=30),
            PlaybookStep(IncidentPhase.CONTAIN, 2, ResponseAction.BLOCK,
                         "T", "D", estimated_minutes=15),
        ]
        pb = IncidentPlaybook(
            title="Test",
            incident_type=IncidentType.DATA_BREACH,
            classification=classification,
            steps=steps,
        )
        assert pb.estimated_total_minutes == 45

    def test_to_dict(self) -> None:
        classification = IncidentClassification(
            incident_type=IncidentType.DATA_BREACH,
            severity="HIGH",
            confidence=0.8,
        )
        pb = IncidentPlaybook(
            title="Test",
            incident_type=IncidentType.DATA_BREACH,
            classification=classification,
            steps=_data_breach_steps(),
            references=["NIST SP 800-61"],
        )
        d = pb.to_dict()
        assert d["title"] == "Test"
        assert d["incident_type"] == "data_breach"
        assert d["total_steps"] == len(_data_breach_steps())
        assert d["critical_steps_count"] > 0
        assert "phases" in d


# ===========================================================================
# classify_incident
# ===========================================================================

class TestClassifyIncident:
    def test_empty_findings(self) -> None:
        c = classify_incident([])
        assert c.confidence == 0.0
        assert c.severity == "LOW"

    def test_injection_findings(self) -> None:
        findings = [
            _make_finding("CMD-001", "CRITICAL"),
            _make_finding("INJ-002", "HIGH"),
        ]
        c = classify_incident(findings)
        assert c.incident_type == IncidentType.INJECTION
        assert c.severity == "CRITICAL"
        assert c.confidence > 0.9

    def test_data_breach_findings(self) -> None:
        findings = [_make_finding("LEAK-001", "HIGH")]
        c = classify_incident(findings)
        assert c.incident_type == IncidentType.DATA_BREACH

    def test_privilege_escalation(self) -> None:
        findings = [_make_finding("PRIV-001", "CRITICAL")]
        c = classify_incident(findings)
        assert c.incident_type == IncidentType.PRIVILEGE_ESCALATION

    def test_dos_findings(self) -> None:
        findings = [_make_finding("DOS-001", "MEDIUM")]
        c = classify_incident(findings)
        assert c.incident_type == IncidentType.DOS

    def test_supply_chain_findings(self) -> None:
        findings = [_make_finding("DEP-001", "HIGH")]
        c = classify_incident(findings)
        assert c.incident_type == IncidentType.SUPPLY_CHAIN

    def test_config_drift_findings(self) -> None:
        findings = [_make_finding("DRIFT-001", "LOW")]
        c = classify_incident(findings)
        assert c.incident_type == IncidentType.CONFIGURATION_DRIFT

    def test_tool_abuse_findings(self) -> None:
        findings = [_make_finding("TOOL-001", "MEDIUM")]
        c = classify_incident(findings)
        assert c.incident_type == IncidentType.TOOL_ABUSE

    def test_mixed_findings_dominant_type(self) -> None:
        findings = [
            _make_finding("CMD-001", "HIGH"),
            _make_finding("CMD-002", "HIGH"),
            _make_finding("LEAK-001", "MEDIUM"),
        ]
        c = classify_incident(findings)
        assert c.incident_type == IncidentType.INJECTION

    def test_affected_findings_tracked(self) -> None:
        findings = [
            _make_finding("CMD-001"),
            _make_finding("CMD-002"),
        ]
        c = classify_incident(findings)
        assert "CMD-001" in c.affected_findings
        assert "CMD-002" in c.affected_findings

    def test_description_populated(self) -> None:
        findings = [_make_finding("CMD-001")]
        c = classify_incident(findings)
        assert c.description  # non-empty


# ===========================================================================
# _classify_rule
# ===========================================================================

class TestClassifyRule:
    def test_cmd_prefix(self) -> None:
        assert _classify_rule("CMD-001") == IncidentType.INJECTION

    def test_leak_prefix(self) -> None:
        assert _classify_rule("LEAK-001") == IncidentType.DATA_BREACH

    def test_priv_prefix(self) -> None:
        assert _classify_rule("PRIV-001") == IncidentType.PRIVILEGE_ESCALATION

    def test_dos_prefix(self) -> None:
        assert _classify_rule("DOS-001") == IncidentType.DOS

    def test_unknown_prefix(self) -> None:
        result = _classify_rule("UNKNOWN-001")
        assert result == IncidentType.UNAUTHORIZED_ACCESS


# ===========================================================================
# _severity_to_confidence
# ===========================================================================

class TestSeverityToConfidence:
    def test_critical(self) -> None:
        assert _severity_to_confidence("CRITICAL") == 0.95

    def test_high(self) -> None:
        assert _severity_to_confidence("HIGH") == 0.85

    def test_medium(self) -> None:
        assert _severity_to_confidence("MEDIUM") == 0.70

    def test_low(self) -> None:
        assert _severity_to_confidence("LOW") == 0.55

    def test_info(self) -> None:
        assert _severity_to_confidence("INFO") == 0.40

    def test_unknown(self) -> None:
        assert _severity_to_confidence("WEIRD") == 0.50


# ===========================================================================
# Template Steps
# ===========================================================================

class TestTemplateSteps:
    def test_data_breach_steps(self) -> None:
        steps = _data_breach_steps()
        assert len(steps) >= 8
        phases = {s.phase for s in steps}
        assert IncidentPhase.DETECT in phases
        assert IncidentPhase.CONTAIN in phases
        assert IncidentPhase.ERADICATE in phases
        assert IncidentPhase.RECOVER in phases
        assert IncidentPhase.LESSONS_LEARNED in phases

    def test_privilege_escalation_steps(self) -> None:
        steps = _privilege_escalation_steps()
        assert len(steps) >= 6
        assert any(s.is_critical for s in steps)

    def test_injection_steps(self) -> None:
        steps = _injection_steps()
        assert len(steps) >= 7
        assert any(s.action == ResponseAction.PATCH for s in steps)

    def test_dos_steps(self) -> None:
        steps = _dos_steps()
        assert len(steps) >= 5

    def test_unauthorized_access_steps(self) -> None:
        steps = _unauthorized_access_steps()
        assert len(steps) >= 4

    def test_supply_chain_steps(self) -> None:
        steps = _supply_chain_steps()
        assert len(steps) >= 4

    def test_config_drift_steps(self) -> None:
        steps = _config_drift_steps()
        assert len(steps) >= 4

    def test_tool_abuse_steps(self) -> None:
        steps = _tool_abuse_steps()
        assert len(steps) >= 4

    def test_generic_steps(self) -> None:
        steps = _generic_steps()
        assert len(steps) >= 4
        phases = {s.phase for s in steps}
        assert len(phases) >= 4

    def test_build_template_for_all_types(self) -> None:
        for itype in IncidentType:
            steps = _build_template_steps(itype)
            assert len(steps) >= 4, f"Type {itype.value} has too few steps"

    def test_all_templates_have_critical_steps(self) -> None:
        for itype in IncidentType:
            steps = _build_template_steps(itype)
            # At least one step should be critical (except config drift)
            if itype != IncidentType.CONFIGURATION_DRIFT:
                assert any(s.is_critical for s in steps), \
                    f"Type {itype.value} has no critical steps"

    def test_get_incident_templates(self) -> None:
        templates = get_incident_templates()
        assert len(templates) == len(IncidentType)
        assert "data_breach" in templates

    def test_get_template_for_type(self) -> None:
        steps = get_template_for_type(IncidentType.INJECTION)
        assert len(steps) >= 5


# ===========================================================================
# Runbook Generation
# ===========================================================================

class TestGenerateRunbook:
    def test_basic_generation(self) -> None:
        rb = generate_runbook(IncidentType.DATA_BREACH)
        assert rb.incident_type == IncidentType.DATA_BREACH
        assert rb.title.startswith("Runbook:")
        assert len(rb.checklist) > 0
        assert rb.estimated_total_minutes > 0

    def test_custom_steps(self) -> None:
        steps = [
            PlaybookStep(IncidentPhase.DETECT, 1, ResponseAction.INVESTIGATE,
                         "T", "D", estimated_minutes=10),
        ]
        rb = generate_runbook(IncidentType.INJECTION, steps)
        assert len(rb.checklist) == 1
        assert rb.estimated_total_minutes == 10

    def test_severity_in_priority(self) -> None:
        rb = generate_runbook(IncidentType.DATA_BREACH, severity="CRITICAL")
        assert rb.priority == "CRITICAL"

    def test_checklist_items_have_phases(self) -> None:
        rb = generate_runbook(IncidentType.INJECTION)
        for item in rb.checklist:
            assert isinstance(item.phase, IncidentPhase)

    def test_build_checklist(self) -> None:
        steps = _injection_steps()
        items = _build_checklist(steps, IncidentType.INJECTION)
        assert len(items) == len(steps)
        # Critical steps should be required
        critical_steps = [s for s in steps if s.is_critical]
        required_items = [i for i in items if i.is_required]
        assert len(required_items) == len(critical_steps)


# ===========================================================================
# generate_playbook
# ===========================================================================

class TestGeneratePlaybook:
    def test_basic_generation(self) -> None:
        findings = [_make_finding("CMD-001", "HIGH")]
        pb = generate_playbook(findings)
        assert pb.title
        assert pb.incident_type == IncidentType.INJECTION
        assert len(pb.steps) > 0
        assert pb.runbook is not None
        assert pb.classification.severity == "HIGH"

    def test_empty_findings(self) -> None:
        pb = generate_playbook([])
        assert pb.classification.confidence == 0.0

    def test_forced_incident_type(self) -> None:
        findings = [_make_finding("CMD-001", "HIGH")]
        pb = generate_playbook(findings, incident_type=IncidentType.DATA_BREACH)
        assert pb.incident_type == IncidentType.DATA_BREACH

    def test_affected_files_collected(self) -> None:
        findings = [
            _make_finding("CMD-001", "HIGH", file="a.py"),
            _make_finding("CMD-002", "HIGH", file="b.py"),
        ]
        pb = generate_playbook(findings)
        assert len(pb.affected_files) >= 2

    def test_references_included(self) -> None:
        findings = [_make_finding("CMD-001")]
        pb = generate_playbook(findings)
        assert len(pb.references) >= 2
        assert any("NIST" in r for r in pb.references)

    def test_data_breach_references(self) -> None:
        findings = [_make_finding("LEAK-001", "CRITICAL")]
        pb = generate_playbook(findings, incident_type=IncidentType.DATA_BREACH)
        assert any("GDPR" in r for r in pb.references)

    def test_supply_chain_references(self) -> None:
        findings = [_make_finding("DEP-001", "HIGH")]
        pb = generate_playbook(findings, incident_type=IncidentType.SUPPLY_CHAIN)
        assert any("Supply Chain" in r for r in pb.references)


# ===========================================================================
# generate_playbooks_from_findings
# ===========================================================================

class TestGeneratePlaybooksFromFindings:
    def test_empty_findings(self) -> None:
        pbs = generate_playbooks_from_findings([])
        assert pbs == []

    def test_single_type(self) -> None:
        findings = [
            _make_finding("CMD-001", "HIGH"),
            _make_finding("CMD-002", "MEDIUM"),
        ]
        pbs = generate_playbooks_from_findings(findings)
        assert len(pbs) == 1
        assert pbs[0].incident_type == IncidentType.INJECTION

    def test_multiple_types(self) -> None:
        findings = [
            _make_finding("CMD-001", "HIGH"),
            _make_finding("LEAK-001", "CRITICAL"),
            _make_finding("DOS-001", "MEDIUM"),
        ]
        pbs = generate_playbooks_from_findings(findings)
        assert len(pbs) == 3
        types = {pb.incident_type for pb in pbs}
        assert IncidentType.INJECTION in types
        assert IncidentType.DATA_BREACH in types
        assert IncidentType.DOS in types


# ===========================================================================
# generate_playbooks_from_scan
# ===========================================================================

class TestGeneratePlaybooksFromScan:
    def test_basic_scan(self) -> None:
        scan = _make_scan([_make_finding("CMD-001")])
        pbs = generate_playbooks_from_scan(scan)
        assert len(pbs) >= 1

    def test_empty_scan(self) -> None:
        scan = _make_scan([])
        pbs = generate_playbooks_from_scan(scan)
        assert pbs == []

    def test_scan_no_results(self) -> None:
        scan = {"results": []}
        pbs = generate_playbooks_from_scan(scan)
        assert pbs == []


# ===========================================================================
# format_playbook_json
# ===========================================================================

class TestFormatPlaybookJson:
    def test_valid_json(self) -> None:
        findings = [_make_finding("CMD-001")]
        pb = generate_playbook(findings)
        j = format_playbook_json(pb)
        data = json.loads(j)
        assert data["incident_type"] == "injection"
        assert "classification" in data
        assert "steps" in data

    def test_round_trip(self) -> None:
        findings = [_make_finding("LEAK-001", "CRITICAL")]
        pb = generate_playbook(findings)
        j = format_playbook_json(pb)
        data = json.loads(j)
        assert data["title"] == pb.title
        assert data["total_steps"] == len(pb.steps)


# ===========================================================================
# format_playbook_text
# ===========================================================================

class TestFormatPlaybookText:
    def test_contains_title(self) -> None:
        findings = [_make_finding("CMD-001")]
        pb = generate_playbook(findings)
        text = format_playbook_text(pb)
        assert "INCIDENT RESPONSE PLAYBOOK" in text

    def test_contains_classification(self) -> None:
        findings = [_make_finding("CMD-001", "HIGH")]
        pb = generate_playbook(findings)
        text = format_playbook_text(pb)
        assert "HIGH" in text
        assert "injection" in text

    def test_contains_phases(self) -> None:
        findings = [_make_finding("CMD-001")]
        pb = generate_playbook(findings)
        text = format_playbook_text(pb)
        assert "Detection" in text
        assert "Containment" in text

    def test_contains_steps(self) -> None:
        findings = [_make_finding("CMD-001")]
        pb = generate_playbook(findings)
        text = format_playbook_text(pb)
        assert "Step 1" in text

    def test_shows_critical_marker(self) -> None:
        findings = [_make_finding("CMD-001")]
        pb = generate_playbook(findings)
        text = format_playbook_text(pb)
        assert "[!]" in text  # critical step marker

    def test_shows_runbook_summary(self) -> None:
        findings = [_make_finding("CMD-001")]
        pb = generate_playbook(findings)
        text = format_playbook_text(pb)
        assert "Runbook:" in text

    def test_shows_affected_files(self) -> None:
        findings = [_make_finding("CMD-001", file="test_file.py")]
        pb = generate_playbook(findings)
        text = format_playbook_text(pb)
        assert "test_file.py" in text

    def test_shows_references(self) -> None:
        findings = [_make_finding("CMD-001")]
        pb = generate_playbook(findings)
        text = format_playbook_text(pb)
        assert "NIST" in text


# ===========================================================================
# format_playbook_html
# ===========================================================================

class TestFormatPlaybookHtml:
    def test_valid_html(self) -> None:
        findings = [_make_finding("CMD-001")]
        pb = generate_playbook(findings)
        html = format_playbook_html(pb)
        assert "<!DOCTYPE html>" in html
        assert "</html>" in html

    def test_contains_title(self) -> None:
        findings = [_make_finding("CMD-001")]
        pb = generate_playbook(findings)
        html = format_playbook_html(pb)
        assert "Incident Response" in html

    def test_contains_severity_badge(self) -> None:
        findings = [_make_finding("CMD-001", "CRITICAL")]
        pb = generate_playbook(findings)
        html = format_playbook_html(pb)
        assert "CRITICAL" in html

    def test_contains_phases(self) -> None:
        findings = [_make_finding("CMD-001")]
        pb = generate_playbook(findings)
        html = format_playbook_html(pb)
        assert "Detection" in html
        assert "Containment" in html

    def test_contains_commands(self) -> None:
        findings = [_make_finding("CMD-001")]
        pb = generate_playbook(findings)
        html = format_playbook_html(pb)
        assert "mcpaudit" in html

    def test_contains_runbook(self) -> None:
        findings = [_make_finding("CMD-001")]
        pb = generate_playbook(findings)
        html = format_playbook_html(pb)
        assert "Runbook" in html

    def test_contains_references(self) -> None:
        findings = [_make_finding("CMD-001")]
        pb = generate_playbook(findings)
        html = format_playbook_html(pb)
        assert "NIST" in html

    def test_escapes_html(self) -> None:
        findings = [_make_finding("CMD-001", title="<script>alert(1)</script>")]
        pb = generate_playbook(findings)
        html = format_playbook_html(pb)
        assert "<script>" not in html
