"""Tests for :func:`mgf.common._io.atomic_write` (rule CF-04).

RA-01 — the shared atomic-write helper used by the vault, crash
reporter, and versioned-file store. The contract:

  1. Body is fsync'd before the rename.
  2. Rename is atomic (same-filesystem ``os.replace``).
  3. Optional parent-directory fsync makes the rename durable.
  4. On failure, no temp file is left behind.
  5. Mode is applied before the rename so the file is never
     readable at mode 0o644 even briefly.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

import pytest

from mgf.common._io import atomic_write

if TYPE_CHECKING:
    from pathlib import Path


class TestAtomicWriteBody:
    """The happy-path contract: file lands with the right bytes
    and the right mode."""

    def test_writes_bytes(self, tmp_path: Path) -> None:
        target = tmp_path / "out.bin"
        atomic_write(target, b"\x00\x01\x02hello")
        assert target.read_bytes() == b"\x00\x01\x02hello"

    def test_writes_str(self, tmp_path: Path) -> None:
        target = tmp_path / "out.txt"
        atomic_write(target, "hello, world\n")
        assert target.read_text(encoding="utf-8") == "hello, world\n"

    def test_writes_str_custom_encoding(self, tmp_path: Path) -> None:
        target = tmp_path / "out.txt"
        atomic_write(target, "ünîcödé", encoding="utf-16")
        # Round-trip via UTF-16 confirms the encoding flag flows.
        assert target.read_text(encoding="utf-16") == "ünîcödé"

    def test_overwrites_existing(self, tmp_path: Path) -> None:
        target = tmp_path / "out.txt"
        target.write_text("old")
        atomic_write(target, "new")
        assert target.read_text() == "new"


class TestAtomicWriteMode:
    """Final mode is applied; default 0o600 matches CF-04 / SC-04."""

    def test_default_mode_is_0o600(self, tmp_path: Path) -> None:
        target = tmp_path / "secret.bin"
        atomic_write(target, b"secret")
        # Last three octal digits are the user/group/world bits.
        assert oct(target.stat().st_mode)[-3:] == "600"

    def test_explicit_mode_overrides(self, tmp_path: Path) -> None:
        target = tmp_path / "public.txt"
        atomic_write(target, "public content", mode=0o644)
        assert oct(target.stat().st_mode)[-3:] == "644"


class TestAtomicWriteFsync:
    """Body fsync runs BEFORE the rename. Pinned via mock."""

    def test_fsync_called_before_rename(
        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        """``os.fsync(fd)`` MUST be called between write and rename.
        Without it, a crash between write and rename can leave the
        target file empty or truncated — the exact failure mode
        RA-01 exists to prevent."""
        target = tmp_path / "out.bin"
        order: list[str] = []

        real_fsync = __import__("os").fsync
        real_replace = __import__("os").replace

        def tracking_fsync(fd: int) -> None:
            order.append("fsync")
            real_fsync(fd)

        def tracking_replace(src: str | Path, dst: str | Path) -> None:
            order.append("replace")
            real_replace(src, dst)

        monkeypatch.setattr("mgf.common._io.os.fsync", tracking_fsync)
        monkeypatch.setattr("mgf.common._io.os.replace", tracking_replace)

        atomic_write(target, b"durable")
        assert order == ["fsync", "replace"], f"expected fsync→replace, got {order}"

    def test_fsync_dir_called_when_requested(
        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        target = tmp_path / "out.bin"
        fsync_targets: list[str] = []

        real_fsync = __import__("os").fsync

        def tracking_fsync(fd: int) -> None:
            # Best-effort label: was the fd opened on a directory?
            try:
                import os as _os

                # /proc/self/fd/N is a procfs symlink to the open
                # file/dir; os.path.isdir is the right shape here
                # (Path.is_dir resolves the symlink and may not
                # match procfs semantics on every kernel).
                if _os.path.isdir(f"/proc/self/fd/{fd}"):  # noqa: PTH112
                    fsync_targets.append("dir")
                else:
                    fsync_targets.append("file")
            except Exception:
                fsync_targets.append("file")
            real_fsync(fd)

        monkeypatch.setattr("mgf.common._io.os.fsync", tracking_fsync)

        atomic_write(target, b"durable", fsync_dir=True)
        # We expect at least two fsyncs — one for the file, one
        # for the parent directory. Exact target detection via
        # /proc/self/fd is Linux-only, so just count.
        assert len(fsync_targets) >= 2, (
            f"fsync_dir=True should produce ≥ 2 fsync calls, got {fsync_targets}"
        )

    def test_fsync_dir_default_off(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
        """fsync_dir defaults to False so most callers don't pay
        the directory fsync cost."""
        target = tmp_path / "out.bin"
        fsync_calls: list[int] = []
        real_fsync = __import__("os").fsync

        def tracking_fsync(fd: int) -> None:
            fsync_calls.append(fd)
            real_fsync(fd)

        monkeypatch.setattr("mgf.common._io.os.fsync", tracking_fsync)
        atomic_write(target, b"content")
        # Exactly one fsync — the body. No parent-dir fsync.
        assert len(fsync_calls) == 1


class TestAtomicWriteFailure:
    """On write failure, no partial file is left behind."""

    def test_no_temp_file_after_write_failure(
        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        target = tmp_path / "out.bin"

        # Force fsync to raise. The temp file should be cleaned
        # up; the target should NOT exist (write never landed).
        def bad_fsync(fd: int) -> None:
            raise OSError("simulated disk full")

        monkeypatch.setattr("mgf.common._io.os.fsync", bad_fsync)
        with pytest.raises(OSError, match="simulated disk full"):
            atomic_write(target, b"never lands")

        # No leftover temp files in the directory.
        leftovers = list(tmp_path.glob(".out.bin*"))
        assert leftovers == [], f"temp file leaked after write failure: {leftovers}"
        # Target was never created.
        assert not target.exists()

    def test_no_temp_file_after_rename_failure(
        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        target = tmp_path / "out.bin"

        def bad_replace(src: str | Path, dst: str | Path) -> None:
            raise OSError("simulated rename failure")

        monkeypatch.setattr("mgf.common._io.os.replace", bad_replace)
        with pytest.raises(OSError, match="simulated rename failure"):
            atomic_write(target, b"never lands")

        leftovers = list(tmp_path.glob(".out.bin*"))
        assert leftovers == []
        assert not target.exists()


class TestAtomicWriteFsyncDirWindowsSkip:
    """fsync_dir is a no-op on Windows — os.open(dir, O_RDONLY)
    isn't supported there."""

    def test_fsync_dir_noop_on_windows(
        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        target = tmp_path / "out.bin"
        fsync_calls: list[int] = []
        real_fsync = __import__("os").fsync

        def tracking_fsync(fd: int) -> None:
            fsync_calls.append(fd)
            real_fsync(fd)

        monkeypatch.setattr("mgf.common._io.sys.platform", "win32")
        monkeypatch.setattr("mgf.common._io.os.fsync", tracking_fsync)

        atomic_write(target, b"content", fsync_dir=True)
        # Only the body fsync — the dir-fsync branch is skipped
        # under sys.platform == "win32".
        assert len(fsync_calls) == 1

    def test_fsync_dir_swallows_oserror(
        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        """Directory fsync failure must not raise — the file is
        already in place; the worst case is a lost rename on a
        crash within the next few ms."""
        target = tmp_path / "out.bin"

        original_open = __import__("os").open

        def maybe_bad_open(*args: object, **kwargs: object) -> int:
            # Only break the directory-open call (the second one).
            if isinstance(args[0], str) and args[0] == str(tmp_path):
                raise OSError("simulated permission denied on dir")
            return original_open(*args, **kwargs)  # type: ignore[arg-type]

        monkeypatch.setattr("mgf.common._io.os.open", maybe_bad_open)
        # Should NOT raise; the body write already succeeded.
        atomic_write(target, b"content", fsync_dir=True)
        assert target.read_bytes() == b"content"
