"""Tests for MCP Remediation Engine (mcpaudit.remediation_engine).

Tests remediation plan generation, prioritization, filtering,
formatting, dry-run mode, and edge cases.
"""

from __future__ import annotations

import json

import pytest

from mcpaudit.remediation_engine import (
    FixType,
    RemediationItem,
    RemediationPlan,
    RemediationPriority,
    RemediationStep,
    build_remediation_item,
    format_remediation_html,
    format_remediation_json,
    format_remediation_text,
    generate_plan_from_dict,
    generate_plan_from_findings,
    generate_remediation_plan,
    get_fix_types,
    get_known_rules,
    get_remediation_info,
    _REMEDIATION_DB,
    _DEFAULT_REMEDIATION,
    _build_steps,
)
from mcpaudit.models import Finding, ScanResult, ScanSummary, Severity


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

def _make_finding(
    rule_id: str = "CMD-001",
    severity: Severity = Severity.HIGH,
    title: str = "Command injection",
    file: str = "server.py",
    line: int = 10,
    **kwargs,
) -> Finding:
    return Finding(
        rule_id=rule_id,
        severity=severity,
        title=title,
        description=kwargs.get("description", "desc"),
        file=file,
        line=line,
        recommendation=kwargs.get("recommendation", ""),
        cwe=kwargs.get("cwe", "CWE-78"),
    )


def _make_summary(specs: list[tuple[str, str]] | None = None) -> ScanSummary:
    """Build ScanSummary from (rule_id, severity_str) tuples."""
    if specs is None:
        specs = [("CMD-001", "HIGH"), ("SQL-001", "CRITICAL")]
    findings = [
        _make_finding(rule_id=rid, severity=Severity.from_string(sev))
        for rid, sev in specs
    ]
    result = ScanResult(file="server.py", findings=findings, lines_scanned=100)
    return ScanSummary(results=[result], total_scan_time_ms=42.5)


# ---------------------------------------------------------------------------
# FixType
# ---------------------------------------------------------------------------

class TestFixType:
    def test_values(self):
        assert FixType.CODE_CHANGE.value == "code_change"
        assert FixType.CONFIG_CHANGE.value == "config_change"
        assert FixType.MANUAL.value == "manual"

    def test_from_string(self):
        assert FixType.from_string("code_change") == FixType.CODE_CHANGE
        assert FixType.from_string("config_change") == FixType.CONFIG_CHANGE
        assert FixType.from_string("manual") == FixType.MANUAL

    def test_from_string_case_insensitive(self):
        assert FixType.from_string("CODE_CHANGE") == FixType.CODE_CHANGE

    def test_from_string_with_hyphens(self):
        assert FixType.from_string("code-change") == FixType.CODE_CHANGE

    def test_from_string_invalid(self):
        with pytest.raises(ValueError, match="Invalid fix type"):
            FixType.from_string("invalid")


# ---------------------------------------------------------------------------
# RemediationPriority
# ---------------------------------------------------------------------------

class TestRemediationPriority:
    def test_from_severity(self):
        assert RemediationPriority.from_severity(Severity.CRITICAL) == RemediationPriority.CRITICAL
        assert RemediationPriority.from_severity(Severity.HIGH) == RemediationPriority.HIGH
        assert RemediationPriority.from_severity(Severity.MEDIUM) == RemediationPriority.MEDIUM
        assert RemediationPriority.from_severity(Severity.LOW) == RemediationPriority.LOW
        assert RemediationPriority.from_severity(Severity.INFO) == RemediationPriority.INFO

    def test_weight(self):
        assert RemediationPriority.CRITICAL.weight < RemediationPriority.HIGH.weight
        assert RemediationPriority.HIGH.weight < RemediationPriority.MEDIUM.weight
        assert RemediationPriority.MEDIUM.weight < RemediationPriority.LOW.weight
        assert RemediationPriority.LOW.weight < RemediationPriority.INFO.weight

    def test_ordering(self):
        assert RemediationPriority.CRITICAL < RemediationPriority.HIGH
        assert RemediationPriority.HIGH < RemediationPriority.MEDIUM

    def test_le(self):
        assert RemediationPriority.CRITICAL <= RemediationPriority.CRITICAL
        assert RemediationPriority.HIGH <= RemediationPriority.MEDIUM


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

class TestRemediationStep:
    def test_basic(self):
        step = RemediationStep(
            step_number=1,
            title="Fix it",
            description="Do the fix",
            fix_type=FixType.CODE_CHANGE,
        )
        assert step.step_number == 1
        assert step.title == "Fix it"

    def test_to_dict(self):
        step = RemediationStep(
            step_number=1,
            title="Fix",
            description="desc",
            fix_type=FixType.CODE_CHANGE,
            code_before="before",
            code_after="after",
        )
        d = step.to_dict()
        assert d["step_number"] == 1
        assert d["code_before"] == "before"
        assert d["code_after"] == "after"
        assert d["fix_type"] == "code_change"

    def test_to_dict_omits_empty(self):
        step = RemediationStep(
            step_number=1,
            title="Fix",
            description="desc",
            fix_type=FixType.MANUAL,
        )
        d = step.to_dict()
        assert "code_before" not in d
        assert "code_after" not in d


# ---------------------------------------------------------------------------
# RemediationItem
# ---------------------------------------------------------------------------

class TestRemediationItem:
    def test_basic(self):
        item = RemediationItem(
            rule_id="CMD-001",
            title="Fix injection",
            priority=RemediationPriority.HIGH,
            fix_type=FixType.CODE_CHANGE,
            description="Fix command injection",
        )
        assert item.rule_id == "CMD-001"
        assert item.priority == RemediationPriority.HIGH

    def test_to_dict(self):
        item = RemediationItem(
            rule_id="CMD-001",
            title="Fix",
            priority=RemediationPriority.CRITICAL,
            fix_type=FixType.CODE_CHANGE,
            description="desc",
            finding_count=3,
            cwe="CWE-78",
        )
        d = item.to_dict()
        assert d["rule_id"] == "CMD-001"
        assert d["priority"] == "critical"
        assert d["fix_type"] == "code_change"
        assert d["finding_count"] == 3
        assert d["cwe"] == "CWE-78"

    def test_to_dict_has_steps(self):
        step = RemediationStep(
            step_number=1, title="s", description="d", fix_type=FixType.CODE_CHANGE,
        )
        item = RemediationItem(
            rule_id="X", title="t", priority=RemediationPriority.LOW,
            fix_type=FixType.MANUAL, description="d", steps=[step],
        )
        d = item.to_dict()
        assert len(d["steps"]) == 1


# ---------------------------------------------------------------------------
# RemediationPlan
# ---------------------------------------------------------------------------

class TestRemediationPlan:
    def test_empty(self):
        plan = RemediationPlan()
        assert plan.total_items == 0
        assert plan.total_findings == 0
        assert plan.total_effort_minutes == 0

    def test_critical_count(self):
        plan = RemediationPlan(priority_counts={"critical": 2, "high": 3})
        assert plan.critical_count == 2

    def test_high_count(self):
        plan = RemediationPlan(priority_counts={"high": 5})
        assert plan.high_count == 5

    def test_to_dict(self):
        plan = RemediationPlan(
            total_findings=10,
            total_effort_minutes=120,
            dry_run=True,
        )
        d = plan.to_dict()
        assert d["total_findings"] == 10
        assert d["dry_run"] is True
        assert d["total_effort_minutes"] == 120

    def test_to_dict_keys(self):
        plan = RemediationPlan()
        d = plan.to_dict()
        expected = {
            "items", "total_findings", "total_items", "total_effort_minutes",
            "priority_counts", "fix_type_counts", "timestamp", "dry_run",
        }
        assert set(d.keys()) == expected


# ---------------------------------------------------------------------------
# Remediation DB
# ---------------------------------------------------------------------------

class TestRemediationDB:
    def test_known_rules_not_empty(self):
        assert len(_REMEDIATION_DB) > 0

    def test_cmd001_exists(self):
        assert "CMD-001" in _REMEDIATION_DB

    def test_sql001_exists(self):
        assert "SQL-001" in _REMEDIATION_DB

    def test_entries_have_title(self):
        for rule_id, entry in _REMEDIATION_DB.items():
            assert "title" in entry, f"{rule_id} missing title"

    def test_entries_have_fix_type(self):
        for rule_id, entry in _REMEDIATION_DB.items():
            assert "fix_type" in entry, f"{rule_id} missing fix_type"

    def test_entries_have_steps(self):
        for rule_id, entry in _REMEDIATION_DB.items():
            assert "steps" in entry, f"{rule_id} missing steps"
            assert len(entry["steps"]) > 0, f"{rule_id} has no steps"

    def test_default_remediation(self):
        assert "title" in _DEFAULT_REMEDIATION
        assert "fix_type" in _DEFAULT_REMEDIATION
        assert "steps" in _DEFAULT_REMEDIATION


# ---------------------------------------------------------------------------
# build_remediation_item
# ---------------------------------------------------------------------------

class TestBuildRemediationItem:
    def test_known_rule(self):
        findings = [_make_finding("CMD-001", Severity.HIGH)]
        item = build_remediation_item("CMD-001", findings)
        assert item.rule_id == "CMD-001"
        assert item.fix_type == FixType.CODE_CHANGE
        assert len(item.steps) > 0

    def test_unknown_rule_uses_default(self):
        findings = [_make_finding("UNKNOWN-999", Severity.MEDIUM)]
        item = build_remediation_item("UNKNOWN-999", findings)
        assert item.fix_type == FixType.MANUAL
        assert len(item.steps) > 0

    def test_multiple_findings(self):
        findings = [
            _make_finding("CMD-001", Severity.HIGH, file="a.py", line=10),
            _make_finding("CMD-001", Severity.HIGH, file="b.py", line=20),
        ]
        item = build_remediation_item("CMD-001", findings)
        assert item.finding_count == 2
        assert len(item.affected_files) == 2
        assert "a.py" in item.affected_files

    def test_priority_from_severity(self):
        findings = [_make_finding("SQL-001", Severity.CRITICAL)]
        item = build_remediation_item("SQL-001", findings)
        assert item.priority == RemediationPriority.CRITICAL

    def test_effort_scales_with_count(self):
        single = [_make_finding("CMD-001", Severity.HIGH)]
        double = [_make_finding("CMD-001", Severity.HIGH)] * 2
        item1 = build_remediation_item("CMD-001", single)
        item2 = build_remediation_item("CMD-001", double)
        assert item2.estimated_effort_minutes > item1.estimated_effort_minutes

    def test_empty_findings_raises(self):
        with pytest.raises(ValueError, match="one finding"):
            build_remediation_item("CMD-001", [])

    def test_cwe_preserved(self):
        findings = [_make_finding("CMD-001", cwe="CWE-78")]
        item = build_remediation_item("CMD-001", findings)
        assert item.cwe == "CWE-78"

    def test_affected_lines(self):
        findings = [
            _make_finding("CMD-001", line=10),
            _make_finding("CMD-001", line=20),
        ]
        item = build_remediation_item("CMD-001", findings)
        assert 10 in item.affected_lines
        assert 20 in item.affected_lines


# ---------------------------------------------------------------------------
# _build_steps
# ---------------------------------------------------------------------------

class TestBuildSteps:
    def test_basic(self):
        raw = [{"title": "Step 1", "description": "Do it"}]
        steps = _build_steps(raw)
        assert len(steps) == 1
        assert steps[0].step_number == 1
        assert steps[0].title == "Step 1"

    def test_numbering(self):
        raw = [{"title": "A"}, {"title": "B"}, {"title": "C"}]
        steps = _build_steps(raw)
        assert [s.step_number for s in steps] == [1, 2, 3]

    def test_code_blocks(self):
        raw = [{"title": "Fix", "code_before": "old", "code_after": "new"}]
        steps = _build_steps(raw)
        assert steps[0].code_before == "old"
        assert steps[0].code_after == "new"

    def test_empty_list(self):
        steps = _build_steps([])
        assert steps == []


# ---------------------------------------------------------------------------
# generate_remediation_plan
# ---------------------------------------------------------------------------

class TestGenerateRemediationPlan:
    def test_basic(self):
        summary = _make_summary()
        plan = generate_remediation_plan(summary)
        assert plan.total_items > 0
        assert plan.total_findings > 0

    def test_dry_run(self):
        summary = _make_summary()
        plan = generate_remediation_plan(summary, dry_run=True)
        assert plan.dry_run is True

    def test_sorted_by_priority(self):
        summary = _make_summary([
            ("LOW-001", "LOW"),
            ("CRIT-001", "CRITICAL"),
            ("MED-001", "MEDIUM"),
        ])
        plan = generate_remediation_plan(summary)
        priorities = [i.priority for i in plan.items]
        # CRITICAL should come first
        assert priorities[0] == RemediationPriority.CRITICAL

    def test_priority_filter(self):
        summary = _make_summary([
            ("CMD-001", "HIGH"),
            ("SQL-001", "CRITICAL"),
            ("X", "LOW"),
        ])
        plan = generate_remediation_plan(summary, priority_filter=["critical"])
        assert all(i.priority == RemediationPriority.CRITICAL for i in plan.items)

    def test_fix_type_filter(self):
        summary = _make_summary([("SEC-001", "HIGH"), ("CMD-001", "HIGH")])
        plan = generate_remediation_plan(summary, fix_type_filter=["config_change"])
        for item in plan.items:
            assert item.fix_type == FixType.CONFIG_CHANGE

    def test_empty_summary(self):
        summary = ScanSummary(results=[])
        plan = generate_remediation_plan(summary)
        assert plan.total_items == 0
        assert plan.total_findings == 0

    def test_timestamp_generated(self):
        summary = _make_summary()
        plan = generate_remediation_plan(summary)
        assert "UTC" in plan.timestamp

    def test_priority_counts(self):
        summary = _make_summary([("A", "HIGH"), ("B", "HIGH"), ("C", "CRITICAL")])
        plan = generate_remediation_plan(summary)
        assert plan.priority_counts.get("high", 0) >= 1
        assert plan.priority_counts.get("critical", 0) >= 1

    def test_fix_type_counts(self):
        summary = _make_summary([("CMD-001", "HIGH")])
        plan = generate_remediation_plan(summary)
        assert "code_change" in plan.fix_type_counts

    def test_effort_computed(self):
        summary = _make_summary([("CMD-001", "HIGH")])
        plan = generate_remediation_plan(summary)
        assert plan.total_effort_minutes > 0

    def test_groups_by_rule_id(self):
        summary = _make_summary([("CMD-001", "HIGH"), ("CMD-001", "HIGH")])
        plan = generate_remediation_plan(summary)
        cmd_items = [i for i in plan.items if i.rule_id == "CMD-001"]
        assert len(cmd_items) == 1
        assert cmd_items[0].finding_count == 2


# ---------------------------------------------------------------------------
# generate_plan_from_findings
# ---------------------------------------------------------------------------

class TestGeneratePlanFromFindings:
    def test_basic(self):
        findings = [_make_finding("CMD-001", Severity.HIGH)]
        plan = generate_plan_from_findings(findings)
        assert plan.total_items == 1

    def test_dry_run(self):
        findings = [_make_finding()]
        plan = generate_plan_from_findings(findings, dry_run=True)
        assert plan.dry_run is True

    def test_empty_list(self):
        plan = generate_plan_from_findings([])
        assert plan.total_items == 0


# ---------------------------------------------------------------------------
# generate_plan_from_dict
# ---------------------------------------------------------------------------

class TestGeneratePlanFromDict:
    def test_from_summary_dict(self):
        summary = _make_summary()
        data = summary.to_dict()
        plan = generate_plan_from_dict(data)
        assert plan.total_items > 0

    def test_empty_dict(self):
        plan = generate_plan_from_dict({})
        assert plan.total_items == 0

    def test_dry_run(self):
        summary = _make_summary()
        data = summary.to_dict()
        plan = generate_plan_from_dict(data, dry_run=True)
        assert plan.dry_run is True


# ---------------------------------------------------------------------------
# Formatters: text
# ---------------------------------------------------------------------------

class TestFormatRemediationText:
    def test_basic(self):
        summary = _make_summary()
        plan = generate_remediation_plan(summary)
        text = format_remediation_text(plan)
        assert "Remediation Plan" in text
        assert "Items:" in text

    def test_dry_run_marker(self):
        summary = _make_summary()
        plan = generate_remediation_plan(summary, dry_run=True)
        text = format_remediation_text(plan)
        assert "DRY RUN" in text

    def test_contains_rule_ids(self):
        summary = _make_summary([("CMD-001", "HIGH")])
        plan = generate_remediation_plan(summary)
        text = format_remediation_text(plan)
        assert "CMD-001" in text

    def test_contains_priority(self):
        summary = _make_summary([("SQL-001", "CRITICAL")])
        plan = generate_remediation_plan(summary)
        text = format_remediation_text(plan)
        assert "CRITICAL" in text

    def test_contains_effort(self):
        summary = _make_summary()
        plan = generate_remediation_plan(summary)
        text = format_remediation_text(plan)
        assert "minutes" in text

    def test_empty_plan(self):
        plan = RemediationPlan()
        text = format_remediation_text(plan)
        assert "Items: 0" in text


# ---------------------------------------------------------------------------
# Formatters: JSON
# ---------------------------------------------------------------------------

class TestFormatRemediationJson:
    def test_valid_json(self):
        summary = _make_summary()
        plan = generate_remediation_plan(summary)
        result = format_remediation_json(plan)
        parsed = json.loads(result)
        assert "items" in parsed
        assert "total_findings" in parsed

    def test_dry_run_flag(self):
        summary = _make_summary()
        plan = generate_remediation_plan(summary, dry_run=True)
        result = format_remediation_json(plan)
        parsed = json.loads(result)
        assert parsed["dry_run"] is True

    def test_items_have_structure(self):
        summary = _make_summary([("CMD-001", "HIGH")])
        plan = generate_remediation_plan(summary)
        result = format_remediation_json(plan)
        parsed = json.loads(result)
        item = parsed["items"][0]
        assert "rule_id" in item
        assert "priority" in item
        assert "steps" in item


# ---------------------------------------------------------------------------
# Formatters: HTML
# ---------------------------------------------------------------------------

class TestFormatRemediationHtml:
    def test_basic_html(self):
        summary = _make_summary()
        plan = generate_remediation_plan(summary)
        html = format_remediation_html(plan)
        assert "<!DOCTYPE html>" in html
        assert "Remediation Plan" in html

    def test_dry_run_marker(self):
        summary = _make_summary()
        plan = generate_remediation_plan(summary, dry_run=True)
        html = format_remediation_html(plan)
        assert "DRY RUN" in html

    def test_contains_css(self):
        summary = _make_summary()
        plan = generate_remediation_plan(summary)
        html = format_remediation_html(plan)
        assert "<style>" in html

    def test_contains_items(self):
        summary = _make_summary([("CMD-001", "HIGH")])
        plan = generate_remediation_plan(summary)
        html = format_remediation_html(plan)
        assert "CMD-001" in html

    def test_contains_footer(self):
        summary = _make_summary()
        plan = generate_remediation_plan(summary)
        html = format_remediation_html(plan)
        assert "mcpaudit" in html

    def test_empty_plan(self):
        plan = RemediationPlan(timestamp="now")
        html = format_remediation_html(plan)
        assert "<!DOCTYPE html>" in html


# ---------------------------------------------------------------------------
# Public helpers
# ---------------------------------------------------------------------------

class TestGetKnownRules:
    def test_returns_list(self):
        rules = get_known_rules()
        assert isinstance(rules, list)
        assert len(rules) > 0

    def test_sorted(self):
        rules = get_known_rules()
        assert rules == sorted(rules)

    def test_contains_cmd001(self):
        rules = get_known_rules()
        assert "CMD-001" in rules


class TestGetFixTypes:
    def test_returns_all_types(self):
        types = get_fix_types()
        assert "code_change" in types
        assert "config_change" in types
        assert "manual" in types

    def test_count(self):
        types = get_fix_types()
        assert len(types) == 3


class TestGetRemediationInfo:
    def test_known_rule(self):
        info = get_remediation_info("CMD-001")
        assert info is not None
        assert info["rule_id"] == "CMD-001"
        assert "title" in info
        assert "fix_type" in info

    def test_unknown_rule(self):
        info = get_remediation_info("NONEXISTENT-999")
        assert info is None

    def test_has_effort(self):
        info = get_remediation_info("CMD-001")
        assert info is not None
        assert info["effort_minutes"] > 0

    def test_has_steps_count(self):
        info = get_remediation_info("SQL-001")
        assert info is not None
        assert info["steps_count"] > 0
