from __future__ import annotations

import os
from collections.abc import Callable
from datetime import date
from pathlib import Path
from tempfile import TemporaryDirectory
from typing import TypeVar
from unittest.mock import patch

from checkin_cli.weekly_operations import (
    CanonicalAuthorityAlreadyRegistered,
    WeeklyOperationsError,
)
from checkin_cli.weekly_operations_authority import open_authority_root
from checkin_cli.weekly_operations_canonical_registry_history import (
    REGISTRY_NAME,
    validate_registration_history,
)
from checkin_cli.weekly_operations_correlation import (
    CanonicalCheckinCorrelationTransaction,
    CorrelationAction,
    CorrelationRequest,
    CorrelationScope,
    CorrelationResult,
)
from checkin_cli.weekly_operations_customer_authority import CanonicalCheckinCustomerAuthority
from checkin_cli.weekly_operations_customer_authority_factory import (
    open_canonical_checkin_customer_authority,
)
from checkin_cli.weekly_operations_store import WeeklyOperationsStore
from tests._weekly_operations_correlation_support import (
    CUSTOMER,
    DAY,
    event,
    register_runtime,
    store_at,
    unregistered_source_at,
)

_T = TypeVar("_T")


def _fails(operation: Callable[[], _T]) -> None:
    try:
        _ = operation()
    except WeeklyOperationsError:
        return
    raise AssertionError("typed weekly-operations failure was not raised")


def _commit(
    source: CanonicalCheckinCustomerAuthority, store: WeeklyOperationsStore
) -> CorrelationResult:
    scope = CorrelationScope(
        source.customer_identity_digest,
        date.fromisoformat(DAY),
        CorrelationAction.CHECKIN,
    )
    return CanonicalCheckinCorrelationTransaction(
        store,
        CorrelationRequest(scope, source),
    ).commit()


def test_missing_registry_blocks_open_and_correlation_before_append() -> None:
    with TemporaryDirectory(prefix="task4-r5-missing-") as raw:
        root = Path(raw)
        fixture = store_at(root / "authority")
        pending = unregistered_source_at(
            root / "profile", CUSTOMER, event("r5_missing_registry_root1")
        )
        source = register_runtime(
            pending.runtime, fixture.authority
        )
        expected = source.registered_binding
        (fixture.authority.observed_path / REGISTRY_NAME).unlink()

        _fails(
            lambda: open_canonical_checkin_customer_authority(
                pending.runtime, expected, fixture.authority
            )
        )
        _fails(lambda: _commit(source, fixture.store))
        assert not any(
            child.name.endswith(".day-status-v1.jsonl")
            for child in fixture.authority.observed_path.iterdir()
        )
        source.close()
        fixture.close()


def test_authority_root_restart_requires_matching_registry_and_binding() -> None:
    with TemporaryDirectory(prefix="task4-r5-restart-") as raw:
        root = Path(raw)
        fixture = store_at(root / "authority")
        pending = unregistered_source_at(
            root / "profile", CUSTOMER, event("r5_authority_restart01")
        )
        source = register_runtime(
            pending.runtime, fixture.authority
        )
        expected = source.registered_binding
        source.close()
        authority_binding = fixture.authority.binding
        fixture.authority.close()
        reopened_root = open_authority_root(fixture.parent, authority_binding)
        reopened = open_canonical_checkin_customer_authority(
            pending.runtime, expected, reopened_root
        )

        assert reopened.registered_binding == expected
        reopened.close()
        reopened_root.close()
        fixture.parent.close()


def test_interrupted_empty_create_can_retry_once_without_duplicate() -> None:
    with TemporaryDirectory(prefix="task4-r5-create-interrupt-") as raw:
        root = Path(raw)
        fixture = store_at(root / "authority")
        pending = unregistered_source_at(
            root / "profile", CUSTOMER, event("r5_create_interrupt01")
        )
        with patch(
            "checkin_cli.weekly_operations_canonical_registry.os.write",
            side_effect=OSError("interrupt"),
        ):
            _fails(
                lambda: register_runtime(
                    pending.runtime, fixture.authority
                )
            )
        registry = fixture.authority.observed_path / REGISTRY_NAME
        assert registry.read_bytes() == b""

        source = register_runtime(
            pending.runtime, fixture.authority
        )
        assert registry.read_bytes().count(b"\n") == 1
        try:
            _ = register_runtime(
                pending.runtime, fixture.authority
            )
        except CanonicalAuthorityAlreadyRegistered as error:
            assert error.reason == "already_registered"
        else:
            raise AssertionError("retry issued a second binding")
        source.close()
        fixture.close()


def test_partial_append_is_torn_and_never_silently_rebound() -> None:
    with TemporaryDirectory(prefix="task4-r5-append-interrupt-") as raw:
        root = Path(raw)
        fixture = store_at(root / "authority")
        pending = unregistered_source_at(
            root / "profile", CUSTOMER, event("r5_append_interrupt01")
        )
        real_write = os.write
        called = False

        def partial(descriptor: int, payload: bytes) -> int:
            nonlocal called
            if not called:
                called = True
                return real_write(descriptor, payload[: len(payload) // 2])
            raise OSError("interrupt")

        with patch(
            "checkin_cli.weekly_operations_canonical_registry.os.write",
            side_effect=partial,
        ):
            _fails(
                lambda: register_runtime(
                    pending.runtime, fixture.authority
                )
            )
        registry = fixture.authority.observed_path / REGISTRY_NAME
        assert registry.read_bytes() and not registry.read_bytes().endswith(b"\n")
        _fails(
            lambda: register_runtime(
                pending.runtime, fixture.authority
            )
        )
        assert fixture.store.authority.observed_path == fixture.authority.observed_path
        fixture.close()



def test_registry_row_binds_required_canonical_identities() -> None:
    with TemporaryDirectory(prefix="task4-r5-row-") as raw:
        root = Path(raw)
        fixture = store_at(root / "authority")
        pending = unregistered_source_at(
            root / "profile", CUSTOMER, event("r5_row_identity_root01")
        )
        source = register_runtime(
            pending.runtime, fixture.authority
        )
        payload = (fixture.authority.observed_path / REGISTRY_NAME).read_bytes()
        row, = validate_registration_history(payload)
        pins = source.binding.pins
        assert row.registration_epoch == 1 and row.binding_digest == source.binding_digest
        for name, pin in (("root", pins.root), ("events", pins.events), ("sequence", pins.sequence), ("lock", pins.lock)):
            observed = tuple(getattr(row, f"{name}_{field}") for field in ("device", "inode", "mode", "owner", "links"))
            assert observed == (pin.device, pin.inode, pin.mode, pin.owner, pin.links)
        source.close()
        fixture.close()

def test_registry_owner_mismatch_fails_closed() -> None:
    with TemporaryDirectory(prefix="task4-r5-owner-") as raw:
        root = Path(raw)
        fixture = store_at(root / "authority")
        pending = unregistered_source_at(
            root / "profile", CUSTOMER, event("r5_owner_mismatch_root1")
        )
        source = register_runtime(
            pending.runtime, fixture.authority
        )
        with patch(
            "checkin_cli.weekly_operations_canonical_registry.os.geteuid",
            return_value=os.geteuid() + 1,
        ):
            _fails(
                lambda: open_canonical_checkin_customer_authority(
                    pending.runtime, source.registered_binding, fixture.authority
                )
            )
        assert fixture.store.read() == ()
        source.close()
        fixture.close()
