"""Tests for RefreshWheel core class — no GPU required."""
import json
import tempfile
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

from llm_refresh.buffer import ReservoirBuffer
from llm_refresh.config import AppConfig
from llm_refresh.core import RefreshWheel
from llm_refresh.metrics import ForgettingTracker
from llm_refresh.models import BufferStrategy, EvalResult, RefreshResult


def make_config() -> AppConfig:
    cfg = AppConfig()
    cfg.eval.auto_rollback = False  # disable rollback in tests
    cfg.buffer.max_size = 100
    cfg.buffer.min_replay_ratio = 0.3
    return cfg


def make_examples(n: int, prefix: str = "text") -> list[dict]:
    return [{"text": f"{prefix} example {i}"} for i in range(n)]


def make_eval_result(ppl: float = 5.0) -> EvalResult:
    return EvalResult(
        timestamp="2026-01-01T00:00:00+00:00",
        perplexity=ppl,
        loss=1.0,
        dataset_size=10,
    )


class TestRefreshWheelInit:
    def test_default_initialization(self):
        rw = RefreshWheel(config=make_config())
        assert rw._model is None
        assert isinstance(rw.buffer, ReservoirBuffer)
        assert isinstance(rw.tracker, ForgettingTracker)

    def test_custom_strategy(self):
        rw = RefreshWheel(config=make_config(), buffer_strategy=BufferStrategy.DIVERSE)
        from llm_refresh.buffer import DiverseBuffer
        assert isinstance(rw.buffer, DiverseBuffer)

    def test_add_data(self):
        rw = RefreshWheel(config=make_config())
        n = rw.add_data(make_examples(20))
        assert n == 20
        assert len(rw._new_data) == 20

    def test_set_anchor(self):
        rw = RefreshWheel(config=make_config())
        n = rw.set_anchor(make_examples(10))
        assert n == 10
        assert len(rw._anchor_data) == 10


class TestStatusMethod:
    def test_status_empty(self):
        rw = RefreshWheel(config=make_config())
        s = rw.status()
        assert "buffer" in s
        assert "metrics" in s
        assert s["last_refresh"] is None
        assert s["anchor_size"] == 0
        assert s["pending_examples"] == 0

    def test_status_after_adding_data(self):
        rw = RefreshWheel(config=make_config())
        rw.add_data(make_examples(15))
        rw.set_anchor(make_examples(5))
        s = rw.status()
        assert s["pending_examples"] == 15
        assert s["anchor_size"] == 5

    def test_status_metrics_keys(self):
        rw = RefreshWheel(config=make_config())
        s = rw.status()
        met = s["metrics"]
        assert "krs" in met
        assert "bwt" in met
        assert "fwt" in met
        assert "cycles" in met


class TestRefreshDryRun:
    def test_dry_run_no_model_needed(self):
        """Dry-run should complete without loading any ML model."""
        rw = RefreshWheel(config=make_config())
        rw.set_anchor(make_examples(5))
        rw.add_data(make_examples(20))

        result = rw.refresh(dry_run=True)

        assert isinstance(result, RefreshResult)
        assert result.new_examples == 20
        assert result.pre_eval is None  # dry-run skips eval
        assert result.post_eval is None
        assert result.bwt == 0.0

    def test_dry_run_updates_buffer(self):
        """Dry-run should still add new examples to the buffer."""
        rw = RefreshWheel(config=make_config())
        rw.set_anchor(make_examples(5))
        rw.add_data(make_examples(10))

        rw.refresh(dry_run=True)

        assert len(rw.buffer) == 10

    def test_dry_run_clears_pending(self):
        rw = RefreshWheel(config=make_config())
        rw.set_anchor(make_examples(5))
        rw.add_data(make_examples(10))
        rw.refresh(dry_run=True)
        assert len(rw._new_data) == 0

    def test_dry_run_records_result(self):
        rw = RefreshWheel(config=make_config())
        rw.set_anchor(make_examples(5))
        rw.add_data(make_examples(8))
        rw.refresh(dry_run=True)
        assert len(rw.tracker.history) == 1

    def test_dry_run_requires_anchor(self):
        rw = RefreshWheel(config=make_config())
        # No anchor set
        with pytest.raises(ValueError, match="anchor"):
            rw.refresh(dry_run=False)

    def test_multiple_dry_runs(self):
        rw = RefreshWheel(config=make_config())
        rw.set_anchor(make_examples(5))
        for i in range(3):
            rw.add_data(make_examples(5))
            rw.refresh(dry_run=True)
        assert len(rw.tracker.history) == 3
        assert len(rw.buffer) == 15


class TestSaveLoad:
    def test_save_creates_files(self, tmp_path):
        rw = RefreshWheel(config=make_config())
        rw.buffer.add(make_examples(10))
        rw.save(tmp_path)
        assert (tmp_path / "buffer.json").exists()
        assert (tmp_path / "metrics.json").exists()
        assert (tmp_path / "meta.json").exists()

    def test_load_restores_buffer(self, tmp_path):
        rw = RefreshWheel(config=make_config())
        rw.buffer.add(make_examples(15))
        rw.save(tmp_path)

        rw2 = RefreshWheel(config=make_config())
        rw2.load(tmp_path)
        assert len(rw2.buffer) == 15

    def test_load_restores_metrics(self, tmp_path):
        rw = RefreshWheel(config=make_config())
        rw.set_anchor(make_examples(3))
        rw.add_data(make_examples(5))
        rw.refresh(dry_run=True)
        rw.save(tmp_path)

        rw2 = RefreshWheel(config=make_config())
        rw2.load(tmp_path)
        assert len(rw2.tracker.history) == 1

    def test_load_restores_model_name(self, tmp_path):
        rw = RefreshWheel(model_name="custom/model", config=make_config())
        rw.save(tmp_path)

        rw2 = RefreshWheel(config=make_config())
        rw2.load(tmp_path)
        assert rw2._model_name == "custom/model"

    def test_roundtrip_state(self, tmp_path):
        rw = RefreshWheel(config=make_config())
        rw.set_anchor(make_examples(5))
        for i in range(3):
            rw.add_data(make_examples(10))
            rw.refresh(dry_run=True)

        krs_before = rw.tracker.compute_krs()
        rw.save(tmp_path)

        rw2 = RefreshWheel(config=make_config())
        rw2.load(tmp_path)
        assert rw2.tracker.compute_krs() == pytest.approx(krs_before)
        assert len(rw2.tracker.history) == 3


class TestAutoSave:
    def test_auto_save_after_refresh(self, tmp_path):
        rw = RefreshWheel(config=make_config(), state_path=tmp_path)
        rw.set_anchor(make_examples(3))
        rw.add_data(make_examples(5))
        rw.refresh(dry_run=True)

        # Should have auto-saved
        assert (tmp_path / "buffer.json").exists()
