from dataclasses import dataclass, field

import pytest

from nutricoach_continuity.contract import (
    SOURCE_FILE_DIGEST,
    SOURCE_ROW_DIGEST,
    canonical_source_frame,
)
from nutricoach_continuity.executor import canonical_reissue
from nutricoach_continuity.recovery import RecoveryError, TargetState, classify_target
from nutricoach_continuity.store import (
    AppendOutcome,
    ContinuityRow,
    ReissueOperation,
    StoreError,
    source_row,
)


@dataclass(frozen=True, slots=True)
class FakeStore:
    rows: list[ContinuityRow] = field(default_factory=list)
    append_calls: list[str] = field(default_factory=list)

    def read(self) -> tuple[ContinuityRow, ...]:
        return tuple(self.rows)

    def append(self, operation: ReissueOperation) -> AppendOutcome:
        self.append_calls.append("append")
        self.rows.append(operation.row)
        return AppendOutcome(operation.row, True)


def test_canonical_reissue_uses_store_append_and_preserves_exact_frame() -> None:
    # Given: one authenticated source row and an empty target
    source = FakeStore([source_row()])
    target = FakeStore()
    # When: continuity is reissued
    result = canonical_reissue(source, target)
    # Then: the installed seam is called once and exact bytes survive
    assert target.append_calls == ["append"]
    assert target.rows[0].canonical_frame() == canonical_source_frame()
    assert (result.row_digest, result.frame_sha256) == (SOURCE_ROW_DIGEST, SOURCE_FILE_DIGEST)


def test_canonical_reissue_rejects_nonempty_target_before_append() -> None:
    # Given: an already occupied target
    target = FakeStore([source_row()])
    # When / Then: a clean launcher cannot treat replay as authorization
    with pytest.raises(StoreError, match="admitted empty"):
        _ = canonical_reissue(FakeStore([source_row()]), target)
    assert target.append_calls == []


@pytest.mark.parametrize("offset", range(1, 53))
def test_recovery_completes_every_strict_prefix_when_intent_is_durable(offset: int) -> None:
    # Given: one of 52 crash points inside the expected frame
    expected = canonical_source_frame()
    current = expected[:offset]
    # When: recovery classifies durable bytes
    decision = classify_target(current, expected, intent_durable=True)
    # Then: only the missing suffix is authorized
    assert decision.state is TargetState.STRICT_PREFIX
    assert current + decision.append_suffix == expected
    assert decision.roll_forward


def test_recovery_rolls_forward_complete_row_without_second_append() -> None:
    # Given / When: the exact frame is already durable
    decision = classify_target(
        canonical_source_frame(), canonical_source_frame(), intent_durable=True
    )
    # Then: receipts roll forward and row bytes are untouched
    assert decision.state is TargetState.COMPLETE
    assert decision.append_suffix == b""


@pytest.mark.parametrize("payload", [b"x", canonical_source_frame() + b"x", b"{}\n"])
def test_recovery_consumes_divergent_or_extra_bytes(payload: bytes) -> None:
    # Given / When / Then: unrecognized bytes never authorize repair
    with pytest.raises(RecoveryError, match="divergent"):
        _ = classify_target(payload, canonical_source_frame(), intent_durable=True)
