"""
Redis 缓存命中/未命中场景验证测试

覆盖：
1. 调用 save_market_data 写入数据后，Redis 与 MySQL 同时存在一致数据
2. 首次调用 get_market_data（Redis 未命中）从 MySQL 读取并回填 Redis
3. 再次调用 get_market_data（Redis 命中）直接返回缓存数据，不访问 MySQL
4. 手动删除 Redis key 后，再次读取回源 MySQL 并重新回填 Redis
"""
import json
import logging
from datetime import datetime
from unittest.mock import MagicMock

import pytest
from sqlalchemy import create_engine, text
from sqlalchemy.orm import sessionmaker

from app.models import Base, MarketData
from app.storage_manager import StorageManager

# test_main_lifespan.py 中的 fixture 会 patch app.main.Base.metadata.create_all 且未正确恢复，
# 导致后续依赖 create_all 的测试失败。这里保留原始方法引用，必要时恢复。
_original_create_all = Base.metadata.create_all


class FakeRedis:
    """轻量级 Redis 替身，仅支持本测试所需的方法。"""

    def __init__(self):
        self._store = {}

    def ping(self):
        return True

    def get(self, key):
        return self._store.get(key)

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

    def delete(self, *keys):
        for key in keys:
            self._store.pop(key, None)

    def keys(self, pattern):
        # 仅支持以 * 结尾的通配模式
        prefix = pattern.rstrip("*")
        return [k for k in self._store if k.startswith(prefix)]

    def exists(self, key):
        return key in self._store


@pytest.fixture
def mysql_engine():
    """提供已创建表结构的内存 SQLite 引擎，作为 MySQL 替身。"""
    # 防御：修复上游 fixture 对 Base.metadata.create_all 的 patch 泄漏
    if isinstance(Base.metadata.create_all, MagicMock):
        Base.metadata.create_all = _original_create_all

    engine = create_engine("sqlite:///:memory:")
    Base.metadata.create_all(bind=engine)

    # 确认表已创建
    with engine.connect() as conn:
        result = conn.execute(
            text("SELECT name FROM sqlite_master WHERE type='table' AND name='market_data'")
        )
        assert result.fetchone() is not None

    yield engine
    engine.dispose()


@pytest.fixture
def fake_redis():
    return FakeRedis()


@pytest.fixture
def sample_data():
    fetched_at = datetime(2026, 7, 4, 10, 0, 0)
    return {
        "type": "futures",
        "current_price": 123.45,
        "timestamp": fetched_at.isoformat(),
        "timeframes": {
            "5min": [{"datetime": "2026-07-04T09:55:00", "open": 100, "close": 110}],
            "15min": [{"datetime": "2026-07-04T09:45:00", "open": 105, "close": 115}],
        },
    }


def _seed_mysql_market_data(engine, symbol="AG2606", data_type="futures"):
    """在 MySQL 替身中写入行情数据。"""
    fetched_at = datetime(2026, 7, 4, 10, 0, 0)
    Session = sessionmaker(bind=engine)
    with Session() as db:
        db.add(
            MarketData(
                symbol=symbol,
                data_type=data_type,
                period="5min",
                candles_json=json.dumps(
                    [{"datetime": "2026-07-04T09:55:00", "open": 100, "close": 110}]
                ),
                current_price=123.45,
                fetched_at=fetched_at,
                candle_count=1,
            )
        )
        db.commit()


class TestSaveMarketData:
    """save_market_data 写入与缓存回填场景。"""

    def test_save_market_data_writes_mysql_and_backfills_redis(
        self, fake_redis, mysql_engine, sample_data
    ):
        """写入后 MySQL 与 Redis 均保存一致数据。"""
        manager = StorageManager(
            redis_client=fake_redis, mysql_engine=mysql_engine
        )

        assert manager.save_market_data("AG2606", sample_data) is True

        # MySQL 中应写入两条记录
        Session = sessionmaker(bind=mysql_engine)
        with Session() as db:
            records = db.query(MarketData).filter_by(symbol="AG2606").all()
            assert len(records) == 2
            periods = {r.period for r in records}
            assert periods == {"5min", "15min"}
            for r in records:
                assert r.current_price == 123.45

        # Redis 中应回填对应缓存键
        assert fake_redis.exists("market_data:AG2606:5min")
        assert fake_redis.exists("market_data:AG2606:15min")

        cached_5min = json.loads(fake_redis.get("market_data:AG2606:5min"))
        assert cached_5min["current_price"] == 123.45
        assert cached_5min["candles"] == sample_data["timeframes"]["5min"]


class TestRedisCacheHitMiss:
    """Redis 缓存命中/未命中读取场景。"""

    def test_first_read_miss_falls_back_to_mysql_and_backfills_redis(
        self, fake_redis, mysql_engine
    ):
        """Redis 未命中时从 MySQL 读取并回填 Redis。"""
        _seed_mysql_market_data(mysql_engine)
        manager = StorageManager(
            redis_client=fake_redis, mysql_engine=mysql_engine
        )

        assert fake_redis.get("market_data:AG2606:5min") is None

        result = manager.get_market_data("AG2606", "futures", "5min")

        expected = {
            "current_price": 123.45,
            "timestamp": datetime(2026, 7, 4, 10, 0, 0).isoformat(),
            "candles": [{"datetime": "2026-07-04T09:55:00", "open": 100, "close": 110}],
        }
        assert result == expected

        # Redis 应已被回填
        assert fake_redis.exists("market_data:AG2606:5min")
        cached = json.loads(fake_redis.get("market_data:AG2606:5min"))
        assert cached == expected

    def test_second_read_hit_returns_from_redis_without_mysql_access(
        self, fake_redis, mysql_engine
    ):
        """Redis 命中时直接返回缓存，不再访问 MySQL。"""
        _seed_mysql_market_data(mysql_engine)
        manager = StorageManager(
            redis_client=fake_redis, mysql_engine=mysql_engine
        )

        first = manager.get_market_data("AG2606", "futures", "5min")
        assert fake_redis.exists("market_data:AG2606:5min")

        # 将 MySQL 引擎替换为会抛异常的 mock，以证明第二次读取不会访问 MySQL
        bad_engine = MagicMock()
        bad_engine.connect.side_effect = Exception("MySQL should not be accessed")
        manager.mysql_engine = bad_engine

        second = manager.get_market_data("AG2606", "futures", "5min")

        assert second == first
        bad_engine.connect.assert_not_called()

    def test_after_redis_key_deleted_falls_back_to_mysql_again(
        self, fake_redis, mysql_engine
    ):
        """手动删除 Redis key 后，读取回源 MySQL 并重新回填 Redis。"""
        _seed_mysql_market_data(mysql_engine)
        manager = StorageManager(
            redis_client=fake_redis, mysql_engine=mysql_engine
        )

        first = manager.get_market_data("AG2606", "futures", "5min")
        assert fake_redis.exists("market_data:AG2606:5min")

        # 模拟 TTL 过期或手动失效
        fake_redis.delete("market_data:AG2606:5min")
        assert fake_redis.get("market_data:AG2606:5min") is None

        second = manager.get_market_data("AG2606", "futures", "5min")

        assert second == first
        assert fake_redis.exists("market_data:AG2606:5min")

    def test_cache_hit_data_matches_mysql_data(
        self, fake_redis, mysql_engine
    ):
        """缓存命中时返回的数据应与 MySQL 中的数据一致。"""
        _seed_mysql_market_data(mysql_engine)
        manager = StorageManager(
            redis_client=fake_redis, mysql_engine=mysql_engine
        )

        # 首次读取触发回填
        from_redis = manager.get_market_data("AG2606", "futures", "5min")

        # 直接从 MySQL 读取原始记录作为基准
        Session = sessionmaker(bind=mysql_engine)
        with Session() as db:
            record = (
                db.query(MarketData)
                .filter_by(symbol="AG2606", data_type="futures", period="5min")
                .first()
            )
            assert record is not None
            assert from_redis["current_price"] == record.current_price
            assert from_redis["candles"] == json.loads(record.candles_json)


class TestCacheLogMessages:
    """验证缓存命中/未命中的日志输出。"""

    @pytest.mark.xfail(
        reason="当前 StorageManager 未输出 'Redis 缓存命中' 日志，仅记录行为断言",
        strict=True,
    )
    def test_cache_hit_logs_redis_hit(self, fake_redis, mysql_engine, caplog):
        """Redis 命中时日志应包含 'Redis 缓存命中'。"""
        _seed_mysql_market_data(mysql_engine)
        manager = StorageManager(
            redis_client=fake_redis, mysql_engine=mysql_engine
        )
        manager.get_market_data("AG2606", "futures", "5min")  # 回填

        with caplog.at_level(logging.INFO, logger="app.storage_manager"):
            manager.get_market_data("AG2606", "futures", "5min")  # 命中

        assert "Redis 缓存命中" in caplog.text

    @pytest.mark.xfail(
        reason="当前 StorageManager 未输出 'Redis 未命中，从 MySQL 读取' 日志，仅记录行为断言",
        strict=True,
    )
    def test_cache_miss_logs_redis_miss_and_mysql_read(
        self, fake_redis, mysql_engine, caplog
    ):
        """Redis 未命中并回源 MySQL 时日志应包含 'Redis 未命中，从 MySQL 读取'。"""
        _seed_mysql_market_data(mysql_engine)
        manager = StorageManager(
            redis_client=fake_redis, mysql_engine=mysql_engine
        )

        with caplog.at_level(logging.INFO, logger="app.storage_manager"):
            manager.get_market_data("AG2606", "futures", "5min")

        assert "Redis 未命中，从 MySQL 读取" in caplog.text
