"""订阅删除应用用例的事务、权限与副作用时序测试。"""

from unittest.mock import AsyncMock, MagicMock

import pytest
from sqlalchemy.orm import Session

from app.application.subscription.delete import (
    DeleteSubscribeCommand,
    SyncDeleteSubscribeCommand,
    SubscribeDeletionActor,
    SubscribeDeletionCandidate,
)
from app.db.models.subscribe import Subscribe
from app.db.oper.subscribe import SubscribeOper


class _Repository:
    """记录订阅删除用例数据访问顺序的仓储替身。"""

    def __init__(self, candidate, calls, delete_error=None):
        """保存候选订阅、共享调用序列和可选删除异常。"""
        self.candidate = candidate
        self.calls = calls
        self.delete_error = delete_error

    async def get_candidate(self, subscribe_id):
        """返回预设候选订阅。"""
        self.calls.append(("get", subscribe_id))
        return self.candidate

    async def stage_delete(self, subscribe_id):
        """记录待删除的订阅编号。"""
        self.calls.append(("delete", subscribe_id))
        if self.delete_error:
            raise self.delete_error


class _UnitOfWork:
    """可注入提交异常的事务替身。"""

    def __init__(self, calls, commit_error=None):
        """保存共享调用序列与可选提交异常。"""
        self.calls = calls
        self.commit_error = commit_error

    async def commit(self):
        """记录提交并按需抛出异常。"""
        self.calls.append(("commit",))
        if self.commit_error:
            raise self.commit_error

    async def rollback(self):
        """记录回滚。"""
        self.calls.append(("rollback",))


class _Outbox:
    """记录订阅删除 intent 暂存和收口顺序的 outbox 替身。"""

    def __init__(self, calls, stage_error=None):
        """保存共享调用序列与可选暂存异常。"""
        self.calls = calls
        self.stage_error = stage_error

    async def stage(self, intent, _now):
        """记录 intent，并按需模拟持久化失败。"""
        self.calls.append(("outbox_stage", intent))
        if self.stage_error:
            raise self.stage_error

    async def complete_by_event_key(self, event_key, _completed_at):
        """记录即时事件成功后的 intent 收口。"""
        self.calls.append(("outbox_complete", event_key))


class _SyncRepository:
    """记录同步订阅删除的数据访问顺序。"""

    def __init__(self, candidate, calls, delete_error=None):
        """保存候选订阅、共享调用序列和可选删除异常。"""
        self.candidate = candidate
        self.calls = calls
        self.delete_error = delete_error

    def get_candidate_sync(self, subscribe_id):
        """返回预设候选订阅。"""
        self.calls.append(("get", subscribe_id))
        return self.candidate

    def stage_delete_sync(self, subscribe_id):
        """记录同步待删除编号并按需失败。"""
        self.calls.append(("delete", subscribe_id))
        if self.delete_error:
            raise self.delete_error


class _SyncUnitOfWork:
    """记录同步删除命令的提交与回滚。"""

    def __init__(self, calls, commit_error=None):
        """保存共享调用序列与可选提交异常。"""
        self.calls = calls
        self.commit_error = commit_error

    def commit(self):
        """记录提交并按需抛出异常。"""
        self.calls.append(("commit",))
        if self.commit_error:
            raise self.commit_error

    def rollback(self):
        """记录回滚。"""
        self.calls.append(("rollback",))


class _SyncOutbox:
    """记录同步删除 intent 的暂存与完成顺序。"""

    def __init__(self, calls):
        """保存共享调用序列。"""
        self.calls = calls

    def stage(self, intent, _now):
        """记录同步暂存的 intent。"""
        self.calls.append(("outbox_stage", intent))

    def complete_by_event_key(self, event_key, _completed_at):
        """记录同步完成的 intent。"""
        self.calls.append(("outbox_complete", event_key))


def _candidate(username="alice"):
    """构造带完整事件身份字段的订阅删除候选。"""
    return SubscribeDeletionCandidate(
        subscribe_id=7,
        username=username,
        event_payload={
            "id": 7,
            "username": username,
            "media_source": "tmdb",
            "media_id": "123",
            "season": 2,
            "name": "测试订阅",
        },
    )


def _command(
    candidate,
    calls,
    commit_error=None,
    event_error=None,
    report_error=None,
    outbox=None,
    delete_error=None,
):
    """构造可观察事件与上报失败的订阅删除用例。"""
    async def publish(payload):
        """记录删除事件并按需失败。"""
        calls.append(("event", payload["subscribe_id"], payload))
        if event_error:
            raise event_error

    def report(payload):
        """记录删除统计并按需失败。"""
        calls.append(("report", payload))
        if report_error:
            raise report_error
        return True

    return DeleteSubscribeCommand(
        repository=_Repository(candidate, calls, delete_error),
        unit_of_work=_UnitOfWork(calls, commit_error),
        publish_deleted=publish,
        report_deleted=report,
        outbox=outbox,
    )


def _async_report_command(candidate, calls, result=True, error=None, outbox=None):
    """构造异步统计 reporter，验证命令可等待真实远端确认。"""
    async def publish(payload):
        """记录删除事件。"""
        calls.append(("event", payload["subscribe_id"], payload))

    async def report(payload):
        """记录异步统计并按需返回未确认或抛错。"""
        calls.append(("report", payload))
        if error:
            raise error
        return result

    return DeleteSubscribeCommand(
        repository=_Repository(candidate, calls),
        unit_of_work=_UnitOfWork(calls),
        publish_deleted=publish,
        report_deleted=report,
        outbox=outbox,
    )


def _sync_command(candidate, calls, commit_error=None, delete_error=None, outbox=None):
    """构造可观察事务和副作用顺序的同步订阅删除命令。"""
    def publish(payload):
        """记录同步删除事件。"""
        calls.append(("event", payload["subscribe_id"], payload))

    def report(payload):
        """记录同步删除统计。"""
        calls.append(("report", payload))
        return True

    return SyncDeleteSubscribeCommand(
        repository=_SyncRepository(candidate, calls, delete_error),
        unit_of_work=_SyncUnitOfWork(calls, commit_error),
        publish_deleted=publish,
        report_deleted=report,
        outbox=outbox,
    )


@pytest.mark.asyncio
async def test_owner_delete_commits_before_event_and_report():
    """owner 删除成功时必须先提交，再按原顺序发送事件和上报。"""
    calls = []
    command = _command(_candidate(), calls)

    deleted = await command.execute(
        7,
        SubscribeDeletionActor(username="alice", is_superuser=False),
    )

    assert deleted is True
    assert [call[0] for call in calls] == ["get", "delete", "commit", "event", "report"]
    assert calls[3][2]["subscribe_info"] == _candidate().event_payload
    assert calls[3][2]["idempotency_key"].startswith("subscribe.deleted:7:")
    assert calls[4][1] == _candidate().event_payload


@pytest.mark.asyncio
@pytest.mark.parametrize("candidate", [None, _candidate("bob"), _candidate(None)])
async def test_regular_user_cannot_delete_missing_other_or_legacy_subscribe(candidate):
    """普通用户对不存在、他人和 legacy 订阅保持无痕成功语义。"""
    calls = []
    command = _command(candidate, calls)

    deleted = await command.execute(
        7,
        SubscribeDeletionActor(username="alice", is_superuser=False),
    )

    assert deleted is False
    assert calls == [("get", 7)]


@pytest.mark.asyncio
async def test_superuser_can_delete_other_users_subscribe():
    """超级用户保留全局订阅删除权限。"""
    calls = []
    command = _command(_candidate("bob"), calls)

    deleted = await command.execute(
        7,
        SubscribeDeletionActor(username="admin", is_superuser=True),
    )

    assert deleted is True
    assert [call[0] for call in calls] == ["get", "delete", "commit", "event", "report"]


@pytest.mark.asyncio
async def test_commit_failure_rolls_back_without_event_or_report():
    """提交失败必须回滚，且不得发送成功事件或统计上报。"""
    calls = []
    command = _command(_candidate(), calls, commit_error=RuntimeError("commit failed"))

    with pytest.raises(RuntimeError, match="commit failed"):
        await command.execute(
            7,
            SubscribeDeletionActor(username="alice", is_superuser=False),
        )

    assert [call[0] for call in calls] == ["get", "delete", "commit", "rollback"]


@pytest.mark.asyncio
async def test_delete_stage_failure_rolls_back_without_effects():
    """异步暂存失败必须显式回滚，且不得写 intent 或发送成功副作用。"""
    calls = []
    command = _command(
        _candidate(),
        calls,
        delete_error=RuntimeError("delete failed"),
        outbox=_Outbox(calls),
    )

    with pytest.raises(RuntimeError, match="delete failed"):
        await command.execute(
            7,
            SubscribeDeletionActor(username="alice", is_superuser=False),
        )

    assert [call[0] for call in calls] == ["get", "delete", "rollback"]


@pytest.mark.asyncio
async def test_event_failure_happens_after_commit_and_stops_report():
    """事件失败保持原有传播语义，但事务必须已经提交且不得继续上报。"""
    calls = []
    command = _command(_candidate(), calls, event_error=RuntimeError("event failed"))

    with pytest.raises(RuntimeError, match="event failed"):
        await command.execute(
            7,
            SubscribeDeletionActor(username="alice", is_superuser=False),
        )

    assert [call[0] for call in calls] == ["get", "delete", "commit", "event"]


@pytest.mark.asyncio
async def test_report_failure_happens_after_commit_and_event():
    """上报失败保持原有传播语义，且不得改变已经提交和发出的事件。"""
    calls = []
    command = _command(_candidate(), calls, report_error=RuntimeError("report failed"))

    with pytest.raises(RuntimeError, match="report failed"):
        await command.execute(
            7,
            SubscribeDeletionActor(username="alice", is_superuser=False),
        )

    assert [call[0] for call in calls] == ["get", "delete", "commit", "event", "report"]


@pytest.mark.asyncio
async def test_delete_stages_outbox_before_commit_and_completes_after_event():
    """订阅删除、intent 与即时事件必须按原子提交和成功收口顺序执行。"""
    calls = []
    command = _command(_candidate(), calls, outbox=_Outbox(calls))

    assert await command.execute(
        7,
        SubscribeDeletionActor(username="alice", is_superuser=False),
    ) is True

    assert [call[0] for call in calls] == [
        "get",
        "delete",
        "outbox_stage",
        "outbox_stage",
        "commit",
        "event",
        "outbox_complete",
        "report",
        "outbox_complete",
    ]
    intent = calls[2][1]
    assert intent.topic == "subscribe.deleted"
    report_intent = calls[3][1]
    assert report_intent.topic == "subscribe.deleted.report"
    assert intent.event_key == calls[5][2]["idempotency_key"]
    assert calls[6][1] == intent.event_key
    assert calls[8][1] == report_intent.event_key


@pytest.mark.asyncio
async def test_delete_outbox_stage_failure_rolls_back_business_delete():
    """订阅删除 intent 无法暂存时不得提交业务删除。"""
    calls = []
    command = _command(
        _candidate(),
        calls,
        outbox=_Outbox(calls, stage_error=RuntimeError("outbox failed")),
    )

    with pytest.raises(RuntimeError, match="outbox failed"):
        await command.execute(
            7,
            SubscribeDeletionActor(username="alice", is_superuser=False),
        )

    assert [call[0] for call in calls] == [
        "get",
        "delete",
        "outbox_stage",
        "rollback",
    ]


@pytest.mark.asyncio
async def test_async_reporter_completes_report_intent_only_after_confirmation():
    """异步 reporter 确认成功后才允许收口统计 intent。"""
    calls = []
    command = _async_report_command(_candidate(), calls, outbox=_Outbox(calls))

    assert await command.execute(
        7,
        SubscribeDeletionActor(username="alice", is_superuser=False),
    ) is True

    assert [call[0] for call in calls] == [
        "get", "delete", "outbox_stage", "outbox_stage", "commit",
        "event", "outbox_complete", "report", "outbox_complete",
    ]


@pytest.mark.asyncio
async def test_async_reporter_false_keeps_report_intent_pending():
    """异步 reporter 未确认时必须保留待重试统计 intent。"""
    calls = []
    command = _async_report_command(_candidate(), calls, result=False, outbox=_Outbox(calls))

    with pytest.raises(RuntimeError, match="未确认"):
        await command.execute(
            7,
            SubscribeDeletionActor(username="alice", is_superuser=False),
        )

    assert [call[0] for call in calls] == [
        "get", "delete", "outbox_stage", "outbox_stage", "commit",
        "event", "outbox_complete", "report",
    ]


@pytest.mark.asyncio
async def test_async_reporter_error_keeps_report_intent_pending():
    """异步 reporter 异常时必须保留待重试统计 intent。"""
    calls = []
    command = _async_report_command(
        _candidate(),
        calls,
        error=RuntimeError("remote failed"),
        outbox=_Outbox(calls),
    )

    with pytest.raises(RuntimeError, match="remote failed"):
        await command.execute(
            7,
            SubscribeDeletionActor(username="alice", is_superuser=False),
        )

    assert [call[0] for call in calls] == [
        "get", "delete", "outbox_stage", "outbox_stage", "commit",
        "event", "outbox_complete", "report",
    ]


@pytest.mark.asyncio
async def test_repository_candidate_uses_loaded_orm_snapshot(monkeypatch):
    """DB 适配器只向应用层暴露权限字段和完整列快照。"""
    subscribe = Subscribe(
        id=7,
        username="alice",
        name="测试订阅",
        media_source="tmdb",
        media_id="123",
        season=2,
    )

    async def async_get(_self, subscribe_id):
        """返回无需真实数据库的订阅模型。"""
        assert subscribe_id == 7
        return subscribe

    monkeypatch.setattr(SubscribeOper, "async_get", async_get)

    candidate = await SubscribeOper(object()).get_candidate(7)

    assert candidate is not None
    assert candidate.subscribe_id == 7
    assert candidate.username == "alice"
    assert candidate.event_payload["id"] == 7
    assert candidate.event_payload["media_source"] == "tmdb"
    assert candidate.event_payload["media_id"] == "123"


@pytest.mark.asyncio
async def test_repository_stage_delete_does_not_commit():
    """真实仓储只登记删除，提交必须由请求级 UnitOfWork 执行。"""
    session = type("SessionStub", (), {})()
    session.execute = AsyncMock()
    session.commit = AsyncMock()

    await SubscribeOper(session).stage_delete(7)

    session.execute.assert_awaited_once()
    session.commit.assert_not_awaited()


def test_sync_delete_uses_same_durable_effect_order():
    """同步消息入口必须复用异步删除命令的事务、事件和统计顺序。"""
    calls = []
    command = _sync_command(_candidate(), calls, outbox=_SyncOutbox(calls))

    assert command.execute(
        7,
        SubscribeDeletionActor(username="", is_superuser=True),
    ) is True

    assert [call[0] for call in calls] == [
        "get", "delete", "outbox_stage", "outbox_stage", "commit",
        "event", "outbox_complete", "report", "outbox_complete",
    ]
    assert calls[2][1].topic == "subscribe.deleted"
    assert calls[3][1].topic == "subscribe.deleted.report"
    assert calls[7][1] == _candidate().event_payload


@pytest.mark.parametrize("failure", ["delete", "commit"])
def test_sync_delete_rolls_back_transaction_failures(failure):
    """同步暂存或提交失败时必须回滚，且不得发送删除成功副作用。"""
    calls = []
    error = RuntimeError(f"{failure} failed")
    command = _sync_command(
        _candidate(),
        calls,
        delete_error=error if failure == "delete" else None,
        commit_error=error if failure == "commit" else None,
    )

    with pytest.raises(RuntimeError, match=f"{failure} failed"):
        command.execute(
            7,
            SubscribeDeletionActor(username="", is_superuser=True),
        )

    assert calls[-1] == ("rollback",)
    assert all(call[0] not in {"event", "report"} for call in calls)


def test_sync_repository_candidate_and_delete_share_caller_session(monkeypatch):
    """同步仓储投影和删除都使用组合根传入的同一个 Session 且不提交。"""
    subscribe = Subscribe(
        id=7,
        username="alice",
        name="测试订阅",
        media_source="tmdb",
        media_id="123",
        season=2,
    )
    session = MagicMock(spec=Session)
    oper = SubscribeOper(session)
    monkeypatch.setattr(oper, "get", lambda subscribe_id: subscribe)

    candidate = oper.get_candidate_sync(7)
    oper.stage_delete_sync(7)

    assert candidate is not None
    assert candidate.event_payload["media_id"] == "123"
