"""IMDb 原生媒体数据源 Module 测试。"""

import asyncio
from unittest.mock import AsyncMock, Mock, patch

import pytest

from app.domain.context import MediaInfo
from app.domain.meta.metabase import MetaBase
from app.modules.imdb import ImdbModule
from app.modules.imdb.api import (
    ImdbAka,
    ImdbApi,
    ImdbCredit,
    ImdbDate,
    ImdbEpisode,
    ImdbImage,
    ImdbPerson,
    ImdbSeason,
    ImdbTitle,
)
from app.runtime.config import settings
from app.runtime.tasks import TaskRegistry
from app.schemas.types import MediaRecognizeType, MediaSource, MediaType


@pytest.fixture
def imdb_title() -> ImdbTitle:
    """构造一条包含完整基础字段的 IMDb 电视剧条目。"""
    return ImdbTitle(
        id="tt0903747",
        type="tvSeries",
        primaryTitle="Breaking Bad",
        originalTitle="Breaking Bad",
        primaryImage={"url": "https://image.example/poster.jpg", "type": "poster"},
        startYear=2008,
        runtimeSeconds=2820,
        genres=["Crime", "Drama"],
        rating={"aggregateRating": 9.5, "voteCount": 2200000},
        plot="A chemistry teacher becomes a methamphetamine producer.",
        originCountries=[{"code": "US", "name": "United States"}],
        spokenLanguages=[{"code": "eng", "name": "English"}],
    )


@pytest.fixture
def imdb_api(imdb_title: ImdbTitle) -> Mock:
    """构造同步和异步方法行为一致的 IMDb API 替身。"""
    api = Mock(spec=ImdbApi)
    api.search_titles.return_value = [imdb_title]
    api.get_title.return_value = imdb_title
    api.list_akas.return_value = [ImdbAka(text="绝命毒师")]
    api.list_credits.return_value = [
        ImdbCredit(
            name=ImdbPerson(
                id="nm0533713",
                displayName="Vince Gilligan",
                primaryImage=ImdbImage(url="https://image.example/director.jpg"),
            ),
            category="DIRECTOR",
        ),
        ImdbCredit(
            name=ImdbPerson(
                id="nm0186505",
                displayName="Bryan Cranston",
                primaryImage=ImdbImage(url="https://image.example/actor.jpg"),
            ),
            category="ACTOR",
            characters=["Walter White"],
        ),
    ]
    api.list_images.return_value = [
        ImdbImage(url="https://image.example/backdrop.jpg", type="still_frame")
    ]
    api.list_episodes.return_value = [
        ImdbEpisode(
            id="tt0959621",
            season="1",
            episodeNumber=1,
            releaseDate=ImdbDate(year=2008, month=1, day=20),
        )
    ]
    api.list_seasons.return_value = [ImdbSeason(season="1", episodeCount=7)]
    api.async_search_titles = AsyncMock(return_value=[imdb_title])
    api.async_get_title = AsyncMock(return_value=imdb_title)
    api.async_list_akas = AsyncMock(return_value=api.list_akas.return_value)
    api.async_list_credits = AsyncMock(return_value=api.list_credits.return_value)
    api.async_list_images = AsyncMock(return_value=api.list_images.return_value)
    api.async_list_episodes = AsyncMock(return_value=api.list_episodes.return_value)
    api.async_list_seasons = AsyncMock(return_value=api.list_seasons.return_value)
    return api


@pytest.fixture
def imdb_module(imdb_api: Mock) -> ImdbModule:
    """构造注入离线 API 替身的 IMDb Module。"""
    module = ImdbModule()
    module.imdb_api = imdb_api
    module.scraper = Mock()
    return module


def test_imdb_module_declares_independent_media_recognizer() -> None:
    """IMDb 应作为独立宿主媒体识别 Module 暴露稳定元数据。"""
    assert ImdbModule.get_name() == "IMDb"
    assert ImdbModule.get_subtype() == MediaRecognizeType.IMDb
    assert ImdbModule.get_priority() == 4


def test_imdb_api_search_uses_keyless_imdb_suggestion_endpoint() -> None:
    """IMDb 标题搜索应直接调用 IMDb 免 Key 建议接口并缓存响应。"""
    api = ImdbApi()
    api.clear_cache()
    api._request = Mock()
    api._request.get_json.return_value = {
        "d": [
            {
                "id": "tt0111161",
                "qid": "movie",
                "l": "The Shawshank Redemption",
                "y": 1994,
            }
        ]
    }

    results = api.search_titles("The Shawshank Redemption", limit=1)
    cached_results = api.search_titles("The Shawshank Redemption", limit=1)

    assert [item.id for item in results] == ["tt0111161"]
    assert [item.id for item in cached_results] == ["tt0111161"]
    api._request.get_json.assert_called_once_with(
        "https://v2.sg.media-imdb.com/suggestion/x/"
        "The%20Shawshank%20Redemption.json",
        params={},
    )

    api.clear_cache()
    api.search_titles("The Shawshank Redemption", limit=1)
    assert api._request.get_json.call_count == 2


def test_imdb_graphql_error_is_not_cached() -> None:
    """GraphQL 错误响应不应污染缓存，后续成功详情仍应正常写入缓存。"""
    api = ImdbApi()
    api.clear_cache()
    api._request = Mock()
    api._request.post_json.side_effect = [
        {"errors": [{"message": "temporary failure"}]},
        {
            "data": {
                "titles": [
                    {
                        "id": "tt0111161",
                        "titleText": {"text": "The Shawshank Redemption"},
                        "titleType": {"id": "movie"},
                        "releaseYear": {"year": 1994},
                    }
                ]
            }
        },
    ]

    assert api.get_title("tt0111161") is None
    detail = api.get_title("tt0111161")
    cached_detail = api.get_title("tt0111161")

    assert detail and detail.primary_title == "The Shawshank Redemption"
    assert cached_detail and cached_detail.id == "tt0111161"
    assert api._request.post_json.call_count == 2


def test_imdb_clear_cache_registers_async_cleanup_in_running_loop() -> None:
    """同步清缓存入口在异步宿主中应登记任务，并保留原同步调用约定。"""

    async def scenario() -> None:
        """验证异步缓存清理直到完成前都由宿主登记器持有。"""
        api = ImdbApi()
        registry = TaskRegistry()
        release = asyncio.Event()

        async def clear_async_cache() -> None:
            """等待测试释放，确保可以观察登记中的任务。"""
            await release.wait()

        with (
            patch("app.modules.imdb.api.get_task_registry", return_value=registry),
            patch.object(api, "async_clear_cache", side_effect=clear_async_cache),
        ):
            assert api.clear_cache() is None
            assert [record.owner for record in registry.records] == [
                "module.imdb.cache_clear"
            ]

            release.set()
            await registry.records[0].task
            await asyncio.sleep(0)

        assert registry.records == ()
        api.close()

    asyncio.run(scenario())


def test_async_imdb_api_merges_paginated_episodes() -> None:
    """IMDb 异步剧集查询应使用 GraphQL 游标合并并缓存所有分页。"""
    api = ImdbApi()
    asyncio.run(api.async_clear_cache())
    api._async_request = Mock()
    api._async_request.post_json = AsyncMock(
        side_effect=[
            {
                "data": {
                    "titles": [
                        {
                            "episodes": {
                                "episodes": {
                                    "edges": [
                                        {
                                            "node": {
                                                "id": "tt-first",
                                                "series": {
                                                    "episodeNumber": {
                                                        "seasonNumber": 1,
                                                        "episodeNumber": 1,
                                                    }
                                                },
                                            }
                                        }
                                    ],
                                    "pageInfo": {
                                        "hasNextPage": True,
                                        "endCursor": "next-page",
                                    },
                                }
                            }
                        }
                    ]
                }
            },
            {
                "data": {
                    "titles": [
                        {
                            "episodes": {
                                "episodes": {
                                    "edges": [
                                        {
                                            "node": {
                                                "id": "tt-second",
                                                "series": {
                                                    "episodeNumber": {
                                                        "seasonNumber": 1,
                                                        "episodeNumber": 2,
                                                    }
                                                },
                                            }
                                        }
                                    ],
                                    "pageInfo": {
                                        "hasNextPage": False,
                                        "endCursor": None,
                                    },
                                }
                            }
                        }
                    ]
                }
            },
            {
                "data": {
                    "titles": [
                        {
                            "episodes": {
                                "episodes": {
                                    "edges": [
                                        {
                                            "node": {
                                                "id": "tt-first",
                                                "series": {
                                                    "episodeNumber": {
                                                        "seasonNumber": 1,
                                                        "episodeNumber": 1,
                                                    }
                                                },
                                            }
                                        }
                                    ],
                                    "pageInfo": {
                                        "hasNextPage": True,
                                        "endCursor": "next-page",
                                    },
                                }
                            }
                        }
                    ]
                }
            },
            {
                "data": {
                    "titles": [
                        {
                            "episodes": {
                                "episodes": {
                                    "edges": [
                                        {
                                            "node": {
                                                "id": "tt-second",
                                                "series": {
                                                    "episodeNumber": {
                                                        "seasonNumber": 1,
                                                        "episodeNumber": 2,
                                                    }
                                                },
                                            }
                                        }
                                    ],
                                    "pageInfo": {
                                        "hasNextPage": False,
                                        "endCursor": None,
                                    },
                                }
                            }
                        }
                    ]
                }
            },
        ]
    )

    results = asyncio.run(api.async_list_episodes("tt-series"))
    cached_results = asyncio.run(api.async_list_episodes("tt-series"))

    assert [item.id for item in results] == ["tt-first", "tt-second"]
    assert [item.id for item in cached_results] == ["tt-first", "tt-second"]
    assert api._async_request.post_json.await_count == 2
    assert api._async_request.post_json.await_args_list[0].kwargs["json"][
        "variables"
    ] == {"after": None, "first": 100, "titles": ("tt-series",)}
    assert api._async_request.post_json.await_args_list[1].kwargs["json"][
        "variables"
    ] == {"after": "next-page", "first": 100, "titles": ("tt-series",)}

    asyncio.run(api.async_clear_cache())
    refreshed_results = asyncio.run(api.async_list_episodes("tt-series"))
    assert [item.id for item in refreshed_results] == ["tt-first", "tt-second"]
    assert api._async_request.post_json.await_count == 4


def test_imdb_proxy_snapshot_reloads_client_and_closes_old_session(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """代理变更应关闭旧 IMDb 客户端并按新代理快照重建。"""
    module = ImdbModule()
    old_client = Mock()
    new_client = Mock()
    monkeypatch.setattr(settings, "PROXY_HOST", "http://old-proxy:7890")

    with patch("app.modules.imdb.ImdbApi", side_effect=[old_client, new_client]) as api_type:
        module.init_module()
        old_proxy = settings.PROXY
        monkeypatch.setattr(settings, "PROXY_HOST", "http://new-proxy:7890")
        new_proxy = settings.PROXY
        module.on_config_changed()

    assert api_type.call_args_list[0].kwargs["proxies"] == old_proxy
    assert api_type.call_args_list[1].kwargs["proxies"] == new_proxy
    old_client.close.assert_called_once_with()
    assert module.imdb_api is new_client


def test_imdb_module_clear_cache_delegates_to_api(
    imdb_module: ImdbModule,
) -> None:
    """系统统一清缓存动作应清理 IMDb HTTP 缓存区。"""
    imdb_module.clear_cache()

    imdb_module.imdb_api.clear_cache.assert_called_once_with()


def test_imdb_search_respects_source_and_returns_generic_identity(
    imdb_module: ImdbModule,
) -> None:
    """IMDb 搜索只响应选中的来源，并返回来源与原生 ID 对。"""
    meta = MetaBase("Breaking Bad")
    meta.name = "Breaking Bad"
    meta.type = MediaType.TV

    assert imdb_module.search_medias(meta, media_source=MediaSource.Douban) is None

    results = imdb_module.search_medias(meta, media_source=MediaSource.IMDb)

    assert results and len(results) == 1
    assert results[0].media_source == MediaSource.IMDb
    assert results[0].media_id == "tt0903747"
    assert results[0].imdb_id == "tt0903747"
    assert results[0].type == MediaType.TV


def test_imdb_explicit_identity_recognition_enriches_media_info(
    imdb_module: ImdbModule,
) -> None:
    """显式 IMDb ID 识别应补齐别名、演职员、剧集和图片。"""
    result = imdb_module.recognize_media(
        media_source=MediaSource.IMDb,
        media_id="TT0903747",
    )

    assert isinstance(result, MediaInfo)
    assert result.media_source == MediaSource.IMDb
    assert result.media_id == "tt0903747"
    assert result.title == "Breaking Bad"
    assert result.names == ["Breaking Bad", "绝命毒师"]
    assert result.backdrop_path == "https://image.example/backdrop.jpg"
    assert result.directors[0]["name"] == "Vince Gilligan"
    assert result.actors[0]["character"] == "Walter White"
    assert result.seasons == {1: [1]}
    assert result.season_years == {1: "2008"}
    assert result.number_of_episodes == 1
    assert result.runtime == 47
    assert result.category == "欧美剧"


def test_imdb_name_recognition_requires_imdb_selection(
    imdb_module: ImdbModule, monkeypatch: pytest.MonkeyPatch
) -> None:
    """名称识别仅在请求级或全局来源选中 IMDb 时执行。"""
    meta = MetaBase("绝命毒师")
    meta.name = "绝命毒师"
    meta.cn_name = "绝命毒师"
    meta.type = MediaType.TV
    monkeypatch.setattr(settings, "RECOGNIZE_SOURCE", MediaSource.TMDB.value)

    assert imdb_module.recognize_media(meta=meta) is None

    result = imdb_module.recognize_media(
        meta=meta,
        media_source=MediaSource.IMDb,
    )

    assert result and result.media_id == "tt0903747"
    imdb_module.imdb_api.search_titles.assert_called_with("绝命毒师")


def test_imdb_recognition_rejects_invalid_or_music_identity(
    imdb_module: ImdbModule,
) -> None:
    """IMDb Module 不应接管非法 ID 或音乐识别请求。"""
    assert (
        imdb_module.recognize_media(
            media_source=MediaSource.IMDb,
            media_id="0903747",
        )
        is None
    )
    assert (
        imdb_module.recognize_media(
            mtype=MediaType.MUSIC,
            media_source=MediaSource.IMDb,
            media_id="tt0903747",
        )
        is None
    )


def test_async_imdb_recognition_uses_async_detail_pipeline(
    imdb_module: ImdbModule,
) -> None:
    """异步显式识别应使用异步详情管线并保持统一身份。"""
    result = asyncio.run(
        imdb_module.async_recognize_media(
            media_source=MediaSource.IMDb,
            media_id="tt0903747",
        )
    )

    assert result and result.media_source == MediaSource.IMDb
    assert result.media_id == "tt0903747"
    assert result.backdrop_path == "https://image.example/backdrop.jpg"
    imdb_module.imdb_api.async_get_title.assert_awaited()
    imdb_module.imdb_api.async_list_credits.assert_awaited_once_with("tt0903747")


def test_imdb_scraping_only_handles_imdb_source(
    imdb_module: ImdbModule,
) -> None:
    """IMDb NFO 和图片刮削只响应显式 IMDb 刮削来源。"""
    media_info = MediaInfo()
    media_info.scrape_source = MediaSource.Douban.value
    assert imdb_module.metadata_nfo(media_info) is None
    assert imdb_module.metadata_img(media_info) is None

    media_info.scrape_source = MediaSource.IMDb.value
    imdb_module.scraper.get_metadata_nfo.return_value = "<movie />"
    imdb_module.scraper.get_metadata_img.return_value = {"poster.jpg": "url"}

    assert imdb_module.metadata_nfo(media_info) == "<movie />"
    assert imdb_module.metadata_img(media_info) == {"poster.jpg": "url"}
