from unittest.mock import AsyncMock, Mock

import pytest

from app.application.site.mutation import SiteMutationCommand


def _command(**overrides):
    """构造可观察站点写用例及其依赖。"""
    repository = Mock()
    repository.get_by_id = AsyncMock(return_value=object())
    repository.async_get_by_domain = AsyncMock(return_value=None)
    repository.stage_create = AsyncMock()
    repository.stage_update = AsyncMock(return_value=True)
    repository.stage_delete = AsyncMock()
    repository.stage_priorities = AsyncMock()
    unit_of_work = Mock()
    unit_of_work.commit = AsyncMock()
    unit_of_work.rollback = AsyncMock()
    dependencies = {
        "repository": repository,
        "unit_of_work": unit_of_work,
        "auth_level_provider": Mock(return_value=2),
        "indexer_loader": AsyncMock(return_value={"name": "Demo", "public": True}),
        "domain_extractor": lambda value: "demo.example",
        "url_normalizer": lambda value: "https://demo.example/",
        "publish_updated": AsyncMock(),
        "publish_deleted": AsyncMock(),
    }
    dependencies.update(overrides)
    return SiteMutationCommand(**dependencies), dependencies


@pytest.mark.asyncio
async def test_create_site_commits_before_updated_event():
    """新增站点必须先提交，再发布站点更新事件。"""
    calls = []
    command, dependencies = _command(
        unit_of_work=Mock(
            commit=AsyncMock(side_effect=lambda: calls.append("commit")),
            rollback=AsyncMock(),
        ),
        publish_updated=AsyncMock(side_effect=lambda _payload: calls.append("event")),
    )

    result = await command.create({"url": "https://demo.example/path"})

    assert result.success is True
    assert calls == ["commit", "event"]
    payload = dependencies["repository"].stage_create.await_args.args[0]
    assert payload["domain"] == "demo.example"
    assert payload["url"] == "https://demo.example/"
    assert payload["name"] == "Demo"
    assert payload["public"] == 1


@pytest.mark.asyncio
async def test_update_site_returns_legacy_not_found_without_writes():
    """更新不存在站点时保持失败响应且不产生事务或事件。"""
    repository = Mock()
    repository.get_by_id = AsyncMock(return_value=None)
    command, dependencies = _command(repository=repository)

    result = await command.update({"id": 7, "url": "https://demo.example"})

    assert result.success is False
    assert result.message == "站点不存在"
    dependencies["unit_of_work"].commit.assert_not_awaited()
    dependencies["publish_updated"].assert_not_awaited()


@pytest.mark.asyncio
async def test_delete_site_commit_failure_rolls_back_without_event():
    """删除提交失败时必须回滚且不得发送 SiteDeleted。"""
    unit_of_work = Mock()
    unit_of_work.commit = AsyncMock(side_effect=RuntimeError("commit failed"))
    unit_of_work.rollback = AsyncMock()
    command, dependencies = _command(unit_of_work=unit_of_work)

    with pytest.raises(RuntimeError, match="commit failed"):
        await command.delete(7)

    unit_of_work.rollback.assert_awaited_once_with()
    dependencies["publish_deleted"].assert_not_awaited()


@pytest.mark.asyncio
async def test_update_priorities_uses_one_transaction():
    """批量站点优先级必须由一个请求级事务统一提交。"""
    command, dependencies = _command()
    priorities = [{"id": 1, "pri": 2}, {"id": 2, "pri": 1}]

    result = await command.update_priorities(priorities)

    assert result.success is True
    dependencies["repository"].stage_priorities.assert_awaited_once_with(priorities)
    dependencies["unit_of_work"].commit.assert_awaited_once_with()
