"""
覆盖「不覆盖裁决不应降级已有成功记录」的行为。

查重闸放行同路径新版本后，若 overwrite_mode 最终裁定不覆盖，媒体库中原有的
成功版本仍然在位——这是一次正常策略裁决而非整理故障。TransferChain 内部的
__is_overwrite_declined 用于识别这一场景，__default_callback 失败分支据此
决定是否写失败历史、发送失败事件与失败通知。本文件覆盖两者。
"""
from types import SimpleNamespace
from unittest.mock import MagicMock, patch

from app.chain.transfer import TransferChain
from app.schemas.transfer import TransferInfo
from app.schemas.types import EventType
from tests.test_transfer_job_manager import FakeMedia, make_task, make_transfer_chain


def make_history_oper(history=None, success_history=None, raise_on_query: bool = False):
    """构造 __is_overwrite_declined 查询整理历史使用的替身。"""

    def get_by_src(src, storage=None):
        if raise_on_query:
            raise RuntimeError("boom")
        return history

    def get_success_by_src(src, storage=None):
        return success_history

    return SimpleNamespace(
        get_by_src=get_by_src,
        get_success_by_src=get_success_by_src,
    )


def make_fail_recorder(calls):
    """替换整理链的失败历史写入函数：只记录调用，不做字段翻译也不落库。"""

    def add_transfer_fail(**kwargs):
        calls.append(kwargs)
        return SimpleNamespace(id=1)

    return add_transfer_fail


# ---------------------------------------------------------------------------
# TransferChain.__is_overwrite_declined
# ---------------------------------------------------------------------------


def test_overwrite_declined_false_when_flag_not_set():
    """overwrite_skipped 为假时直接判定为 False，且不应触发历史查询。"""
    task = make_task(1)
    transferinfo = TransferInfo(success=False, overwrite_skipped=False)
    transferhis = make_history_oper(raise_on_query=True)

    result = TransferChain._is_overwrite_declined(
        task, transferinfo, transferhis
    )

    assert result is False


def test_overwrite_declined_true_when_success_history_exists():
    """overwrite_skipped 为真且同源已有成功记录时，应判定为保护场景。"""
    task = make_task(1)
    success_history = SimpleNamespace(id=1, status=True)
    transferinfo = TransferInfo(success=False, overwrite_skipped=True)
    transferhis = make_history_oper(history=success_history)

    result = TransferChain._is_overwrite_declined(
        task, transferinfo, transferhis
    )

    assert result is True


def test_overwrite_declined_false_when_no_history():
    """overwrite_skipped 为真但没有任何整理记录时，不应判定为保护场景。"""
    task = make_task(1)
    transferinfo = TransferInfo(success=False, overwrite_skipped=True)
    transferhis = make_history_oper(history=None)

    result = TransferChain._is_overwrite_declined(
        task, transferinfo, transferhis
    )

    assert result is False


def test_overwrite_declined_false_when_only_failed_history():
    """overwrite_skipped 为真但只有失败记录时，不应判定为保护场景。"""
    task = make_task(1)
    failed_history = SimpleNamespace(id=2, status=False)
    transferinfo = TransferInfo(success=False, overwrite_skipped=True)
    transferhis = make_history_oper(history=failed_history, success_history=None)

    result = TransferChain._is_overwrite_declined(
        task, transferinfo, transferhis
    )

    assert result is False


def test_overwrite_declined_false_when_query_raises():
    """查询整理历史异常时应保守返回 False，不阻断原有失败语义。"""
    task = make_task(1)
    transferinfo = TransferInfo(success=False, overwrite_skipped=True)
    transferhis = make_history_oper(raise_on_query=True)

    result = TransferChain._is_overwrite_declined(
        task, transferinfo, transferhis
    )

    assert result is False


# ---------------------------------------------------------------------------
# __default_callback 失败分支
# ---------------------------------------------------------------------------


def _make_failed_task():
    """构造一个失败回调测试所需的最小整理任务。"""
    task = make_task(1)
    task.mediainfo = FakeMedia()
    # __default_callback 失败通知路径需要读取海报图，FakeMedia 本身不提供该接口
    task.mediainfo.get_message_image = lambda: "poster.jpg"
    task.background = False
    task.manual = True
    return task


def test_default_callback_skips_history_and_notification_when_overwrite_declined():
    """
    同源已有成功记录时，覆盖裁决不覆盖不应写失败历史、不应发送失败事件与通知。
    """
    chain = make_transfer_chain()
    chain.eventmanager = MagicMock()
    chain.post_message = MagicMock()

    task = _make_failed_task()
    success_history = SimpleNamespace(id=99, status=True)
    add_fail_calls = []
    transfer_history_oper = make_history_oper(history=success_history)

    transferinfo = TransferInfo(
        success=False,
        fileitem=task.fileitem,
        message="目标已存在，按覆盖策略跳过覆盖",
        transfer_type="copy",
        overwrite_skipped=True,
        need_notify=False,
    )

    with patch(
        "app.chain.transfer.TransferHistoryOper",
        return_value=transfer_history_oper,
    ), patch(
        "app.chain.transfer.add_transfer_fail",
        make_fail_recorder(add_fail_calls),
    ), patch(
        "app.runtime.config.settings.AI_AGENT_ENABLE", False
    ), patch(
        "app.runtime.config.settings.AI_AGENT_RETRY_TRANSFER", False
    ):
        state, errmsg = chain._TransferChain__default_callback(task, transferinfo)

    assert state is False
    assert errmsg == transferinfo.message
    assert add_fail_calls == []
    assert chain.post_message.call_count == 0
    transfer_failed_events = [
        call
        for call in chain.eventmanager.send_event.call_args_list
        if call.args[0] == EventType.TransferFailed
    ]
    assert transfer_failed_events == []


def test_default_callback_keeps_original_failure_semantics_without_success_history():
    """
    没有已有成功记录时（即使 overwrite_skipped 为真），仍应按原有语义写失败历史并通知。
    """
    chain = make_transfer_chain()
    chain.eventmanager = MagicMock()
    chain.post_message = MagicMock()

    task = _make_failed_task()
    add_fail_calls = []
    transfer_history_oper = make_history_oper(history=None)

    transferinfo = TransferInfo(
        success=False,
        fileitem=task.fileitem,
        message="目标已存在，按覆盖策略跳过覆盖",
        transfer_type="copy",
        overwrite_skipped=True,
        need_notify=False,
    )

    with patch(
        "app.chain.transfer.TransferHistoryOper",
        return_value=transfer_history_oper,
    ), patch(
        "app.chain.transfer.add_transfer_fail",
        make_fail_recorder(add_fail_calls),
    ), patch(
        "app.runtime.config.settings.AI_AGENT_ENABLE", False
    ), patch(
        "app.runtime.config.settings.AI_AGENT_RETRY_TRANSFER", False
    ):
        state, errmsg = chain._TransferChain__default_callback(task, transferinfo)

    assert state is False
    assert errmsg == transferinfo.message
    assert len(add_fail_calls) == 1
    assert chain.post_message.call_count == 1
    transfer_failed_events = [
        call
        for call in chain.eventmanager.send_event.call_args_list
        if call.args[0] == EventType.TransferFailed
    ]
    assert len(transfer_failed_events) == 1


def test_default_callback_delegates_primary_failure_to_durable_writer():
    """正式上下文存在 writer 时，主要媒体失败历史和事件必须走同一事务端口。"""
    chain = make_transfer_chain()
    chain.eventmanager = MagicMock()
    chain.post_message = MagicMock()
    chain.durable_event_writer = MagicMock()
    task = _make_failed_task()
    add_fail_calls = []
    transfer_history_oper = make_history_oper(history=None)
    transferinfo = TransferInfo(
        success=False,
        fileitem=task.fileitem,
        message="copy failed",
        transfer_type="copy",
        need_notify=False,
    )

    def durable_transfer_result(**kwargs):
        """执行 writer 收到的历史暂存与提交后发布回调。"""
        history = kwargs["stage_history"](SimpleNamespace())
        payload = dict(kwargs["event_payload"])
        payload["transfer_history_id"] = history.id
        payload["idempotency_key"] = f"transfer.failed:{history.id}:v1"
        kwargs["publish"](payload)
        return history

    chain.durable_event_writer.transfer_result.side_effect = durable_transfer_result
    with patch(
        "app.chain.transfer.TransferHistoryOper",
        return_value=transfer_history_oper,
    ), patch(
        "app.chain.transfer.add_transfer_fail",
        make_fail_recorder(add_fail_calls),
    ), patch(
        "app.runtime.config.settings.AI_AGENT_ENABLE",
        False,
    ):
        state, errmsg = chain._TransferChain__default_callback(task, transferinfo)

    assert state is False
    assert errmsg == "copy failed"
    assert len(add_fail_calls) == 1
    chain.durable_event_writer.transfer_result.assert_called_once()
    topic = chain.durable_event_writer.transfer_result.call_args.kwargs["topic"]
    assert topic == "transfer.failed"
    event_type, event_payload = chain.eventmanager.send_event.call_args.args
    assert event_type == EventType.TransferFailed
    assert event_payload["idempotency_key"] == "transfer.failed:1:v1"
