"""Unit tests for atomic file-write utilities (U3 — R6).

Covers:
- Happy path: write_text_atomic writes correct content
- Edge case: no temp file residue after success
- Edge case: original file intact when write fails (mocked os.replace)
- Edge case: concurrent writes don't produce mixed content
- Error path: permission error on target directory
- Integration: settings _write_yaml_config uses atomic write
- Integration: settings _write_env_var uses atomic write
"""

from __future__ import annotations

import os
import threading
from pathlib import Path
from unittest.mock import patch

import pytest

from agentkit.server.utils.atomic_write import write_text_atomic


# ---------------------------------------------------------------------------
# Happy path
# ---------------------------------------------------------------------------


class TestWriteTextAtomic:
    def test_writes_content_correctly(self, tmp_path: Path):
        target = tmp_path / "output.yaml"
        write_text_atomic(target, "key: value\n")
        assert target.read_text(encoding="utf-8") == "key: value\n"

    def test_overwrites_existing_file(self, tmp_path: Path):
        target = tmp_path / "output.yaml"
        target.write_text("old: content\n", encoding="utf-8")
        write_text_atomic(target, "new: content\n")
        assert target.read_text(encoding="utf-8") == "new: content\n"

    def test_no_temp_file_residue(self, tmp_path: Path):
        target = tmp_path / "output.yaml"
        write_text_atomic(target, "key: value\n")
        # No temp/partial files should remain in the directory.
        residue = [
            f.name
            for f in tmp_path.iterdir()
            if f.name != "output.yaml" and not f.name.startswith(".")
        ]
        assert residue == [], f"Unexpected temp files: {residue}"

    def test_accepts_str_path(self, tmp_path: Path):
        target = str(tmp_path / "output.txt")
        write_text_atomic(target, "hello\n")
        assert Path(target).read_text(encoding="utf-8") == "hello\n"

    def test_creates_parent_dirs(self, tmp_path: Path):
        target = tmp_path / "subdir" / "nested" / "output.yaml"
        write_text_atomic(target, "key: value\n")
        assert target.read_text(encoding="utf-8") == "key: value\n"


# ---------------------------------------------------------------------------
# Crash safety
# ---------------------------------------------------------------------------


class TestWriteTextAtomicCrashSafety:
    def test_original_intact_when_replace_fails(self, tmp_path: Path):
        """If os.replace raises, the original file must be unchanged."""
        target = tmp_path / "output.yaml"
        target.write_text("original: true\n", encoding="utf-8")

        with patch("agentkit.server.utils.atomic_write.os.replace") as mock_replace:
            mock_replace.side_effect = OSError("simulated crash")
            with pytest.raises(OSError, match="simulated crash"):
                write_text_atomic(target, "new: true\n")

        # Original content must be intact.
        assert target.read_text(encoding="utf-8") == "original: true\n"
        # Temp file must be cleaned up.
        residue = [
            f.name
            for f in tmp_path.iterdir()
            if f.name != "output.yaml" and not f.name.startswith(".")
        ]
        assert residue == [], f"Temp file not cleaned up: {residue}"

    def test_original_intact_when_fsync_fails(self, tmp_path: Path):
        """If fsync raises (e.g. disk full), the original must be intact."""
        target = tmp_path / "output.yaml"
        target.write_text("original: true\n", encoding="utf-8")

        with patch("agentkit.server.utils.atomic_write.os.fsync") as mock_fsync:
            mock_fsync.side_effect = OSError("disk full")
            with pytest.raises(OSError, match="disk full"):
                write_text_atomic(target, "new: true\n")

        assert target.read_text(encoding="utf-8") == "original: true\n"


# ---------------------------------------------------------------------------
# Concurrency
# ---------------------------------------------------------------------------


class TestWriteTextAtomicConcurrency:
    def test_concurrent_writes_no_mixed_content(self, tmp_path: Path):
        """Two threads writing different content — no mixed lines."""
        target = tmp_path / "output.yaml"
        target.write_text("init: 0\n", encoding="utf-8")

        barrier = threading.Barrier(2)
        results: list[bool] = []

        def writer(content: str) -> None:
            barrier.wait()
            write_text_atomic(target, content)
            results.append(True)

        t1 = threading.Thread(target=writer, args=("aaa: 1\nbbb: 2\nccc: 3\n",))
        t2 = threading.Thread(target=writer, args=("xxx: 1\nyyy: 2\nzzz: 3\n",))
        t1.start()
        t2.start()
        t1.join(timeout=5)
        t2.join(timeout=5)

        assert len(results) == 2
        # The final content must be one of the two writes, not a mix.
        final = target.read_text(encoding="utf-8")
        assert final in ("aaa: 1\nbbb: 2\nccc: 3\n", "xxx: 1\nyyy: 2\nzzz: 3\n")


# ---------------------------------------------------------------------------
# Error paths
# ---------------------------------------------------------------------------


class TestWriteTextAtomicErrors:
    def test_permission_error_on_unwritable_dir(self, tmp_path: Path):
        """Writing to a read-only directory raises PermissionError."""
        ro_dir = tmp_path / "readonly"
        ro_dir.mkdir()
        os.chmod(ro_dir, 0o444)
        try:
            target = ro_dir / "output.yaml"
            with pytest.raises(PermissionError):
                write_text_atomic(target, "key: value\n")
        finally:
            os.chmod(ro_dir, 0o755)


# ---------------------------------------------------------------------------
# Integration: settings.py
# ---------------------------------------------------------------------------


class TestSettingsAtomicWrite:
    """Verify that settings.py helpers use atomic writes."""

    def test_write_env_var_uses_atomic_write(self, tmp_path: Path):
        """_write_env_var should write .env atomically (no partial writes)."""
        from agentkit.server.routes.settings import _write_env_var

        config_path = tmp_path / "agentkit.yaml"
        config_path.write_text("llm:\n  default_model: gpt-4\n", encoding="utf-8")

        _write_env_var(str(config_path), "OPENAI_API_KEY", "sk-test-123")

        env_path = tmp_path / ".env"
        assert env_path.exists()
        content = env_path.read_text(encoding="utf-8")
        assert "OPENAI_API_KEY=sk-test-123" in content

        # No temp files should remain.
        residue = [
            f.name
            for f in tmp_path.iterdir()
            if not f.name.startswith(".") and f.name != "agentkit.yaml"
        ]
        assert residue == [], f"Unexpected files: {residue}"

    def test_write_env_var_preserves_existing_content(self, tmp_path: Path):
        """Updating an existing key preserves other lines."""
        from agentkit.server.routes.settings import _write_env_var

        config_path = tmp_path / "agentkit.yaml"
        config_path.write_text("llm:\n  default_model: gpt-4\n", encoding="utf-8")
        env_path = tmp_path / ".env"
        env_path.write_text(
            "# Header comment\nOPENAI_API_KEY=old-value\nANTHROPIC_API_KEY=sk-ant\n",
            encoding="utf-8",
        )

        _write_env_var(str(config_path), "OPENAI_API_KEY", "new-value")

        content = env_path.read_text(encoding="utf-8")
        assert "# Header comment" in content
        assert "OPENAI_API_KEY=new-value" in content
        assert "ANTHROPIC_API_KEY=sk-ant" in content
        assert "old-value" not in content
