"""Tests for the Redis backend (all I/O faked in-memory)."""

import json
import sys
import datetime
import pytest
from unittest.mock import MagicMock

# ---------------------------------------------------------------------------
# Fake redis module — registered before any import touches it
# ---------------------------------------------------------------------------

class _FakeRedis:
    """Dict-backed stand-in for ``redis.StrictRedis``."""
    def __init__(self, **kw):
        self._store = {}

    def get(self, key):
        return self._store.get(key)
    
    def set(self, key, value):
        self._store[key] = value

    def setex(self, key, ttl, value):
        self._store[key] = value

    def delete(self, key):
        self._store.pop(key, None)

    def ping(self):
        return True

_fake_redis_mod = MagicMock()
_fake_redis_mod.StrictRedis = _FakeRedis
sys.modules.setdefault("redis", _fake_redis_mod)

# ---------------------------------------------------------------------------
# Imports
# ---------------------------------------------------------------------------

from st_page_state.utils.converters import serialize_state, deserialize_state
from st_page_state.backends.redis_backend import (
    RedisConfig, RedisClient, configure_redis, redis_session,
)
from st_page_state.backends import redis_backend


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

@pytest.fixture(autouse=True)
def _reset_redis_config():
    original = redis_backend._config
    redis_backend._config = None

    yield
    redis_backend._config = original


@pytest.fixture()
def client():
    cfg = RedisConfig(default_ttl=300)
    return RedisClient(cfg)


# ═══════════════════════════════════════════════════════════════════════════
# Serialization
# ═══════════════════════════════════════════════════════════════════════════

class TestStateSerialization:

    @pytest.mark.parametrize("data", [
        {"count": 42, "name": "hello"},
        {"flag": True, "ratio": 3.14},
        {},
    ])
    def test_primitive_roundtrip(self, data):
        assert deserialize_state(serialize_state(data)) == data

    def test_datetime_roundtrip(self):
        data = {"dt": datetime.datetime(2025, 6, 15, 12, 0, 0)}
        assert deserialize_state(serialize_state(data)) == data

    def test_date_roundtrip(self):
        data = {"d": datetime.date(2025, 1, 1)}
        assert deserialize_state(serialize_state(data)) == data

    def test_time_roundtrip(self):
        data = {"t": datetime.time(9, 30)}
        assert deserialize_state(serialize_state(data)) == data

    def test_set_roundtrip(self):
        data = {"s": {1, 2, 3}}
        assert deserialize_state(serialize_state(data)) == data

    def test_tuple_roundtrip(self):
        data = {"tp": (10, 20)}
        assert deserialize_state(serialize_state(data)) == data


# ═══════════════════════════════════════════════════════════════════════════
# RedisClient
# ═══════════════════════════════════════════════════════════════════════════

class TestRedisClient:

    def test_save_and_load(self, client):
        client.save("MyState", "sess-1", {"count": 42}, ttl=None)
        assert client.load("MyState", "sess-1") == {"count": 42}

    def test_load_missing_returns_none(self, client):
        assert client.load("Missing", "sess-x") is None

    def test_save_with_ttl_uses_setex(self, client):
        client._client.setex = MagicMock()
        client.save("S", "sess", {"a": 1}, ttl=60)
        client._client.setex.assert_called_once()
        assert client._client.setex.call_args[0][1] == 60

    def test_save_without_ttl_uses_set(self, client):
        client._client.set = MagicMock()
        client.save("S", "sess", {"a": 1}, ttl=None)
        client._client.set.assert_called_once()

    def test_delete(self, client):
        client.save("S", "sess", {"x": 1}, ttl=None)
        client.delete("S", "sess")
        assert client.load("S", "sess") is None

    def test_ping(self, client):
        assert client.ping() is True

    def test_key_format(self, client):
        assert client._key("Cls", "abc") == "st_page_state:abc:Cls"

    def test_load_survives_error(self, client):
        client._client.get = MagicMock(side_effect=Exception("boom"))
        assert client.load("S", "sess") is None

    def test_save_survives_error(self, client):
        client._client.set = MagicMock(side_effect=Exception("boom"))
        client.save("S", "sess", {"a": 1}, ttl=None)


# ═══════════════════════════════════════════════════════════════════════════
# TTL resolution
# ═══════════════════════════════════════════════════════════════════════════

class TestResolveTTL:

    def test_class_ttl_wins(self, client):
        class State:
            class Config:
                ttl = 120
        assert client.resolve_ttl(State) == 120

    def test_falls_back_to_global(self, client):
        class State:
            class Config:
                url_selfish = True
        assert client.resolve_ttl(State) == 300

    def test_no_config_falls_back_to_global(self, client):
        class State:
            pass
        assert client.resolve_ttl(State) == 300

    def test_class_ttl_none_means_no_expiry(self):
        c = RedisClient(RedisConfig(default_ttl=3600))
        class State:
            class Config:
                ttl = None
        assert c.resolve_ttl(State) is None


# ═══════════════════════════════════════════════════════════════════════════
# configure_redis
# ═══════════════════════════════════════════════════════════════════════════

class TestConfigureRedis:

    def test_sets_module_config(self):
        configure_redis(host="myhost", default_ttl=120)
        assert redis_backend._config is not None
        assert redis_backend._config.host == "myhost"
        assert redis_backend._config.default_ttl == 120

    def test_returns_none(self):
        assert configure_redis() is None


# ═══════════════════════════════════════════════════════════════════════════
# redis_session context manager
# ═══════════════════════════════════════════════════════════════════════════

class TestRedisSession:

    def test_noop_when_not_configured(self):
        ran = []
        with redis_session():
            ran.append(True)
        assert ran == [True]

    def test_load_and_save_roundtrip(self):
        import streamlit as st
        from st_page_state import PageState, StateVar
        from st_page_state.core.meta import SESSION_STATE_KEY

        configure_redis(default_ttl=600)

        shared_fake = _FakeRedis()
        original_connect = RedisClient._connect
        RedisClient._connect = staticmethod(lambda cfg: shared_fake)

        try:
            client = RedisClient(redis_backend._config)
            client.save("RoundTripState", "default", {"count": 99}, ttl=None)

            class RoundTripState(PageState):
                count: int = StateVar(default=0)

            with redis_session():
                ns = st.session_state.get(SESSION_STATE_KEY, {}).get("RoundTripState", {})
                assert ns.get("count") == 99
                RoundTripState.count = 200

            saved = client.load("RoundTripState", "default")
            assert saved["count"] == 200
        finally:
            RedisClient._connect = original_connect

    def test_per_class_ttl(self):
        import streamlit as st
        from st_page_state import PageState, StateVar

        configure_redis(default_ttl=3600)

        class ShortLivedState(PageState):
            val: str = StateVar(default="x")
            class Config:
                ttl = 30

        original_connect = RedisClient._connect
        spy_setex = MagicMock(side_effect=_FakeRedis().setex)

        def patched_connect(config):
            c = original_connect(config)
            c.setex = spy_setex
            return c

        RedisClient._connect = staticmethod(patched_connect)
        try:
            with redis_session():
                ShortLivedState.val = "hello"

            assert spy_setex.called
            assert spy_setex.call_args[0][1] == 30
        finally:
            RedisClient._connect = original_connect
