"""
Tests for pulse.core.brain_io — critical path coverage.
Targets: load_json, load_json_dict, save_json, atomic_update_json,
         _NoOpLock, _get_lock, _acquire
"""
from __future__ import annotations

import json
import sys
import shutil
from pathlib import Path
import pytest

PROJECT_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(PROJECT_ROOT))

from pulse.core.brain_io import (
    load_json,
    load_json_dict,
    save_json,
    atomic_update_json,
    _NoOpLock,
    _acquire,
)


# ---------------------------------------------------------------------------
# _NoOpLock
# ---------------------------------------------------------------------------

class TestNoOpLock:
    def test_context_manager_works(self):
        lock = _NoOpLock()
        with lock:
            pass  # must not raise

    def test_enter_returns_self(self):
        lock = _NoOpLock()
        result = lock.__enter__()
        assert result is lock

    def test_exit_accepts_args(self):
        lock = _NoOpLock()
        lock.__exit__(None, None, None)  # must not raise


# ---------------------------------------------------------------------------
# load_json
# ---------------------------------------------------------------------------

class TestLoadJson:
    def test_returns_empty_list_for_missing_file(self, tmp_path):
        result = load_json(tmp_path / "nonexistent.json")
        assert result == []

    def test_loads_valid_list(self, tmp_path):
        f = tmp_path / "data.json"
        f.write_text(json.dumps([1, 2, 3]), encoding="utf-8")
        result = load_json(f)
        assert result == [1, 2, 3]

    def test_loads_list_of_dicts(self, tmp_path):
        f = tmp_path / "data.json"
        data = [{"key": "val"}, {"key": "val2"}]
        f.write_text(json.dumps(data), encoding="utf-8")
        result = load_json(f)
        assert result == data

    def test_returns_empty_on_corrupt_no_backup(self, tmp_path):
        f = tmp_path / "corrupt.json"
        f.write_text("{not valid json{{", encoding="utf-8")
        result = load_json(f)
        assert result == []

    def test_recovers_from_backup(self, tmp_path):
        f = tmp_path / "data.json"
        bak = tmp_path / "data.bak"
        # Valid backup
        bak.write_text(json.dumps([{"recovered": True}]), encoding="utf-8")
        # Corrupt primary
        f.write_text("{{broken", encoding="utf-8")
        result = load_json(f)
        assert result == [{"recovered": True}]
        # Primary should now be restored
        assert json.loads(f.read_text(encoding="utf-8")) == [{"recovered": True}]

    def test_returns_empty_if_backup_also_corrupt(self, tmp_path):
        f = tmp_path / "data.json"
        bak = tmp_path / "data.bak"
        f.write_text("{{broken", encoding="utf-8")
        bak.write_text("{{also broken", encoding="utf-8")
        result = load_json(f)
        assert result == []

    def test_returns_empty_on_oserror(self, tmp_path):
        # Pass a directory path — will trigger OSError on open
        result = load_json(tmp_path)  # directory, not file
        assert result == []


# ---------------------------------------------------------------------------
# load_json_dict
# ---------------------------------------------------------------------------

class TestLoadJsonDict:
    def test_returns_empty_dict_for_missing_file(self, tmp_path):
        result = load_json_dict(tmp_path / "nonexistent.json")
        assert result == {}

    def test_loads_valid_dict(self, tmp_path):
        f = tmp_path / "config.json"
        f.write_text(json.dumps({"key": "val", "num": 42}), encoding="utf-8")
        result = load_json_dict(f)
        assert result == {"key": "val", "num": 42}

    def test_returns_empty_for_non_dict_json(self, tmp_path):
        f = tmp_path / "list.json"
        f.write_text(json.dumps([1, 2, 3]), encoding="utf-8")
        result = load_json_dict(f)
        assert result == {}

    def test_returns_empty_on_corrupt_no_backup(self, tmp_path):
        f = tmp_path / "bad.json"
        f.write_text("{not json", encoding="utf-8")
        result = load_json_dict(f)
        assert result == {}

    def test_recovers_dict_from_backup(self, tmp_path):
        f = tmp_path / "config.json"
        bak = tmp_path / "config.bak"
        bak.write_text(json.dumps({"recovered": True}), encoding="utf-8")
        f.write_text("{{broken", encoding="utf-8")
        result = load_json_dict(f)
        assert result == {"recovered": True}

    def test_ignores_backup_if_not_dict(self, tmp_path):
        f = tmp_path / "config.json"
        bak = tmp_path / "config.bak"
        bak.write_text(json.dumps([1, 2, 3]), encoding="utf-8")  # list, not dict
        f.write_text("{{broken", encoding="utf-8")
        result = load_json_dict(f)
        assert result == {}


# ---------------------------------------------------------------------------
# save_json
# ---------------------------------------------------------------------------

class TestSaveJson:
    def test_saves_list(self, tmp_path):
        f = tmp_path / "out.json"
        save_json(f, [1, 2, 3])
        assert json.loads(f.read_text(encoding="utf-8")) == [1, 2, 3]

    def test_saves_dict(self, tmp_path):
        f = tmp_path / "out.json"
        save_json(f, {"hello": "world"})
        assert json.loads(f.read_text(encoding="utf-8")) == {"hello": "world"}

    def test_creates_parent_dirs(self, tmp_path):
        f = tmp_path / "nested" / "deep" / "out.json"
        save_json(f, {"ok": True})
        assert f.exists()

    def test_creates_backup_on_overwrite(self, tmp_path):
        f = tmp_path / "data.json"
        save_json(f, {"v": 1})
        save_json(f, {"v": 2})
        # Backup now lives in undo stack, not .bak — verify via undo module
        from pulse.core.undo import list_backups
        backups = list_backups(f)
        assert len(backups) >= 1

    def test_no_tmp_file_left_after_write(self, tmp_path):
        f = tmp_path / "data.json"
        save_json(f, [1, 2])
        tmp = tmp_path / "data.tmp"
        assert not tmp.exists()

    def test_custom_indent(self, tmp_path):
        f = tmp_path / "data.json"
        save_json(f, {"a": 1}, indent=4)
        raw = f.read_text(encoding="utf-8")
        assert "    " in raw  # 4-space indent

    def test_roundtrip(self, tmp_path):
        f = tmp_path / "roundtrip.json"
        original = {"list": [1, 2, 3], "nested": {"x": True}}
        save_json(f, original)
        assert load_json_dict(f) == original


# ---------------------------------------------------------------------------
# atomic_update_json
# ---------------------------------------------------------------------------

class TestAtomicUpdateJson:
    def test_creates_file_with_default(self, tmp_path):
        f = tmp_path / "state.json"
        result = atomic_update_json(f, lambda d: d, default={"count": 0})
        assert result == {"count": 0}
        assert f.exists()

    def test_applies_updater(self, tmp_path):
        f = tmp_path / "state.json"
        atomic_update_json(f, lambda d: {**d, "count": 0}, default={})
        result = atomic_update_json(f, lambda d: {**d, "count": d["count"] + 1})
        assert result["count"] == 1

    def test_multiple_updates_accumulate(self, tmp_path):
        f = tmp_path / "counter.json"
        for i in range(5):
            atomic_update_json(f, lambda d, i=i: {**d, "val": i}, default={})
        data = load_json_dict(f)
        assert data["val"] == 4

    def test_creates_backup_on_overwrite(self, tmp_path):
        f = tmp_path / "state.json"
        atomic_update_json(f, lambda d: {"v": 1}, default={})
        atomic_update_json(f, lambda d: {"v": 2})
        # Backup now lives in undo stack, not .bak — verify via undo module
        from pulse.core.undo import list_backups
        backups = list_backups(f)
        assert len(backups) >= 1

    def test_handles_corrupt_file_gracefully(self, tmp_path):
        f = tmp_path / "state.json"
        f.write_text("{{not json", encoding="utf-8")
        # Should use default and not raise
        result = atomic_update_json(f, lambda d: d, default={"recovered": True})
        assert result == {"recovered": True}

    def test_list_default(self, tmp_path):
        f = tmp_path / "items.json"
        result = atomic_update_json(f, lambda d: d + [1], default=[])
        assert result == [1]

    def test_no_tmp_left_after_update(self, tmp_path):
        f = tmp_path / "state.json"
        atomic_update_json(f, lambda d: d, default={})
        tmp = tmp_path / "state.tmp"
        assert not tmp.exists()
