"""Tests for src/ralph_flow.py — short-lived per-user UX state."""

from datetime import datetime, timedelta, timezone

import pytest

from src import ralph_flow


@pytest.fixture(autouse=True)
def _isolate_state_file(tmp_path, monkeypatch):
    """Redirect state file to a tmp location for each test."""
    monkeypatch.setattr(ralph_flow, "_STATE_FILE", tmp_path / "ralph_flow.json")
    monkeypatch.setattr(ralph_flow, "SESSIONS_DIR", tmp_path)


def test_get_state_returns_none_when_absent():
    assert ralph_flow.get_state("discord", "c1", "u1") is None


def test_set_then_get_round_trip():
    ralph_flow.set_state(
        "discord", "c1", "u1",
        step=ralph_flow.STEP_INPUT_DESCRIPTION,
        project="roa2web",
    )
    state = ralph_flow.get_state("discord", "c1", "u1")
    assert state is not None
    assert state["step"] == ralph_flow.STEP_INPUT_DESCRIPTION
    assert state["project"] == "roa2web"
    assert "expires_at" in state


def test_clear_state_removes_entry():
    ralph_flow.set_state("telegram", "42", "7", step="input_description")
    assert ralph_flow.clear_state("telegram", "42", "7") is True
    assert ralph_flow.get_state("telegram", "42", "7") is None
    # Second clear is a no-op
    assert ralph_flow.clear_state("telegram", "42", "7") is False


def test_state_keyed_by_adapter_chat_user():
    """Different adapters / chats / users have isolated state."""
    ralph_flow.set_state("discord", "c1", "u1", step="input_description", project="A")
    ralph_flow.set_state("telegram", "c1", "u1", step="input_description", project="B")
    ralph_flow.set_state("discord", "c2", "u1", step="input_description", project="C")
    ralph_flow.set_state("discord", "c1", "u2", step="input_description", project="D")

    assert ralph_flow.get_state("discord", "c1", "u1")["project"] == "A"
    assert ralph_flow.get_state("telegram", "c1", "u1")["project"] == "B"
    assert ralph_flow.get_state("discord", "c2", "u1")["project"] == "C"
    assert ralph_flow.get_state("discord", "c1", "u2")["project"] == "D"


def test_expired_state_returns_none_and_self_cleans(monkeypatch):
    """get_state on an expired entry should return None and drop the entry."""
    # Set with 0s TTL — already expired
    ralph_flow.set_state(
        "discord", "c1", "u1",
        step="input_description",
        ttl_seconds=0,
    )
    assert ralph_flow.get_state("discord", "c1", "u1") is None
    # Verify entry was dropped from disk
    assert ralph_flow._load() == {}


def test_cleanup_expired_drops_only_expired():
    ralph_flow.set_state("discord", "c1", "u1", step="x", ttl_seconds=0)  # expired
    ralph_flow.set_state("discord", "c2", "u2", step="y", ttl_seconds=600)  # fresh

    dropped = ralph_flow.cleanup_expired()

    assert dropped == 1
    assert ralph_flow.get_state("discord", "c1", "u1") is None
    assert ralph_flow.get_state("discord", "c2", "u2") is not None


def test_set_state_overwrites_previous():
    ralph_flow.set_state("discord", "c1", "u1", step="step_a", project="P1")
    ralph_flow.set_state("discord", "c1", "u1", step="step_b", project="P2")
    state = ralph_flow.get_state("discord", "c1", "u1")
    assert state["step"] == "step_b"
    assert state["project"] == "P2"


def test_set_state_extras_propagate():
    ralph_flow.set_state(
        "discord", "c1", "u1",
        step="x",
        custom_field="hello",
        nested={"a": 1},
    )
    state = ralph_flow.get_state("discord", "c1", "u1")
    assert state["custom_field"] == "hello"
    assert state["nested"] == {"a": 1}


def test_corrupted_state_file_returns_empty(tmp_path):
    """If state file is corrupt JSON, _load returns {} so get_state stays robust."""
    ralph_flow._STATE_FILE.write_text("not json {")
    assert ralph_flow.get_state("discord", "c1", "u1") is None


def test_atomic_write_does_not_leave_temp_files(tmp_path):
    ralph_flow.set_state("discord", "c1", "u1", step="x")
    leftovers = [p for p in tmp_path.iterdir() if p.name.startswith(".ralph_flow_")]
    assert leftovers == []
