from __future__ import annotations

import threading
import time

import pytest

from pulselog.checkpoint import CheckpointStore
from pulselog.exceptions import InvalidStatusError


class TestCheckpointStore:
    def setup_method(self):
        self.store = CheckpointStore(":memory:")

    def test_save_and_load_roundtrip(self):
        self.store.save("step1", {"acc": 0.9}, status="DONE", note="first", progress=50)
        result = self.store.load("step1")
        assert result is not None
        assert result["acc"] == 0.9
        assert result["_meta"]["status"] == "DONE"
        assert result["_meta"]["note"] == "first"
        assert result["_meta"]["progress"] == 50

    def test_load_missing_returns_none(self):
        assert self.store.load("nonexistent") is None

    def test_invalid_status_raises(self):
        with pytest.raises(InvalidStatusError):
            self.store.save("x", {}, status="MAYBE")

    def test_invalid_status_case(self):
        # Valid statuses in any case should not raise
        self.store.save("a", {}, status="done")
        self.store.save("b", {}, status="IN_PROGRESS")

    def test_save_overwrites(self):
        self.store.save("s", {"v": 1})
        self.store.save("s", {"v": 2})
        assert self.store.load("s")["v"] == 2

    def test_checkpoints_ordered_by_recent(self):
        self.store.save("a", {})
        time.sleep(0.01)
        self.store.save("b", {})
        names = self.store.checkpoints()
        assert names[0] == "b"
        assert names[1] == "a"

    def test_delete_checkpoint(self):
        self.store.save("d", {"x": 1})
        self.store.delete_checkpoint("d")
        assert self.store.load("d") is None

    def test_delete_missing_is_silent(self):
        self.store.delete_checkpoint("ghost")  # must not raise

    def test_meta_key_present(self):
        self.store.save("m", {"foo": "bar"}, note="hi", progress=75)
        r = self.store.load("m")
        assert "_meta" in r
        assert r["_meta"]["name"] == "m"
        assert r["_meta"]["progress"] == 75
        assert "saved_at" in r["_meta"]

    def test_progress_none_allowed(self):
        self.store.save("p", {"x": 1}, progress=None)
        r = self.store.load("p")
        assert r["_meta"]["progress"] is None

    def test_thread_safe_concurrent_saves(self):
        errors = []

        def saver(n):
            try:
                self.store.save(f"cp_{n}", {"n": n})
            except Exception as e:
                errors.append(e)

        threads = [threading.Thread(target=saver, args=(i,)) for i in range(20)]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        assert not errors, f"Concurrent save errors: {errors}"
        assert len(self.store.checkpoints()) == 20

    def test_saved_count(self):
        assert self.store.saved_count() == 0
        self.store.save("a", {})
        self.store.save("b", {})
        assert self.store.saved_count() == 2
