"""Tests for remediation playbooks — generation, formatting, and CLI."""

from __future__ import annotations

import json
import tempfile
from pathlib import Path

import pytest

from mcpaudit.models import Finding, ScanResult, Severity
from mcpaudit.playbooks import (
    Playbook,
    PlaybookLibrary,
    RemediationStep,
    export_playbooks_html,
    export_playbooks_json,
    export_playbooks_markdown,
    export_playbooks_text,
    generate_all_playbooks,
    generate_playbook,
    generate_playbook_by_id,
    generate_playbooks,
    generate_playbooks_from_findings,
    generate_playbooks_from_scan_data,
    get_covered_rule_ids,
)


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

def _finding(rule_id: str = "CMD-001", severity: Severity = Severity.CRITICAL,
             title: str = "Command Injection") -> Finding:
    return Finding(
        rule_id=rule_id,
        severity=severity,
        title=title,
        description=f"Test finding: {title}",
        file="server.py",
        line=10,
    )


def _scan_data_dict(rule_ids: list[str]) -> dict:
    """Create a mock scan data dict with given rule IDs."""
    findings = [
        {
            "rule_id": rid,
            "severity": "HIGH",
            "title": f"Finding {rid}",
            "description": f"Test for {rid}",
            "file": "server.py",
            "line": i * 10,
        }
        for i, rid in enumerate(rule_ids)
    ]
    return {
        "results": [{"file": "server.py", "findings": findings}],
    }


# ---------------------------------------------------------------------------
# RemediationStep
# ---------------------------------------------------------------------------

class TestRemediationStep:
    def test_to_dict(self) -> None:
        step = RemediationStep(
            step_number=1,
            action="Fix the thing",
            code_before="bad()",
            code_after="good()",
            explanation="Because safety",
        )
        d = step.to_dict()
        assert d["step_number"] == 1
        assert d["action"] == "Fix the thing"
        assert d["code_before"] == "bad()"
        assert d["code_after"] == "good()"
        assert d["explanation"] == "Because safety"

    def test_to_dict_minimal(self) -> None:
        step = RemediationStep(step_number=1, action="Do thing")
        d = step.to_dict()
        assert d["step_number"] == 1
        assert d["action"] == "Do thing"
        # code_before/code_after are empty strings, may or may not be in dict


# ---------------------------------------------------------------------------
# Playbook
# ---------------------------------------------------------------------------

class TestPlaybook:
    def test_to_dict(self) -> None:
        pb = Playbook(
            finding_rule_id="CMD-001",
            title="Command Injection",
            severity=Severity.CRITICAL,
            steps=[RemediationStep(step_number=1, action="Fix")],
            estimated_effort="30 minutes",
            references=["CWE-78"],
        )
        d = pb.to_dict()
        assert d["finding_rule_id"] == "CMD-001"
        assert d["severity"] == "CRITICAL"
        assert len(d["steps"]) == 1
        assert d["estimated_effort"] == "30 minutes"
        assert "CWE-78" in d["references"]

    def test_to_dict_empty_steps(self) -> None:
        pb = Playbook(
            finding_rule_id="X-001",
            title="Test",
            severity=Severity.LOW,
        )
        d = pb.to_dict()
        assert d["steps"] == []
        assert d["references"] == []


# ---------------------------------------------------------------------------
# generate_playbook (from Finding)
# ---------------------------------------------------------------------------

class TestGeneratePlaybook:
    def test_known_rule(self) -> None:
        f = _finding("CMD-001")
        pb = generate_playbook(f)
        assert pb is not None
        assert pb.finding_rule_id == "CMD-001"
        assert pb.severity == Severity.CRITICAL
        assert len(pb.steps) > 0

    def test_unknown_rule(self) -> None:
        f = _finding("NONEXIST-999")
        pb = generate_playbook(f)
        assert pb is None

    def test_sql_injection_playbook(self) -> None:
        f = _finding("SQL-001", Severity.CRITICAL, "SQL Injection")
        pb = generate_playbook(f)
        assert pb is not None
        assert len(pb.steps) >= 2
        assert any("parameterized" in s.action.lower() for s in pb.steps)

    def test_path_traversal_playbook(self) -> None:
        f = _finding("PATH-001", Severity.HIGH, "Path Traversal")
        pb = generate_playbook(f)
        assert pb is not None
        assert any("path" in s.action.lower() or "resolve" in s.action.lower() for s in pb.steps)

    def test_playbook_has_effort(self) -> None:
        f = _finding("CMD-001")
        pb = generate_playbook(f)
        assert pb is not None
        assert pb.estimated_effort != ""

    def test_playbook_has_references(self) -> None:
        f = _finding("CMD-001")
        pb = generate_playbook(f)
        assert pb is not None
        assert len(pb.references) > 0


# ---------------------------------------------------------------------------
# generate_playbooks (from ScanResult)
# ---------------------------------------------------------------------------

class TestGeneratePlaybooks:
    def test_multiple_findings(self) -> None:
        findings = [
            _finding("CMD-001"),
            _finding("SQL-001"),
            _finding("PATH-001"),
        ]
        result = ScanResult(file="server.py", findings=findings)
        pbs = generate_playbooks(result)
        assert len(pbs) >= 2  # at least CMD-001, SQL-001
        rule_ids = [pb.finding_rule_id for pb in pbs]
        assert "CMD-001" in rule_ids

    def test_deduplicates_by_rule(self) -> None:
        findings = [_finding("CMD-001"), _finding("CMD-001")]
        result = ScanResult(file="server.py", findings=findings)
        pbs = generate_playbooks(result)
        assert len(pbs) == 1

    def test_sorted_by_severity(self) -> None:
        findings = [
            _finding("ERR-001", Severity.MEDIUM),
            _finding("CMD-001", Severity.CRITICAL),
        ]
        result = ScanResult(file="server.py", findings=findings)
        pbs = generate_playbooks(result)
        if len(pbs) >= 2:
            # CRITICAL should come before MEDIUM
            sev_order = [pb.severity for pb in pbs]
            crit_idx = next(i for i, s in enumerate(sev_order) if s == Severity.CRITICAL)
            med_idx = next(i for i, s in enumerate(sev_order) if s == Severity.MEDIUM)
            assert crit_idx < med_idx

    def test_empty_result(self) -> None:
        result = ScanResult(file="server.py", findings=[])
        pbs = generate_playbooks(result)
        assert pbs == []


# ---------------------------------------------------------------------------
# generate_playbooks_from_findings
# ---------------------------------------------------------------------------

class TestGeneratePlaybooksFromFindings:
    def test_flat_list(self) -> None:
        findings = [_finding("CMD-001"), _finding("SQL-001")]
        pbs = generate_playbooks_from_findings(findings)
        assert len(pbs) >= 1

    def test_empty_list(self) -> None:
        pbs = generate_playbooks_from_findings([])
        assert pbs == []


# ---------------------------------------------------------------------------
# generate_playbook_by_id
# ---------------------------------------------------------------------------

class TestGeneratePlaybookById:
    def test_known_id(self) -> None:
        pb = generate_playbook_by_id("CMD-001")
        assert pb is not None
        assert pb.finding_rule_id == "CMD-001"
        assert len(pb.steps) > 0

    def test_case_insensitive(self) -> None:
        pb = generate_playbook_by_id("cmd-001")
        assert pb is not None

    def test_unknown_id(self) -> None:
        pb = generate_playbook_by_id("FAKE-999")
        assert pb is None

    def test_sql_001(self) -> None:
        pb = generate_playbook_by_id("SQL-001")
        assert pb is not None
        assert pb.severity == Severity.CRITICAL

    def test_auth_001(self) -> None:
        pb = generate_playbook_by_id("AUTH-001")
        assert pb is not None
        assert len(pb.steps) >= 2

    def test_ssrf_001(self) -> None:
        pb = generate_playbook_by_id("SSRF-001")
        assert pb is not None

    def test_err_001(self) -> None:
        pb = generate_playbook_by_id("ERR-001")
        assert pb is not None

    def test_deser_001(self) -> None:
        pb = generate_playbook_by_id("DESER-001")
        assert pb is not None

    def test_sec_001(self) -> None:
        pb = generate_playbook_by_id("SEC-001")
        assert pb is not None


# ---------------------------------------------------------------------------
# generate_all_playbooks
# ---------------------------------------------------------------------------

class TestGenerateAllPlaybooks:
    def test_returns_multiple(self) -> None:
        pbs = generate_all_playbooks()
        assert len(pbs) >= 10  # at least 10 rules in the DB

    def test_all_have_steps(self) -> None:
        pbs = generate_all_playbooks()
        for pb in pbs:
            assert len(pb.steps) > 0, f"{pb.finding_rule_id} has no steps"

    def test_all_have_severity(self) -> None:
        pbs = generate_all_playbooks()
        for pb in pbs:
            assert isinstance(pb.severity, Severity)


# ---------------------------------------------------------------------------
# generate_playbooks_from_scan_data
# ---------------------------------------------------------------------------

class TestGeneratePlaybooksFromScanData:
    def test_standard_format(self) -> None:
        data = _scan_data_dict(["CMD-001", "SQL-001"])
        pbs = generate_playbooks_from_scan_data(data)
        rule_ids = [pb.finding_rule_id for pb in pbs]
        assert "CMD-001" in rule_ids

    def test_flat_findings_format(self) -> None:
        data = {
            "findings": [
                {"rule_id": "CMD-001", "severity": "CRITICAL"},
                {"rule_id": "PATH-001", "severity": "HIGH"},
            ]
        }
        pbs = generate_playbooks_from_scan_data(data)
        assert len(pbs) >= 1

    def test_empty_scan(self) -> None:
        pbs = generate_playbooks_from_scan_data({"results": []})
        assert pbs == []

    def test_unknown_rules_skipped(self) -> None:
        data = _scan_data_dict(["FAKE-999"])
        pbs = generate_playbooks_from_scan_data(data)
        assert pbs == []

    def test_deduplicates_rules(self) -> None:
        data = _scan_data_dict(["CMD-001", "CMD-001", "CMD-001"])
        pbs = generate_playbooks_from_scan_data(data)
        cmd_pbs = [pb for pb in pbs if pb.finding_rule_id == "CMD-001"]
        assert len(cmd_pbs) == 1


# ---------------------------------------------------------------------------
# get_covered_rule_ids
# ---------------------------------------------------------------------------

class TestGetCoveredRuleIds:
    def test_returns_sorted_list(self) -> None:
        ids = get_covered_rule_ids()
        assert isinstance(ids, list)
        assert ids == sorted(ids)
        assert len(ids) >= 10

    def test_known_ids_present(self) -> None:
        ids = get_covered_rule_ids()
        assert "CMD-001" in ids
        assert "SQL-001" in ids
        assert "PATH-001" in ids


# ---------------------------------------------------------------------------
# PlaybookLibrary
# ---------------------------------------------------------------------------

class TestPlaybookLibrary:
    def test_default_init(self) -> None:
        lib = PlaybookLibrary()
        assert lib.count >= 10

    def test_get_existing(self) -> None:
        lib = PlaybookLibrary()
        pb = lib.get("CMD-001")
        assert pb is not None
        assert pb.finding_rule_id == "CMD-001"

    def test_get_nonexistent(self) -> None:
        lib = PlaybookLibrary()
        pb = lib.get("FAKE-999")
        assert pb is None

    def test_all_sorted(self) -> None:
        lib = PlaybookLibrary()
        pbs = lib.all()
        ids = [pb.finding_rule_id for pb in pbs]
        assert ids == sorted(ids)

    def test_by_severity(self) -> None:
        lib = PlaybookLibrary()
        criticals = lib.by_severity("CRITICAL")
        for pb in criticals:
            assert pb.severity == Severity.CRITICAL

    def test_total_estimated_effort(self) -> None:
        lib = PlaybookLibrary()
        effort = lib.total_estimated_effort()
        assert effort != ""
        # Should contain hours or minutes
        assert "h" in effort or "m" in effort

    def test_custom_playbooks(self) -> None:
        pbs = [
            Playbook(
                finding_rule_id="CUSTOM-001",
                title="Custom",
                severity=Severity.LOW,
                steps=[RemediationStep(step_number=1, action="Fix")],
            )
        ]
        lib = PlaybookLibrary(pbs)
        assert lib.count == 1
        assert lib.get("CUSTOM-001") is not None


# ---------------------------------------------------------------------------
# Export: Text
# ---------------------------------------------------------------------------

class TestExportText:
    def test_nonempty_output(self) -> None:
        pbs = generate_all_playbooks()[:3]
        text = export_playbooks_text(pbs)
        assert "Playbooks" in text
        assert len(text) > 100

    def test_empty_playbooks(self) -> None:
        text = export_playbooks_text([])
        assert "No playbooks" in text

    def test_contains_rule_ids(self) -> None:
        pb = generate_playbook_by_id("CMD-001")
        text = export_playbooks_text([pb])
        assert "CMD-001" in text

    def test_contains_effort(self) -> None:
        pb = generate_playbook_by_id("CMD-001")
        text = export_playbooks_text([pb])
        assert "minute" in text.lower() or "hour" in text.lower()


# ---------------------------------------------------------------------------
# Export: JSON
# ---------------------------------------------------------------------------

class TestExportJson:
    def test_valid_json(self) -> None:
        pbs = generate_all_playbooks()[:3]
        result = export_playbooks_json(pbs)
        data = json.loads(result)
        assert isinstance(data, list)
        assert len(data) == 3

    def test_contains_fields(self) -> None:
        pb = generate_playbook_by_id("CMD-001")
        result = export_playbooks_json([pb])
        data = json.loads(result)
        assert data[0]["finding_rule_id"] == "CMD-001"
        assert "steps" in data[0]

    def test_empty_list(self) -> None:
        result = export_playbooks_json([])
        data = json.loads(result)
        assert data == []


# ---------------------------------------------------------------------------
# Export: Markdown
# ---------------------------------------------------------------------------

class TestExportMarkdown:
    def test_contains_headers(self) -> None:
        pbs = generate_all_playbooks()[:2]
        md = export_playbooks_markdown(pbs)
        assert "# mcpaudit" in md
        assert "##" in md

    def test_contains_code_blocks(self) -> None:
        pb = generate_playbook_by_id("CMD-001")
        md = export_playbooks_markdown([pb])
        assert "```python" in md

    def test_empty_playbooks(self) -> None:
        md = export_playbooks_markdown([])
        # Should still produce valid output
        assert isinstance(md, str)


# ---------------------------------------------------------------------------
# Export: HTML
# ---------------------------------------------------------------------------

class TestExportHtml:
    def test_valid_html(self) -> None:
        pbs = generate_all_playbooks()[:3]
        html = export_playbooks_html(pbs)
        assert "<!DOCTYPE html>" in html
        assert "</html>" in html

    def test_contains_playbooks(self) -> None:
        pb = generate_playbook_by_id("CMD-001")
        html = export_playbooks_html([pb])
        assert "CMD-001" in html

    def test_severity_badge(self) -> None:
        pb = generate_playbook_by_id("CMD-001")
        html = export_playbooks_html([pb])
        assert "severity-badge" in html
        assert "CRITICAL" in html

    def test_empty_playbooks(self) -> None:
        html = export_playbooks_html([])
        assert "<!DOCTYPE html>" in html


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

class TestPlaybookCLI:
    def test_single_rule(self) -> None:
        from mcpaudit.__main__ import main
        exit_code = main(["playbook", "CMD-001"])
        assert exit_code == 0

    def test_all_playbooks(self) -> None:
        from mcpaudit.__main__ import main
        exit_code = main(["playbook", "--all"])
        assert exit_code == 0

    def test_from_scan(self, tmp_path: Path) -> None:
        scan_file = tmp_path / "scan.json"
        scan_data = _scan_data_dict(["CMD-001", "SQL-001"])
        scan_file.write_text(json.dumps(scan_data))
        from mcpaudit.__main__ import main
        exit_code = main(["playbook", "--from-scan", str(scan_file)])
        assert exit_code == 0

    def test_json_output(self, tmp_path: Path) -> None:
        out = tmp_path / "playbooks.json"
        from mcpaudit.__main__ import main
        exit_code = main(["playbook", "CMD-001", "--format", "json", "-o", str(out)])
        assert exit_code == 0
        data = json.loads(out.read_text())
        assert isinstance(data, list)

    def test_markdown_output(self) -> None:
        from mcpaudit.__main__ import main
        exit_code = main(["playbook", "CMD-001", "--format", "markdown"])
        assert exit_code == 0

    def test_unknown_rule(self) -> None:
        from mcpaudit.__main__ import main
        exit_code = main(["playbook", "FAKE-999"])
        assert exit_code == 1

    def test_from_scan_nonexistent(self) -> None:
        from mcpaudit.__main__ import main
        exit_code = main(["playbook", "--from-scan", "/nonexistent/scan.json"])
        assert exit_code == 1
