"""R71b maintenance controller source contracts."""

from __future__ import annotations

from dataclasses import replace
from pathlib import Path
from typing import final

import pytest

from scripts.nutricoach_v150_phase_journal import PhaseJournal
from scripts.nutricoach_v150_r71b_maintenance_transaction import (
    KNOWN_R70_CRON_ERROR,
    KNOWN_R70_CRON_ERROR_EVIDENCE_SHA256,
    KNOWN_R70_CRON_JOB_ID,
    KNOWN_R70_CRON_JOB_NAME,
    KNOWN_R70_CRON_SCHEDULE,
    KNOWN_R70_RUNTIME_CANDIDATE_DIGEST,
    MaintenancePhase,
    MaintenanceTransactionError,
    R71bMaintenanceController,
    R71bMaintenanceProof,
)


@final
class FakeMaintenanceHost:
    """Mutable recorder because the transaction contract is order-sensitive."""

    def __init__(self, *, fail_at: str | None = None) -> None:
        self.fail_at = fail_at
        self.calls: list[str] = []
        self.reserved = False
        self.confirmed = False

    def reserve(self) -> None:
        self._record("reserve")
        self.reserved = True

    def arm_maintenance(self) -> None:
        self._record("arm_maintenance")

    def arm_start_watch(self) -> None:
        self._record("arm_start_watch")

    def start_successor(self) -> None:
        self._record("start_successor")

    def confirm_maintenance(self) -> None:
        self._record("confirm_maintenance")
        self.confirmed = True

    def commit(self) -> None:
        self._record("commit")

    def pause_predecessor_scheduler(self) -> None:
        self._record("pause_predecessor_scheduler")

    def restore_predecessor(self) -> None:
        self._record("restore_predecessor")

    def recover_confirmed_maintenance(self) -> None:
        self._record("recover_confirmed_maintenance")

    def _record(self, call: str) -> None:
        self.calls.append(call)
        if self.fail_at == call:
            raise MaintenanceTransactionError(call)


def _proof() -> R71bMaintenanceProof:
    return R71bMaintenanceProof(
        candidate_digest="a" * 64,
        cron_job_id=KNOWN_R70_CRON_JOB_ID,
        cron_job_name=KNOWN_R70_CRON_JOB_NAME,
        cron_schedule=KNOWN_R70_CRON_SCHEDULE,
        cron_error=KNOWN_R70_CRON_ERROR,
        cron_error_evidence_sha256=KNOWN_R70_CRON_ERROR_EVIDENCE_SHA256,
        cron_ledger_sha256="b" * 64,
        expected_cron_ledger_sha256="b" * 64,
        runtime_candidate_digest=KNOWN_R70_RUNTIME_CANDIDATE_DIGEST,
        expected_candidate_digest="a" * 64,
        final_package_digest="d" * 64,
    )


def test_transaction_arms_hold_watch_and_confirmation_before_commit() -> None:
    # Given
    host = FakeMaintenanceHost()
    controller = R71bMaintenanceController(host, _proof())
    approval = "AUTHORIZE NUTRICOACH V1.5 R71B MAINTENANCE UPGRADE " + "d" * 64

    # When
    controller.execute(approval)

    # Then
    assert host.calls == [
        "reserve",
        "arm_maintenance",
        "arm_start_watch",
        "start_successor",
        "confirm_maintenance",
        "commit",
    ]
    assert controller.phase is MaintenancePhase.COMMITTED


@pytest.mark.parametrize(
    "fault",
    ("arm_maintenance", "arm_start_watch", "start_successor", "confirm_maintenance"),
)
def test_transaction_pauses_before_rollback_when_preconfirmation_fault(
    fault: str,
) -> None:
    # Given
    host = FakeMaintenanceHost(fail_at=fault)
    controller = R71bMaintenanceController(host, _proof())
    approval = "AUTHORIZE NUTRICOACH V1.5 R71B MAINTENANCE UPGRADE " + "d" * 64

    # When / Then
    with pytest.raises(MaintenanceTransactionError, match=fault):
        controller.execute(approval)

    assert host.calls[-2:] == ["pause_predecessor_scheduler", "restore_predecessor"]
    assert controller.phase is MaintenancePhase.ROLLED_BACK_SAFE_CRON_PAUSED


@pytest.mark.parametrize(
    "field,value",
    (
        ("candidate_digest", "e" * 64),
        ("cron_ledger_sha256", "f" * 64),
    ),
)
def test_spoofed_proof_is_rejected_before_reservation(
    field: str,
    value: str,
) -> None:
    # Given
    host = FakeMaintenanceHost()
    proof = (
        replace(_proof(), candidate_digest=value)
        if field == "candidate_digest"
        else replace(_proof(), cron_ledger_sha256=value)
    )
    controller = R71bMaintenanceController(host, proof)

    # When / Then
    with pytest.raises(MaintenanceTransactionError, match="r70_exception_drift"):
        controller.execute("AUTHORIZE NUTRICOACH V1.5 R71B MAINTENANCE UPGRADE " + "d" * 64)
    assert not host.calls


def test_foreign_r71b_approval_is_rejected_before_reservation() -> None:
    # Given
    host = FakeMaintenanceHost()
    controller = R71bMaintenanceController(host, _proof())

    # When / Then
    with pytest.raises(MaintenanceTransactionError, match="r71b_approval"):
        controller.execute("AUTHORIZE NUTRICOACH V1.5 R71B MAINTENANCE UPGRADE " + "e" * 64)
    assert not host.calls


def test_old_approval_is_rejected_before_reservation() -> None:
    # Given
    host = FakeMaintenanceHost()
    controller = R71bMaintenanceController(host, _proof())

    # When / Then
    with pytest.raises(MaintenanceTransactionError, match="approval"):
        controller.execute("AUTHORIZE NUTRICOACH V1.5 LIVE UPGRADE " + "d" * 64)

    assert host.reserved is False


@pytest.mark.parametrize(
    "phase",
    (
        MaintenancePhase.PREFLIGHT,
        MaintenancePhase.RESERVED,
        MaintenancePhase.MAINTENANCE_ARMED,
        MaintenancePhase.START_WATCH_ARMED,
        MaintenancePhase.STARTED,
    ),
)
def test_every_unconfirmed_recovery_phase_rolls_back_with_cron_paused(
    phase: MaintenancePhase,
) -> None:
    # Given
    host = FakeMaintenanceHost()
    controller = R71bMaintenanceController(host, _proof())

    # When
    controller.recover(phase)

    # Then
    assert host.calls == ["pause_predecessor_scheduler", "restore_predecessor"]
    assert controller.phase is MaintenancePhase.ROLLED_BACK_SAFE_CRON_PAUSED


def test_confirmed_recovery_rolls_forward_without_rollback() -> None:
    # Given
    host = FakeMaintenanceHost()
    controller = R71bMaintenanceController(host, _proof())

    # When
    controller.recover(MaintenancePhase.MAINTENANCE_CONFIRMED)

    # Then
    assert host.calls == ["recover_confirmed_maintenance", "commit"]
    assert controller.phase is MaintenancePhase.COMMITTED


def test_safe_paused_rollback_phase_is_not_recovered_a_second_time(
    tmp_path: Path,
) -> None:
    # Given
    journal = PhaseJournal(tmp_path / "phase.json")
    journal.advance("ROLLED_BACK_SAFE_CRON_PAUSED")

    # Then
    assert journal.recovery_required() is False


def test_arm_precedes_systemd_switch_and_completion_starts_only_afterward() -> None:
    # Given
    host = FakeMaintenanceHost()
    controller = R71bMaintenanceController(host, _proof())
    approval = "AUTHORIZE NUTRICOACH V1.5 R71B MAINTENANCE UPGRADE " + "d" * 64

    # When
    controller.arm(approval)

    # Then
    assert host.calls == ["reserve", "arm_maintenance"]
    assert controller.phase is MaintenancePhase.MAINTENANCE_ARMED

    # When
    controller.complete_start()

    # Then
    assert host.calls == [
        "reserve", "arm_maintenance", "arm_start_watch", "start_successor",
        "confirm_maintenance", "commit",
    ]
