from dataclasses import dataclass, field
from pathlib import Path

import pytest

from nutricoach_continuity.contract import PackageContract, RootPin, approval_phrase
from nutricoach_continuity.controller import ControllerError, ControllerPorts, execute
from nutricoach_continuity.journal import OneUseLedger, PhaseContext, PhaseJournal
from nutricoach_continuity.observer import ObserverHead

_DIGEST = "a" * 64


@dataclass(frozen=True, slots=True)
class FakeVerifier:
    calls: list[str] = field(default_factory=list)

    def verify(self, contract: PackageContract) -> None:
        del contract
        self.calls.append("verify")


@dataclass(frozen=True, slots=True)
class FakeExecutor:
    calls: list[str] = field(default_factory=list)

    def append(self, *, recovery: bool = False) -> None:
        self.calls.append("recovery" if recovery else "append")


@dataclass(frozen=True, slots=True)
class FakeReceipts:
    calls: list[str] = field(default_factory=list)

    def write_intent(self, baseline_digest: str) -> None:
        assert baseline_digest == "b" * 64
        self.calls.append("intent")

    def write_provenance(self) -> None:
        self.calls.append("provenance")


@dataclass(frozen=True, slots=True)
class FakeSubscription:
    events: list[str]

    def await_pass(self) -> ObserverHead:
        self.events.append("timer-pass")
        return ObserverHead(
            "t" * 64, "PASS", "m" * 64, (), "observer-r71-continuity-r1", "invocation-1"
        )


@dataclass(frozen=True, slots=True)
class FakeObserver:
    events: list[str] = field(default_factory=list)

    def fence_timer_and_capture_failed_head(self) -> ObserverHead:
        self.events.append("fence")
        return ObserverHead("b" * 64, "FAIL", "0" * 64, ("first_day_status",), "observer-r71")

    def run_manual(self, previous: ObserverHead) -> ObserverHead:
        self.events.append("manual")
        return ObserverHead("m" * 64, "PASS", previous.digest, (), "observer-r71-continuity-r1")

    def subscribe_timer(self, previous: ObserverHead) -> FakeSubscription:
        assert previous.digest == "m" * 64
        self.events.append("subscribe")
        return FakeSubscription(self.events)

    def restore_timer(self) -> None:
        self.events.append("restore")


def contract(tmp_path: Path) -> PackageContract:
    root = RootPin.model_construct(
        path=tmp_path / "source", device=1, inode=1, uid=1, gid=1, mode=0o700, nlink=2
    )
    return PackageContract.model_construct(
        schema_version="continuity-v1",
        package_id="nutricoach-v150-day-status-continuity-r71-r1",
        authorization_root=tmp_path / "authorization",
        execution_root=tmp_path / "execution",
        observer_namespace="observer-r71-continuity-r1",
        source_root=root,
        target_root=root.model_copy(update={"path": tmp_path / "target", "inode": 2}),
        file_pins=(),
        controller_closure=(),
        installed_records=(),
        protected_files=(),
        candidate_digest="c" * 64,
        sequence_zero_digest="dd9cdc73c717380a6aa3e1d35edfcafc8ba949eaf7203029b159422330257cdf",
        source_row_digest="405b519c7f0ae51d413d26e4ca5bb1fd69786c3eb11a5dbe1f00e2701bd90d26",
        expected_frame_sha256="7a8d7d6ecc66c863bc27793df6a5f3efd1ba0661cf3568095b6b9bcf4cb45208",
        zero_send_oracle_sha256="z" * 64,
        package_digest=_DIGEST,
    )


def test_manual_then_subscribe_restore_natural_timer_lifecycle(tmp_path: Path) -> None:
    # Given: fresh disposable authorization/execution roots and zero-effect ports
    authorization = tmp_path / "authorization"
    execution = tmp_path / "execution"
    authorization.mkdir(mode=0o700)
    execution.mkdir(mode=0o700)
    verifier, executor, receipts, observer = (
        FakeVerifier(),
        FakeExecutor(),
        FakeReceipts(),
        FakeObserver(),
    )
    context = PhaseContext(_DIGEST, "4" * 64, "7" * 64)
    ports = ControllerPorts(
        verifier,
        OneUseLedger(authorization, _DIGEST),
        PhaseJournal(execution, context),
        executor,
        receipts,
        observer,
    )
    # When: the authorized package runs
    digest = execute(approval_phrase(_DIGEST), contract(tmp_path), ports)
    # Then: manual precedes an armed subscription, natural restore, and timer PASS
    assert observer.events == ["fence", "manual", "subscribe", "restore", "timer-pass"]
    assert executor.calls == ["append"]
    assert receipts.calls == ["intent", "provenance"]
    assert verifier.calls == ["verify", "verify"]
    assert len(digest) == 64
    assert (authorization / "authorization-consumed.json").is_file()


def test_wrong_approval_has_zero_mutation(tmp_path: Path) -> None:
    # Given: no authorized phrase and absent authority roots
    verifier, executor, receipts, observer = (
        FakeVerifier(),
        FakeExecutor(),
        FakeReceipts(),
        FakeObserver(),
    )
    authorization = tmp_path / "authorization"
    execution = tmp_path / "execution"
    authorization.mkdir(mode=0o700)
    execution.mkdir(mode=0o700)
    ports = ControllerPorts(
        verifier,
        OneUseLedger(authorization, _DIGEST),
        PhaseJournal(execution, PhaseContext(_DIGEST, "4" * 64, "7" * 64)),
        executor,
        receipts,
        observer,
    )
    # When / Then: approval mismatch is rejected before reservation or timer action
    with pytest.raises(ControllerError, match="approval"):
        _ = execute("wrong", contract(tmp_path), ports)
    assert tuple(authorization.iterdir()) == ()
    assert observer.events == []
