"""Tests for rbsp.runtime.snapshot."""

from __future__ import annotations

import json
import time
from pathlib import Path

import pytest

from rbsp.runtime.snapshot import SnapshotInfo, SnapshotManager


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _sample_index() -> dict:
    return {
        "entry_a": {"tokens": ["foo", "bar"], "score": 1.0},
        "entry_b": {"tokens": ["baz"], "score": 0.5},
    }


# ---------------------------------------------------------------------------
# SnapshotInfo
# ---------------------------------------------------------------------------

class TestSnapshotInfo:
    def test_fields(self):
        info = SnapshotInfo(
            snapshot_id="123-abc",
            created_at=1000.0,
            index_entries=5,
            size_bytes=2048,
            path="/tmp/snaps/123-abc",
        )
        assert info.snapshot_id == "123-abc"
        assert info.created_at == 1000.0
        assert info.index_entries == 5
        assert info.size_bytes == 2048
        assert info.path == "/tmp/snaps/123-abc"


# ---------------------------------------------------------------------------
# SnapshotManager — create / restore
# ---------------------------------------------------------------------------

class TestCreateRestore:
    def test_create_returns_info(self, tmp_path):
        mgr = SnapshotManager(tmp_path / "snaps")
        info = mgr.create_snapshot(_sample_index())
        assert isinstance(info, SnapshotInfo)
        assert info.index_entries == 2
        assert info.size_bytes > 0
        assert Path(info.path).is_dir()

    def test_restore_roundtrip(self, tmp_path):
        mgr = SnapshotManager(tmp_path / "snaps")
        data = _sample_index()
        info = mgr.create_snapshot(data)
        restored = mgr.restore_snapshot(info.snapshot_id)
        assert restored == data

    def test_restore_nonexistent_raises(self, tmp_path):
        mgr = SnapshotManager(tmp_path / "snaps")
        with pytest.raises(FileNotFoundError):
            mgr.restore_snapshot("no-such-id")

    def test_create_with_metadata(self, tmp_path):
        mgr = SnapshotManager(tmp_path / "snaps")
        meta = {"version": 3, "tag": "release"}
        info = mgr.create_snapshot(_sample_index(), metadata=meta)
        # Verify user metadata persisted in meta.json
        meta_path = Path(info.path) / "meta.json"
        raw = json.loads(meta_path.read_text(encoding="utf-8"))
        assert raw["user_metadata"] == meta

    def test_snapshot_dir_contents(self, tmp_path):
        mgr = SnapshotManager(tmp_path / "snaps")
        info = mgr.create_snapshot(_sample_index())
        snap = Path(info.path)
        assert (snap / "meta.json").is_file()
        assert (snap / "index.dat").is_file()


# ---------------------------------------------------------------------------
# list / get_info / delete
# ---------------------------------------------------------------------------

class TestListGetDelete:
    def test_list_empty(self, tmp_path):
        mgr = SnapshotManager(tmp_path / "snaps")
        assert mgr.list_snapshots() == []

    def test_list_ordering(self, tmp_path):
        mgr = SnapshotManager(tmp_path / "snaps")
        ids = []
        for _ in range(3):
            info = mgr.create_snapshot({"k": "v"})
            ids.append(info.snapshot_id)
        listed = mgr.list_snapshots()
        assert len(listed) == 3
        # sorted by creation time ascending
        assert [s.snapshot_id for s in listed] == ids

    def test_get_info(self, tmp_path):
        mgr = SnapshotManager(tmp_path / "snaps")
        info = mgr.create_snapshot(_sample_index())
        got = mgr.get_info(info.snapshot_id)
        assert got is not None
        assert got.snapshot_id == info.snapshot_id
        assert got.index_entries == 2

    def test_get_info_missing(self, tmp_path):
        mgr = SnapshotManager(tmp_path / "snaps")
        assert mgr.get_info("nonexistent") is None

    def test_delete(self, tmp_path):
        mgr = SnapshotManager(tmp_path / "snaps")
        info = mgr.create_snapshot(_sample_index())
        assert mgr.delete_snapshot(info.snapshot_id) is True
        assert mgr.get_info(info.snapshot_id) is None

    def test_delete_missing(self, tmp_path):
        mgr = SnapshotManager(tmp_path / "snaps")
        assert mgr.delete_snapshot("ghost") is False


# ---------------------------------------------------------------------------
# cleanup
# ---------------------------------------------------------------------------

class TestCleanup:
    def test_cleanup_keeps_n(self, tmp_path):
        mgr = SnapshotManager(tmp_path / "snaps")
        for _ in range(7):
            mgr.create_snapshot({"x": 1})
        assert len(mgr.list_snapshots()) == 7
        mgr.cleanup(keep_last_n=3)
        remaining = mgr.list_snapshots()
        assert len(remaining) == 3

    def test_cleanup_noop_when_few(self, tmp_path):
        mgr = SnapshotManager(tmp_path / "snaps")
        for _ in range(2):
            mgr.create_snapshot({"x": 1})
        mgr.cleanup(keep_last_n=5)
        assert len(mgr.list_snapshots()) == 2

    def test_cleanup_keeps_newest(self, tmp_path):
        mgr = SnapshotManager(tmp_path / "snaps")
        all_ids = []
        for _ in range(5):
            info = mgr.create_snapshot({"x": 1})
            all_ids.append(info.snapshot_id)
        mgr.cleanup(keep_last_n=2)
        remaining = mgr.list_snapshots()
        remaining_ids = [s.snapshot_id for s in remaining]
        # The two newest should survive
        assert remaining_ids == all_ids[-2:]


# ---------------------------------------------------------------------------
# Edge cases
# ---------------------------------------------------------------------------

class TestEdgeCases:
    def test_snapshot_id_format(self, tmp_path):
        mgr = SnapshotManager(tmp_path / "snaps")
        info = mgr.create_snapshot({})
        # Format: <timestamp_ms>-<hex>
        parts = info.snapshot_id.split("-")
        assert len(parts) == 2
        assert parts[0].isdigit()
        assert len(parts[1]) == 8  # 4 bytes hex

    def test_empty_index(self, tmp_path):
        mgr = SnapshotManager(tmp_path / "snaps")
        info = mgr.create_snapshot({})
        assert info.index_entries == 0
        restored = mgr.restore_snapshot(info.snapshot_id)
        assert restored == {}

    def test_large_values(self, tmp_path):
        mgr = SnapshotManager(tmp_path / "snaps")
        data = {f"key_{i}": list(range(100)) for i in range(50)}
        info = mgr.create_snapshot(data)
        assert info.index_entries == 50
        assert mgr.restore_snapshot(info.snapshot_id) == data

    def test_creates_snapshot_dir_if_missing(self, tmp_path):
        deep = tmp_path / "a" / "b" / "c" / "snaps"
        mgr = SnapshotManager(deep)
        assert deep.is_dir()

    def test_concurrent_ids_unique(self, tmp_path):
        mgr = SnapshotManager(tmp_path / "snaps")
        ids = set()
        for _ in range(20):
            info = mgr.create_snapshot({})
            ids.add(info.snapshot_id)
        assert len(ids) == 20
