"""Unit tests for ``ChatSessionsStore``.

Covers:
  • Missing file → empty read, no exception, file not auto-created on read.
  • Corrupt JSON → ``ChatSessionsCorruptError``.
  • Happy path: record → get → list → bump round-trip.
  • Schema version is persisted and round-trips.
"""
from __future__ import annotations

import json
import sys
from pathlib import Path

# Path bootstrap — pytest's rootdir lands on recoil/pyproject.toml, which
# leaves the repo root (parent of recoil/) off sys.path. Without this the
# ``recoil.*`` import below can't resolve when invoked from the repo root.
_REPO_ROOT = Path(__file__).resolve().parents[2]
if str(_REPO_ROOT) not in sys.path:
    sys.path.insert(0, str(_REPO_ROOT))

import pytest  # noqa: E402

from recoil.api.chat_sessions import (  # noqa: E402
    SCHEMA_VERSION,
    ChatSessionsCorruptError,
    ChatSessionsStore,
)


def _store(tmp_path: Path) -> ChatSessionsStore:
    return ChatSessionsStore(path=tmp_path / "chat-sessions.json")


def test_missing_file_returns_empty(tmp_path: Path) -> None:
    store = _store(tmp_path)
    assert store.get_session("proj-a") is None
    assert store.list_sessions() == {}
    assert store.lookup_project_by_session_id("anything") is None
    # Read-only ops must not create the file on disk.
    assert not store.path.exists()


def test_corrupt_json_raises_typed_error(tmp_path: Path) -> None:
    store = _store(tmp_path)
    store.path.parent.mkdir(parents=True, exist_ok=True)
    store.path.write_text("{not valid json", encoding="utf-8")
    with pytest.raises(ChatSessionsCorruptError):
        store.get_session("proj-a")
    with pytest.raises(ChatSessionsCorruptError):
        store.list_sessions()


def test_corrupt_schema_version_raises(tmp_path: Path) -> None:
    store = _store(tmp_path)
    store.path.parent.mkdir(parents=True, exist_ok=True)
    store.path.write_text(
        json.dumps({"schema_version": 999, "sessions": {}}),
        encoding="utf-8",
    )
    with pytest.raises(ChatSessionsCorruptError):
        store.list_sessions()


def test_record_and_get_roundtrip(tmp_path: Path) -> None:
    store = _store(tmp_path)
    store.record_session("proj-a", "sid-aaa")
    store.record_session("proj-b", "sid-bbb")

    assert store.get_session("proj-a") == "sid-aaa"
    assert store.get_session("proj-b") == "sid-bbb"
    assert store.get_session("proj-missing") is None

    sessions = store.list_sessions()
    assert set(sessions.keys()) == {"proj-a", "proj-b"}
    assert sessions["proj-a"]["session_id"] == "sid-aaa"
    assert "last_used" in sessions["proj-a"]


def test_record_upserts_existing_project(tmp_path: Path) -> None:
    store = _store(tmp_path)
    store.record_session("proj-a", "sid-old")
    store.record_session("proj-a", "sid-new")
    assert store.get_session("proj-a") == "sid-new"
    assert len(store.list_sessions()) == 1


def test_bump_last_used_updates_timestamp(tmp_path: Path) -> None:
    store = _store(tmp_path)
    store.record_session("proj-a", "sid-aaa")

    # Mutate underlying doc to force a different timestamp on bump.
    raw = json.loads(store.path.read_text(encoding="utf-8"))
    raw["sessions"]["proj-a"]["last_used"] = "1970-01-01T00:00:00Z"
    store.path.write_text(json.dumps(raw), encoding="utf-8")

    store.bump_last_used("proj-a")
    after = store.list_sessions()["proj-a"]["last_used"]
    assert after != "1970-01-01T00:00:00Z"
    # Session id must NOT change on bump.
    assert store.get_session("proj-a") == "sid-aaa"


def test_bump_last_used_missing_project_is_noop(tmp_path: Path) -> None:
    store = _store(tmp_path)
    store.record_session("proj-a", "sid-aaa")
    store.bump_last_used("proj-missing")  # must not raise
    assert set(store.list_sessions().keys()) == {"proj-a"}


def test_lookup_project_by_session_id(tmp_path: Path) -> None:
    store = _store(tmp_path)
    store.record_session("proj-a", "sid-aaa")
    store.record_session("proj-b", "sid-bbb")
    assert store.lookup_project_by_session_id("sid-aaa") == "proj-a"
    assert store.lookup_project_by_session_id("sid-bbb") == "proj-b"
    assert store.lookup_project_by_session_id("sid-missing") is None


def test_schema_version_is_persisted(tmp_path: Path) -> None:
    store = _store(tmp_path)
    store.record_session("proj-a", "sid-aaa")
    raw = json.loads(store.path.read_text(encoding="utf-8"))
    assert raw["schema_version"] == SCHEMA_VERSION
    assert "sessions" in raw and "proj-a" in raw["sessions"]


def test_atomic_write_leaves_no_tempfile(tmp_path: Path) -> None:
    store = _store(tmp_path)
    store.record_session("proj-a", "sid-aaa")
    # Ignore the json file itself + the fcntl.flock sentinel (.lock).
    leftovers = [
        p for p in store.path.parent.iterdir()
        if p.name != store.path.name and p.suffix != ".lock"
    ]
    assert leftovers == [], f"unexpected leftover files: {leftovers}"


def test_write_failure_cleans_up_tempfile(tmp_path: Path, monkeypatch) -> None:
    store = _store(tmp_path)
    import json as _json
    real_dump = _json.dump
    calls = {"n": 0}

    def boom(*args, **kwargs):
        calls["n"] += 1
        if calls["n"] == 1:
            raise RuntimeError("simulated write failure")
        return real_dump(*args, **kwargs)

    monkeypatch.setattr("recoil.api.chat_sessions.json.dump", boom)
    with pytest.raises(RuntimeError, match="simulated"):
        store.record_session("proj-a", "sid-aaa")
    leftovers = [
        p for p in store.path.parent.iterdir()
        if p.suffix == ".tmp"
    ]
    assert leftovers == [], f"tempfile not cleaned up: {leftovers}"


def test_auto_creates_parent_directory(tmp_path: Path) -> None:
    nested = tmp_path / "deeply" / "nested" / "chat-sessions.json"
    store = ChatSessionsStore(path=nested)
    store.record_session("proj-a", "sid-aaa")
    assert nested.exists()
    assert store.get_session("proj-a") == "sid-aaa"
