from __future__ import annotations

import os
from pathlib import Path
from typing import Literal, assert_never

import pytest

from nutricoach_continuity.contract import SOURCE_FILE_DIGEST, SOURCE_ROW_DIGEST, approval_phrase
from nutricoach_continuity.controller import ControllerPorts, execute, recover
from nutricoach_continuity.faults import CrashAt, CrashFaultError, FaultInjector
from nutricoach_continuity.journal import OneUseLedger, PhaseContext, PhaseJournal
from nutricoach_continuity.observer import ObserverControl
from nutricoach_continuity.permission import PermissionPackage
from nutricoach_continuity.production import DurableReceiptWriter, InstalledRowExecutor
from nutricoach_continuity.verifier import ExactInputVerifier

from .real_support import build_permission
from .systemd_support import systemd_control

pytestmark = pytest.mark.skipif(
    "NUTRICOACH_TEST_CUSTOMER_KEY_FILE" not in os.environ,
    reason="private installed-store customer fixture is unavailable",
)


def ports(
    package: PermissionPackage,
    observer: ObserverControl,
    *,
    fault: FaultInjector | None = None,
) -> ControllerPorts:
    selected = CrashAt("never") if fault is None else fault
    context = PhaseContext(package.package_digest, SOURCE_ROW_DIGEST, SOURCE_FILE_DIGEST)
    return ControllerPorts(
        ExactInputVerifier(),
        OneUseLedger(package.contract.authorization_root, package.package_digest),
        PhaseJournal(package.contract.execution_root, context, selected),
        InstalledRowExecutor(package.contract, package.runtime.customer_key_file),
        DurableReceiptWriter(package.contract.execution_root, package.contract),
        observer,
        selected,
    )


def test_crash_after_first_row_bytes_recovers_same_reservation_without_rollback(
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    package = build_permission(tmp_path)
    package.contract.authorization_root.mkdir(mode=0o700)
    package.contract.execution_root.mkdir(mode=0o700)
    observer, _ = systemd_control(tmp_path, monkeypatch)
    with pytest.raises(CrashFaultError, match="row-append:after-effect"):
        _ = execute(
            approval_phrase(package.package_digest),
            package.contract,
            ports(package, observer, fault=CrashAt("row-append:after-effect")),
            authorized_digest=package.package_digest,
        )
    data = next(package.contract.target_root.path.glob("*.day-status-v1.jsonl"))
    inode = data.stat().st_ino
    assert data.read_bytes()
    committed = recover(package.contract, ports(package, observer))
    assert len(committed) == 64
    assert data.stat().st_ino == inode
    assert data.read_bytes().count(b"\n") == 1
    assert (
        OneUseLedger(
            package.contract.authorization_root,
            package.package_digest,
        ).consumed_outcome()
        == "SUCCEEDED"
    )
    with pytest.raises(RuntimeError, match="pending"):
        _ = recover(package.contract, ports(package, observer))


@pytest.mark.parametrize("drift", ["bytes", "inode"])
def test_divergent_or_replaced_target_is_consumed_failed_without_rollback(
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
    drift: Literal["bytes", "inode"],
) -> None:
    package = build_permission(tmp_path)
    package.contract.authorization_root.mkdir(mode=0o700)
    package.contract.execution_root.mkdir(mode=0o700)
    observer, _ = systemd_control(tmp_path, monkeypatch)
    with pytest.raises(CrashFaultError):
        _ = execute(
            approval_phrase(package.package_digest),
            package.contract,
            ports(
                package,
                observer,
                fault=CrashAt("phase-03-APPEND_INTENT_DURABLE:after-dir-fsync"),
            ),
            authorized_digest=package.package_digest,
        )
    data = next(package.contract.target_root.path.glob("*.day-status-v1.jsonl"))
    match drift:
        case "bytes":
            expected = b"divergent"
            _ = data.write_bytes(expected)
        case "inode":
            expected = b""
            original = data.with_name("original-data")
            _ = data.rename(original)
            _ = data.write_bytes(expected)
        case unreachable:
            assert_never(unreachable)
    data.chmod(0o600)
    with pytest.raises(RuntimeError):
        _ = recover(package.contract, ports(package, observer))
    assert data.read_bytes() == expected
    assert (
        OneUseLedger(
            package.contract.authorization_root,
            package.package_digest,
        ).consumed_outcome()
        == "FAILED"
    )
