"""Unit tests for the journal-entry pydantic models.

Persistence boundary — serialise / deserialise round-trips MUST be
byte-stable for the discriminator to land on the right concrete class
on read.
"""

from __future__ import annotations

from datetime import UTC, datetime

from pydantic import TypeAdapter

from attractor.engine import OutcomeStatus, RunStatus
from attractor.engine.journal import (
    GateResponded,
    JournalEntry,
    NodeCompleted,
    PausedAtGate,
    RunFinalized,
    RunInitialized,
    event_path,
)

_ADAPTER: TypeAdapter[JournalEntry] = TypeAdapter(JournalEntry)


def _ts() -> datetime:
    return datetime(2026, 1, 1, 12, 0, 0, tzinfo=UTC)


# [unit->REQ-CHECKPOINT-DUAL-COMMIT]
def test_run_initialized_round_trips() -> None:
    entry = RunInitialized(
        seq=0,
        run_id="abc-def",
        timestamp=_ts(),
        workflow_dot="<dot>",
        workflow_hash="aaa",
        base_ref="HEAD",
    )
    payload = _ADAPTER.dump_json(entry)
    rebuilt = _ADAPTER.validate_json(payload)
    assert isinstance(rebuilt, RunInitialized)
    assert rebuilt.workflow_dot == "<dot>"
    assert rebuilt.seq == 0


# [unit->REQ-CHECKPOINT-DUAL-COMMIT]
def test_node_completed_round_trips_with_optional_fields() -> None:
    entry = NodeCompleted(
        seq=4,
        run_id="r",
        timestamp=_ts(),
        node_id="build",
        visit=1,
        status=OutcomeStatus.SUCCESS,
        captured_output="built",
        duration_ms=120,
        tokens_in=10,
        tokens_out=5,
        next_node="verify",
        worktree_commit_after="abcdef1",
    )
    rebuilt = _ADAPTER.validate_json(_ADAPTER.dump_json(entry))
    assert isinstance(rebuilt, NodeCompleted)
    assert rebuilt.status == OutcomeStatus.SUCCESS
    assert rebuilt.tokens_in == 10
    assert rebuilt.worktree_commit_after == "abcdef1"


# [unit->REQ-CHECKPOINT-DUAL-COMMIT]
def test_node_completed_round_trips_without_tokens_or_worktree() -> None:
    """Token + worktree fields default to None for tool nodes etc."""
    entry = NodeCompleted(
        seq=7,
        run_id="r",
        timestamp=_ts(),
        node_id="t",
        visit=1,
        status=OutcomeStatus.FAILURE,
        captured_output="cmd failed",
        duration_ms=0,
        next_node=None,
    )
    rebuilt = _ADAPTER.validate_json(_ADAPTER.dump_json(entry))
    assert isinstance(rebuilt, NodeCompleted)
    assert rebuilt.tokens_in is None
    assert rebuilt.tokens_out is None
    assert rebuilt.worktree_commit_after is None
    assert rebuilt.next_node is None


# [unit->REQ-HUMAN-GATE]
def test_paused_at_gate_round_trips() -> None:
    entry = PausedAtGate(
        seq=3,
        run_id="r",
        timestamp=_ts(),
        node_id="decide",
        prompt="Proceed?",
        choices=["approve", "revise"],
    )
    rebuilt = _ADAPTER.validate_json(_ADAPTER.dump_json(entry))
    assert isinstance(rebuilt, PausedAtGate)
    assert rebuilt.choices == ["approve", "revise"]


# [unit->REQ-HUMAN-GATE]
def test_gate_responded_round_trips_with_reason() -> None:
    entry = GateResponded(
        seq=4,
        run_id="r",
        timestamp=_ts(),
        choice="approve",
        reason="LGTM",
    )
    rebuilt = _ADAPTER.validate_json(_ADAPTER.dump_json(entry))
    assert isinstance(rebuilt, GateResponded)
    assert rebuilt.choice == "approve"
    assert rebuilt.reason == "LGTM"


# [unit->REQ-CHECKPOINT-DUAL-COMMIT]
def test_run_finalized_round_trips_with_status_enum() -> None:
    entry = RunFinalized(
        seq=10,
        run_id="r",
        timestamp=_ts(),
        status=RunStatus.COMPLETED,
    )
    rebuilt = _ADAPTER.validate_json(_ADAPTER.dump_json(entry))
    assert isinstance(rebuilt, RunFinalized)
    assert rebuilt.status == RunStatus.COMPLETED


# [unit->REQ-CHECKPOINT-DUAL-COMMIT]
def test_event_path_format_is_zero_padded() -> None:
    """Path uses six zero-padded digits so list sorts numerically."""
    assert event_path(0) == "events/000000.json"
    assert event_path(7) == "events/000007.json"
    assert event_path(123) == "events/000123.json"


# [unit->REQ-CHECKPOINT-DUAL-COMMIT]
def test_discriminator_dispatches_on_kind_field() -> None:
    """Reading an arbitrary entry by JSON picks the right concrete class."""
    payload = (
        b'{"kind": "PausedAtGate", "seq": 1, "run_id": "x", '
        b'"timestamp": "2026-01-01T12:00:00Z", "node_id": "g", '
        b'"prompt": "P", "choices": ["a"]}'
    )
    rebuilt = _ADAPTER.validate_json(payload)
    assert isinstance(rebuilt, PausedAtGate)
    assert rebuilt.node_id == "g"


# [unit->REQ-EXEC-PARALLEL-FANOUT]
def test_branch_name_defaults_to_none() -> None:
    """NodeCompleted constructed without branch_name has branch_name=None."""
    entry = NodeCompleted(
        seq=1,
        run_id="r",
        timestamp=_ts(),
        node_id="build",
        visit=1,
        status=OutcomeStatus.SUCCESS,
        captured_output="",
        duration_ms=0,
    )
    assert entry.branch_name is None


# [unit->REQ-EXEC-PARALLEL-FANOUT]
def test_branch_name_explicit_survives_serialization() -> None:
    """NodeCompleted with branch_name='lint' round-trips via Pydantic JSON."""
    entry = NodeCompleted(
        seq=2,
        run_id="r",
        timestamp=_ts(),
        node_id="lint",
        visit=1,
        status=OutcomeStatus.SUCCESS,
        captured_output="lint-ok",
        duration_ms=55,
        branch_name="lint",
    )
    rebuilt = _ADAPTER.validate_json(_ADAPTER.dump_json(entry))
    assert isinstance(rebuilt, NodeCompleted)
    assert rebuilt.branch_name == "lint"
    assert rebuilt.node_id == "lint"
