"""Tests for the Automated Fix Planner module."""

from __future__ import annotations

import json

import pytest

from mcpaudit.fix_planner import (
    CodeSnippet,
    FixCategory,
    FixItem,
    FixPlan,
    FixPriority,
    Sprint,
    create_fix_plan,
    create_sprints,
    format_fix_plan_json,
    format_fix_plan_text,
    format_sprints_json,
    format_sprints_text,
)


# ===========================================================================
# FixPriority
# ===========================================================================

class TestFixPriority:
    def test_weight_order(self):
        assert FixPriority.CRITICAL.weight < FixPriority.HIGH.weight
        assert FixPriority.HIGH.weight < FixPriority.MEDIUM.weight
        assert FixPriority.MEDIUM.weight < FixPriority.LOW.weight
        assert FixPriority.LOW.weight < FixPriority.INFO.weight

    def test_from_severity(self):
        assert FixPriority.from_severity("CRITICAL") == FixPriority.CRITICAL
        assert FixPriority.from_severity("HIGH") == FixPriority.HIGH
        assert FixPriority.from_severity("MEDIUM") == FixPriority.MEDIUM
        assert FixPriority.from_severity("LOW") == FixPriority.LOW
        assert FixPriority.from_severity("INFO") == FixPriority.INFO

    def test_from_severity_case_insensitive(self):
        assert FixPriority.from_severity("critical") == FixPriority.CRITICAL

    def test_from_severity_unknown(self):
        assert FixPriority.from_severity("UNKNOWN") == FixPriority.MEDIUM

    def test_lt(self):
        assert FixPriority.CRITICAL < FixPriority.HIGH
        assert not FixPriority.HIGH < FixPriority.CRITICAL

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


# ===========================================================================
# FixCategory
# ===========================================================================

class TestFixCategory:
    def test_values(self):
        assert FixCategory.CODE_CHANGE.value == "code_change"
        assert FixCategory.CONFIG_CHANGE.value == "config_change"
        assert FixCategory.ARCHITECTURE.value == "architecture"
        assert FixCategory.PROCESS.value == "process"
        assert FixCategory.MANUAL_REVIEW.value == "manual_review"


# ===========================================================================
# CodeSnippet
# ===========================================================================

class TestCodeSnippet:
    def test_to_dict_full(self):
        s = CodeSnippet(
            language="python",
            before="old_code()",
            after="new_code()",
            description="Fix the code",
            file_pattern="*.py",
        )
        d = s.to_dict()
        assert d["language"] == "python"
        assert d["before"] == "old_code()"
        assert d["after"] == "new_code()"
        assert d["file_pattern"] == "*.py"

    def test_to_dict_minimal(self):
        s = CodeSnippet(description="test")
        d = s.to_dict()
        assert "before" not in d
        assert "after" not in d

    def test_defaults(self):
        s = CodeSnippet()
        assert s.language == "python"
        assert s.before == ""


# ===========================================================================
# FixItem
# ===========================================================================

class TestFixItem:
    def test_to_dict(self):
        item = FixItem(
            fix_id="FIX-001",
            rule_id="CMD-001",
            title="Fix injection",
            description="test",
            priority=FixPriority.CRITICAL,
            category=FixCategory.CODE_CHANGE,
            effort_hours=1.0,
            trust_score_impact=15,
            depends_on=["FIX-002"],
            affected_files=["server.py"],
            finding_count=3,
        )
        d = item.to_dict()
        assert d["fix_id"] == "FIX-001"
        assert d["priority"] == "critical"
        assert d["effort_hours"] == 1.0
        assert d["trust_score_impact"] == 15
        assert d["depends_on"] == ["FIX-002"]

    def test_snippets_serialized(self):
        item = FixItem(
            fix_id="FIX-001", rule_id="X", title="T", description="",
            priority=FixPriority.MEDIUM, category=FixCategory.CODE_CHANGE,
            snippets=[CodeSnippet(before="a", after="b", description="fix")],
        )
        d = item.to_dict()
        assert len(d["snippets"]) == 1


# ===========================================================================
# Sprint
# ===========================================================================

class TestSprint:
    def test_item_count(self):
        s = Sprint(sprint_number=1, name="S1", max_hours=8)
        assert s.item_count == 0

    def test_remaining_hours(self):
        s = Sprint(sprint_number=1, name="S1", max_hours=8, total_hours=3)
        assert s.remaining_hours == 5.0

    def test_total_trust_impact(self):
        item1 = FixItem(
            fix_id="F1", rule_id="R1", title="T1", description="",
            priority=FixPriority.HIGH, category=FixCategory.CODE_CHANGE,
            trust_score_impact=10,
        )
        item2 = FixItem(
            fix_id="F2", rule_id="R2", title="T2", description="",
            priority=FixPriority.MEDIUM, category=FixCategory.CODE_CHANGE,
            trust_score_impact=5,
        )
        s = Sprint(sprint_number=1, name="S1", items=[item1, item2], max_hours=8)
        assert s.total_trust_impact == 15

    def test_priority_breakdown(self):
        item1 = FixItem(
            fix_id="F1", rule_id="R1", title="T1", description="",
            priority=FixPriority.HIGH, category=FixCategory.CODE_CHANGE,
        )
        item2 = FixItem(
            fix_id="F2", rule_id="R2", title="T2", description="",
            priority=FixPriority.HIGH, category=FixCategory.CODE_CHANGE,
        )
        s = Sprint(sprint_number=1, name="S1", items=[item1, item2], max_hours=8)
        breakdown = s.priority_breakdown
        assert breakdown["high"] == 2

    def test_to_dict(self):
        s = Sprint(sprint_number=1, name="Sprint 1", max_hours=8, total_hours=3)
        d = s.to_dict()
        assert d["sprint_number"] == 1
        assert d["remaining_hours"] == 5.0


# ===========================================================================
# FixPlan
# ===========================================================================

class TestFixPlan:
    def test_empty_plan(self):
        p = FixPlan()
        assert p.item_count == 0
        assert p.total_effort_hours == 0

    def test_properties(self):
        p = FixPlan(
            priority_counts={"critical": 2, "high": 1},
        )
        assert p.critical_count == 2
        assert p.high_count == 1

    def test_get_item(self):
        item = FixItem(
            fix_id="FIX-001", rule_id="R1", title="T", description="",
            priority=FixPriority.HIGH, category=FixCategory.CODE_CHANGE,
        )
        p = FixPlan(items=[item])
        assert p.get_item("FIX-001") is not None
        assert p.get_item("FIX-999") is None

    def test_to_dict(self):
        p = FixPlan(total_effort_hours=5.5, total_trust_impact=30)
        d = p.to_dict()
        assert d["total_effort_hours"] == 5.5
        assert d["total_trust_impact"] == 30
        assert "timestamp" in d


# ===========================================================================
# create_fix_plan
# ===========================================================================

class TestCreateFixPlan:
    def test_empty_findings(self):
        plan = create_fix_plan([])
        assert plan.item_count == 0

    def test_single_finding(self):
        findings = [
            {"rule_id": "CMD-001", "severity": "CRITICAL", "file": "server.py",
             "title": "Command injection", "description": "Unsafe os call"},
        ]
        plan = create_fix_plan(findings)
        assert plan.item_count == 1
        assert plan.items[0].rule_id == "CMD-001"
        assert plan.items[0].priority == FixPriority.CRITICAL

    def test_multiple_same_rule(self):
        findings = [
            {"rule_id": "CMD-001", "severity": "CRITICAL", "file": "a.py"},
            {"rule_id": "CMD-001", "severity": "CRITICAL", "file": "b.py"},
        ]
        plan = create_fix_plan(findings)
        assert plan.item_count == 1
        assert plan.items[0].finding_count == 2
        assert len(plan.items[0].affected_files) == 2

    def test_multiple_rules(self):
        findings = [
            {"rule_id": "CMD-001", "severity": "CRITICAL"},
            {"rule_id": "SQL-001", "severity": "CRITICAL"},
            {"rule_id": "VAL-001", "severity": "MEDIUM"},
        ]
        plan = create_fix_plan(findings)
        assert plan.item_count == 3

    def test_effort_estimation(self):
        findings = [{"rule_id": "CMD-001", "severity": "CRITICAL"}]
        plan = create_fix_plan(findings)
        assert plan.items[0].effort_hours == 1.0
        assert plan.total_effort_hours == 1.0

    def test_trust_impact(self):
        findings = [{"rule_id": "CMD-001", "severity": "CRITICAL"}]
        plan = create_fix_plan(findings)
        assert plan.items[0].trust_score_impact > 0
        assert plan.total_trust_impact > 0

    def test_code_snippets_included(self):
        findings = [{"rule_id": "CMD-001", "severity": "CRITICAL"}]
        plan = create_fix_plan(findings)
        assert len(plan.items[0].snippets) > 0

    def test_unknown_rule_uses_default(self):
        findings = [{"rule_id": "UNKNOWN-999", "severity": "HIGH"}]
        plan = create_fix_plan(findings)
        assert plan.item_count == 1
        assert plan.items[0].category == FixCategory.MANUAL_REVIEW

    def test_dependency_ordering(self):
        # CMD-002 depends on CMD-001, AUTHZ-001 depends on AUTH-001
        findings = [
            {"rule_id": "CMD-002", "severity": "HIGH"},
            {"rule_id": "CMD-001", "severity": "CRITICAL"},
        ]
        plan = create_fix_plan(findings)
        # CMD-001 should come before CMD-002 in execution order
        order = plan.execution_order
        fix_map = {i.rule_id: i.fix_id for i in plan.items}
        cmd1_idx = order.index(fix_map["CMD-001"])
        cmd2_idx = order.index(fix_map["CMD-002"])
        assert cmd1_idx < cmd2_idx

    def test_auth_dependency_chain(self):
        findings = [
            {"rule_id": "IDOR-001", "severity": "HIGH"},
            {"rule_id": "AUTHZ-001", "severity": "HIGH"},
            {"rule_id": "AUTH-001", "severity": "CRITICAL"},
        ]
        plan = create_fix_plan(findings)
        order = plan.execution_order
        fix_map = {i.rule_id: i.fix_id for i in plan.items}
        auth_idx = order.index(fix_map["AUTH-001"])
        authz_idx = order.index(fix_map["AUTHZ-001"])
        idor_idx = order.index(fix_map["IDOR-001"])
        assert auth_idx < authz_idx
        assert authz_idx < idor_idx

    def test_priority_counts(self):
        findings = [
            {"rule_id": "CMD-001", "severity": "CRITICAL"},
            {"rule_id": "SQL-001", "severity": "CRITICAL"},
            {"rule_id": "VAL-001", "severity": "MEDIUM"},
        ]
        plan = create_fix_plan(findings)
        assert plan.priority_counts.get("critical", 0) == 2
        assert plan.priority_counts.get("medium", 0) == 1

    def test_category_counts(self):
        findings = [
            {"rule_id": "CMD-001", "severity": "CRITICAL"},
            {"rule_id": "SEC-001", "severity": "MEDIUM"},
        ]
        plan = create_fix_plan(findings)
        assert "code_change" in plan.category_counts
        assert "config_change" in plan.category_counts

    def test_execution_order_complete(self):
        findings = [
            {"rule_id": "CMD-001", "severity": "CRITICAL"},
            {"rule_id": "VAL-001", "severity": "MEDIUM"},
        ]
        plan = create_fix_plan(findings)
        assert len(plan.execution_order) == plan.item_count

    def test_affected_files_collected(self):
        findings = [
            {"rule_id": "CMD-001", "severity": "CRITICAL", "file": "a.py"},
            {"rule_id": "CMD-001", "severity": "CRITICAL", "file": "b.py"},
            {"rule_id": "CMD-001", "severity": "CRITICAL", "file": "a.py"},
        ]
        plan = create_fix_plan(findings)
        assert plan.items[0].affected_files == ["a.py", "b.py"]

    def test_tags_included(self):
        findings = [{"rule_id": "CMD-001", "severity": "CRITICAL"}]
        plan = create_fix_plan(findings)
        assert "injection" in plan.items[0].tags


# ===========================================================================
# create_sprints
# ===========================================================================

class TestCreateSprints:
    def test_empty_plan(self):
        plan = FixPlan()
        sprints = create_sprints(plan, 8)
        assert len(sprints) == 0

    def test_single_sprint(self):
        findings = [
            {"rule_id": "CMD-001", "severity": "CRITICAL"},  # 1h
            {"rule_id": "VAL-001", "severity": "MEDIUM"},    # 1h
        ]
        plan = create_fix_plan(findings)
        sprints = create_sprints(plan, 8)
        assert len(sprints) == 1
        assert sprints[0].item_count == 2

    def test_multiple_sprints(self):
        findings = [
            {"rule_id": "AUTH-001", "severity": "CRITICAL"},  # 4h
            {"rule_id": "CMD-001", "severity": "CRITICAL"},   # 1h
            {"rule_id": "SQL-001", "severity": "CRITICAL"},   # 1h
            {"rule_id": "RATE-001", "severity": "HIGH"},      # 2h
            {"rule_id": "AUTHZ-001", "severity": "HIGH"},     # 2h
        ]
        plan = create_fix_plan(findings)
        sprints = create_sprints(plan, 4)
        assert len(sprints) >= 2

    def test_sprint_respects_capacity(self):
        findings = [
            {"rule_id": "CMD-001", "severity": "CRITICAL"},
            {"rule_id": "SQL-001", "severity": "CRITICAL"},
            {"rule_id": "PATH-001", "severity": "HIGH"},
        ]
        plan = create_fix_plan(findings)
        sprints = create_sprints(plan, 2)
        for s in sprints:
            # Each sprint should not greatly exceed capacity
            # (single items may exceed if they're larger than sprint size)
            if s.item_count > 1:
                assert s.total_hours <= 2.0 + max(
                    i.effort_hours for i in s.items
                )

    def test_sprint_numbering(self):
        findings = [
            {"rule_id": "AUTH-001", "severity": "CRITICAL"},
            {"rule_id": "CMD-001", "severity": "CRITICAL"},
            {"rule_id": "SQL-001", "severity": "CRITICAL"},
        ]
        plan = create_fix_plan(findings)
        sprints = create_sprints(plan, 2)
        for i, s in enumerate(sprints):
            assert s.sprint_number == i + 1

    def test_sprint_names(self):
        findings = [
            {"rule_id": "CMD-001", "severity": "CRITICAL"},
            {"rule_id": "AUTH-001", "severity": "CRITICAL"},
        ]
        plan = create_fix_plan(findings)
        sprints = create_sprints(plan, 1)
        for s in sprints:
            assert s.name.startswith("Sprint")

    def test_zero_hours_returns_empty(self):
        findings = [{"rule_id": "CMD-001", "severity": "CRITICAL"}]
        plan = create_fix_plan(findings)
        sprints = create_sprints(plan, 0)
        assert len(sprints) == 0

    def test_sprint_to_dict(self):
        findings = [{"rule_id": "CMD-001", "severity": "CRITICAL"}]
        plan = create_fix_plan(findings)
        sprints = create_sprints(plan, 8)
        d = sprints[0].to_dict()
        assert "sprint_number" in d
        assert "total_hours" in d
        assert "items" in d

    def test_all_items_in_sprints(self):
        findings = [
            {"rule_id": "CMD-001", "severity": "CRITICAL"},
            {"rule_id": "SQL-001", "severity": "CRITICAL"},
            {"rule_id": "VAL-001", "severity": "MEDIUM"},
            {"rule_id": "LOG-001", "severity": "LOW"},
        ]
        plan = create_fix_plan(findings)
        sprints = create_sprints(plan, 3)
        total_in_sprints = sum(s.item_count for s in sprints)
        assert total_in_sprints == plan.item_count

    def test_sprint_trust_impact(self):
        findings = [
            {"rule_id": "CMD-001", "severity": "CRITICAL"},
            {"rule_id": "SQL-001", "severity": "CRITICAL"},
        ]
        plan = create_fix_plan(findings)
        sprints = create_sprints(plan, 10)
        total_impact = sum(s.total_trust_impact for s in sprints)
        assert total_impact == plan.total_trust_impact


# ===========================================================================
# Formatters
# ===========================================================================

class TestFormatters:
    def _make_plan(self):
        return create_fix_plan([
            {"rule_id": "CMD-001", "severity": "CRITICAL", "file": "s.py"},
            {"rule_id": "SQL-001", "severity": "CRITICAL"},
            {"rule_id": "VAL-001", "severity": "MEDIUM"},
        ])

    def test_format_plan_text(self):
        plan = self._make_plan()
        text = format_fix_plan_text(plan)
        assert "Automated Fix Plan" in text
        assert "CMD-001" in text or "FIX-" in text
        assert "Execution order" in text

    def test_format_plan_json(self):
        plan = self._make_plan()
        j = format_fix_plan_json(plan)
        data = json.loads(j)
        assert "item_count" in data
        assert data["item_count"] == 3

    def test_format_sprints_text(self):
        plan = self._make_plan()
        sprints = create_sprints(plan, 4)
        text = format_sprints_text(sprints)
        assert "Sprint Plan" in text
        assert "Sprint 1" in text

    def test_format_sprints_json(self):
        plan = self._make_plan()
        sprints = create_sprints(plan, 4)
        j = format_sprints_json(sprints)
        data = json.loads(j)
        assert isinstance(data, list)
        assert len(data) >= 1

    def test_format_plan_text_with_deps(self):
        plan = create_fix_plan([
            {"rule_id": "CMD-001", "severity": "CRITICAL"},
            {"rule_id": "CMD-002", "severity": "HIGH"},
        ])
        text = format_fix_plan_text(plan)
        assert "FIX-" in text

    def test_format_plan_text_with_snippets(self):
        plan = create_fix_plan([
            {"rule_id": "SQL-001", "severity": "CRITICAL"},
        ])
        text = format_fix_plan_text(plan)
        assert "Before:" in text or "After:" in text

    def test_format_empty_plan(self):
        plan = FixPlan()
        text = format_fix_plan_text(plan)
        assert "Items: 0" in text

    def test_format_empty_sprints(self):
        text = format_sprints_text([])
        assert "Sprint Plan (0 sprints)" in text
