from __future__ import annotations

import shutil
from datetime import date
from pathlib import Path
from tempfile import TemporaryDirectory
from threading import Barrier, Event as ThreadEvent, Thread
from unittest.mock import patch

from checkin_cli.models import EventType
from checkin_cli import weekly_operations_canonical_snapshot as snapshot_module
from checkin_cli.store import CanonicalEventSnapshot, CanonicalEventTransaction
from checkin_cli.weekly_operations_correlation import (
    CanonicalCheckinCorrelationTransaction,
    CorrelationAction,
    CorrelationRequest,
    CorrelationResult,
    CorrelationScope,
)
from checkin_cli.weekly_operations_canonical_snapshot import CanonicalDescriptorSet
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,
    expect_conflict,
    registered_source_at,
    store_at,
)


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


def test_reacquisition_rejects_replaced_customer_root() -> None:
    with TemporaryDirectory(prefix="task4-r3-root-") as raw:
        root = Path(raw)
        base = event("r3_replaced_root_base01")
        fixture = store_at(root / "sidecar")
        canonical = registered_source_at(
            root / "profile", CUSTOMER, base, registry_authority=fixture.authority
        )
        _ = _commit(fixture.store, canonical.source)
        expected_binding = canonical.source.registered_binding
        canonical.source.close()
        customer_root = canonical.runtime.data_root
        sealed = customer_root.with_name("sealed-customer-root")
        _ = customer_root.rename(sealed)
        _ = shutil.copytree(sealed, customer_root)
        replacement = CanonicalEventTransaction.for_customer_runtime(canonical.runtime)
        correction = event(
            "r3_replaced_root_correct1", EventType.CORRECTION, supersedes=base.event_id
        )
        _ = replacement.append_one(correction)

        with expect_conflict("reacquisition drift"):
            _ = open_canonical_checkin_customer_authority(
                canonical.runtime, expected_binding, fixture.authority
            )
        assert len(fixture.store.read()) == 1
        fixture.close()


def test_lock_path_substitution_fails_before_sidecar_mutation() -> None:
    with TemporaryDirectory(prefix="task4-r3-lock-") as raw:
        root = Path(raw)
        fixture = store_at(root / "sidecar")
        canonical = registered_source_at(
            root / "profile",
            CUSTOMER,
            event("r3_lock_substitute_root1"),
            registry_authority=fixture.authority,
        )
        rogue_lock = canonical.runtime.wizard_root / "rogue.lock"
        rogue_lock.touch(mode=0o600)
        rogue_lock.chmod(0o600)
        canonical.transaction.lock_path = rogue_lock

        with expect_conflict("transaction path drift"):
            _ = _commit(fixture.store, canonical.source)
        assert fixture.store.read() == ()
        canonical.close()
        fixture.close()


def test_swap_restore_cannot_commit_rogue_snapshot() -> None:
    with TemporaryDirectory(prefix="task4-r3-swap-") as raw:
        root = Path(raw)
        fixture = store_at(root / "sidecar")
        canonical = registered_source_at(
            root / "profile",
            CUSTOMER,
            event("r3_sealed_source_root01"),
            registry_authority=fixture.authority,
        )
        rogue = CanonicalEventTransaction(
            root / "rogue" / "wizard" / "events.jsonl",
            root / "rogue" / "nutrition-plans" / "canonical-sequence.jsonl",
        )
        rogue.events_path.parent.mkdir(parents=True, mode=0o700)
        rogue.sequence_path.parent.mkdir(parents=True, mode=0o700)
        rogue_event = event("r3_rogue_source_root001")
        _ = rogue.append_one(rogue_event)
        before_read, after_read = ThreadEvent(), ThreadEvent()
        continue_read, continue_return = Barrier(2), Barrier(2)
        original = snapshot_module.read_canonical_snapshot
        rogue_digest = rogue.read_snapshot_readonly().sequence_rows[0]["event_digest"]

        def controlled(descriptors: CanonicalDescriptorSet) -> CanonicalEventSnapshot:
            before_read.set()
            _ = continue_read.wait(timeout=5)
            snapshot = original(descriptors)
            after_read.set()
            _ = continue_return.wait(timeout=5)
            return snapshot

        results: list[CorrelationResult] = []
        worker = Thread(
            target=lambda: results.append(_commit(fixture.store, canonical.source))
        )
        events_path = canonical.transaction.events_path
        sequence_path = canonical.transaction.sequence_path
        sealed_events = events_path.with_suffix(".sealed")
        sealed_sequence = sequence_path.with_suffix(".sealed")
        displaced_events = events_path.with_suffix(".rogue")
        displaced_sequence = sequence_path.with_suffix(".rogue")
        with patch.object(snapshot_module, "read_canonical_snapshot", controlled):
            worker.start()
            assert before_read.wait(timeout=5)
            _ = events_path.rename(sealed_events)
            _ = sequence_path.rename(sealed_sequence)
            _ = rogue.events_path.rename(events_path)
            _ = rogue.sequence_path.rename(sequence_path)
            _ = continue_read.wait(timeout=5)
            assert after_read.wait(timeout=5)
            _ = events_path.rename(displaced_events)
            _ = sequence_path.rename(displaced_sequence)
            _ = sealed_events.rename(events_path)
            _ = sealed_sequence.rename(sequence_path)
            _ = continue_return.wait(timeout=5)
            worker.join(timeout=5)
        assert not worker.is_alive()
        assert len(results) == 1
        rows = fixture.store.read()
        assert len(rows) == 1
        assert rows[0].source_event_id == "r3_sealed_source_root01"
        assert rows[0].source_event_digest != rogue_digest
        canonical.close()
        fixture.close()


def _assert_child_replacement(child: str) -> None:
    with TemporaryDirectory(prefix=f"task4-r3-{child}-") as raw:
        root = Path(raw)
        fixture = store_at(root / "sidecar")
        canonical = registered_source_at(
            root / "profile",
            CUSTOMER,
            event(f"r3_{child}_replace_root1"),
            registry_authority=fixture.authority,
        )
        paths = {
            "events": canonical.transaction.events_path,
            "sequence": canonical.transaction.sequence_path,
            "lock": canonical.transaction.lock_path,
        }
        target = paths[child]
        original = target.with_suffix(target.suffix + ".original")
        _ = target.rename(original)
        _ = target.write_bytes(original.read_bytes())
        target.chmod(0o600)

        with expect_conflict("identity drift"):
            _ = _commit(fixture.store, canonical.source)
        assert fixture.store.read() == ()
        canonical.close()
        fixture.close()


def test_event_child_replacement_fails_closed() -> None:
    _assert_child_replacement("events")


def test_sequence_child_replacement_fails_closed() -> None:
    _assert_child_replacement("sequence")


def test_lock_child_replacement_fails_closed() -> None:
    _assert_child_replacement("lock")
