import hashlib
import os
from pathlib import Path

import pytest

from nutricoach_continuity.contract import FilePin, RootPin
from nutricoach_continuity.durable import reserve_exclusive
from nutricoach_continuity.fdio import SecureIoError, open_root, read_pinned, write_all
from nutricoach_continuity.preparation import PreparationError, pin_file


def root_pin(path: Path) -> RootPin:
    info = path.stat(follow_symlinks=False)
    return RootPin(
        path=path,
        device=info.st_dev,
        inode=info.st_ino,
        uid=info.st_uid,
        gid=info.st_gid,
        mode=0o700,
        nlink=info.st_nlink,
    )


def test_no_follow_component_walk_rejects_symlink_root(tmp_path: Path) -> None:
    # Given: a symlink substituted for a bound root component
    real = tmp_path / "real"
    real.mkdir(mode=0o700)
    alias = tmp_path / "alias"
    _ = alias.symlink_to(real, target_is_directory=True)
    pin = root_pin(real).model_copy(update={"path": alias})
    # When / Then: component traversal fails closed
    with pytest.raises(OSError), open_root(pin):
        pass


def test_pinned_file_rejects_hardlink_and_mode_drift(tmp_path: Path) -> None:
    # Given: a valid private file captured before adversarial drift
    root = tmp_path / "root"
    root.mkdir(mode=0o700)
    source = root / "input"
    _ = source.write_bytes(b"bound")
    source.chmod(0o600)
    info = source.stat()
    pin = FilePin(
        path=source,
        sha256=hashlib.sha256(b"bound").hexdigest(),
        device=info.st_dev,
        inode=info.st_ino,
        uid=info.st_uid,
        gid=info.st_gid,
        mode=0o600,
        nlink=1,
        size=5,
    )
    os.link(source, root / "second")
    # When / Then: nlink drift is rejected
    with open_root(root_pin(root)) as opened, pytest.raises(SecureIoError, match="identity"):
        _ = read_pinned(opened, pin)


def test_preparation_rejects_permission_drift(tmp_path: Path) -> None:
    # Given: a group-readable proposed package input
    path = tmp_path / "input"
    _ = path.write_bytes(b"x")
    _ = path.chmod(0o640)
    # When / Then: it cannot enter a sealed draft
    with pytest.raises(PreparationError, match="unsafe"):
        _ = pin_file(path, expected_mode=0o600)


def test_write_all_handles_eintr_and_short_writes(tmp_path: Path) -> None:
    # Given: a writer that interrupts once and accepts only three bytes
    path = tmp_path / "out"
    descriptor = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
    calls = 0

    def hostile(descriptor: int, payload: bytes, /) -> int:
        nonlocal calls
        calls += 1
        if calls == 1:
            raise InterruptedError
        return os.write(descriptor, payload[:3])

    # When: the full-write contract runs
    try:
        write_all(descriptor, b"abcdefgh", hostile)
    finally:
        os.close(descriptor)
    # Then: no byte is lost
    assert path.read_bytes() == b"abcdefgh"


def test_second_launcher_is_refused_without_replacing_reservation(tmp_path: Path) -> None:
    # Given: one globally reserved launcher
    root = tmp_path / "authorization"
    root.mkdir(mode=0o700)
    reserve_exclusive(root, "authorization-reserved.json", b"one")
    # When / Then: a second launcher is refused and the original remains
    with pytest.raises(SecureIoError, match="consumed"):
        reserve_exclusive(root, "authorization-reserved.json", b"two")
    assert (root / "authorization-reserved.json").read_bytes() == b"one"
