from datetime import UTC, datetime

import pytest

from dobby.models import (
    Artifact,
    InfraConfig,
    InvalidTransitionError,
    Priority,
    Project,
    ProjectPolicies,
    Result,
    Schedule,
    ScheduleType,
    Task,
    TaskMode,
    TaskStatus,
    validate_transition,
)


def test_task_creation_quick_mode():
    task = Task(
        project="spare",
        description="Add warmup scheduler",
        exit_criteria="PR ready",
    )
    assert task.id.startswith("t-")
    assert task.status == TaskStatus.PENDING
    assert task.mode == TaskMode.QUICK
    assert task.priority == Priority.NORMAL
    assert task.schedule.type == ScheduleType.ONCE
    assert task.checkpoint is False
    assert task.plan == []
    assert task.created_at is not None


def test_task_creation_planned_mode():
    task = Task(
        project="memagent",
        description="Refactor orchestrator",
        mode=TaskMode.PLANNED,
        exit_criteria="tests pass",
    )
    assert task.status == TaskStatus.DRAFTING
    assert task.mode == TaskMode.PLANNED


def test_task_creation_with_schedule():
    task = Task(
        project="spare",
        description="Nightly eval",
        schedule=Schedule(type=ScheduleType.CRON, expr="0 3 * * *"),
    )
    assert task.schedule.type == ScheduleType.CRON
    assert task.schedule.expr == "0 3 * * *"
    assert task.status == TaskStatus.ACTIVE


def test_task_to_dict_roundtrip():
    task = Task(
        project="spare",
        description="Test task",
        priority=Priority.HIGH,
        infra="gpu-box-1",
        timeout=1800,
    )
    d = task.to_dict()
    restored = Task.from_dict(d)
    assert restored.id == task.id
    assert restored.project == task.project
    assert restored.priority == Priority.HIGH
    assert restored.infra == "gpu-box-1"
    assert restored.timeout == 1800
    assert restored.created_at == task.created_at


def test_result_creation():
    result = Result(
        task_id="t-20260321-001",
        status="completed",
        summary="Done",
    )
    assert result.artifacts == []
    assert result.metrics == {}
    assert result.gpu_info is None
    assert result.cost is not None
    assert result.completed_at is not None


def test_result_to_dict_roundtrip():
    result = Result(
        task_id="t-20260321-001",
        status="completed",
        summary="Added scheduler",
        artifacts=[Artifact(type="pr", url="https://github.com/test/pull/1")],
        metrics={"loss": 0.5},
        started_at=datetime.now(UTC),
    )
    d = result.to_dict()
    restored = Result.from_dict(d)
    assert restored.task_id == result.task_id
    assert restored.metrics == {"loss": 0.5}
    assert restored.started_at == result.started_at
    assert len(restored.artifacts) == 1
    assert restored.artifacts[0].type == "pr"
    assert restored.artifacts[0].url == "https://github.com/test/pull/1"


def test_project_creation():
    project = Project(
        name="spare",
        repo="/Users/simonyu/local/local_orby/spiral_refactor",
        context="Dual-role RL framework",
    )
    assert project.git_remote is None
    assert project.claude_md is True
    assert project.infra == []


def test_project_with_infra():
    project = Project(
        name="spare",
        repo="/path/to/repo",
        infra=[
            InfraConfig(name="gpu-box-1", type="ssh", host="user@gpu-box"),
            InfraConfig(name="modal", type="modal", gpu="H100"),
        ],
    )
    assert len(project.infra) == 2
    assert project.infra[0].host == "user@gpu-box"


def test_schedule_interval_parsing():
    s = Schedule(type=ScheduleType.INTERVAL, every="30m")
    assert s.interval_seconds() == 1800

    s2 = Schedule(type=ScheduleType.INTERVAL, every="2h")
    assert s2.interval_seconds() == 7200


def test_priority_ordering():
    assert Priority.URGENT.weight > Priority.HIGH.weight
    assert Priority.HIGH.weight > Priority.NORMAL.weight
    assert Priority.NORMAL.weight > Priority.LOW.weight


# ---------------------------------------------------------------------------
# Event journal
# ---------------------------------------------------------------------------

def test_task_has_created_event():
    task = Task(project="spare", description="Test")
    assert len(task.events) == 1
    assert task.events[0].status == "pending"
    assert task.events[0].detail == "created"


def test_task_record_event():
    task = Task(project="spare", description="Test")
    task.record_event(TaskStatus.RUNNING)
    assert len(task.events) == 2
    assert task.events[1].status == "running"


def test_events_roundtrip():
    task = Task(project="spare", description="Test")
    task.record_event(TaskStatus.RUNNING)
    task.record_event(TaskStatus.COMPLETED, detail="all tests passed")
    d = task.to_dict()
    restored = Task.from_dict(d)
    assert len(restored.events) == 3
    assert restored.events[2].detail == "all tests passed"


# ---------------------------------------------------------------------------
# Attempt number
# ---------------------------------------------------------------------------

def test_task_default_attempt_number():
    task = Task(project="spare", description="Test")
    assert task.attempt_number == 0


def test_result_default_attempt_number():
    result = Result(task_id="t-001", status="completed", summary="Done")
    assert result.attempt_number == 0


def test_result_attempt_number_roundtrip():
    result = Result(task_id="t-001", status="completed", summary="Done", attempt_number=3)
    d = result.to_dict()
    restored = Result.from_dict(d)
    assert restored.attempt_number == 3


def test_task_attempt_number_roundtrip():
    task = Task(project="spare", description="Test")
    task.attempt_number = 2
    d = task.to_dict()
    restored = Task.from_dict(d)
    assert restored.attempt_number == 2


# ---------------------------------------------------------------------------
# State transition guardrails
# ---------------------------------------------------------------------------

def test_validate_transition_legal():
    validate_transition(TaskStatus.PENDING, TaskStatus.RUNNING)
    validate_transition(TaskStatus.RUNNING, TaskStatus.COMPLETED)
    validate_transition(TaskStatus.RUNNING, TaskStatus.FAILED)
    validate_transition(TaskStatus.FAILED, TaskStatus.PENDING)
    validate_transition(TaskStatus.COMPLETED, TaskStatus.PENDING)


def test_validate_transition_noop():
    # Same-status transitions are always allowed
    validate_transition(TaskStatus.RUNNING, TaskStatus.RUNNING)


def test_validate_transition_illegal():
    with pytest.raises(InvalidTransitionError):
        validate_transition(TaskStatus.PENDING, TaskStatus.COMPLETED)
    with pytest.raises(InvalidTransitionError):
        validate_transition(TaskStatus.COMPLETED, TaskStatus.RUNNING)
    with pytest.raises(InvalidTransitionError):
        validate_transition(TaskStatus.DRAFTING, TaskStatus.COMPLETED)


# ---------------------------------------------------------------------------
# Artifact model
# ---------------------------------------------------------------------------

def test_artifact_creation():
    a = Artifact(type="pr", url="https://github.com/user/repo/pull/42")
    assert a.type == "pr"
    assert a.url == "https://github.com/user/repo/pull/42"
    assert a.path is None
    assert a.metadata == {}


def test_artifact_with_all_fields():
    a = Artifact(
        type="log",
        path="/tmp/results/t-001/agent.log",
        url="https://example.com/logs/123",
        metadata={"lines": 500, "format": "text"},
    )
    assert a.path == "/tmp/results/t-001/agent.log"
    assert a.metadata["lines"] == 500


def test_artifact_to_dict_roundtrip():
    a = Artifact(
        type="diff",
        path="/tmp/patch.diff",
        metadata={"files_changed": 3},
    )
    d = a.to_dict()
    restored = Artifact.from_dict(d)
    assert restored.type == "diff"
    assert restored.path == "/tmp/patch.diff"
    assert restored.url is None  # not serialized when None
    assert restored.metadata == {"files_changed": 3}


def test_artifact_to_dict_minimal():
    """Only type is required; path/url/metadata omitted from dict when empty."""
    a = Artifact(type="file")
    d = a.to_dict()
    assert d == {"type": "file"}
    assert "path" not in d
    assert "url" not in d
    assert "metadata" not in d


# ---------------------------------------------------------------------------
# PAUSED status and failure tracking
# ---------------------------------------------------------------------------

def test_paused_status_transitions():
    validate_transition(TaskStatus.ACTIVE, TaskStatus.PAUSED)
    validate_transition(TaskStatus.RUNNING, TaskStatus.PAUSED)
    validate_transition(TaskStatus.PAUSED, TaskStatus.ACTIVE)
    validate_transition(TaskStatus.PAUSED, TaskStatus.CANCELLED)
    with pytest.raises(InvalidTransitionError):
        validate_transition(TaskStatus.PAUSED, TaskStatus.RUNNING)


def test_task_default_failure_fields():
    task = Task(project="spare", description="Test")
    assert task.max_failures == 3
    assert task.consecutive_failures == 0


def test_task_failure_fields_roundtrip():
    task = Task(project="spare", description="Test")
    task.max_failures = 5
    task.consecutive_failures = 2
    d = task.to_dict()
    restored = Task.from_dict(d)
    assert restored.max_failures == 5
    assert restored.consecutive_failures == 2


# ---------------------------------------------------------------------------
# next_run_at
# ---------------------------------------------------------------------------

def test_task_default_next_run_at():
    task = Task(project="spare", description="Test")
    assert task.next_run_at is None


def test_task_next_run_at_roundtrip():
    task = Task(project="spare", description="Test")
    now = datetime.now(UTC)
    task.next_run_at = now
    d = task.to_dict()
    restored = Task.from_dict(d)
    assert restored.next_run_at == now


# ---------------------------------------------------------------------------
# ProjectPolicies
# ---------------------------------------------------------------------------

def test_project_policies_defaults():
    policies = ProjectPolicies()
    assert policies.default_backend is None
    assert policies.default_timeout is None
    assert policies.max_concurrent is None
    assert policies.default_priority is None
    assert policies.auto_chain is False


def test_project_policies_roundtrip():
    policies = ProjectPolicies(
        default_backend="codex",
        default_timeout=3600,
        max_concurrent=3,
        default_priority="high",
        auto_chain=True,
    )
    d = policies.to_dict()
    restored = ProjectPolicies.from_dict(d)
    assert restored.default_backend == "codex"
    assert restored.default_timeout == 3600
    assert restored.max_concurrent == 3
    assert restored.default_priority == "high"
    assert restored.auto_chain is True


def test_project_with_policies():
    project = Project(
        name="spare",
        repo="/path/to/repo",
        policies=ProjectPolicies(
            default_backend="claude",
            auto_chain=True,
            max_concurrent=2,
        ),
    )
    d = project.to_dict()
    assert d["policies"]["default_backend"] == "claude"
    assert d["policies"]["auto_chain"] is True
    restored = Project.from_dict(d)
    assert restored.policies.default_backend == "claude"
    assert restored.policies.auto_chain is True
    assert restored.policies.max_concurrent == 2


def test_project_without_policies_in_dict():
    """Projects created before policies existed should deserialize fine."""
    d = {"name": "old", "repo": "/old", "infra": []}
    project = Project.from_dict(d)
    assert project.policies.auto_chain is False
    assert project.policies.max_concurrent is None
