from __future__ import annotations

from datetime import timedelta
import os
import signal
from pathlib import Path
from unittest import TestCase
from unittest.mock import patch

import checkin_cli.weekly_operations_customer_authority as customer_authority
from checkin_cli.weekly_operations import WeeklyOperationsError
from checkin_cli.weekly_operations_canonical_registry import CanonicalAuthorityRegistrationSlot
from checkin_cli.weekly_operations_authority import open_authority_root
from checkin_cli.weekly_operations_canonical_registry_history import (
    REGISTRY_NAME, canonical_registration_row, registration_row_digest,
    validate_registration_history,
)
from checkin_cli.weekly_operations_canonical_snapshot import CanonicalCheckinCustomerBinding
from checkin_cli.weekly_operations_customer_authority_factory import open_canonical_checkin_customer_authority
from checkin_cli.weekly_operations_signals import (
    InitializationCancellation, InitializationCancellationKind,
)
from checkin_cli.weekly_operations_registration_handoff import (
    CanonicalAuthorityRegistrationCommitted, begin_canonical_authority_registration,
)
from checkin_cli.weekly_operations_registered_binding import RegisteredCanonicalCheckinCustomerBinding
from tests._weekly_operations_correlation_support import (
    CUSTOMER, event, register_runtime, registered_source_at, store_at,
    unregistered_source_at,
)

ASSERTIONS = TestCase()


def test_direct_registration_success_api_is_absent() -> None:
    assert not hasattr(customer_authority, "register_canonical_checkin_customer_authority")


def test_timestamp_rehash_fails_retained_registered_binding(tmp_path: Path) -> None:
    fixture = store_at(tmp_path / "authority")
    source = registered_source_at(
        tmp_path / "profile", CUSTOMER, event("r6_timestamp_root_0001"),
        registry_authority=fixture.authority,
    )
    retained = source.source.registered_binding
    registry = fixture.authority.observed_path / REGISTRY_NAME
    row, = validate_registration_history(registry.read_bytes())
    changed = row.model_copy(update={
        "occurred_at_utc": row.occurred_at_utc + timedelta(seconds=1),
        "row_digest": "0" * 64,
    })
    changed = changed.model_copy(update={"row_digest": registration_row_digest(changed)})
    _ = registry.write_bytes(canonical_registration_row(changed) + b"\n")
    with ASSERTIONS.assertRaises(WeeklyOperationsError):
        _ = open_canonical_checkin_customer_authority(
            source.runtime, retained, fixture.authority
        )
    source.close()
    fixture.close()


def test_byte_identical_registry_inode_replacement_blocks_authority_open(tmp_path: Path) -> None:
    fixture = store_at(tmp_path / "authority")
    binding = fixture.authority.binding
    registry = fixture.authority.observed_path / REGISTRY_NAME
    payload = registry.read_bytes()
    _ = registry.rename(tmp_path / "original-registry")
    _ = registry.write_bytes(payload)
    registry.chmod(0o600)
    with ASSERTIONS.assertRaises(WeeklyOperationsError):
        _ = open_authority_root(fixture.parent, binding)
    fixture.authority.close()
    fixture.parent.close()


def test_missing_ack_returns_exact_committed_registered_binding(tmp_path: Path) -> None:
    fixture = store_at(tmp_path / "authority")
    pending = unregistered_source_at(
        tmp_path / "profile", CUSTOMER, event("r6_handoff_root_000001")
    )
    captured = None
    with ASSERTIONS.assertRaises(CanonicalAuthorityRegistrationCommitted) as raised:
        with begin_canonical_authority_registration(pending.runtime, fixture.authority) as transaction:
            captured = transaction.binding
    assert captured is not None and raised.exception.binding == captured
    reopened = open_canonical_checkin_customer_authority(
        pending.runtime, captured, fixture.authority
    )
    reopened.close()
    fixture.close()

def test_registry_loss_cannot_be_recreated_by_registration(tmp_path: Path) -> None:
    fixture = store_at(tmp_path / "authority")
    pending = unregistered_source_at(
        tmp_path / "profile", CUSTOMER, event("r6_registry_loss_root01")
    )
    source = register_runtime(pending.runtime, fixture.authority)
    (fixture.authority.observed_path / REGISTRY_NAME).unlink()
    with ASSERTIONS.assertRaises(WeeklyOperationsError):
        _ = register_runtime(pending.runtime, fixture.authority)
    source.close()
    fixture.close()


def test_cross_authority_empty_registry_copy_fails_exact_inode(tmp_path: Path) -> None:
    first = store_at(tmp_path / "first")
    second = store_at(tmp_path / "second")
    binding = first.authority.binding
    first_registry = first.authority.observed_path / REGISTRY_NAME
    second_registry = second.authority.observed_path / REGISTRY_NAME
    _ = first_registry.rename(tmp_path / "first-original")
    _ = first_registry.write_bytes(second_registry.read_bytes())
    first_registry.chmod(0o600)
    with ASSERTIONS.assertRaises(WeeklyOperationsError):
        _ = open_authority_root(first.parent, binding)
    first.authority.close()
    first.parent.close()
    second.close()


def test_registration_signals_after_append_expose_acknowledged_binding_twice(
    tmp_path: Path,
) -> None:
    signals = (
        (signal.SIGTERM, InitializationCancellationKind.SIGTERM),
        (signal.SIGINT, InitializationCancellationKind.SIGINT),
    )
    real_append = CanonicalAuthorityRegistrationSlot.append
    for signal_number, expected_kind in signals:
        for attempt in (1, 2):
            fixture = store_at(tmp_path / f"authority-{signal_number}-{attempt}")
            pending = unregistered_source_at(
                tmp_path / f"profile-{signal_number}-{attempt}",
                CUSTOMER, event(f"r6_signal_root_{signal_number}_{attempt}"),
            )
            sent = False

            def append_then_signal(
                slot: CanonicalAuthorityRegistrationSlot,
                canonical: CanonicalCheckinCustomerBinding,
            ) -> RegisteredCanonicalCheckinCustomerBinding:
                nonlocal sent
                binding = real_append(slot, canonical)
                if not sent:
                    sent = True
                    os.kill(os.getpid(), signal_number)
                return binding

            captured = None
            with patch.object(
                CanonicalAuthorityRegistrationSlot, "append", new=append_then_signal
            ):
                with ASSERTIONS.assertRaises(InitializationCancellation) as raised:
                    with begin_canonical_authority_registration(
                        pending.runtime, fixture.authority
                    ) as transaction:
                        captured = transaction.binding
                        transaction.acknowledge_binding()
            assert raised.exception.cancellation_kind is expected_kind
            assert captured is not None
            reopened = open_canonical_checkin_customer_authority(
                pending.runtime, captured, fixture.authority
            )
            reopened.close()
            fixture.close()

