"""
降级场景验证测试

覆盖：
1. Redis 不可用但 MySQL 可用时，自动降级到 MySQL
2. Redis 和 MySQL 均不可用时，自动降级到 SQLite
3. 各降级场景下接口正常返回，无 500 错误
4. 恢复 Redis 服务后，等待惰性检测间隔验证自动恢复

实现说明：
- 使用 FastAPI TestClient 验证接口层行为。
- 使用内存 SQLite 引擎作为 MySQL 替身（StorageManager 基于 SQLAlchemy 通用接口）。
- 通过 patch `app.services.cache.get_storage_manager` 注入已配置好的 StorageManager，
  从而绕过当前 `app/main.py` 启动时尚未将初始化后的 Redis/MySQL 客户端注入 StorageManager
  全局单例的问题，专注于验证 cache / API 层的降级与恢复逻辑。
"""
import json
import logging
import time
from datetime import datetime
from unittest.mock import MagicMock, patch

import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine, text
from sqlalchemy.orm import sessionmaker
from sqlalchemy.pool import StaticPool

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


# ===== Fixtures =====


@pytest.fixture
def sqlite_engine():
    """应用主数据库使用的内存 SQLite 引擎。"""
    # 防御：修复上游 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:",
        connect_args={"check_same_thread": False},
        poolclass=StaticPool,
    )
    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 mysql_engine():
    """作为 MySQL 替身的内存 SQLite 引擎（StorageManager 使用 SQLAlchemy 通用接口）。"""
    # 防御：修复上游 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:",
        connect_args={"check_same_thread": False},
        poolclass=StaticPool,
    )
    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 app_with_db(sqlite_engine):
    """返回替换 get_db 依赖为内存 SQLite 的 FastAPI app。"""
    from app.main import app
    from app.database import get_db as original_get_db

    SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=sqlite_engine)

    def override_get_db():
        db = SessionLocal()
        try:
            yield db
        finally:
            db.close()

    app.dependency_overrides[original_get_db] = override_get_db
    yield app
    app.dependency_overrides.clear()


@pytest.fixture
def patch_lifespan_deps():
    """Patch lifespan 依赖，避免真实连接数据库/Redis/MySQL 和启动调度器。"""
    mock_db = MagicMock()
    mock_session_local = MagicMock(return_value=mock_db)

    patches = [
        patch("app.main.Base.metadata.create_all"),
        patch("app.main.UserBase.metadata.create_all"),
        patch("app.analysis_db.init_analysis_db"),
        patch("app.database.SessionLocal", mock_session_local),
        patch("app.auth_service.create_default_admin"),
        patch("app.main.start_scheduler"),
        patch("app.services.cache.list_tasks", return_value=[]),
        patch("app.services.scheduler.add_job"),
        patch("app.main.stop_scheduler"),
        patch("app.redis_client.init_redis"),
        patch("app.mysql_database.init_mysql"),
    ]
    started = [p.start() for p in patches]
    yield {
        "Base_create_all": started[0],
        "UserBase_create_all": started[1],
        "init_analysis_db": started[2],
        "SessionLocal": started[3],
        "create_default_admin": started[4],
        "start_scheduler": started[5],
        "list_tasks": started[6],
        "add_job": started[7],
        "stop_scheduler": started[8],
        "init_redis": started[9],
        "init_mysql": started[10],
    }
    for p in patches:
        p.stop()


class FakeRedis:
    """可切换可用状态的轻量级 Redis 替身。"""

    def __init__(self, available=True):
        self._store = {}
        self._available = available

    def set_available(self, available):
        self._available = available

    def ping(self):
        if not self._available:
            raise Exception("redis down")
        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)]


# ===== Helpers =====


def _seed_market_data(
    engine,
    symbol="AG2606",
    data_type="futures",
    period="5min",
    current_price=123.45,
):
    """在指定引擎中写入一条行情数据。"""
    Session = sessionmaker(bind=engine)
    with Session() as db:
        db.add(
            MarketData(
                symbol=symbol,
                data_type=data_type,
                period=period,
                candles_json=json.dumps(
                    [
                        {
                            "datetime": "2026-07-04T09:55:00",
                            "open": 100,
                            "high": 115,
                            "low": 95,
                            "close": 110,
                            "volume": 1000,
                        }
                    ]
                ),
                current_price=current_price,
                fetched_at=datetime(2026, 7, 4, 10, 0, 0),
                candle_count=1,
            )
        )
        db.commit()


# ===== Lifespan 日志测试 =====


class TestLifespanFallbackLogs:
    """启动时降级检测日志测试。"""

    @pytest.mark.asyncio
    async def test_logs_mysql_mode_when_redis_unavailable(
        self, patch_lifespan_deps, caplog
    ):
        """Redis 不可用但 MySQL 可用时，lifespan 应输出 MySQL 降级模式日志。"""
        from app.main import lifespan, app

        patch_lifespan_deps["init_redis"].return_value = None
        patch_lifespan_deps["init_mysql"].return_value = MagicMock()

        with caplog.at_level(logging.WARNING, logger="app.main"):
            async with lifespan(app):
                pass

        assert "存储模式: MySQL (Redis 不可用)" in caplog.text

    @pytest.mark.asyncio
    async def test_logs_sqlite_mode_when_both_unavailable(
        self, patch_lifespan_deps, caplog
    ):
        """Redis 和 MySQL 均不可用时，lifespan 应输出 SQLite 降级模式日志。"""
        from app.main import lifespan, app

        patch_lifespan_deps["init_redis"].return_value = None
        patch_lifespan_deps["init_mysql"].return_value = None

        with caplog.at_level(logging.ERROR, logger="app.main"):
            async with lifespan(app):
                pass

        assert "存储模式: SQLite (Redis 和 MySQL 均不可用)" in caplog.text


# ===== StorageManager 运行时降级与恢复测试 =====


class TestStorageManagerFallback:
    """StorageManager 运行时降级与恢复测试。"""

    def test_falls_back_to_mysql_when_redis_unavailable(self, mysql_engine):
        """Redis 不可用但 MySQL 可用时，从 MySQL 读取数据。"""
        _seed_market_data(mysql_engine, current_price=123.45)

        fake_redis = FakeRedis(available=False)
        manager = StorageManager(redis_client=fake_redis, mysql_engine=mysql_engine)

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

        assert result is not None
        assert result["current_price"] == 123.45

    def test_returns_none_when_both_unavailable(self):
        """Redis 和 MySQL 均不可用时，StorageManager 返回 None（由调用方降级到 SQLite）。"""
        fake_redis = FakeRedis(available=False)
        manager = StorageManager(redis_client=fake_redis, mysql_engine=None)

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

        assert result is None

    def test_recovers_to_redis_after_interval(self, mysql_engine):
        """Redis 恢复后，超过惰性检测间隔应重新使用 Redis 并回填缓存。"""
        _seed_market_data(mysql_engine, current_price=123.45)

        fake_redis = FakeRedis(available=False)
        manager = StorageManager(
            redis_client=fake_redis, mysql_engine=mysql_engine, check_interval=0.1
        )

        # Redis 不可用，首次读取应来自 MySQL
        result = manager.get_market_data("AG2606", "futures", "5min")
        assert result["current_price"] == 123.45
        # Redis 不可用，不应有回填
        assert fake_redis.get("market_data:AG2606:5min") is None

        # 恢复 Redis
        fake_redis.set_available(True)
        # 等待检测间隔过期
        time.sleep(0.15)

        # 再次读取：Redis ping 通过，get 返回 None，回源 MySQL 后回填 Redis
        result = manager.get_market_data("AG2606", "futures", "5min")
        assert result["current_price"] == 123.45
        assert fake_redis.get("market_data:AG2606:5min") is not None


# ===== API 接口降级测试 =====


class TestApiFallbackResponses:
    """API 接口在不同存储模式下的响应测试。"""

    def test_api_returns_mysql_data_when_redis_unavailable(
        self, patch_lifespan_deps, app_with_db, sqlite_engine, mysql_engine
    ):
        """Redis 不可用但 MySQL 可用时，接口从 MySQL 返回数据且无 500 错误。"""
        _seed_market_data(mysql_engine, current_price=123.45)
        _seed_market_data(sqlite_engine, current_price=456.78)

        fake_redis = FakeRedis(available=False)
        storage_manager = StorageManager(
            redis_client=fake_redis, mysql_engine=mysql_engine, check_interval=0.1
        )

        patch_lifespan_deps["init_redis"].return_value = None
        patch_lifespan_deps["init_mysql"].return_value = mysql_engine

        with patch("app.mysql_database.mysql_engine", mysql_engine):
            with patch("app.services.cache.get_storage_manager", return_value=storage_manager):
                with TestClient(app_with_db) as client:
                    response = client.get("/api/v1/data/latest/AG2606?period=5min")

        assert response.status_code == 200
        data = response.json()
        assert data["current_price"] == 123.45

    def test_api_returns_sqlite_data_when_both_unavailable(
        self, patch_lifespan_deps, app_with_db, sqlite_engine
    ):
        """Redis 和 MySQL 均不可用时，接口降级到 SQLite 返回数据且无 500 错误。"""
        _seed_market_data(sqlite_engine, current_price=456.78)

        fake_redis = FakeRedis(available=False)
        storage_manager = StorageManager(
            redis_client=fake_redis, mysql_engine=None, check_interval=0.1
        )

        patch_lifespan_deps["init_redis"].return_value = None
        patch_lifespan_deps["init_mysql"].return_value = None

        with patch("app.services.cache.get_storage_manager", return_value=storage_manager):
            with TestClient(app_with_db) as client:
                response = client.get("/api/v1/data/latest/AG2606?period=5min")

        assert response.status_code == 200
        data = response.json()
        assert data["current_price"] == 456.78

    def test_api_returns_404_not_500_when_no_data_anywhere(
        self, patch_lifespan_deps, app_with_db
    ):
        """任何存储层均无数据时，接口应返回 404 而非 500。"""
        fake_redis = FakeRedis(available=False)
        storage_manager = StorageManager(
            redis_client=fake_redis, mysql_engine=None, check_interval=0.1
        )

        patch_lifespan_deps["init_redis"].return_value = None
        patch_lifespan_deps["init_mysql"].return_value = None

        with patch("app.services.cache.get_storage_manager", return_value=storage_manager):
            with TestClient(app_with_db) as client:
                response = client.get("/api/v1/data/latest/UNKNOWN?period=5min")

        assert response.status_code == 404
