"""
Unit tests for safeio - The Atomic I/O Kernel.

Tests critical atomicity and correctness guarantees.
NO MOCKS - uses real filesystem operations in temp directories.
"""

from __future__ import annotations

import hashlib
import os
import warnings
from pathlib import Path

import pytest


class TestAtomicWrite:
    """Test atomic write operations - critical for data integrity."""

    def test_atomic_write_creates_parent_directories(self, temp_sandbox: Path) -> None:
        """atomic_write creates parent directories with make_dirs=True."""
        from aperion_fsal.core.safeio import atomic_write

        target_path = "deep/nested/file.txt"
        assert not (temp_sandbox / "deep" / "nested").exists()

        atomic_write(target_path, b"test content", make_dirs=True, root=temp_sandbox)

        target = temp_sandbox / target_path
        assert target.exists()
        assert target.read_bytes() == b"test content"

    def test_atomic_write_overwrites_existing(self, temp_sandbox: Path) -> None:
        """atomic_write correctly overwrites existing files."""
        from aperion_fsal.core.safeio import atomic_write

        target_path = "atomic.txt"

        atomic_write(target_path, b"initial", root=temp_sandbox)
        assert (temp_sandbox / target_path).read_bytes() == b"initial"

        atomic_write(target_path, b"updated", root=temp_sandbox)
        assert (temp_sandbox / target_path).read_bytes() == b"updated"

    def test_atomic_write_cleans_temp_on_error(
        self, temp_sandbox: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        """atomic_write cleans up temp file on error."""
        from aperion_fsal.core.safeio import atomic_write

        target = temp_sandbox / "data.txt"
        payload = b"x" * 8

        called = {"replace": 0}

        def boom(src: str, dst: str) -> None:
            called["replace"] += 1
            raise RuntimeError("simulated failure during replace")

        monkeypatch.setattr(os, "replace", boom)

        with pytest.raises(RuntimeError):
            atomic_write(target, payload)

        # No destination created
        assert not target.exists()

        # No temp files left behind (they start with tmp prefix from tempfile)
        leftovers = list(temp_sandbox.iterdir())
        assert all(not p.name.startswith("tmp") for p in leftovers)

    def test_atomic_write_respects_perms(self, temp_sandbox: Path) -> None:
        """atomic_write applies requested permissions."""
        from aperion_fsal.core.safeio import atomic_write

        target_path = "perms.txt"
        atomic_write(target_path, b"data", perms=0o600, root=temp_sandbox)

        target = temp_sandbox / target_path
        assert target.exists()
        # Check permissions (may vary by umask, but should have 0600 base)
        mode = target.stat().st_mode & 0o777
        assert mode == 0o600


class TestReadOperations:
    """Test read operations."""

    def test_read_success(self, temp_sandbox: Path) -> None:
        """read() returns file content as bytes."""
        from aperion_fsal.core.safeio import read

        target = temp_sandbox / "test.txt"
        target.write_bytes(b"Hello, World!")

        content = read("test.txt", root=temp_sandbox)
        assert content == b"Hello, World!"

    def test_read_with_offset_and_length(self, temp_sandbox: Path) -> None:
        """read() supports offset and length parameters."""
        from aperion_fsal.core.safeio import read

        target = temp_sandbox / "test.txt"
        target.write_bytes(b"0123456789")

        content = read("test.txt", offset=3, length=5, root=temp_sandbox)
        assert content == b"34567"

    def test_read_file_not_found(self, temp_sandbox: Path) -> None:
        """read() raises FileNotFoundError for missing files."""
        from aperion_fsal.core.safeio import read

        with pytest.raises(FileNotFoundError):
            read("nonexistent.txt", root=temp_sandbox)

    def test_read_bytes_success(self, temp_sandbox: Path) -> None:
        """read_bytes returns file content as bytes."""
        from aperion_fsal.core.safeio import read_bytes

        target = temp_sandbox / "test.bin"
        data = b"\x00\x01\x02\x03"
        target.write_bytes(data)

        content = read_bytes("test.bin", root=temp_sandbox)
        assert content == data


class TestWriteOperations:
    """Test write operations."""

    def test_write_bytes_creates_file(self, temp_sandbox: Path) -> None:
        """write_bytes creates file with binary content."""
        from aperion_fsal.core.safeio import write_bytes

        data = b"\x00\x01\x02\x03"
        write_bytes("test.bin", data, root=temp_sandbox)

        target = temp_sandbox / "test.bin"
        assert target.exists()
        assert target.read_bytes() == data

    def test_write_b64_success(self, temp_sandbox: Path) -> None:
        """write() decodes base64 and writes correctly."""
        import base64

        from aperion_fsal.core.safeio import read_bytes, write

        content = b"Hello, Base64!"
        content_b64 = base64.b64encode(content).decode("ascii")

        write("b64.txt", content_b64, root=temp_sandbox)
        assert read_bytes("b64.txt", root=temp_sandbox) == content

    def test_write_invalid_b64(self, temp_sandbox: Path) -> None:
        """write() raises ValueError for invalid base64."""
        from aperion_fsal.core.safeio import write

        with pytest.raises(ValueError, match="invalid base64"):
            write("test.txt", "not-valid-base64!!!", root=temp_sandbox)


class TestDirectoryOperations:
    """Test directory operations."""

    def test_mkdir_creates_directory(self, temp_sandbox: Path) -> None:
        """mkdir creates directory."""
        from aperion_fsal.core.safeio import mkdir

        mkdir("newdir", root=temp_sandbox)

        target = temp_sandbox / "newdir"
        assert target.exists()
        assert target.is_dir()

    def test_listdir_basic(self, populated_sandbox: Path) -> None:
        """listdir returns all entries with metadata."""
        from aperion_fsal.core.safeio import listdir

        entries = listdir(".", root=populated_sandbox)

        # Should have files and directories
        assert len(entries) >= 4
        assert any(e["is_dir"] for e in entries)
        assert any(not e["is_dir"] for e in entries)
        assert all("mtime" in e for e in entries)

    def test_listdir_files_only(self, populated_sandbox: Path) -> None:
        """listdir with files_only filter."""
        from aperion_fsal.core.safeio import listdir

        entries = listdir(".", files_only=True, root=populated_sandbox)

        assert len(entries) >= 1
        assert all(not e["is_dir"] for e in entries)

    def test_listdir_glob_filter(self, populated_sandbox: Path) -> None:
        """listdir with glob pattern filter."""
        from aperion_fsal.core.safeio import listdir

        entries = listdir(".", glob="*.txt", root=populated_sandbox)

        assert len(entries) >= 1
        assert all(e["path"].endswith(".txt") for e in entries)

    def test_delete_removes_file(self, temp_sandbox: Path) -> None:
        """delete removes a file."""
        from aperion_fsal.core.safeio import delete

        target = temp_sandbox / "file.txt"
        target.write_bytes(b"content")

        delete("file.txt", root=temp_sandbox)
        assert not target.exists()

    def test_delete_recursive(self, populated_sandbox: Path) -> None:
        """delete with recursive=True removes directory tree."""
        from aperion_fsal.core.safeio import delete

        delete("subdir", recursive=True, root=populated_sandbox)
        assert not (populated_sandbox / "subdir").exists()


class TestMoveAndCopy:
    """Test move and copy operations."""

    def test_move_renames_file(self, temp_sandbox: Path) -> None:
        """move renames/moves a file."""
        from aperion_fsal.core.safeio import move

        source = temp_sandbox / "source.txt"
        source.write_bytes(b"content")

        move("source.txt", "dest.txt", root=temp_sandbox)

        assert not source.exists()
        assert (temp_sandbox / "dest.txt").read_bytes() == b"content"

    def test_copy_duplicates_file(self, temp_sandbox: Path) -> None:
        """copy creates a duplicate of a file."""
        from aperion_fsal.core.safeio import copy

        source = temp_sandbox / "source.txt"
        source.write_bytes(b"content")

        copy("source.txt", "dest.txt", root=temp_sandbox)

        assert source.exists()
        assert (temp_sandbox / "dest.txt").read_bytes() == b"content"

    def test_move_fails_if_dest_exists(self, temp_sandbox: Path) -> None:
        """move fails if destination exists without overwrite."""
        from aperion_fsal.core.safeio import move

        (temp_sandbox / "src.txt").write_bytes(b"src")
        (temp_sandbox / "dst.txt").write_bytes(b"dst")

        with pytest.raises(FileExistsError):
            move("src.txt", "dst.txt", root=temp_sandbox)


class TestHashing:
    """Test file hashing."""

    def test_digest_sha256(self, temp_sandbox: Path) -> None:
        """digest computes SHA256 hash correctly."""
        from aperion_fsal.core.safeio import digest

        target = temp_sandbox / "file.txt"
        content = b"Hello, World!"
        target.write_bytes(content)

        expected = hashlib.sha256(content).hexdigest()
        result = digest("file.txt", algo="sha256", root=temp_sandbox)

        assert result == expected

    def test_digest_md5(self, temp_sandbox: Path) -> None:
        """digest supports MD5 algorithm."""
        from aperion_fsal.core.safeio import digest

        target = temp_sandbox / "file.txt"
        content = b"test content"
        target.write_bytes(content)

        expected = hashlib.md5(content).hexdigest()  # noqa: S324
        result = digest("file.txt", algo="md5", root=temp_sandbox)

        assert result == expected


class TestResourceLeaks:
    """Test for file descriptor leaks."""

    def test_read_chunks_no_leak(self, temp_sandbox: Path) -> None:
        """read_chunks properly closes file handles."""
        from aperion_fsal.core.safeio import read_chunks, write_bytes

        target_path = "blob.bin"
        write_bytes(target_path, b"A" * (128 * 1024), root=temp_sandbox)

        with warnings.catch_warnings():
            warnings.simplefilter("error", ResourceWarning)

            total = 0
            with read_chunks(target_path, chunk_size=8192, root=temp_sandbox) as chunks:
                for part in chunks:
                    total += len(part)
            assert total == 128 * 1024


class TestWriteReadRoundtrip:
    """Integration tests for write/read cycles."""

    def test_atomic_write_read_roundtrip(self, temp_sandbox: Path) -> None:
        """Atomic write and read maintains data integrity."""
        from aperion_fsal.core.safeio import atomic_write, read_bytes

        data = b"Binary content \x00\xff\xaa"

        atomic_write("test.bin", data, root=temp_sandbox)
        result = read_bytes("test.bin", root=temp_sandbox)

        assert result == data

    def test_multiple_operations_sequence(self, temp_sandbox: Path) -> None:
        """Multiple operations work correctly in sequence."""
        from aperion_fsal.core.safeio import listdir, mkdir, read, write_bytes

        mkdir("dir1", root=temp_sandbox)
        mkdir("dir2", root=temp_sandbox)
        write_bytes("dir1/file1.txt", b"content1", root=temp_sandbox)
        write_bytes("dir2/file2.txt", b"content2", root=temp_sandbox)

        entries = listdir(".", root=temp_sandbox)
        assert len(entries) >= 2

        assert read("dir1/file1.txt", root=temp_sandbox) == b"content1"
        assert read("dir2/file2.txt", root=temp_sandbox) == b"content2"
