"""Tests for netanel_core._atomic — crash-safe atomic file-write utilities.

BUG-033: state file corruption on crash-mid-write.

Strategy:
- Verify normal write produces correct content.
- Verify original file preserved when write fails (simulate crash by
  patching Path.replace to raise OSError).
- Verify no temp files are left on failure.
- Verify parent directories are created automatically.
"""

from __future__ import annotations

import json
from pathlib import Path
from unittest.mock import patch

import pytest

from netanel_core._atomic import atomic_write_json, atomic_write_text

# ────────────────────────────────────────────
# atomic_write_json
# ────────────────────────────────────────────


class TestAtomicWriteJson:
    """Tests for atomic_write_json()."""

    def test_writes_correct_content(self, tmp_path):
        """Normal write produces parseable JSON with correct data."""
        dest = tmp_path / "state.json"
        data = {"key": "value", "count": 42, "nested": {"a": 1}}
        atomic_write_json(dest, data)

        assert dest.exists()
        loaded = json.loads(dest.read_text(encoding="utf-8"))
        assert loaded == data

    def test_creates_parent_dirs(self, tmp_path):
        """Destination dir is created if it does not exist."""
        dest = tmp_path / "subdir" / "deep" / "state.json"
        atomic_write_json(dest, {"x": 1})
        assert dest.exists()

    def test_overwrites_existing_file(self, tmp_path):
        """Calling twice overwrites the previous content."""
        dest = tmp_path / "state.json"
        atomic_write_json(dest, {"v": 1})
        atomic_write_json(dest, {"v": 2})

        assert json.loads(dest.read_text())["v"] == 2

    def test_preserves_original_on_rename_failure(self, tmp_path):
        """BUG-033: original file is intact when rename (crash) fails.

        Simulates a crash between temp-write and rename by patching
        Path.replace to raise OSError. The original must survive unchanged.
        """
        dest = tmp_path / "state.json"
        original = {"original": True}
        dest.write_text(json.dumps(original), encoding="utf-8")

        with (
            patch.object(Path, "replace", side_effect=OSError("disk full")),
            pytest.raises(OSError),
        ):
            atomic_write_json(dest, {"corrupted": True})

        # Original unchanged
        loaded = json.loads(dest.read_text(encoding="utf-8"))
        assert loaded == original

    def test_no_temp_files_left_on_failure(self, tmp_path):
        """BUG-033: temp file is cleaned up when rename fails."""
        dest = tmp_path / "state.json"
        dest.write_text("{}", encoding="utf-8")

        with (
            patch.object(Path, "replace", side_effect=OSError("disk full")),
            pytest.raises(OSError),
        ):
            atomic_write_json(dest, {"new": "data"})

        # Only the original file remains
        files = list(tmp_path.iterdir())
        assert len(files) == 1
        assert files[0].name == "state.json"

    def test_empty_dict(self, tmp_path):
        """Empty dict is written as empty JSON object."""
        dest = tmp_path / "empty.json"
        atomic_write_json(dest, {})
        assert json.loads(dest.read_text()) == {}


# ────────────────────────────────────────────
# atomic_write_text
# ────────────────────────────────────────────


class TestAtomicWriteText:
    """Tests for atomic_write_text()."""

    def test_writes_correct_content(self, tmp_path):
        """Normal write produces the exact text content."""
        dest = tmp_path / "data.jsonl"
        content = '{"a": 1}\n{"b": 2}\n'
        atomic_write_text(dest, content)

        assert dest.read_text(encoding="utf-8") == content

    def test_creates_parent_dirs(self, tmp_path):
        """Destination dir is created if it does not exist."""
        dest = tmp_path / "sub" / "data.jsonl"
        atomic_write_text(dest, "line\n")
        assert dest.exists()

    def test_preserves_original_on_rename_failure(self, tmp_path):
        """BUG-033: original file is intact when rename fails."""
        dest = tmp_path / "history.jsonl"
        original = '{"old": true}\n'
        dest.write_text(original, encoding="utf-8")

        with (
            patch.object(Path, "replace", side_effect=OSError("disk full")),
            pytest.raises(OSError),
        ):
            atomic_write_text(dest, '{"new": true}\n')

        assert dest.read_text(encoding="utf-8") == original

    def test_no_temp_files_left_on_failure(self, tmp_path):
        """BUG-033: temp file cleaned up on rename failure."""
        dest = tmp_path / "history.jsonl"
        dest.write_text("original\n", encoding="utf-8")

        with (
            patch.object(Path, "replace", side_effect=OSError("disk full")),
            pytest.raises(OSError),
        ):
            atomic_write_text(dest, "new\n")

        files = list(tmp_path.iterdir())
        assert len(files) == 1
        assert files[0].name == "history.jsonl"

    def test_overwrites_existing_file(self, tmp_path):
        """Calling twice overwrites the previous content."""
        dest = tmp_path / "data.txt"
        atomic_write_text(dest, "first\n")
        atomic_write_text(dest, "second\n")
        assert dest.read_text(encoding="utf-8") == "second\n"
