"""Tests for plan models and checkpoint management."""

import pytest
from xyglue.plan.models import Plan, Step, Checkpoint
from xyglue.plan.checkpoint import CheckpointManager, CheckpointRules


class TestStep:
    def test_basic_step(self):
        s = Step(number=1, action="fetch", tool="http")
        assert s.number == 1
        assert s.tool == "http"
        assert s.idempotent is True

    def test_has_fallback(self):
        s = Step(number=1, tool="http", fallback_tool="curl")
        assert s.has_fallback is True
        s2 = Step(number=2, tool="http")
        assert s2.has_fallback is False

    def test_has_rollback(self):
        s = Step(number=1, tool="db", rollback_action="undo")
        assert s.has_rollback is True

    def test_retry_tracking(self):
        s = Step(number=1, tool="http", max_retries=2)
        assert s.can_retry is True
        s.record_retry()
        assert s.retry_count == 1
        assert s.can_retry is True
        s.record_retry()
        assert s.retry_count == 2
        assert s.can_retry is False

    def test_to_dict_and_from_dict(self):
        s = Step(
            number=1, action="fetch", tool="http",
            params={"url": "example.com"},
            fallback_tool="curl",
            depends_on=[0],
        )
        d = s.to_dict()
        assert d["number"] == 1
        assert d["fallback_tool"] == "curl"

        s2 = Step.from_dict(d)
        assert s2.number == 1
        assert s2.fallback_tool == "curl"
        assert s2.depends_on == [0]

    def test_timeout_default(self):
        s = Step(number=1, tool="http")
        assert s.timeout_ms == 30000


class TestCheckpoint:
    def test_basic_checkpoint(self):
        cp = Checkpoint(step_number=5, state={"key": "val"})
        assert cp.step_number == 5
        assert cp.state["key"] == "val"

    def test_to_dict_and_from_dict(self):
        cp = Checkpoint(step_number=3, state={"x": 1}, outputs=["out1"])
        d = cp.to_dict()
        cp2 = Checkpoint.from_dict(d)
        assert cp2.step_number == 3
        assert cp2.state == {"x": 1}


class TestPlan:
    def test_empty_plan_validation(self):
        p = Plan()
        issues = p.validate()
        assert "Plan has no steps" in issues

    def test_plan_step_count(self, sample_plan):
        assert sample_plan.step_count == 3

    def test_duplicate_step_numbers(self):
        p = Plan(steps=[
            Step(number=1, tool="a"),
            Step(number=1, tool="b"),
        ])
        issues = p.validate()
        assert "Duplicate step numbers found" in issues

    def test_invalid_dependency(self):
        p = Plan(steps=[
            Step(number=1, tool="a"),
            Step(number=2, tool="b", depends_on=[99]),
        ])
        issues = p.validate()
        assert any("non-existent" in i for i in issues)

    def test_forward_dependency(self):
        p = Plan(steps=[
            Step(number=1, tool="a", depends_on=[2]),
            Step(number=2, tool="b"),
        ])
        issues = p.validate()
        assert any("later step" in i for i in issues)

    def test_get_step(self, sample_plan):
        s = sample_plan.get_step(2)
        assert s is not None
        assert s.tool == "parser"

    def test_get_step_none(self, sample_plan):
        assert sample_plan.get_step(99) is None

    def test_to_dict_and_from_dict(self, sample_plan):
        d = sample_plan.to_dict()
        p2 = Plan.from_dict(d)
        assert p2.step_count == 3
        assert p2.steps[0].tool == "http"

    def test_checkpoint_steps(self):
        p = Plan(steps=[
            Step(number=1, tool="a", checkpoint=True),
            Step(number=2, tool="b", checkpoint=False),
            Step(number=3, tool="c", checkpoint=True),
        ])
        cp_steps = p.get_checkpoint_steps()
        assert len(cp_steps) == 2


class TestCheckpointRules:
    def test_first_step_checkpoint(self):
        rules = CheckpointRules(checkpoint_first_step=True)
        s = Step(number=0, tool="a")
        assert rules.should_checkpoint(s, 0, 5, None) is True

    def test_last_step_checkpoint(self):
        rules = CheckpointRules(checkpoint_last_step=True)
        s = Step(number=4, tool="a")
        assert rules.should_checkpoint(s, 4, 5, None) is True

    def test_every_n_steps(self):
        rules = CheckpointRules(checkpoint_every_n_steps=3)
        s = Step(number=3, tool="a")
        assert rules.should_checkpoint(s, 2, 10, None) is True  # index 2 -> step 3

    def test_tool_change(self):
        rules = CheckpointRules(checkpoint_on_tool_change=True)
        s = Step(number=2, tool="parser")
        assert rules.should_checkpoint(s, 1, 5, "http") is True

    def test_risky_tool(self):
        rules = CheckpointRules(checkpoint_before_risky=True)
        s = Step(number=1, tool="database")
        assert rules.should_checkpoint(s, 1, 5, None) is True

    def test_risky_action(self):
        rules = CheckpointRules(checkpoint_before_risky=True)
        s = Step(number=1, tool="custom", action="delete records")
        assert rules.should_checkpoint(s, 1, 5, None) is True


class TestCheckpointManager:
    def test_save_and_get(self):
        mgr = CheckpointManager()
        cp = mgr.save(1, {"key": "val"})
        assert cp.step_number == 1
        assert mgr.count == 1

    def test_get_latest(self):
        mgr = CheckpointManager()
        mgr.save(1, {"a": 1})
        mgr.save(3, {"b": 2})
        latest = mgr.get_latest()
        assert latest is not None
        assert latest.step_number == 3

    def test_get_by_step(self):
        mgr = CheckpointManager()
        mgr.save(1, {"a": 1})
        mgr.save(2, {"b": 2})
        cp = mgr.get_by_step(1)
        assert cp is not None
        assert cp.state == {"a": 1}

    def test_rollback_to(self):
        mgr = CheckpointManager()
        mgr.save(1, {"a": 1})
        mgr.save(3, {"b": 2})
        mgr.save(5, {"c": 3})
        cp = mgr.rollback_to(3)
        assert cp is not None
        assert cp.step_number == 3
        assert mgr.count == 2  # Only 1 and 3 remain

    def test_state_diff(self):
        mgr = CheckpointManager()
        cp1 = mgr.save(1, {"a": 1, "b": 2})
        cp2 = mgr.save(2, {"a": 1, "c": 3})
        diff = mgr.state_diff(cp1, cp2)
        assert "b" in diff["removed"]
        assert "c" in diff["added"]

    def test_integrity_check_valid(self):
        mgr = CheckpointManager()
        mgr.save(1, {})
        mgr.save(2, {})
        mgr.save(3, {})
        assert mgr.integrity_check() == []

    def test_integrity_check_invalid(self):
        mgr = CheckpointManager()
        mgr.save(3, {})
        mgr.save(1, {})  # Out of order
        issues = mgr.integrity_check()
        assert len(issues) > 0

    def test_compute_state_hash(self):
        mgr = CheckpointManager()
        h1 = mgr.compute_state_hash({"a": 1, "b": 2})
        h2 = mgr.compute_state_hash({"b": 2, "a": 1})  # Same keys, diff order
        assert h1 == h2  # Deterministic

    def test_enrich_plan_checkpoints(self):
        mgr = CheckpointManager()
        steps = [
            Step(number=1, tool="http"),
            Step(number=2, tool="parser"),
            Step(number=3, tool="database"),
        ]
        enriched = mgr.enrich_plan_checkpoints(steps)
        # First and last should be checkpointed
        assert enriched[0].checkpoint is True
        assert enriched[2].checkpoint is True

    def test_clear(self):
        mgr = CheckpointManager()
        mgr.save(1, {})
        mgr.save(2, {})
        mgr.clear()
        assert mgr.count == 0
