from types import SimpleNamespace

from app.schemas import ActionContext, DownloadTask, FileItem
from app.schemas.workflow import ActionResult
from app.workflow.actions import BaseAction
from app.workflow.actions import fetch_downloads as fetch_downloads_module
from app.workflow.actions import fetch_torrents as fetch_torrents_module
from app.workflow.actions import scrape_file as scrape_file_module
from app.workflow.actions.fetch_downloads import FetchDownloadsAction
from app.workflow.actions.fetch_torrents import FetchTorrentsAction
from app.workflow.actions.scrape_file import ScrapeFileAction
from app.workflow.actions.fetch_rss import FetchRssAction
from app.workflow import WorkFlowManager


def test_fetch_downloads_updates_context_downloads(monkeypatch):
    """获取下载任务动作应更新上游上下文中的下载任务。"""
    calls = []

    class FakeActionChain:
        """模拟下载器查询链。"""

        def list_torrents(self, hashs=None, downloader=None, **kwargs):
            calls.append((hashs, downloader))
            return [SimpleNamespace(path="/downloads/movie.mkv", progress=100)]

    monkeypatch.setattr(fetch_downloads_module, "ActionChain", FakeActionChain)
    monkeypatch.setattr(fetch_downloads_module.global_vars, "is_workflow_stopped", lambda workflow_id: False)

    context = ActionContext(
        downloads=[
            DownloadTask(download_id="hash-1", downloader="qbittorrent"),
        ]
    )

    result = FetchDownloadsAction("fetch-downloads").execute(
        workflow_id=1,
        params={},
        context=context,
    )

    assert calls == [(["hash-1"], "qbittorrent")]
    assert result.downloads[0].completed is True
    assert result.downloads[0].path == "/downloads/movie.mkv"


def test_fetch_torrents_filters_special_season_zero(monkeypatch):
    """工作流显式选择季 0 时只能保留特别季资源。"""

    class FakeSearchChain:
        """返回特别季和第一季候选，验证动作层季过滤。"""

        def search_by_title(self, **_kwargs):
            return [
                SimpleNamespace(
                    meta_info=SimpleNamespace(year=None, begin_season=0),
                    media_info=None,
                    torrent_info=SimpleNamespace(title="Test S00"),
                ),
                SimpleNamespace(
                    meta_info=SimpleNamespace(year=None, begin_season=1),
                    media_info=None,
                    torrent_info=SimpleNamespace(title="Test S01"),
                ),
            ]

    monkeypatch.setattr(fetch_torrents_module, "SearchChain", FakeSearchChain)
    monkeypatch.setattr(fetch_torrents_module.global_vars, "is_workflow_stopped", lambda _workflow_id: False)

    action = FetchTorrentsAction("fetch-torrents")
    action.job_done = lambda *_args, **_kwargs: None
    result = action.execute(
        workflow_id=1,
        params={"search_type": "keyword", "name": "Test", "season": 0},
        context=ActionContext(),
    )

    assert [item.meta_info.begin_season for item in result.torrents] == [0]


def test_scrape_file_keeps_workflow_action_context(monkeypatch):
    """刮削文件动作不应将工作流上下文替换为媒体识别上下文。"""
    scraped = []

    class FakeStorageChain:
        """模拟存储链。"""

        def exists(self, fileitem):
            return True

    class FakeMediaChain:
        """模拟媒体识别链。"""

        def recognize_by_path(self, path, obtain_images=False):
            return SimpleNamespace(meta_info="meta", media_info="media")

    class FakeScrapingChain:
        """模拟独立刮削链。"""

        def scrape_metadata(self, fileitem, meta=None, mediainfo=None):
            scraped.append((fileitem.path, meta, mediainfo))

    monkeypatch.setattr(scrape_file_module, "StorageChain", FakeStorageChain)
    monkeypatch.setattr(scrape_file_module, "MediaChain", FakeMediaChain)
    monkeypatch.setattr(scrape_file_module, "ScrapingChain", FakeScrapingChain)
    monkeypatch.setattr(scrape_file_module.global_vars, "is_workflow_stopped", lambda workflow_id: False)
    monkeypatch.setattr(ScrapeFileAction, "check_cache", lambda self, workflow_id, key: False)
    monkeypatch.setattr(ScrapeFileAction, "save_cache", lambda self, workflow_id, data: None)

    context = ActionContext(
        fileitems=[
            FileItem(path="/library/movie.mkv", storage="local", type="file"),
        ]
    )

    result = ScrapeFileAction("scrape-file").execute(
        workflow_id=1,
        params={},
        context=context,
    )

    assert result is context
    assert result.fileitems[0].path == "/library/movie.mkv"
    assert scraped == [("/library/movie.mkv", "meta", "media")]


def test_scrape_file_does_not_cache_failed_music_scrape(monkeypatch):
    """音乐刮削失败时工作流必须计为失败，不能写入成功缓存。"""
    saved_cache = []

    class FakeStorageChain:
        """模拟存在的音乐文件。"""

        def exists(self, fileitem):
            return True

    class FakeMediaChain:
        """模拟媒体识别成功。"""

        def recognize_by_path(self, path, obtain_images=False):
            return SimpleNamespace(meta_info="music-meta", media_info="music")

    class FakeScrapingChain:
        """模拟音乐产物写入失败。"""

        def scrape_metadata(self, fileitem, meta=None, mediainfo=None):
            return False, "歌词保存失败"

    monkeypatch.setattr(scrape_file_module, "StorageChain", FakeStorageChain)
    monkeypatch.setattr(scrape_file_module, "MediaChain", FakeMediaChain)
    monkeypatch.setattr(scrape_file_module, "ScrapingChain", FakeScrapingChain)
    monkeypatch.setattr(
        scrape_file_module.global_vars,
        "is_workflow_stopped",
        lambda workflow_id: False,
    )
    monkeypatch.setattr(
        ScrapeFileAction,
        "check_cache",
        lambda self, workflow_id, key: False,
    )
    monkeypatch.setattr(
        ScrapeFileAction,
        "save_cache",
        lambda self, workflow_id, data: saved_cache.append(data),
    )

    action = ScrapeFileAction("scrape-music")
    action.execute(
        workflow_id=1,
        params={},
        context=ActionContext(
            fileitems=[
                FileItem(path="/library/晴天.flac", storage="local", type="file")
            ]
        ),
    )

    assert action.success is False
    assert action._scraped_files == []
    assert saved_cache == []


def test_execute_with_inputs_maps_contract_inputs_outputs_and_runtime(monkeypatch):
    """新版动作桥接方法应按契约映射输入、输出和运行期信息。"""

    class ContractAction(BaseAction):
        """测试动作契约桥接。"""

        contract = {
            "inputs": [{"name": "torrents", "label": "资源", "kind": "list"}],
            "outputs": [{"name": "downloads", "label": "下载任务", "kind": "list"}],
        }

        name = "契约动作"
        description = "测试契约动作"
        data = {}

        @property
        def success(self) -> bool:
            return True

        def execute(self, workflow_id: int, params: dict, context: ActionContext) -> ActionContext:
            """执行测试动作。"""
            _ = workflow_id, params
            context.downloads = [
                DownloadTask(download_id=f"{item}-hash", downloader="qbittorrent")
                for item in context.torrents
            ]
            self.job_done("完成")
            return context

    result = ContractAction("contract").execute_with_inputs(
        workflow_id=1,
        params={},
        inputs={"torrents": ["movie"]},
        runtime={"attempt": 1, "max_attempts": 1, "cancel_token": object()},
        context=ActionContext(),
    )

    assert isinstance(result, ActionResult)
    assert result.outputs["downloads"][0].download_id == "movie-hash"
    assert result.context.runtime_state["current_action_runtime"] == {
        "attempt": 1,
        "max_attempts": 1,
    }

    path_result = ContractAction("contract").execute_with_inputs(
        workflow_id=1,
        params={},
        inputs={"outputs.FetchRssAction.torrents": ["legacy"]},
        runtime={},
        context=ActionContext(),
    )

    assert path_result.outputs["downloads"][0].download_id == "legacy-hash"


def test_workflow_manager_list_actions_exposes_contract():
    """动作列表应返回固定输入输出契约。"""
    manager = object.__new__(WorkFlowManager)
    manager._actions = {"FetchRssAction": FetchRssAction}

    actions = manager.list_actions()

    assert actions[0]["name"] == "获取RSS资源"
    assert actions[0]["description"] == "订阅RSS地址获取资源"
    assert isinstance(actions[0]["data"], dict)
    assert actions[0]["contract"]["outputs"][0]["name"] == "torrents"
    assert actions[0]["contract"]["condition_fields"][0]["label"] == "资源"
