"""Tests for openjarvis.learning.spec_search.models module."""

from __future__ import annotations

# ---------------------------------------------------------------------------
# Enums
# ---------------------------------------------------------------------------


class TestEditPillar:
    """Tests for EditPillar enum."""

    def test_has_four_pillars(self) -> None:
        from openjarvis.learning.spec_search.models import EditPillar

        assert EditPillar.INTELLIGENCE.value == "intelligence"
        assert EditPillar.AGENT.value == "agent"
        assert EditPillar.TOOLS.value == "tools"
        assert EditPillar.ENGINE.value == "engine"

    def test_is_string_enum(self) -> None:
        from openjarvis.learning.spec_search.models import EditPillar

        assert isinstance(EditPillar.AGENT, str)
        assert EditPillar("agent") is EditPillar.AGENT


class TestEditRiskTier:
    """Tests for EditRiskTier enum."""

    def test_has_three_tiers(self) -> None:
        from openjarvis.learning.spec_search.models import EditRiskTier

        assert EditRiskTier.AUTO.value == "auto"
        assert EditRiskTier.REVIEW.value == "review"
        assert EditRiskTier.MANUAL.value == "manual"


class TestEditOp:
    """Tests for EditOp enum — must contain all v1 ops plus v2 placeholders."""

    def test_intelligence_ops(self) -> None:
        from openjarvis.learning.spec_search.models import EditOp

        assert EditOp.SET_MODEL_FOR_QUERY_CLASS.value == "set_model_for_query_class"
        assert EditOp.SET_MODEL_PARAM.value == "set_model_param"

    def test_agent_ops(self) -> None:
        from openjarvis.learning.spec_search.models import EditOp

        assert EditOp.PATCH_SYSTEM_PROMPT.value == "patch_system_prompt"
        assert EditOp.REPLACE_SYSTEM_PROMPT.value == "replace_system_prompt"
        assert EditOp.SET_AGENT_CLASS.value == "set_agent_class"
        assert EditOp.SET_AGENT_PARAM.value == "set_agent_param"
        assert EditOp.EDIT_FEW_SHOT_EXEMPLARS.value == "edit_few_shot_exemplars"

    def test_tools_ops(self) -> None:
        from openjarvis.learning.spec_search.models import EditOp

        assert EditOp.ADD_TOOL_TO_AGENT.value == "add_tool_to_agent"
        assert EditOp.REMOVE_TOOL_FROM_AGENT.value == "remove_tool_from_agent"
        assert EditOp.EDIT_TOOL_DESCRIPTION.value == "edit_tool_description"

    def test_v2_placeholder_ops(self) -> None:
        from openjarvis.learning.spec_search.models import EditOp

        assert EditOp.LORA_FINETUNE.value == "lora_finetune"


class TestTriggerKind:
    """Tests for TriggerKind enum."""

    def test_four_trigger_kinds(self) -> None:
        from openjarvis.learning.spec_search.models import TriggerKind

        assert TriggerKind.SCHEDULED.value == "scheduled"
        assert TriggerKind.CLUSTER.value == "cluster"
        assert TriggerKind.USER_FLAG.value == "user_flag"
        assert TriggerKind.ON_DEMAND.value == "on_demand"


class TestAutonomyMode:
    """Tests for AutonomyMode enum."""

    def test_three_modes(self) -> None:
        from openjarvis.learning.spec_search.models import AutonomyMode

        assert AutonomyMode.AUTO.value == "auto"
        assert AutonomyMode.TIERED.value == "tiered"
        assert AutonomyMode.MANUAL.value == "manual"


class TestSessionStatus:
    """Tests for SessionStatus enum."""

    def test_all_statuses(self) -> None:
        from openjarvis.learning.spec_search.models import SessionStatus

        assert SessionStatus.INITIATED.value == "initiated"
        assert SessionStatus.DIAGNOSING.value == "diagnosing"
        assert SessionStatus.PLANNING.value == "planning"
        assert SessionStatus.EXECUTING.value == "executing"
        assert SessionStatus.AWAITING_REVIEW.value == "awaiting_review"
        assert SessionStatus.COMPLETED.value == "completed"
        assert SessionStatus.FAILED.value == "failed"
        assert SessionStatus.ROLLED_BACK.value == "rolled_back"


# ---------------------------------------------------------------------------
# Edit
# ---------------------------------------------------------------------------


class TestEdit:
    """Tests for Edit pydantic model."""

    def _valid_edit_kwargs(self) -> dict:
        from openjarvis.learning.spec_search.models import (
            EditOp,
            EditPillar,
            EditRiskTier,
        )

        return {
            "id": "11111111-2222-3333-4444-555555555555",
            "pillar": EditPillar.INTELLIGENCE,
            "op": EditOp.SET_MODEL_FOR_QUERY_CLASS,
            "target": "learning.routing.policy_map.math",
            "payload": {"query_class": "math", "model": "qwen2.5-coder:14b"},
            "rationale": "Math queries are misrouted to qwen-3b",
            "expected_improvement": "math_failures cluster",
            "risk_tier": EditRiskTier.AUTO,
            "references": ["trace-001", "trace-002"],
        }

    def test_constructs_with_valid_fields(self) -> None:
        from openjarvis.learning.spec_search.models import Edit

        edit = Edit(**self._valid_edit_kwargs())

        assert edit.id == "11111111-2222-3333-4444-555555555555"
        assert edit.target == "learning.routing.policy_map.math"
        assert edit.payload == {"query_class": "math", "model": "qwen2.5-coder:14b"}
        assert edit.references == ["trace-001", "trace-002"]

    def test_round_trip_via_json(self) -> None:
        from openjarvis.learning.spec_search.models import Edit

        edit = Edit(**self._valid_edit_kwargs())
        as_json = edit.model_dump_json()
        restored = Edit.model_validate_json(as_json)

        assert restored == edit

    def test_pillar_must_be_valid_enum(self) -> None:
        import pytest
        from pydantic import ValidationError

        from openjarvis.learning.spec_search.models import Edit

        kwargs = self._valid_edit_kwargs()
        kwargs["pillar"] = "not_a_pillar"

        with pytest.raises(ValidationError):
            Edit(**kwargs)

    def test_op_must_be_valid_enum(self) -> None:
        import pytest
        from pydantic import ValidationError

        from openjarvis.learning.spec_search.models import Edit

        kwargs = self._valid_edit_kwargs()
        kwargs["op"] = "not_an_op"

        with pytest.raises(ValidationError):
            Edit(**kwargs)

    def test_payload_can_be_empty_dict(self) -> None:
        from openjarvis.learning.spec_search.models import Edit

        kwargs = self._valid_edit_kwargs()
        kwargs["payload"] = {}

        edit = Edit(**kwargs)
        assert edit.payload == {}

    def test_references_default_empty_list(self) -> None:
        from openjarvis.learning.spec_search.models import Edit

        kwargs = self._valid_edit_kwargs()
        del kwargs["references"]

        edit = Edit(**kwargs)
        assert edit.references == []


# ---------------------------------------------------------------------------
# FailureCluster
# ---------------------------------------------------------------------------


class TestFailureCluster:
    """Tests for FailureCluster pydantic model."""

    def _valid_cluster_kwargs(self) -> dict:
        return {
            "id": "cluster-001",
            "description": "Math word problems routed to qwen-3b",
            "sample_trace_ids": ["trace-001", "trace-002", "trace-003"],
            "student_failure_rate": 0.85,
            "teacher_success_rate": 0.95,
            "skill_gap": (
                "Student lacks chain-of-thought reasoning on multi-step arithmetic."
            ),
            "addressed_by_edit_ids": ["edit-001", "edit-002"],
        }

    def test_constructs_with_valid_fields(self) -> None:
        from openjarvis.learning.spec_search.models import FailureCluster

        cluster = FailureCluster(**self._valid_cluster_kwargs())

        assert cluster.id == "cluster-001"
        assert cluster.student_failure_rate == 0.85
        assert cluster.teacher_success_rate == 0.95
        assert len(cluster.sample_trace_ids) == 3
        assert len(cluster.addressed_by_edit_ids) == 2

    def test_round_trip_via_json(self) -> None:
        from openjarvis.learning.spec_search.models import FailureCluster

        cluster = FailureCluster(**self._valid_cluster_kwargs())
        as_json = cluster.model_dump_json()
        restored = FailureCluster.model_validate_json(as_json)

        assert restored == cluster

    def test_addressed_by_edit_ids_defaults_empty(self) -> None:
        from openjarvis.learning.spec_search.models import FailureCluster

        kwargs = self._valid_cluster_kwargs()
        del kwargs["addressed_by_edit_ids"]

        cluster = FailureCluster(**kwargs)
        assert cluster.addressed_by_edit_ids == []

    def test_failure_rate_must_be_between_zero_and_one(self) -> None:
        import pytest
        from pydantic import ValidationError

        from openjarvis.learning.spec_search.models import FailureCluster

        kwargs = self._valid_cluster_kwargs()
        kwargs["student_failure_rate"] = 1.5

        with pytest.raises(ValidationError):
            FailureCluster(**kwargs)

    def test_success_rate_must_be_between_zero_and_one(self) -> None:
        import pytest
        from pydantic import ValidationError

        from openjarvis.learning.spec_search.models import FailureCluster

        kwargs = self._valid_cluster_kwargs()
        kwargs["teacher_success_rate"] = -0.1

        with pytest.raises(ValidationError):
            FailureCluster(**kwargs)


# ---------------------------------------------------------------------------
# LearningPlan
# ---------------------------------------------------------------------------


class TestLearningPlan:
    """Tests for LearningPlan pydantic model."""

    def _valid_plan_kwargs(self) -> dict:
        from datetime import datetime, timezone

        from openjarvis.learning.spec_search.models import (
            Edit,
            EditOp,
            EditPillar,
            EditRiskTier,
            FailureCluster,
        )

        cluster = FailureCluster(
            id="cluster-001",
            description="Math routed to qwen-3b",
            sample_trace_ids=["t1", "t2", "t3"],
            student_failure_rate=0.8,
            teacher_success_rate=0.9,
            skill_gap="needs CoT",
            addressed_by_edit_ids=["edit-001"],
        )
        edit = Edit(
            id="edit-001",
            pillar=EditPillar.INTELLIGENCE,
            op=EditOp.SET_MODEL_FOR_QUERY_CLASS,
            target="learning.routing.policy_map.math",
            payload={"query_class": "math", "model": "qwen2.5-coder:14b"},
            rationale="Math fails on small model",
            expected_improvement="cluster-001",
            risk_tier=EditRiskTier.AUTO,
            references=["t1"],
        )
        return {
            "session_id": "session-001",
            "diagnosis_summary": "## Diagnosis\nThe student misroutes math.",
            "failure_clusters": [cluster],
            "edits": [edit],
            "teacher_model": "claude-opus-4-6",
            "estimated_cost_usd": 1.42,
            "created_at": datetime(2026, 4, 8, 14, 22, 1, tzinfo=timezone.utc),
        }

    def test_constructs_with_valid_fields(self) -> None:
        from openjarvis.learning.spec_search.models import LearningPlan

        plan = LearningPlan(**self._valid_plan_kwargs())

        assert plan.session_id == "session-001"
        assert len(plan.failure_clusters) == 1
        assert len(plan.edits) == 1
        assert plan.teacher_model == "claude-opus-4-6"
        assert plan.estimated_cost_usd == 1.42

    def test_round_trip_via_json(self) -> None:
        from openjarvis.learning.spec_search.models import LearningPlan

        plan = LearningPlan(**self._valid_plan_kwargs())
        as_json = plan.model_dump_json()
        restored = LearningPlan.model_validate_json(as_json)

        assert restored == plan

    def test_empty_clusters_and_edits_allowed(self) -> None:
        # An aborted session may produce a plan with no clusters and no edits.
        from openjarvis.learning.spec_search.models import LearningPlan

        kwargs = self._valid_plan_kwargs()
        kwargs["failure_clusters"] = []
        kwargs["edits"] = []

        plan = LearningPlan(**kwargs)
        assert plan.failure_clusters == []
        assert plan.edits == []

    def test_estimated_cost_must_be_non_negative(self) -> None:
        import pytest
        from pydantic import ValidationError

        from openjarvis.learning.spec_search.models import LearningPlan

        kwargs = self._valid_plan_kwargs()
        kwargs["estimated_cost_usd"] = -1.0

        with pytest.raises(ValidationError):
            LearningPlan(**kwargs)


# ---------------------------------------------------------------------------
# BenchmarkSnapshot
# ---------------------------------------------------------------------------


class TestBenchmarkSnapshot:
    """Tests for BenchmarkSnapshot pydantic model."""

    def _valid_snapshot_kwargs(self) -> dict:
        return {
            "benchmark_version": "personal_v3",
            "overall_score": 0.72,
            "cluster_scores": {"cluster-001": 0.65, "cluster-002": 0.80},
            "task_count": 50,
            "elapsed_seconds": 184.3,
        }

    def test_constructs_with_valid_fields(self) -> None:
        from openjarvis.learning.spec_search.models import BenchmarkSnapshot

        snap = BenchmarkSnapshot(**self._valid_snapshot_kwargs())

        assert snap.benchmark_version == "personal_v3"
        assert snap.overall_score == 0.72
        assert snap.cluster_scores["cluster-001"] == 0.65
        assert snap.task_count == 50
        assert snap.elapsed_seconds == 184.3

    def test_round_trip_via_json(self) -> None:
        from openjarvis.learning.spec_search.models import BenchmarkSnapshot

        snap = BenchmarkSnapshot(**self._valid_snapshot_kwargs())
        restored = BenchmarkSnapshot.model_validate_json(snap.model_dump_json())

        assert restored == snap

    def test_score_bounds(self) -> None:
        import pytest
        from pydantic import ValidationError

        from openjarvis.learning.spec_search.models import BenchmarkSnapshot

        kwargs = self._valid_snapshot_kwargs()
        kwargs["overall_score"] = 1.5

        with pytest.raises(ValidationError):
            BenchmarkSnapshot(**kwargs)

    def test_task_count_must_be_non_negative(self) -> None:
        import pytest
        from pydantic import ValidationError

        from openjarvis.learning.spec_search.models import BenchmarkSnapshot

        kwargs = self._valid_snapshot_kwargs()
        kwargs["task_count"] = -1

        with pytest.raises(ValidationError):
            BenchmarkSnapshot(**kwargs)


# ---------------------------------------------------------------------------
# EditOutcome
# ---------------------------------------------------------------------------


class TestEditOutcome:
    """Tests for EditOutcome pydantic model."""

    def _valid_outcome_kwargs(self) -> dict:
        from datetime import datetime, timezone

        return {
            "edit_id": "edit-001",
            "status": "applied",
            "benchmark_delta": 0.04,
            "cluster_deltas": {"cluster-001": 0.10, "cluster-002": 0.0},
            "error": None,
            "applied_at": datetime(2026, 4, 8, 14, 25, 0, tzinfo=timezone.utc),
        }

    def test_applied_outcome(self) -> None:
        from openjarvis.learning.spec_search.models import EditOutcome

        outcome = EditOutcome(**self._valid_outcome_kwargs())
        assert outcome.status == "applied"
        assert outcome.benchmark_delta == 0.04

    def test_rejected_outcome_has_no_applied_at(self) -> None:
        from openjarvis.learning.spec_search.models import EditOutcome

        outcome = EditOutcome(
            edit_id="edit-002",
            status="rejected_by_gate",
            benchmark_delta=-0.02,
            cluster_deltas={},
            error="regression: cluster-001 dropped 0.06",
            applied_at=None,
        )
        assert outcome.status == "rejected_by_gate"
        assert outcome.applied_at is None
        assert outcome.error is not None

    def test_status_must_be_valid_literal(self) -> None:
        import pytest
        from pydantic import ValidationError

        from openjarvis.learning.spec_search.models import EditOutcome

        kwargs = self._valid_outcome_kwargs()
        kwargs["status"] = "totally_made_up"

        with pytest.raises(ValidationError):
            EditOutcome(**kwargs)

    def test_round_trip_via_json(self) -> None:
        from openjarvis.learning.spec_search.models import EditOutcome

        outcome = EditOutcome(**self._valid_outcome_kwargs())
        restored = EditOutcome.model_validate_json(outcome.model_dump_json())

        assert restored == outcome


# ---------------------------------------------------------------------------
# LearningSession
# ---------------------------------------------------------------------------


class TestLearningSession:
    """Tests for LearningSession pydantic model."""

    def _valid_session_kwargs(self) -> dict:
        from datetime import datetime, timezone
        from pathlib import Path

        from openjarvis.learning.spec_search.models import (
            AutonomyMode,
            BenchmarkSnapshot,
            SessionStatus,
            TriggerKind,
        )

        snap = BenchmarkSnapshot(
            benchmark_version="personal_v1",
            overall_score=0.65,
            cluster_scores={"cluster-001": 0.50},
            task_count=30,
            elapsed_seconds=92.0,
        )
        return {
            "id": "session-001",
            "parent_session_id": None,
            "trigger": TriggerKind.SCHEDULED,
            "trigger_metadata": {"cron": "0 3 * * *"},
            "status": SessionStatus.INITIATED,
            "autonomy_mode": AutonomyMode.TIERED,
            "started_at": datetime(2026, 4, 8, 3, 0, 0, tzinfo=timezone.utc),
            "ended_at": None,
            "diagnosis_path": Path("/tmp/sessions/session-001/diagnosis.md"),
            "plan_path": Path("/tmp/sessions/session-001/plan.json"),
            "benchmark_before": snap,
            "benchmark_after": None,
            "edit_outcomes": [],
            "git_checkpoint_pre": "abc1234",
            "git_checkpoint_post": None,
            "teacher_cost_usd": 0.0,
            "error": None,
        }

    def test_constructs_with_valid_fields(self) -> None:
        from openjarvis.learning.spec_search.models import LearningSession

        session = LearningSession(**self._valid_session_kwargs())
        assert session.id == "session-001"
        assert session.parent_session_id is None
        assert session.git_checkpoint_pre == "abc1234"
        assert session.benchmark_after is None

    def test_round_trip_via_json(self) -> None:
        from openjarvis.learning.spec_search.models import LearningSession

        session = LearningSession(**self._valid_session_kwargs())
        as_json = session.model_dump_json()
        restored = LearningSession.model_validate_json(as_json)

        assert restored == session

    def test_supports_parent_session_chain(self) -> None:
        from openjarvis.learning.spec_search.models import LearningSession

        kwargs = self._valid_session_kwargs()
        kwargs["parent_session_id"] = "session-000"

        session = LearningSession(**kwargs)
        assert session.parent_session_id == "session-000"

    def test_status_must_be_valid_enum(self) -> None:
        import pytest
        from pydantic import ValidationError

        from openjarvis.learning.spec_search.models import LearningSession

        kwargs = self._valid_session_kwargs()
        kwargs["status"] = "not_a_status"

        with pytest.raises(ValidationError):
            LearningSession(**kwargs)

    def test_teacher_cost_must_be_non_negative(self) -> None:
        import pytest
        from pydantic import ValidationError

        from openjarvis.learning.spec_search.models import LearningSession

        kwargs = self._valid_session_kwargs()
        kwargs["teacher_cost_usd"] = -0.01

        with pytest.raises(ValidationError):
            LearningSession(**kwargs)
