import asyncio
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import MagicMock

import pytest

import app.agent.tools.impl.add_download_tasks as add_tasks_module
import app.agent.tools.impl.update_download_tasks as update_tasks_module
import app.chain.download as download_module
from app.agent.tools.impl.add_download_tasks import AddDownloadTasksTool
from app.agent.tools.impl.update_download_tasks import UpdateDownloadTasksTool
from app.chain.download import DownloadChain
from app.domain.context import Context, MediaInfo, SubtitleInfo, TorrentInfo
from app.domain.metainfo import MetaInfo
from app.application.directory import validate_download_save_path
from app.schemas import DownloaderTorrent, TransferDirectoryConf
from app.schemas.types import MediaSource, MediaType


@pytest.fixture(autouse=True)
def _mock_tmdb_supplement(monkeypatch):
    """隔离下载路径用例中的 TMDB 辅助识别外部边界。"""

    class _NoopMediaChain:
        """保持原媒体对象不变的 TMDB 辅助识别替身。"""

        @staticmethod
        def supplement_tmdb_info(media, _meta):
            """返回原媒体对象。"""
            return media

    monkeypatch.setattr(download_module, "MediaChain", _NoopMediaChain)


def _download_dirs():
    return [
        TransferDirectoryConf(
            name="本地下载",
            priority=1,
            storage="local",
            download_path="/downloads",
        ),
        TransferDirectoryConf(
            name="动漫远程下载",
            priority=2,
            storage="rclone",
            download_path="/media/anime",
        ),
    ]


def _windows_download_dirs():
    return [
        TransferDirectoryConf(
            name="Windows 下载",
            priority=1,
            storage="local",
            download_path="C:/downloads",
        ),
    ]


def _classified_download_dirs():
    return [
        TransferDirectoryConf(
            name="分类下载",
            priority=1,
            storage="local",
            download_path="/downloads",
            download_type_folder=True,
            download_category_folder=True,
        ),
        TransferDirectoryConf(
            name="远程分类下载",
            priority=2,
            storage="rclone",
            download_path="/media",
            download_type_folder=True,
            download_category_folder=True,
        ),
    ]


def _media_specific_download_dirs():
    return [
        TransferDirectoryConf(
            name="电影下载",
            priority=1,
            storage="local",
            download_path="/downloads",
            media_type=MediaType.MOVIE.value,
            download_category_folder=True,
        ),
        TransferDirectoryConf(
            name="电视剧下载",
            priority=2,
            storage="local",
            download_path="/downloads",
            media_type=MediaType.TV.value,
            download_category_folder=True,
        ),
    ]


def _nested_download_dirs():
    return [
        TransferDirectoryConf(
            name="A",
            priority=1,
            storage="local",
            download_path="/downloads",
            download_type_folder=True,
        ),
        TransferDirectoryConf(
            name="B",
            priority=2,
            storage="local",
            download_path="/downloads/tv",
            download_category_folder=True,
        ),
        TransferDirectoryConf(
            name="C",
            priority=3,
            storage="local",
            download_path="/downloads/tv/collection",
            download_type_folder=True,
            download_category_folder=True,
        ),
    ]


@pytest.fixture(autouse=True)
def patch_download_dirs(monkeypatch):
    monkeypatch.setattr(
        "app.application.directory.DirectoryHelper.get_download_dirs",
        lambda _self: _download_dirs(),
    )


@pytest.mark.parametrize(
    ("save_path", "expected"),
    [
        ("/downloads", "/downloads"),
        ("/downloads/movie/demo", "/downloads/movie/demo"),
        ("rclone:/media/anime/sub", "rclone:/media/anime/sub"),
    ],
)
def test_validate_download_save_path_accepts_configured_roots_and_children(save_path, expected):
    assert validate_download_save_path(save_path) == expected


def test_validate_download_save_path_accepts_legacy_remote_path_without_storage_prefix():
    """旧版订阅保存的远程原始路径应恢复为带存储前缀的 FileURI。"""
    assert validate_download_save_path("/media/anime/sub") == "rclone:/media/anime/sub"


def test_validate_download_save_path_prefers_configured_local_root(monkeypatch):
    """无前缀路径同时命中本地和远程根目录时应保持本地语义。"""
    monkeypatch.setattr(
        "app.application.directory.DirectoryHelper.get_download_dirs",
        lambda _self: [
            TransferDirectoryConf(
                name="远程下载",
                priority=1,
                storage="rclone",
                download_path="/shared",
            ),
            TransferDirectoryConf(
                name="本地下载",
                priority=2,
                storage="local",
                download_path="/shared",
            ),
        ],
    )

    assert validate_download_save_path("/shared/movie") == "/shared/movie"


@pytest.mark.parametrize(
    ("save_path", "expected"),
    [
        ("C:/downloads", "C:/downloads"),
        ("C:/downloads/movie", "C:/downloads/movie"),
    ],
)
def test_validate_download_save_path_accepts_windows_configured_root_and_children(
    monkeypatch,
    save_path,
    expected,
):
    monkeypatch.setattr(
        "app.application.directory.DirectoryHelper.get_download_dirs",
        lambda _self: _windows_download_dirs(),
    )

    assert validate_download_save_path(save_path) == expected


@pytest.mark.parametrize(
    "save_path",
    [
        "C:/other",
        "D:/downloads",
        "C:/downloads/../Windows",
        "C:\\downloads\\movie",
        "\\\\server\\share\\downloads",
    ],
)
def test_validate_download_save_path_rejects_windows_paths_outside_configured_root(
    monkeypatch,
    save_path,
):
    monkeypatch.setattr(
        "app.application.directory.DirectoryHelper.get_download_dirs",
        lambda _self: _windows_download_dirs(),
    )

    with pytest.raises(ValueError):
        validate_download_save_path(save_path)


@pytest.mark.parametrize(
    "save_path",
    [
        "/etc",
        "/downloads/../etc",
        "/downloads\\..\\etc",
        "C:/downloads",
        "\\\\server\\share\\downloads",
        "//server/share/downloads",
        "relative/downloads",
        "",
        "   ",
        "rclone:/media/movies",
        "smb:/media/anime/sub",
    ],
)
def test_validate_download_save_path_rejects_paths_outside_configured_roots(save_path):
    with pytest.raises(ValueError):
        validate_download_save_path(save_path)


def _build_tv_media() -> MediaInfo:
    return MediaInfo(
        type=MediaType.TV,
        title="Demo Show",
        year="2026",
        tmdb_id=2,
        genre_ids=[16],
        category="动漫",
    )


def test_resolve_media_download_dir_applies_configured_root_classification(monkeypatch):
    monkeypatch.setattr(
        "app.application.directory.DirectoryHelper.get_download_dirs",
        lambda _self: _classified_download_dirs(),
    )

    storage, target_dir, error_msg = DownloadChain._resolve_media_download_dir(
        media_info=_build_tv_media(),
        save_path="/downloads",
    )

    assert storage == "local"
    assert target_dir == Path("/downloads/电视剧/动漫")
    assert error_msg == ""


def test_resolve_media_download_dir_keeps_configured_child_path_exact(monkeypatch):
    monkeypatch.setattr(
        "app.application.directory.DirectoryHelper.get_download_dirs",
        lambda _self: _classified_download_dirs(),
    )

    storage, target_dir, error_msg = DownloadChain._resolve_media_download_dir(
        media_info=_build_tv_media(),
        save_path="/downloads/收藏区",
    )

    assert storage == "local"
    assert target_dir == Path("/downloads/收藏区")
    assert error_msg == ""


def test_resolve_media_download_dir_applies_remote_root_classification(monkeypatch):
    monkeypatch.setattr(
        "app.application.directory.DirectoryHelper.get_download_dirs",
        lambda _self: _classified_download_dirs(),
    )

    storage, target_dir, error_msg = DownloadChain._resolve_media_download_dir(
        media_info=_build_tv_media(),
        save_path="rclone:/media",
    )

    assert storage == "rclone"
    assert target_dir == Path("/media/电视剧/动漫")
    assert error_msg == ""


def test_resolve_media_download_dir_accepts_legacy_remote_root_without_storage_prefix(monkeypatch):
    """订阅中的旧版远程根路径应按对应存储和分类配置解析。"""
    monkeypatch.setattr(
        "app.application.directory.DirectoryHelper.get_download_dirs",
        lambda _self: _classified_download_dirs(),
    )

    storage, target_dir, error_msg = DownloadChain._resolve_media_download_dir(
        media_info=_build_tv_media(),
        save_path="/media",
    )

    assert storage == "rclone"
    assert target_dir == Path("/media/电视剧/动漫")
    assert error_msg == ""


def test_resolve_media_download_dir_uses_matching_media_specific_root(monkeypatch):
    monkeypatch.setattr(
        "app.application.directory.DirectoryHelper.get_download_dirs",
        lambda _self: _media_specific_download_dirs(),
    )

    storage, target_dir, error_msg = DownloadChain._resolve_media_download_dir(
        media_info=_build_tv_media(),
        save_path="/downloads",
    )

    assert storage == "local"
    assert target_dir == Path("/downloads/动漫")
    assert error_msg == ""


@pytest.mark.parametrize(
    ("save_path", "expected"),
    [
        ("/downloads", "/downloads/电视剧"),
        ("/downloads/tv", "/downloads/tv/动漫"),
        ("/downloads/tv/collection", "/downloads/tv/collection/电视剧/动漫"),
    ],
)
def test_resolve_media_download_dir_uses_exact_nested_root_configuration(
    monkeypatch,
    save_path,
    expected,
):
    monkeypatch.setattr(
        "app.application.directory.DirectoryHelper.get_download_dirs",
        lambda _self: _nested_download_dirs(),
    )

    storage, target_dir, error_msg = DownloadChain._resolve_media_download_dir(
        media_info=_build_tv_media(),
        save_path=save_path,
    )

    assert storage == "local"
    assert target_dir == Path(expected)
    assert error_msg == ""


def _build_context() -> Context:
    return Context(
        meta_info=MetaInfo("Demo Movie 2026"),
        media_info=MediaInfo(
            type=MediaType.MOVIE,
            title="Demo Movie",
            year="2026",
            tmdb_id=1,
            genre_ids=[18],
        ),
        torrent_info=TorrentInfo(
            title="Demo Movie 2026",
            enclosure="https://example.test/demo.torrent",
            site_cookie="uid=1",
            site_name="TestSite",
        ),
    )


def _build_download_chain() -> DownloadChain:
    chain = DownloadChain.__new__(DownloadChain)
    chain.download = MagicMock()
    chain.post_message = MagicMock()
    chain.messagehelper = MagicMock()
    return chain


def test_download_single_rejects_bad_save_path_before_downloader(monkeypatch):
    monkeypatch.setattr(download_module.eventmanager, "send_event", lambda *args, **kwargs: None)
    chain = _build_download_chain()

    download_id, error_msg = chain.download_single(
        context=_build_context(),
        torrent_content=b"torrent-content",
        save_path="/etc",
        return_detail=True,
    )

    assert download_id is None
    assert "保存路径" in error_msg
    chain.download.assert_not_called()


def test_download_single_rejects_event_overridden_bad_save_path_before_downloader(monkeypatch):
    event_data = SimpleNamespace(cancel=False, source="plugin", reason="", options={"save_path": "/etc"})
    monkeypatch.setattr(
        download_module.eventmanager,
        "send_event",
        lambda *args, **kwargs: SimpleNamespace(event_data=event_data),
    )
    chain = _build_download_chain()

    download_id, error_msg = chain.download_single(
        context=_build_context(),
        torrent_content=b"torrent-content",
        save_path="/downloads",
        return_detail=True,
    )

    assert download_id is None
    assert "保存路径" in error_msg
    chain.download.assert_not_called()


def test_download_single_applies_configured_root_classification(monkeypatch):
    monkeypatch.setattr(download_module.eventmanager, "send_event", lambda *args, **kwargs: None)
    monkeypatch.setattr(
        "app.application.directory.DirectoryHelper.get_download_dirs",
        lambda _self: _classified_download_dirs(),
    )
    chain = _build_download_chain()
    chain.download.return_value = ("qb", None, "Original", "test stop")
    context = _build_context()
    context.media_info = _build_tv_media()

    chain.download_single(
        context=context,
        torrent_content=b"torrent-content",
        save_path="/downloads",
    )

    assert chain.download.call_args.kwargs["download_dir"] == Path("/downloads/电视剧/动漫")


def test_download_single_accepts_legacy_remote_root_without_storage_prefix(monkeypatch):
    """旧订阅的无前缀远程根应以正确 FileURI 提交给下载模块。"""
    monkeypatch.setattr(download_module.eventmanager, "send_event", lambda *args, **kwargs: None)
    monkeypatch.setattr(
        "app.application.directory.DirectoryHelper.get_download_dirs",
        lambda _self: _classified_download_dirs(),
    )
    chain = _build_download_chain()
    chain.download.return_value = ("qb", None, "Original", "test stop")
    context = _build_context()
    context.media_info = _build_tv_media()

    chain.download_single(
        context=context,
        torrent_content=b"torrent-content",
        save_path="/media",
    )

    assert chain.download.call_args.kwargs["download_dir"] == Path("rclone:/media/电视剧/动漫")


@pytest.mark.parametrize("save_path", ["", "   "])
def test_download_single_rejects_explicit_empty_save_path_before_default_fallback(monkeypatch, save_path):
    monkeypatch.setattr(download_module.eventmanager, "send_event", lambda *args, **kwargs: None)
    monkeypatch.setattr(
        download_module.DirectoryHelper,
        "get_dir",
        lambda *_args, **_kwargs: TransferDirectoryConf(storage="local", download_path="/downloads"),
    )
    chain = _build_download_chain()

    download_id, error_msg = chain.download_single(
        context=_build_context(),
        torrent_content=b"torrent-content",
        save_path=save_path,
        return_detail=True,
    )

    assert download_id is None
    assert "保存路径" in error_msg
    chain.download.assert_not_called()


def test_download_single_rejects_event_empty_save_path_override_before_downloader(monkeypatch):
    event_data = SimpleNamespace(cancel=False, source="plugin", reason="", options={"save_path": ""})
    monkeypatch.setattr(
        download_module.eventmanager,
        "send_event",
        lambda *args, **kwargs: SimpleNamespace(event_data=event_data),
    )
    chain = _build_download_chain()

    download_id, error_msg = chain.download_single(
        context=_build_context(),
        torrent_content=b"torrent-content",
        save_path="/downloads",
        return_detail=True,
    )

    assert download_id is None
    assert "保存路径" in error_msg
    chain.download.assert_not_called()


def test_resolve_media_download_dir_rejects_bad_subtitle_save_path():
    media_info = MediaInfo(
        type=MediaType.MOVIE,
        title="Demo Movie",
        year="2026",
        tmdb_id=1,
    )

    storage, target_dir, error_msg = DownloadChain._resolve_media_download_dir(
        media_info=media_info,
        save_path="/etc",
    )

    assert storage is None
    assert target_dir is None
    assert error_msg == "保存路径不在允许的下载目录范围内"


def test_download_subtitle_returns_specific_error_for_bad_save_path(monkeypatch):
    chain = DownloadChain.__new__(DownloadChain)
    mediainfo = MediaInfo(
        type=MediaType.MOVIE,
        title="Demo Movie",
        year="2026",
        tmdb_id=1,
    )
    media_chain = MagicMock()
    media_chain.recognize_media.return_value = mediainfo
    media_chain.supplement_tmdb_info.return_value = mediainfo
    monkeypatch.setattr(download_module, "MediaChain", MagicMock(return_value=media_chain))
    subtitle = SubtitleInfo(
        title="Demo Movie",
        enclosure="https://example.test/subtitle.srt",
    )

    success, message, saved_files = chain.download_subtitle(
        subtitle=subtitle,
        media_source=MediaSource.TMDB,
        media_id="1",
        save_path="/etc",
    )

    assert not success
    assert message == "保存路径不在允许的下载目录范围内"
    assert saved_files == []


@pytest.mark.parametrize("save_path", ["", "   "])
def test_resolve_media_download_dir_rejects_explicit_empty_save_path_before_default_fallback(
    monkeypatch,
    save_path,
):
    monkeypatch.setattr(
        download_module.DirectoryHelper,
        "get_dir",
        lambda *_args, **_kwargs: TransferDirectoryConf(storage="local", download_path="/downloads"),
    )
    media_info = MediaInfo(
        type=MediaType.MOVIE,
        title="Demo Movie",
        year="2026",
        tmdb_id=1,
    )

    storage, target_dir, error_msg = DownloadChain._resolve_media_download_dir(
        media_info=media_info,
        save_path=save_path,
    )

    assert storage is None
    assert target_dir is None
    assert "保存路径" in error_msg


def test_add_download_tasks_direct_magnet_rejects_bad_save_path_before_downloader():
    with pytest.raises(ValueError):
        AddDownloadTasksTool._resolve_direct_download_dir("/etc")


@pytest.mark.parametrize("save_path", ["", "   "])
def test_add_download_tasks_direct_magnet_rejects_explicit_empty_save_path_before_default_fallback(save_path):
    with pytest.raises(ValueError):
        AddDownloadTasksTool._resolve_direct_download_dir(save_path)


def test_add_download_tasks_cached_context_rejects_bad_save_path_before_download_single(monkeypatch):
    download_chain = MagicMock()
    monkeypatch.setattr(add_tasks_module, "DownloadChain", lambda: download_chain)

    with pytest.raises(ValueError):
        AddDownloadTasksTool._download_single_sync(
            context=_build_context(),
            downloader="qb",
            save_path="/etc",
            merged_labels=None,
        )

    download_chain.download_single.assert_not_called()


@pytest.mark.parametrize("save_path", ["", "   "])
def test_add_download_tasks_cached_context_rejects_explicit_empty_save_path_before_download_single(
    monkeypatch,
    save_path,
):
    download_chain = MagicMock()
    monkeypatch.setattr(add_tasks_module, "DownloadChain", lambda: download_chain)

    with pytest.raises(ValueError):
        AddDownloadTasksTool._download_single_sync(
            context=_build_context(),
            downloader="qb",
            save_path=save_path,
            merged_labels=None,
        )

    download_chain.download_single.assert_not_called()


def test_update_download_tasks_rejects_bad_save_path_before_update_torrent(monkeypatch):
    hash_value = "a" * 40
    download_chain = MagicMock()
    download_chain.list_torrents.return_value = [
        DownloaderTorrent(downloader="qb", hash=hash_value, title="Demo")
    ]
    monkeypatch.setattr(update_tasks_module, "DownloadChain", lambda: download_chain)

    result = UpdateDownloadTasksTool._update_download_sync(
        hash_value=hash_value,
        save_path="/etc",
    )

    assert result["downloader"] == "qb"
    assert result["results"] == [
        {
            "operation": "save_path",
            "success": False,
            "message": "保存目录不在允许的下载目录范围内",
        }
    ]
    download_chain.update_torrent.assert_not_called()


def test_update_download_tasks_passes_normalized_save_path_to_update_torrent(monkeypatch):
    hash_value = "b" * 40
    download_chain = MagicMock()
    download_chain.list_torrents.return_value = [
        DownloaderTorrent(downloader="qb", hash=hash_value, title="Demo")
    ]
    download_chain.update_torrent.return_value = {"save_path": True}
    monkeypatch.setattr(update_tasks_module, "DownloadChain", lambda: download_chain)

    result = UpdateDownloadTasksTool._update_download_sync(
        hash_value=hash_value,
        save_path="rclone:/media/anime/sub",
    )

    assert result["results"][0]["success"] is True
    download_chain.update_torrent.assert_called_once_with(
        hash_string=hash_value,
        downloader="qb",
        download_limit=None,
        upload_limit=None,
        tracker_list=None,
        save_path="rclone:/media/anime/sub",
        category=None,
        ratio_limit=None,
        seeding_time_limit=None,
    )


def test_add_download_tasks_run_rejects_bad_save_path_before_partial_download(monkeypatch):
    tool = AddDownloadTasksTool(session_id="session-1", user_id="10001")
    download_chain = MagicMock()
    monkeypatch.setattr(add_tasks_module, "DownloadChain", lambda: download_chain)

    result = asyncio.run(
        tool.run(
            torrent_url=["magnet:?xt=urn:btih:123"],
            save_path="/etc",
        )
    )

    assert "save_path" in result
    download_chain.download.assert_not_called()


@pytest.mark.parametrize("save_path", ["", "   "])
def test_add_download_tasks_run_rejects_explicit_empty_save_path_before_partial_download(
    monkeypatch,
    save_path,
):
    tool = AddDownloadTasksTool(session_id="session-1", user_id="10001")
    download_chain = MagicMock()
    monkeypatch.setattr(add_tasks_module, "DownloadChain", lambda: download_chain)

    result = asyncio.run(
        tool.run(
            torrent_url=["magnet:?xt=urn:btih:123"],
            save_path=save_path,
        )
    )

    assert "save_path" in result
    download_chain.download.assert_not_called()
