"""
tests/test_session_persistence.py — Tests for session state persistence.

Covers:
  - SessionLedger.export_state / import_state round-trip
  - save_session_state / restore_session_state (persistence.py)
  - get_session_persistence_info diagnostics
  - Rotation of session checkpoints
  - Schema version validation
  - Empty ledger guard (no save when call_count == 0)
  - Corrupt checkpoint resilience (KV₃)
  - Score ledger truncation at MAX_SCORE_LEDGER_SIZE
"""

from __future__ import annotations

import json
from pathlib import Path

import pytest

from fw_server.session import (
    CallRecord,
    SessionLedger,
    _SCHEMA_VERSION,
    _SUPPORTED_SCHEMA_VERSIONS,
    MAX_SCORE_LEDGER_SIZE,
)
from fw_server.persistence import (
    SESSION_FILENAME,
    save_session_state,
    restore_session_state,
    get_session_persistence_info,
    MAX_CHECKPOINTS,
)


# ── Fixtures ──────────────────────────────────────────────────────────


@pytest.fixture()
def state_dir(tmp_path: Path) -> Path:
    d = tmp_path / ".hcp_state"
    d.mkdir()
    return d


@pytest.fixture()
def ledger() -> SessionLedger:
    return SessionLedger()


@pytest.fixture()
def seeded_ledger() -> SessionLedger:
    """Ledger with a few calls recorded."""
    lg = SessionLedger()
    lg.record_call("dusun", grade="Tasdik", composite_score=0.78)
    lg.record_call("run_single_lens", grade="İlmelyakîn", composite_score=0.85)
    lg.record_call("check_kavaid", composite_score=0.60)
    lg.record_kavaid_failure("KV₄_convergence_too_high")
    return lg


# ── export_state / import_state ───────────────────────────────────────


class TestExportImport:
    def test_export_has_required_keys(self, seeded_ledger):
        state = seeded_ledger.export_state()
        assert state["schema_version"] == _SCHEMA_VERSION
        assert "saved_at" in state
        assert "calls" in state
        assert "grade_ceiling" in state
        assert "gate_history" in state
        assert "kavaid_failures" in state
        assert "score_ledger" in state

    def test_round_trip_preserves_calls(self, seeded_ledger):
        state = seeded_ledger.export_state()
        new_ledger = SessionLedger()
        new_ledger.import_state(state)
        assert new_ledger.call_count == 3
        assert new_ledger.calls[0].tool_name == "dusun"
        assert new_ledger.calls[1].grade == "İlmelyakîn"
        assert new_ledger.calls[2].composite_score == 0.60

    def test_round_trip_preserves_grade_ceiling(self, seeded_ledger):
        state = seeded_ledger.export_state()
        new_ledger = SessionLedger()
        new_ledger.import_state(state)
        # Ceiling should be "Tasdik" (lowest recorded grade)
        assert new_ledger.get_grade_ceiling() == seeded_ledger.get_grade_ceiling()

    def test_round_trip_preserves_kavaid_failures(self, seeded_ledger):
        state = seeded_ledger.export_state()
        new_ledger = SessionLedger()
        new_ledger.import_state(state)
        assert "KV₄_convergence_too_high" in new_ledger.kavaid_failures

    def test_import_rejects_unsupported_schema(self, ledger):
        with pytest.raises(ValueError, match="Unsupported session schema"):
            ledger.import_state({"schema_version": 999})

    def test_import_empty_data(self, ledger):
        """Import with schema version 1 but no calls — should work."""
        ledger.import_state({
            "schema_version": 1,
            "calls": [],
            "grade_ceiling": None,
            "gate_history": [],
            "kavaid_failures": [],
            "score_ledger": [],
        })
        assert ledger.call_count == 0

    def test_score_ledger_truncated_on_export(self, ledger):
        """Score ledger should be truncated to MAX_SCORE_LEDGER_SIZE."""
        # record_lens_scores populates _score_ledger
        for i in range(MAX_SCORE_LEDGER_SIZE + 50):
            ledger.record_lens_scores({"fol": float(i) / 1000, "bayes": 0.5})
        state = ledger.export_state()
        assert len(state["score_ledger"]) == MAX_SCORE_LEDGER_SIZE


# ── save_session_state / restore_session_state ────────────────────────


class TestSaveRestore:
    def test_save_returns_none_when_empty(self, state_dir, ledger):
        """Empty ledger → nothing to save."""
        assert save_session_state(state_dir, ledger) is None

    def test_save_writes_json(self, state_dir, seeded_ledger):
        path = save_session_state(state_dir, seeded_ledger)
        assert path is not None
        assert path.name == SESSION_FILENAME
        data = json.loads(path.read_text(encoding="utf-8"))
        assert data["schema_version"] == _SCHEMA_VERSION
        assert len(data["calls"]) == 3

    def test_restore_into_fresh_ledger(self, state_dir, seeded_ledger):
        save_session_state(state_dir, seeded_ledger)
        new_ledger = SessionLedger()
        assert restore_session_state(state_dir, new_ledger)
        assert new_ledger.call_count == 3
        assert new_ledger.calls[0].tool_name == "dusun"

    def test_restore_no_file(self, state_dir, ledger):
        """No checkpoint → returns False, ledger stays empty."""
        assert not restore_session_state(state_dir, ledger)
        assert ledger.call_count == 0

    def test_restore_corrupt_json(self, state_dir, ledger):
        """Corrupt JSON → returns False (KV₃ resilience)."""
        (state_dir / SESSION_FILENAME).write_text("NOT JSON", encoding="utf-8")
        assert not restore_session_state(state_dir, ledger)
        assert ledger.call_count == 0

    def test_restore_wrong_schema(self, state_dir, ledger):
        """Wrong schema version → returns False (KV₃ resilience)."""
        (state_dir / SESSION_FILENAME).write_text(
            json.dumps({"schema_version": 999, "calls": []}),
            encoding="utf-8",
        )
        assert not restore_session_state(state_dir, ledger)

    def test_save_creates_state_dir(self, tmp_path, seeded_ledger):
        """State dir is auto-created if missing."""
        new_dir = tmp_path / "new_state"
        path = save_session_state(new_dir, seeded_ledger)
        assert path is not None
        assert new_dir.exists()

    def test_save_rotate(self, state_dir, seeded_ledger):
        """Multiple saves rotate old checkpoints."""
        import time
        # First save creates the canonical file
        save_session_state(state_dir, seeded_ledger)
        time.sleep(1.1)  # ensure distinct timestamp
        # Second save rotates the first into a backup
        save_session_state(state_dir, seeded_ledger)
        backups = sorted(state_dir.glob("session_state.*.json"))
        assert len(backups) >= 1  # at least one rotated backup exists
        # Canonical file still exists
        assert (state_dir / SESSION_FILENAME).exists()


# ── get_session_persistence_info ──────────────────────────────────────


class TestSessionPersistenceInfo:
    def test_no_state_dir(self):
        info = get_session_persistence_info(None)
        assert not info["session_persistence_enabled"]

    def test_no_checkpoint(self, state_dir):
        info = get_session_persistence_info(state_dir)
        assert info["session_persistence_enabled"]
        assert not info["session_checkpoint_exists"]
        assert info["session_n_backups"] == 0

    def test_with_checkpoint(self, state_dir, seeded_ledger):
        save_session_state(state_dir, seeded_ledger)
        info = get_session_persistence_info(state_dir)
        assert info["session_checkpoint_exists"]
        assert info["session_checkpoint_size_bytes"] > 0
        assert "session_checkpoint_modified" in info
        assert info["session_call_count"] == 3
        assert info["session_score_entries"] >= 0


# ── Integration: full lifecycle ───────────────────────────────────────


class TestLifecycle:
    def test_full_cycle(self, state_dir):
        """Simulate startup → tool calls → shutdown → restart."""
        # Phase 1: initial session
        lg1 = SessionLedger()
        lg1.record_call("dusun", grade="Tasdik", composite_score=0.65)
        lg1.record_call("validate_stage", gate="PASS")
        save_session_state(state_dir, lg1)

        # Phase 2: restart — restore, add more calls, re-save
        lg2 = SessionLedger()
        assert restore_session_state(state_dir, lg2)
        assert lg2.call_count == 2
        lg2.record_call("run_bileshke_pipeline", composite_score=0.82)
        save_session_state(state_dir, lg2)

        # Phase 3: verify final state
        lg3 = SessionLedger()
        assert restore_session_state(state_dir, lg3)
        assert lg3.call_count == 3
        names = [c.tool_name for c in lg3.calls]
        assert names == ["dusun", "validate_stage", "run_bileshke_pipeline"]
