import logging
from unittest.mock import MagicMock, patch

import pytest


@pytest.fixture
def patch_lifespan_deps():
    """Patch all dependencies around lifespan so only storage init is exercised."""
    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()

    # 清理 StorageManager 全局单例，避免测试间状态泄漏
    import app.storage_manager as _sm_module

    _sm_module._storage_manager = None


@pytest.mark.asyncio
async def test_lifespan_calls_redis_and_mysql_init(patch_lifespan_deps, caplog):
    """ lifespan 应调用 Redis 与 MySQL 初始化函数。 """
    from app.main import lifespan, app

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

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

    patch_lifespan_deps["init_redis"].assert_called_once()
    patch_lifespan_deps["init_mysql"].assert_called_once()


@pytest.mark.asyncio
async def test_lifespan_triggers_migration_when_mysql_ok(patch_lifespan_deps):
    """ MySQL 可用时，lifespan 应触发 SQLite 到 MySQL 的迁移。 """
    from app.main import lifespan, app

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

    with patch("app.migration.migrate_sqlite_to_mysql") as mock_migrate:
        async with lifespan(app):
            pass

    mock_migrate.assert_called_once()


@pytest.mark.asyncio
async def test_lifespan_skips_migration_when_mysql_unavailable(patch_lifespan_deps):
    """ MySQL 不可用时，lifespan 不应调用迁移函数。 """
    from app.main import lifespan, app

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

    with patch("app.migration.migrate_sqlite_to_mysql") as mock_migrate:
        async with lifespan(app):
            pass

    mock_migrate.assert_not_called()


@pytest.mark.asyncio
async def test_lifespan_storage_mode_redis_and_mysql(patch_lifespan_deps, caplog):
    """ Redis 与 MySQL 均可用时，日志显示 Redis + MySQL 模式。 """
    from app.main import lifespan, app

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

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

    assert "存储模式: Redis + MySQL" in caplog.text


@pytest.mark.asyncio
async def test_lifespan_storage_mode_mysql_only(patch_lifespan_deps, caplog):
    """ 仅 MySQL 可用时，日志显示 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_lifespan_storage_mode_sqlite_fallback(patch_lifespan_deps, caplog):
    """ Redis 与 MySQL 均不可用时，日志显示 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


@pytest.mark.asyncio
async def test_lifespan_storage_init_failure_does_not_raise(patch_lifespan_deps, caplog):
    """ 初始化 Redis/MySQL 失败抛出异常时，应用仍可正常启动。 """
    from app.main import lifespan, app

    patch_lifespan_deps["init_redis"].side_effect = Exception("redis down")
    patch_lifespan_deps["init_mysql"].side_effect = Exception("mysql down")

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

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


def test_storage_manager_initialize_accepts_clients_and_updates_references():
    """initialize 应可选接收 redis_client 和 mysql_engine 并更新内部引用。"""
    from app.storage_manager import StorageManager

    manager = StorageManager(redis_client=None, mysql_engine=None)
    fake_redis = MagicMock()
    fake_mysql = MagicMock()

    manager.initialize(redis_client=fake_redis, mysql_engine=fake_mysql)

    assert manager.redis_client is fake_redis
    assert manager.mysql_engine is fake_mysql


@pytest.mark.asyncio
async def test_lifespan_injects_redis_and_mysql_into_storage_manager(patch_lifespan_deps):
    """lifespan 初始化后应将 Redis/MySQL 客户端注入 StorageManager 单例。"""
    from app.main import lifespan, app
    from app.storage_manager import StorageManager

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

    mock_manager = MagicMock(spec=StorageManager)
    with patch("app.storage_manager.get_storage_manager", return_value=mock_manager):
        async with lifespan(app):
            pass

    mock_manager.initialize.assert_called_once_with(fake_redis, fake_mysql)
