"""onmyoji_gacha HTTP DataManager 测试。"""

from __future__ import annotations

import aiohttp
import importlib.util
import inspect
import pytest
import sys
import types
from pathlib import Path


def load_gacha_modules():
    """直接加载 onmyoji_gacha 子模块，避免测试环境执行 nonebot 插件入口。"""

    plugin_dir = Path(__file__).resolve().parents[1] / "danding_bot" / "plugins" / "onmyoji_gacha"
    package_name = "_onmyoji_gacha_under_test"
    package = types.ModuleType(package_name)
    package.__path__ = [str(plugin_dir)]
    sys.modules[package_name] = package

    for module_name in ("config", "data_manager", "gacha"):
        full_name = f"{package_name}.{module_name}"
        spec = importlib.util.spec_from_file_location(full_name, plugin_dir / f"{module_name}.py")
        module = importlib.util.module_from_spec(spec)
        sys.modules[full_name] = module
        assert spec and spec.loader
        spec.loader.exec_module(module)

    return (
        sys.modules[f"{package_name}.config"],
        sys.modules[f"{package_name}.data_manager"],
        sys.modules[f"{package_name}.gacha"],
    )


config_module, data_manager_module, gacha_module = load_gacha_modules()
Config = config_module.Config
DataManager = data_manager_module.DataManager
GachaSystem = gacha_module.GachaSystem


def load_sign_in_module():
    """直接加载签到编排模块，避免导入 NoneBot 插件入口。"""

    plugin_dir = Path(__file__).resolve().parents[1] / "danding_bot" / "plugins" / "onmyoji_gacha"
    module_name = "_onmyoji_gacha_sign_in_under_test"
    spec = importlib.util.spec_from_file_location(module_name, plugin_dir / "sign_in.py")
    module = importlib.util.module_from_spec(spec)
    sys.modules[module_name] = module
    assert spec and spec.loader
    spec.loader.exec_module(module)
    return module


sign_in_module = load_sign_in_module()


class FakeResponse:
    """模拟 aiohttp 响应上下文。"""

    def __init__(self, payload, status=200):
        self.payload = payload
        self.status = status

    async def __aenter__(self):
        return self

    async def __aexit__(self, exc_type, exc, tb):
        return None

    async def json(self):
        return self.payload


class FakeSession:
    """记录请求参数的 aiohttp ClientSession 替身。"""

    def __init__(self, responses, calls):
        self.responses = responses
        self.calls = calls

    async def __aenter__(self):
        return self

    async def __aexit__(self, exc_type, exc, tb):
        return None

    def get(self, url, params=None, timeout=None):
        self.calls.append({"method": "GET", "url": url, "params": params, "timeout": timeout})
        return FakeResponse(self.responses.pop(0))

    def post(self, url, json=None, timeout=None):
        self.calls.append({"method": "POST", "url": url, "json": json, "timeout": timeout})
        return FakeResponse(self.responses.pop(0))


@pytest.fixture
def fake_aiohttp(monkeypatch):
    calls = []
    responses = []
    monkeypatch.setattr(data_manager_module.aiohttp, "ClientSession", lambda: FakeSession(responses, calls))
    test_config = Config(
        GACHA_API_HOST="http://xapi.test/bot/gacha/",
        BOT_USER_ID="robot",
        BOT_TOKEN="secret",
    )
    monkeypatch.setattr(data_manager_module, "config", test_config)
    monkeypatch.setattr(gacha_module, "config", test_config)
    return responses, calls


def success_data(data):
    return {"code": 200, "message": "", "data": data}


@pytest.mark.asyncio
async def test_shikigami_cache_and_draw_send_auth_to_xapi(fake_aiohttp):
    responses, calls = fake_aiohttp
    responses.extend(
        [
            success_data(
                {
                    "items": [
                        {"id": 1, "name": "灯笼鬼", "rarity": "R", "image_path": "/r/灯笼鬼.png"},
                        {"id": 3, "name": "茨木童子", "rarity": "SSR", "image_path": "/ssr/茨木童子.png"},
                    ]
                }
            ),
            success_data(
                {
                    "success": True,
                    "rarity": "SSR",
                    "name": "茨木童子",
                    "image_path": "/ssr/茨木童子.png",
                    "image_url": "/ssr/茨木童子.png",
                    "draws_left": 2,
                    "unlocked_achievements": ["no_ssr_60"],
                }
            ),
        ]
    )
    manager = DataManager()

    grouped = await manager.refresh_shikigami_data()
    draw_result = await manager.record_draw_result("10001", "SSR", grouped["SSR"][0])

    assert grouped["SSR"][0]["image_url"] == "/ssr/茨木童子.png"
    assert draw_result["unlocked_achievements"] == ["no_ssr_60"]
    assert calls[0]["method"] == "GET"
    assert calls[0]["url"] == "http://xapi.test/bot/gacha/shikigami"
    assert calls[0]["params"] == {"user": "robot", "token": "secret"}
    assert calls[1]["method"] == "POST"
    assert calls[1]["json"] == {
        "user": "robot",
        "token": "secret",
        "user_id": "10001",
        "shikigami_id": 3,
        "rarity": "SSR",
        "name": "茨木童子",
    }


@pytest.mark.asyncio
async def test_gacha_system_draw_keeps_probability_local_and_uses_xapi(fake_aiohttp, monkeypatch):
    responses, calls = fake_aiohttp
    responses.extend(
        [
            success_data({"draws_left": 3, "daily_limit": 3}),
            success_data({"items": [{"id": 3, "name": "茨木童子", "rarity": "SSR", "image_path": "/ssr/茨木童子.png"}]}),
            success_data(
                {
                    "success": True,
                    "rarity": "SSR",
                    "name": "茨木童子",
                    "image_path": "/ssr/茨木童子.png",
                    "image_url": "/ssr/茨木童子.png",
                    "draws_left": 2,
                    "unlocked_achievements": [],
                }
            ),
        ]
    )
    manager = DataManager()
    monkeypatch.setattr(gacha_module, "data_manager", manager)
    system = GachaSystem()
    monkeypatch.setattr(system, "_draw_rarity", lambda user_id=None: "SSR")

    result = await system.draw("10001")

    assert result == {
        "success": True,
        "rarity": "SSR",
        "name": "茨木童子",
        "image_url": "/ssr/茨木童子.png",
        "draws_left": 2,
        "unlocked_achievements": [],
    }
    assert [call["url"].rsplit("/", 1)[-1] for call in calls] == ["draws-left", "shikigami", "draw"]


@pytest.mark.asyncio
async def test_triple_sign_in_claim_and_query_shapes(fake_aiohttp):
    responses, calls = fake_aiohttp
    responses.extend(
        [
            success_data({"success": True, "results": [], "draws_left": 0, "unlocked_achievements": ["no_ssr_60"]}),
            success_data({"success": True, "signed_already": True}),
            success_data({"success": True, "reward_type": "天卡"}),
            success_data({"success": True, "total_draws": 1, "R_count": 1, "SR_count": 0, "SSR_count": 0, "SP_count": 0, "recent_draws": []}),
            success_data({"success": True, "date": "2026-06-20", "stats": {"total_users": 1}}),
            success_data({"items": [{"user_id": "10001", "total_draws": 1, "R_count": 1, "SR_count": 0, "SSR_count": 0, "SP_count": 0, "ssr_sp_total": 0}]}),
            success_data({"achievements": {"no_ssr_60": {"unlocked_date": "2026-06-20", "reward_claimed": False}}, "progress": {"no_ssr_streak": 60}}),
            success_data({"success": True, "date": "2026-06-20", "records": [], "total_count": 0}),
        ]
    )
    manager = DataManager()
    draws = [
        {"id": 1, "name": "灯笼鬼", "rarity": "R", "image_url": "/r/灯笼鬼.png"},
        {"id": 2, "name": "雪女", "rarity": "SR", "image_url": "/sr/雪女.png"},
        {"id": 3, "name": "茨木童子", "rarity": "SSR", "image_url": "/ssr/茨木童子.png"},
    ]

    triple = await manager.record_triple_draw_result("10001", draws)
    signed = await manager.record_sign_in("10001", 20)
    claimed = await manager.claim_achievement_reward("10001", "no_ssr_60")
    stats = await manager.get_user_stats("10001")
    daily = await manager.get_daily_stats("2026-06-20")
    rank = await manager.get_rank()
    achievements = await manager.get_user_achievements("10001")
    records = await manager.get_daily_records("2026-06-20")

    assert triple["unlocked_achievements"] == ["no_ssr_60"]
    assert signed is False
    assert claimed is True
    assert stats["success"] is True
    assert daily["date"] == "2026-06-20"
    assert rank[0]["user_id"] == "10001"
    assert "unlocked" in achievements and "progress" in achievements
    assert records["total_count"] == 0
    assert calls[0]["json"]["draws"][0] == {"shikigami_id": 1, "rarity": "R", "name": "灯笼鬼"}
    assert calls[1]["json"]["points_awarded"] == 20
    assert calls[2]["json"]["achievement_id"] == "no_ssr_60"
    assert calls[3]["params"]["user_id"] == "10001"


@pytest.mark.asyncio
async def test_network_error_keeps_failure_shapes_and_no_sqlite(monkeypatch):
    class FailingSession:
        async def __aenter__(self):
            raise aiohttp.ClientError("offline")

        async def __aexit__(self, exc_type, exc, tb):
            return None

    monkeypatch.setattr(data_manager_module.aiohttp, "ClientSession", lambda: FailingSession())
    manager = DataManager()

    assert await manager.get_draws_left("10001") == 0
    assert await manager.record_sign_in("10001", 20) is False
    assert await manager.get_rank() == []
    assert "sqlite3" not in inspect.getsource(data_manager_module)


@pytest.mark.asyncio
async def test_daily_sign_in_awards_points_only_after_sign_recorded():
    calls = []

    class FakeDataManager:
        async def record_sign_in(self, user_id, points_awarded):
            calls.append(("sign-in", user_id, points_awarded))
            return True

    class FakePointsAPI:
        async def add_points(self, user_id, amount, source, reason):
            calls.append(("add-points", user_id, amount, source, reason))
            return True, 66

    success, balance, status = await sign_in_module.award_daily_sign_in_points(
        data_manager=FakeDataManager(),
        points_api=FakePointsAPI(),
        user_id="10001",
        points=20,
        source="gacha_sign",
        reason="抽卡签到",
    )

    assert (success, balance, status) == (True, 66, "awarded")
    assert calls == [
        ("sign-in", "10001", 20),
        ("add-points", "10001", 20, "gacha_sign", "抽卡签到"),
    ]


@pytest.mark.asyncio
async def test_daily_sign_in_repeat_does_not_award_points():
    calls = []

    class FakeDataManager:
        async def record_sign_in(self, user_id, points_awarded):
            calls.append(("sign-in", user_id, points_awarded))
            return False

    class FakePointsAPI:
        async def add_points(self, user_id, amount, source, reason):
            calls.append(("add-points", user_id, amount, source, reason))
            return True, 66

    success, balance, status = await sign_in_module.award_daily_sign_in_points(
        data_manager=FakeDataManager(),
        points_api=FakePointsAPI(),
        user_id="10001",
        points=20,
        source="gacha_sign",
        reason="抽卡签到",
    )

    assert (success, balance, status) == (False, 0, "signed_already_or_failed")
    assert calls == [("sign-in", "10001", 20)]
