"""配置存储测试：MySQL + JSON fallback。"""
import json
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

from app.config_store import ConfigStore


def _make_session_maker(mock_db):
    """构造正确的 sessionmaker mock，使 with 语句返回 mock_db。"""
    session_obj = MagicMock()
    session_obj.__enter__ = MagicMock(return_value=mock_db)
    session_obj.__exit__ = MagicMock(return_value=False)

    session_maker = MagicMock(return_value=session_obj)
    return session_maker


class TestConfigStoreGet:
    """ConfigStore.get_config 测试。"""

    def test_get_config_returns_mysql_value_on_hit(self, tmp_path):
        """MySQL 命中时返回数据库中的配置。"""
        mock_config = MagicMock()
        mock_config.config_value = {"futures": {"沪银": "AG2608"}}

        mock_db = MagicMock()
        mock_db.query.return_value.filter.return_value.first.return_value = mock_config

        storage = MagicMock()
        storage.check_mysql.return_value = True

        store = ConfigStore(
            storage_manager=storage,
            config_dir=tmp_path,
            session_maker=_make_session_maker(mock_db),
        )

        result = store.get_config("symbols", {"futures": {}})

        assert result == {"futures": {"沪银": "AG2608"}}

    def test_get_config_reads_json_and_backfills_when_mysql_miss(self, tmp_path):
        """MySQL 未命中时读取 JSON 并回填数据库。"""
        json_path = tmp_path / "symbols_config.json"
        json_path.write_text(json.dumps({"futures": {"沪银": "AG2608"}}, ensure_ascii=False), encoding="utf-8")

        mock_db = MagicMock()
        mock_db.query.return_value.filter.return_value.first.return_value = None

        storage = MagicMock()
        storage.check_mysql.return_value = True

        store = ConfigStore(
            storage_manager=storage,
            config_dir=tmp_path,
            session_maker=_make_session_maker(mock_db),
        )

        result = store.get_config("symbols", {"futures": {}})

        assert result == {"futures": {"沪银": "AG2608"}}
        mock_db.add.assert_called_once()
        mock_db.commit.assert_called_once()

    def test_get_config_falls_back_to_json_when_mysql_unavailable(self, tmp_path):
        """MySQL 不可用时直接读取 JSON。"""
        json_path = tmp_path / "symbols_config.json"
        json_path.write_text(json.dumps({"futures": {"沪银": "AG2608"}}, ensure_ascii=False), encoding="utf-8")

        storage = MagicMock()
        storage.check_mysql.return_value = False

        store = ConfigStore(
            storage_manager=storage,
            config_dir=tmp_path,
            session_maker=None,
        )

        result = store.get_config("symbols", {"futures": {}})

        assert result == {"futures": {"沪银": "AG2608"}}

    def test_get_config_returns_fallback_when_no_mysql_and_no_json(self, tmp_path):
        """MySQL 和 JSON 都不存在时返回 fallback。"""
        storage = MagicMock()
        storage.check_mysql.return_value = False

        store = ConfigStore(
            storage_manager=storage,
            config_dir=tmp_path,
            session_maker=None,
        )

        result = store.get_config("symbols", {"futures": {"默认": "DF2609"}})

        assert result == {"futures": {"默认": "DF2609"}}


class TestConfigStoreSet:
    """ConfigStore.set_config 测试。"""

    def test_set_config_writes_to_mysql_and_json(self, tmp_path):
        """MySQL 可用时同时写入数据库和 JSON。"""
        mock_existing = MagicMock()

        mock_db = MagicMock()
        mock_db.query.return_value.filter.return_value.first.return_value = mock_existing

        storage = MagicMock()
        storage.check_mysql.return_value = True

        store = ConfigStore(
            storage_manager=storage,
            config_dir=tmp_path,
            session_maker=_make_session_maker(mock_db),
        )

        result = store.set_config("symbols", {"futures": {"沪银": "AG2608"}})

        assert result is True
        assert mock_existing.config_value == {"futures": {"沪银": "AG2608"}}
        mock_db.commit.assert_called_once()

        json_path = tmp_path / "symbols_config.json"
        assert json_path.exists()
        assert json.loads(json_path.read_text(encoding="utf-8")) == {"futures": {"沪银": "AG2608"}}

    def test_set_config_writes_only_json_when_mysql_unavailable(self, tmp_path):
        """MySQL 不可用时只写入 JSON。"""
        storage = MagicMock()
        storage.check_mysql.return_value = False

        store = ConfigStore(
            storage_manager=storage,
            config_dir=tmp_path,
            session_maker=None,
        )

        result = store.set_config("symbols", {"futures": {"沪银": "AG2608"}})

        assert result is True

        json_path = tmp_path / "symbols_config.json"
        assert json_path.exists()
        assert json.loads(json_path.read_text(encoding="utf-8")) == {"futures": {"沪银": "AG2608"}}


class TestConfigMigration:
    """配置迁移测试。"""

    def test_migrate_skips_when_config_exists(self, tmp_path):
        """数据库已有配置时跳过迁移。"""
        from app.config_migration import migrate_configs_to_mysql

        with patch("app.config_migration.get_config_store") as mock_get_store:
            store = MagicMock()
            store._config_exists_in_mysql.return_value = True
            store.storage_manager.check_mysql.return_value = True
            store.session_maker = MagicMock()
            mock_get_store.return_value = store

            result = migrate_configs_to_mysql()

            assert result is True
            store._load_json.assert_not_called()

    def test_migrate_loads_json_and_saves(self, tmp_path):
        """从 JSON 读取并保存到数据库。"""
        from app.config_migration import migrate_configs_to_mysql

        symbols_path = tmp_path / "symbols_config.json"
        symbols_path.write_text(json.dumps({"futures": {"沪银": "AG2608"}}, ensure_ascii=False), encoding="utf-8")

        ai_path = tmp_path / "ai_config.json"
        ai_path.write_text(json.dumps({"models": []}, ensure_ascii=False), encoding="utf-8")

        with patch("app.config_migration.get_config_store") as mock_get_store:
            store = MagicMock()
            store._config_exists_in_mysql.return_value = False
            store.storage_manager.check_mysql.return_value = True
            store.session_maker = MagicMock()
            store.config_dir = tmp_path
            store.config_files = {
                "symbols": symbols_path,
                "ai": ai_path,
            }
            store._load_json.side_effect = lambda key, default: json.loads(store.config_files[key].read_text(encoding="utf-8")) if store.config_files[key].exists() else default
            store._save_to_mysql.return_value = True
            mock_get_store.return_value = store

            result = migrate_configs_to_mysql()

            assert result is True
            assert store._save_to_mysql.call_count == 2
