from types import SimpleNamespace

from app.api.endpoints.transfer import (
    manual_transfer as manual_transfer_endpoint,
    query_manual_transfer_history,
)
from app.chain.transfer import TransferChain
from app.runtime.config import settings
from app.db.oper.transferhistory import TransferHistoryOper
from app.application.history import (
    clear_transfer_failures,
    failed_retry_count,
    max_failed_retries,
    record_transfer_failure,
)
from app.schemas import ManualTransferItem
from tests.test_transfer_sync_extra_files import (
    FakeMeta,
    make_fileitem,
    make_transfer_chain,
)


def _reset_failed_retries(src_path, storage=None):
    """清空失败重试计数，隔离用例之间共享的模块级计数缓存。"""
    clear_transfer_failures(src_path, storage)


def _patch_transfer_planning(monkeypatch, chain, fileitem, history, planned, deleted):
    """隔离整理规划依赖，并记录历史及旧目标清理动作。"""
    monkeypatch.setattr(
        chain,
        "_TransferChain__get_trans_fileitems",
        lambda current_fileitem, predicate: [(fileitem, False)],
    )
    monkeypatch.setattr(chain, "_TransferChain__put_to_jobview", lambda task: True)
    monkeypatch.setattr(
        chain,
        "_register_scrape_batch_task",
        lambda task: None,
    )
    monkeypatch.setattr(
        chain,
        "_close_scrape_batch",
        lambda batch_id: None,
    )

    def fake_handle_transfer(task, callback=None):
        """记录进入实际整理阶段的文件。"""
        planned.append(task.fileitem.path)
        return True, ""

    monkeypatch.setattr(chain, "_TransferChain__handle_transfer", fake_handle_transfer)

    history_oper = SimpleNamespace(
        get_by_src=lambda src, storage=None: history,
        get_success_by_src=lambda src, storage=None: history if history and history.status else None,
        get_by_dest=lambda dest, storage=None: None,
        delete=lambda history_id: deleted.append(("history", history_id)),
    )
    monkeypatch.setattr(
        "app.chain.transfer.TransferHistoryOper",
        lambda: history_oper,
    )
    monkeypatch.setattr("app.chain._transfer.TransferHistoryOper", lambda: history_oper)
    monkeypatch.setattr(
        "app.chain.transfer.DownloadHistoryOper",
        lambda: SimpleNamespace(
            get_by_hash=lambda download_hash: None,
            get_file_by_fullpath=lambda fullpath: None,
            get_files_by_savepath=lambda savepath: [],
            get_by_path=lambda path: None,
        ),
    )
    monkeypatch.setattr("app.chain._transfer.DownloadHistoryOper", lambda: SimpleNamespace(
            get_by_hash=lambda download_hash: None,
            get_file_by_fullpath=lambda fullpath: None,
            get_files_by_savepath=lambda savepath: [],
            get_by_path=lambda path: None,
        ))
    monkeypatch.setattr(
        "app.chain.transfer.get_configured_system_config",
        lambda: SimpleNamespace(get=lambda key: None),
    )
    monkeypatch.setattr("app.chain._transfer.get_configured_system_config", lambda: SimpleNamespace(get=lambda key: None))
    monkeypatch.setattr(
        "app.chain.transfer.StorageChain",
        lambda: SimpleNamespace(
            exists=lambda current_fileitem: True,
            delete_media_file=lambda current_fileitem: deleted.append(
                ("target", current_fileitem.path)
            )
            or True,
        ),
    )
    monkeypatch.setattr("app.chain._transfer.StorageChain", lambda: SimpleNamespace(
            exists=lambda current_fileitem: True,
            delete_media_file=lambda current_fileitem: deleted.append(
                ("target", current_fileitem.path)
            )
            or True,
        ))
    monkeypatch.setattr(
        "app.chain.transfer.MetaInfoPath",
        lambda path, custom_words=None, **kwargs: FakeMeta(1),
    )


def test_query_manual_transfer_history_returns_success_summary(monkeypatch):
    """手动整理初始化接口应只返回成功历史摘要供前端切换重整状态。"""
    fileitem = make_fileitem("/downloads/Test.Show.S01E01.mkv")
    captured = []

    class _FakeTransferChain:
        """记录历史查询收到的文件项。"""

        def get_manual_transfer_histories(self, fileitems):
            """返回两条成功历史记录。"""
            captured.extend(fileitems)
            return [SimpleNamespace(id=1), SimpleNamespace(id=2)]

    monkeypatch.setattr(
        "app.api.endpoints.transfer.TransferChain",
        _FakeTransferChain,
    )

    response = query_manual_transfer_history(
        transer_item=ManualTransferItem(fileitem=fileitem),
        history_query=SimpleNamespace(get=lambda _history_id: None),
        _="token",
    )

    assert response.success is True
    assert response.data == {"reorganize": True, "history_count": 2}
    assert captured == [fileitem]


def test_manual_transfer_endpoint_passes_reorganize_confirmation(monkeypatch):
    """手动整理接口应把前端重整确认传入整理链。"""
    fileitem = make_fileitem("/downloads/Test.Show.S01E01.mkv")
    captured = {}

    class _FakeTransferChain:
        """记录手动整理调用参数。"""

        def manual_transfer(self, **kwargs):
            """保存整理参数并返回成功。"""
            captured.update(kwargs)
            return True, ""

    monkeypatch.setattr(
        "app.api.endpoints.transfer.TransferChain",
        _FakeTransferChain,
    )

    response = manual_transfer_endpoint(
        transer_item=ManualTransferItem(
            fileitem=fileitem,
            reorganize=True,
        ),
        background=False,
        history_query=SimpleNamespace(get=lambda _history_id: None),
        _="token",
    )

    assert response.success is True
    assert captured["reorganize"] is True


def test_history_endpoint_reorganize_uses_chain_cleanup(monkeypatch):
    """历史 ID 重整应交由整理链清理历史，不能再传入旧目标重复删除。"""
    src_fileitem = make_fileitem("/downloads/Test.Show.S01E01.mkv")
    dest_fileitem = make_fileitem("/library/Test Show/Test.Show.S01E01.mkv")
    history = SimpleNamespace(
        id=21,
        status=True,
        mode="copy",
        src_fileitem=src_fileitem.model_dump(),
        dest_fileitem=dest_fileitem.model_dump(),
    )
    captured = {}

    class _FakeTransferChain:
        """记录历史 ID 重整调用参数。"""

        def manual_transfer(self, **kwargs):
            """保存整理参数并返回成功。"""
            captured.update(kwargs)
            return True, ""

    monkeypatch.setattr(
        "app.api.endpoints.transfer.TransferChain",
        _FakeTransferChain,
    )

    response = manual_transfer_endpoint(
        transer_item=ManualTransferItem(
            logid=history.id,
            reorganize=True,
        ),
        background=False,
        history_query=SimpleNamespace(get=lambda _history_id: history),
        _="token",
    )

    assert response.success is True
    assert captured["force"] is True
    assert captured["reorganize"] is True
    assert captured["cleanup_dest_fileitem"] is None


def test_success_history_directory_query_excludes_failed_and_siblings():
    """目录历史查询应限定路径边界，并且只返回成功记录。"""
    transfer_history_oper = TransferHistoryOper()
    created_histories = [
        transfer_history_oper.add_force(
            src="/issue-6191/show/episode-1.mkv",
            src_storage="local",
            status=True,
        ),
        transfer_history_oper.add_force(
            src="/issue-6191/show/episode-2.mkv",
            src_storage="local",
            status=False,
        ),
        transfer_history_oper.add_force(
            src="/issue-6191/show-other/episode-3.mkv",
            src_storage="local",
            status=True,
        ),
    ]
    try:
        histories = transfer_history_oper.list_success_by_src(
            "/issue-6191/show",
            storage="local",
            recursive=True,
        )

        assert [history.src for history in histories] == [
            "/issue-6191/show/episode-1.mkv"
        ]
    finally:
        for history in created_histories:
            transfer_history_oper.delete(history.id)


def test_success_history_directory_query_escapes_sql_wildcards():
    """目录名中的 SQL 通配符应按普通字符匹配，不能扩大查询范围。"""
    transfer_history_oper = TransferHistoryOper()
    created_histories = [
        transfer_history_oper.add_force(
            src="/issue-6191/show_100%/episode-1.mkv",
            src_storage="local",
            status=True,
        ),
        transfer_history_oper.add_force(
            src="/issue-6191/showA100B/episode-2.mkv",
            src_storage="local",
            status=True,
        ),
    ]
    try:
        histories = transfer_history_oper.list_success_by_src(
            "/issue-6191/show_100%",
            storage="local",
            recursive=True,
        )

        assert [history.src for history in histories] == [
            "/issue-6191/show_100%/episode-1.mkv"
        ]
    finally:
        for history in created_histories:
            transfer_history_oper.delete(history.id)


def test_successful_move_history_is_found_by_current_destination():
    """成功移动后从媒体库现址打开整理界面时，应识别原整理历史。"""
    transfer_history_oper = TransferHistoryOper()
    destination = make_fileitem("/library/Test Show/Test.Show.S01E01.mkv")
    history = transfer_history_oper.add_force(
        src="/downloads/Test.Show.S01E01.mkv",
        src_storage="local",
        dest=destination.path,
        dest_storage="local",
        dest_fileitem=destination.model_dump(),
        mode="move",
        status=True,
    )
    try:
        histories = TransferChain.get_manual_transfer_histories(
            make_transfer_chain(),
            [destination],
        )

        assert [current.id for current in histories] == [history.id]
    finally:
        transfer_history_oper.delete(history.id)


def test_manual_transfer_removes_failed_history_before_retry(monkeypatch):
    """失败历史不应阻塞手动整理，并应在重试前清理旧目标和记录。"""
    chain = make_transfer_chain()
    fileitem = make_fileitem("/downloads/Test.Show.S01E01.mkv")
    old_dest = make_fileitem("/library/Test Show/Test.Show.S01E01.mkv")
    history = SimpleNamespace(
        id=7,
        status=False,
        mode="copy",
        dest_fileitem=old_dest.model_dump(),
        download_hash=None,
        downloader=None,
        src=None,
        src_storage=None,
    )
    planned = []
    deleted = []
    _patch_transfer_planning(
        monkeypatch,
        chain,
        fileitem,
        history,
        planned,
        deleted,
    )

    state, message = TransferChain.do_transfer(
        chain,
        fileitem=fileitem,
        background=False,
        manual=True,
    )

    assert state is True
    assert message == ""
    assert deleted == [
        ("target", old_dest.path),
        ("history", history.id),
    ]
    assert planned == [fileitem.path]


def test_automatic_transfer_retries_failed_history_within_retry_budget(monkeypatch):
    """失败重试次数未达上限（默认计数为 0）时，自动整理遇到失败记录应放行重试，避免一次瞬时故障永久锁死文件。"""
    chain = make_transfer_chain()
    fileitem = make_fileitem("/downloads/Test.Show.S01E01.mkv")
    history = SimpleNamespace(
        id=12,
        status=False,
        mode="copy",
        dest_fileitem=make_fileitem(
            "/library/Test Show/Test.Show.S01E01.mkv"
        ).model_dump(),
        download_hash=None,
        downloader=None,
        src=fileitem.path,
        src_storage=fileitem.storage,
    )
    planned = []
    deleted = []
    _patch_transfer_planning(
        monkeypatch,
        chain,
        fileitem,
        history,
        planned,
        deleted,
    )

    _reset_failed_retries(fileitem.path, fileitem.storage)
    try:
        state, message = TransferChain.do_transfer(
            chain,
            fileitem=fileitem,
            background=False,
            manual=False,
        )

        assert state is True
        assert message == ""
        assert deleted == []
        assert planned == [fileitem.path]
    finally:
        _reset_failed_retries(fileitem.path, fileitem.storage)


def test_automatic_transfer_skips_failed_history_when_retry_budget_exhausted(monkeypatch):
    """失败重试次数已达上限时，自动整理遇到失败记录仍应被拦截、不进入整理链，避免失败任务自动循环。"""
    monkeypatch.setattr(settings, "TRANSFER_MAX_FAILED_RETRIES", 1)
    chain = make_transfer_chain()
    fileitem = make_fileitem("/downloads/Test.Show.S01E01.mkv")
    history = SimpleNamespace(
        id=13,
        status=False,
        mode="copy",
        dest_fileitem=make_fileitem(
            "/library/Test Show/Test.Show.S01E01.mkv"
        ).model_dump(),
        download_hash=None,
        downloader=None,
        src=fileitem.path,
        src_storage=fileitem.storage,
    )
    planned = []
    deleted = []
    _patch_transfer_planning(
        monkeypatch,
        chain,
        fileitem,
        history,
        planned,
        deleted,
    )

    _reset_failed_retries(fileitem.path, fileitem.storage)
    try:
        record_transfer_failure(fileitem.path, fileitem.storage)

        state, message = TransferChain.do_transfer(
            chain,
            fileitem=fileitem,
            background=False,
            manual=False,
        )

        assert state is False
        assert message == f"{fileitem.name} 已整理过"
        assert deleted == []
        assert planned == []
    finally:
        _reset_failed_retries(fileitem.path, fileitem.storage)


def test_automatic_transfer_new_version_bypasses_exhausted_retry_budget(monkeypatch):
    """失败预算耗尽后同路径新版本必须进入整理，且不能给下载任务补打已整理标签。"""
    monkeypatch.setattr(settings, "TRANSFER_MAX_FAILED_RETRIES", 1)
    chain = make_transfer_chain()
    fileitem = make_fileitem("/downloads/Test.Show.S01E01.mkv")
    fileitem.size = 200
    history = SimpleNamespace(
        id=15,
        status=False,
        mode="copy",
        dest_fileitem=None,
        download_hash="abc123",
        downloader="qbittorrent",
        src=fileitem.path,
        src_storage=fileitem.storage,
        src_fileitem={"size": 100},
    )
    planned = []
    deleted = []
    _patch_transfer_planning(
        monkeypatch,
        chain,
        fileitem,
        history,
        planned,
        deleted,
    )
    completed = []
    chain.transfer_completed = lambda download_hash, downloader: completed.append(
        (download_hash, downloader)
    )

    _reset_failed_retries(fileitem.path, fileitem.storage)
    try:
        record_transfer_failure(fileitem.path, fileitem.storage, file_size=100)

        state, message = TransferChain.do_transfer(
            chain,
            fileitem=fileitem,
            downloader="qbittorrent",
            download_hash="abc123",
            background=False,
            manual=False,
        )

        assert state is True
        assert message == ""
        assert deleted == []
        assert planned == [fileitem.path]
        assert completed == []
    finally:
        _reset_failed_retries(fileitem.path, fileitem.storage)


def test_manual_transfer_bypasses_retry_budget_when_exhausted(monkeypatch):
    """
    手动整理不受重试次数上限限制：manual=True 时查重闸根本不参与判定
    （do_transfer 中 `if transferd and not manual` 已把 manual 路径排除在外），
    即使失败计数已达上限（自动路径会因此拦截），仍应清理失败记录、重置计数并放行重试。
    """
    monkeypatch.setattr(settings, "TRANSFER_MAX_FAILED_RETRIES", 1)
    chain = make_transfer_chain()
    fileitem = make_fileitem("/downloads/Test.Show.S01E01.mkv")
    old_dest = make_fileitem("/library/Test Show/Test.Show.S01E01.mkv")
    history = SimpleNamespace(
        id=14,
        status=False,
        mode="copy",
        dest_fileitem=old_dest.model_dump(),
        download_hash=None,
        downloader=None,
        src=fileitem.path,
        src_storage=fileitem.storage,
    )
    planned = []
    deleted = []
    _patch_transfer_planning(
        monkeypatch,
        chain,
        fileitem,
        history,
        planned,
        deleted,
    )

    _reset_failed_retries(fileitem.path, fileitem.storage)
    try:
        record_transfer_failure(fileitem.path, fileitem.storage)
        assert failed_retry_count(fileitem.path, fileitem.storage) >= max_failed_retries()

        state, message = TransferChain.do_transfer(
            chain,
            fileitem=fileitem,
            background=False,
            manual=True,
        )

        assert state is True
        assert message == ""
        assert deleted == [
            ("target", old_dest.path),
            ("history", history.id),
        ]
        assert planned == [fileitem.path]
        assert failed_retry_count(fileitem.path, fileitem.storage) == 0
    finally:
        _reset_failed_retries(fileitem.path, fileitem.storage)


def test_manual_transfer_keeps_success_history_without_confirmation(monkeypatch):
    """未携带重整标志时，成功历史仍应阻止普通手动整理。"""
    chain = make_transfer_chain()
    fileitem = make_fileitem("/downloads/Test.Show.S01E01.mkv")
    history = SimpleNamespace(
        id=8,
        status=True,
        mode="copy",
        dest_fileitem=make_fileitem(
            "/library/Test Show/Test.Show.S01E01.mkv"
        ).model_dump(),
        download_hash=None,
        downloader=None,
    )
    planned = []
    deleted = []
    _patch_transfer_planning(
        monkeypatch,
        chain,
        fileitem,
        history,
        planned,
        deleted,
    )

    state, message = TransferChain.do_transfer(
        chain,
        fileitem=fileitem,
        background=False,
        manual=True,
    )

    assert state is True
    assert message == f"{fileitem.name} 已整理过"
    assert deleted == []
    assert planned == []


def test_manual_reorganize_removes_success_history_and_old_target(monkeypatch):
    """用户确认重新整理后，应清理成功历史及非移动模式旧目标再执行。"""
    chain = make_transfer_chain()
    fileitem = make_fileitem("/downloads/Test.Show.S01E01.mkv")
    old_dest = make_fileitem("/library/Wrong Show/Wrong.Show.S02E01.mkv")
    history = SimpleNamespace(
        id=9,
        status=True,
        mode="copy",
        dest_fileitem=old_dest.model_dump(),
        download_hash=None,
        downloader=None,
        src=None,
        src_storage=None,
    )
    planned = []
    deleted = []
    _patch_transfer_planning(
        monkeypatch,
        chain,
        fileitem,
        history,
        planned,
        deleted,
    )

    state, message = TransferChain.do_transfer(
        chain,
        fileitem=fileitem,
        background=False,
        manual=True,
        reorganize=True,
    )

    assert state is True
    assert message == ""
    assert deleted == [
        ("target", old_dest.path),
        ("history", history.id),
    ]
    assert planned == [fileitem.path]


def test_manual_reorganize_keeps_successful_move_target_as_source(monkeypatch):
    """成功移动后的目标是当前重整源，只能删历史记录，不能先删除文件。"""
    chain = make_transfer_chain()
    fileitem = make_fileitem("/library/Test Show/Test.Show.S01E01.mkv")
    history = SimpleNamespace(
        id=10,
        status=True,
        mode="move",
        dest_fileitem=fileitem.model_dump(),
        download_hash=None,
        downloader=None,
        src=None,
        src_storage=None,
    )
    planned = []
    deleted = []
    _patch_transfer_planning(
        monkeypatch,
        chain,
        fileitem,
        history,
        planned,
        deleted,
    )

    state, message = TransferChain.do_transfer(
        chain,
        fileitem=fileitem,
        background=False,
        manual=True,
        reorganize=True,
    )

    assert state is True
    assert message == ""
    assert deleted == [("history", history.id)]
    assert planned == [fileitem.path]


def test_forced_manual_reorganize_still_removes_history(monkeypatch):
    """从历史入口强制重新整理时，仍应按重整确认清理旧目标和记录。"""
    chain = make_transfer_chain()
    fileitem = make_fileitem("/downloads/Test.Show.S01E01.mkv")
    old_dest = make_fileitem("/library/Wrong Show/Wrong.Show.S02E01.mkv")
    history = SimpleNamespace(
        id=11,
        status=True,
        mode="copy",
        dest_fileitem=old_dest.model_dump(),
        download_hash=None,
        downloader=None,
        src=None,
        src_storage=None,
    )
    planned = []
    deleted = []
    _patch_transfer_planning(
        monkeypatch,
        chain,
        fileitem,
        history,
        planned,
        deleted,
    )

    state, message = TransferChain.do_transfer(
        chain,
        fileitem=fileitem,
        background=False,
        manual=True,
        force=True,
        reorganize=True,
    )

    assert state is True
    assert message == ""
    assert deleted == [
        ("target", old_dest.path),
        ("history", history.id),
    ]
    assert planned == [fileitem.path]
