from __future__ import annotations

import importlib.util
import os
import stat
import threading
from pathlib import Path
from typing import Protocol, cast

import pytest

MODULE_NAME = "checkin_cli.nutrition_onboarding_store"


def _load_module():
    if importlib.util.find_spec(MODULE_NAME) is None:
        pytest.fail("nutrition onboarding store contract missing")
    return __import__(MODULE_NAME, fromlist=["*"])


def _new_store(mod, root: Path):
    return mod.NutritionOnboardingStore(root=root)


def test_contract_present_or_missing(tmp_path: Path) -> None:
    _load_module()


def test_initialize_creates_private_0700_0600_layout(tmp_path: Path) -> None:
    mod = _load_module()
    store = _new_store(mod, tmp_path / "nutrition-onboarding")
    store.initialize()

    assert stat.S_IMODE(store.root.stat().st_mode) == 0o700
    assert stat.S_IMODE(store.lock_path.stat().st_mode) == 0o600
    assert stat.S_IMODE(store.state_path.stat().st_mode) == 0o600


def test_initialize_creates_every_profile_customer_ancestor_0700_under_umask_0002(
    tmp_path: Path,
) -> None:
    mod = _load_module()
    profile = tmp_path / "profile"
    profile.mkdir(mode=0o700)
    root = profile / "data" / "customers" / "customer-a" / "nutrition-onboarding"
    previous = os.umask(0o002)
    try:
        store = mod.NutritionOnboardingStore(root=root, profile_root=profile)
        store.initialize()
    finally:
        os.umask(previous)
    for directory in (
        profile / "data",
        profile / "data" / "customers",
        profile / "data" / "customers" / "customer-a",
        root,
        root / "transient",
    ):
        assert stat.S_IMODE(directory.stat().st_mode) == 0o700


def test_initialize_rejects_root_symlink(tmp_path: Path) -> None:
    mod = _load_module()
    real_root = tmp_path / "real"
    real_root.mkdir()
    linked_root = tmp_path / "linked"
    linked_root.symlink_to(real_root, target_is_directory=True)

    with pytest.raises(ValueError, match="symlink"):
        _new_store(mod, linked_root).initialize()


def test_initialize_rejects_state_symlink(tmp_path: Path) -> None:
    mod = _load_module()
    store = _new_store(mod, tmp_path / "nutrition-onboarding")
    store.initialize()

    store.state_path.unlink()
    store.state_path.symlink_to(tmp_path / "foreign-state.json")

    with pytest.raises(ValueError, match="symlink"):
        store.reload()


class _GenerationResult(Protocol):
    generation: int


def test_generation_cas_has_one_winner(tmp_path: Path) -> None:
    mod = _load_module()
    store = _new_store(mod, tmp_path / "nutrition-onboarding")
    store.initialize()

    barrier = threading.Barrier(2)
    winners: list[_GenerationResult] = []
    losers: list[BaseException] = []

    def worker() -> None:
        barrier.wait()
        try:
            winners.append(cast(
                _GenerationResult,
                store.compare_and_swap_generation(
                    session_id="session-1",
                    expected_generation=0,
                    next_generation=1,
                ),
            ))
        except BaseException as exc:  # noqa: BLE001
            losers.append(exc)

    left = threading.Thread(target=worker)
    right = threading.Thread(target=worker)
    left.start()
    right.start()
    left.join()
    right.join()

    assert len(winners) == 1
    assert len(losers) == 1
    assert winners[0].generation == 1


def test_prepared_transitions_to_committed(tmp_path: Path) -> None:
    mod = _load_module()
    store = _new_store(mod, tmp_path / "nutrition-onboarding")
    store.initialize()

    prepared = store.mark_prepared(
        session_id="session-1",
        generation=1,
        payload={"provider": "telegram"},
    )
    assert prepared.state == "PREPARED"
    with pytest.raises(mod.StoreConflictError, match="already prepared"):
        store.mark_prepared(
            session_id="session-1",
            generation=1,
            payload={"provider": "telegram"},
        )

    committed = store.mark_committed(session_id="session-1", generation=1)
    assert committed.state == "COMMITTED"
    assert committed.generation == 1


def test_provider_unknown_becomes_uncertain_with_no_retry(tmp_path: Path) -> None:
    mod = _load_module()
    store = _new_store(mod, tmp_path / "nutrition-onboarding")
    store.initialize()

    calls: list[dict[str, object]] = []

    def provider(payload: dict[str, object]) -> None:
        calls.append(payload)
        raise LookupError("provider unknown")

    result = store.deliver(
        session_id="session-1",
        generation=1,
        provider=provider,
    )

    assert result.state == "UNCERTAIN"
    assert len(calls) == 1

    replay = store.deliver(
        session_id="session-1",
        generation=1,
        provider=provider,
    )
    assert replay.state == "UNCERTAIN"
    assert len(calls) == 1


def test_restart_reuses_existing_session_state(tmp_path: Path) -> None:
    mod = _load_module()
    root = tmp_path / "nutrition-onboarding"

    first = _new_store(mod, root)
    first.initialize()
    first.mark_prepared(
        session_id="session-1",
        generation=1,
        payload={"provider": "telegram"},
    )

    second = _new_store(mod, root)
    session = second.load_session("session-1")

    assert session.generation == 1
    assert session.state == "PREPARED"
    calls = 0

    def provider(_: dict[str, object]) -> None:
        nonlocal calls
        calls += 1

    with pytest.raises(mod.StoreConflictError, match="already prepared"):
        second.deliver(
            session_id="session-1",
            generation=1,
            provider=provider,
        )
    assert calls == 0


def test_purge_transient_removes_stale_artifacts(tmp_path: Path) -> None:
    mod = _load_module()
    store = _new_store(mod, tmp_path / "nutrition-onboarding")
    store.initialize()

    store.transient_path.mkdir(parents=True, exist_ok=True, mode=0o700)
    stale = store.transient_path / "stale.json"
    stale.write_text("{}", encoding="utf-8")
    os.chmod(stale, 0o600)

    removed = store.purge_transient()

    assert removed == 1
    assert list(store.transient_path.iterdir()) == []
