"""预览生成（Phase 1）单元测试.

覆盖：
- 领域模型 is_preview 字段
- CreateGenerationTaskCommand is_preview 字段
- UseCase 传递 is_preview
- Schema 校验（CreatePreviewGenerationTaskRequest / PreviewGenerationTaskResponse）
- 仓储层 _to_domain 兼容旧数据（getattr + 默认值）
- 仓储层 create/update 保留 is_preview 字段
- 状态流转验证
"""

from __future__ import annotations

import os
import sys
from datetime import datetime, timezone
from pathlib import Path
from unittest.mock import MagicMock

import pytest

# 设置必要环境变量（必须在导入 app 模块之前）
os.environ.setdefault("JWT_SECRET_KEY", "unit-test-secret-key-for-testing")
os.environ.setdefault("DATABASE_URL", "sqlite:///test.db")

# 确保 app 模块可导入
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "apps" / "api"))

from packages.application.generation_tasks import (
    CreateGenerationTaskCommand,
    CreateGenerationTaskUseCase,
)
from packages.domain.generation_task import GenerationTask, GenerationTaskStatus

# ── 领域模型测试 ──────────────────────────────────────────────────────────────


class TestGenerationTaskIsPreviewField:
    """GenerationTask 领域模型 is_preview 字段测试"""

    def test_is_preview_default_false(self):
        """默认 is_preview=False"""
        task = GenerationTask.create(
            project_id="proj1",
            asset_library_id="lib1",
        )
        assert task.is_preview is False

    def test_is_preview_true_when_specified(self):
        """显式指定 is_preview=True"""
        task = GenerationTask.create(
            project_id="proj1",
            asset_library_id="lib1",
            is_preview=True,
        )
        assert task.is_preview is True

    def test_is_preview_false_when_explicit_false(self):
        """显式指定 is_preview=False"""
        task = GenerationTask.create(
            project_id="proj1",
            asset_library_id="lib1",
            is_preview=False,
        )
        assert task.is_preview is False

    def test_is_preview_with_template_mode(self):
        """模板模式下 is_preview 正常工作"""
        task = GenerationTask.create(
            project_id="",
            asset_library_id="",
            template_id="tmpl1",
            asset_ids=["a1", "a2"],
            is_preview=True,
        )
        assert task.is_preview is True
        assert task.template_id == "tmpl1"
        assert task.asset_ids == ["a1", "a2"]

    def test_is_preview_preserved_in_dataclass(self):
        """is_preview 是 dataclass 字段，可以被赋值"""
        task = GenerationTask.create(
            project_id="proj1",
            asset_library_id="lib1",
        )
        task.is_preview = True
        assert task.is_preview is True


# ── UseCase 测试 ────────────────────────────────────────────────────────────


class TestCreateGenerationTaskUseCaseIsPreview:
    """CreateGenerationTaskUseCase 中 is_preview 传递测试"""

    def test_command_default_is_preview_false(self):
        """CreateGenerationTaskCommand 默认 is_preview=False"""
        cmd = CreateGenerationTaskCommand(
            project_id="proj1",
            asset_library_id="lib1",
        )
        assert cmd.is_preview is False

    def test_command_is_preview_true(self):
        """CreateGenerationTaskCommand 设置 is_preview=True"""
        cmd = CreateGenerationTaskCommand(
            project_id="proj1",
            asset_library_id="lib1",
            is_preview=True,
        )
        assert cmd.is_preview is True

    def test_use_case_passes_is_preview_to_task(self):
        """UseCase 将 is_preview 传递给 GenerationTask"""
        mock_repo = MagicMock()
        mock_repo.create.side_effect = lambda t: t
        use_case = CreateGenerationTaskUseCase(mock_repo)

        command = CreateGenerationTaskCommand(
            project_id="proj1",
            asset_library_id="lib1",
            is_preview=True,
        )
        result = use_case.execute(command)

        assert result.is_preview is True
        mock_repo.create.assert_called_once()

    def test_use_case_default_is_preview_false(self):
        """UseCase 默认 is_preview=False"""
        mock_repo = MagicMock()
        mock_repo.create.side_effect = lambda t: t
        use_case = CreateGenerationTaskUseCase(mock_repo)

        command = CreateGenerationTaskCommand(
            project_id="proj1",
            asset_library_id="lib1",
        )
        result = use_case.execute(command)

        assert result.is_preview is False


# ── Schema 测试 ──────────────────────────────────────────────────────────────


class TestCreatePreviewGenerationTaskRequest:
    """CreatePreviewGenerationTaskRequest schema 校验测试"""

    def test_valid_request_with_asset_ids(self):
        """有效请求：template_id + asset_ids"""
        from app.schemas.generation_task import CreatePreviewGenerationTaskRequest

        req = CreatePreviewGenerationTaskRequest(
            template_id="tmpl_123",
            asset_ids=["asset_1", "asset_2"],
        )
        assert req.template_id == "tmpl_123"
        assert req.asset_ids == ["asset_1", "asset_2"]
        assert req.title_ids == []
        assert req.video_title == ""
        assert req.duration == 0.0
        assert req.bgm_config == {}

    def test_valid_request_with_title_ids_only(self):
        """有效请求：template_id + title_ids（替代 asset_ids）"""
        from app.schemas.generation_task import CreatePreviewGenerationTaskRequest

        req = CreatePreviewGenerationTaskRequest(
            template_id="tmpl_123",
            title_ids=["title_1"],
        )
        assert req.template_id == "tmpl_123"
        assert req.title_ids == ["title_1"]

    def test_valid_request_with_voice_library_id_and_asset_ids(self):
        """有效请求：template_id + asset_ids + 统一配音 voice_library_id（#1749 替换旧 voice_ids 字段）"""
        from app.schemas.generation_task import CreatePreviewGenerationTaskRequest

        req = CreatePreviewGenerationTaskRequest(
            template_id="tmpl_123",
            asset_ids=["asset_1"],
            voice_library_id="voice_1",
        )
        assert req.voice_library_id == "voice_1"

    def test_missing_template_id_raises(self):
        """缺少 template_id 报错"""
        from app.schemas.generation_task import CreatePreviewGenerationTaskRequest
        from pydantic import ValidationError

        with pytest.raises(ValidationError, match="template_id"):
            CreatePreviewGenerationTaskRequest(
                template_id="",
                asset_ids=["asset_1"],
            )

    def test_missing_template_id_not_provided_raises(self):
        """完全不提供 template_id 报错"""
        from app.schemas.generation_task import CreatePreviewGenerationTaskRequest
        from pydantic import ValidationError

        with pytest.raises(ValidationError):
            CreatePreviewGenerationTaskRequest(
                asset_ids=["asset_1"],
            )

    def test_empty_asset_ids_raises(self):
        """asset_ids/title_ids/voice_ids 全空报错"""
        from app.schemas.generation_task import CreatePreviewGenerationTaskRequest
        from pydantic import ValidationError

        with pytest.raises(ValidationError, match="asset_ids"):
            CreatePreviewGenerationTaskRequest(
                template_id="tmpl_123",
                asset_ids=[],
                title_ids=[],
                voice_ids=[],
            )

    def test_request_with_all_fields(self):
        """所有字段都设置的请求"""
        from app.schemas.generation_task import CreatePreviewGenerationTaskRequest

        req = CreatePreviewGenerationTaskRequest(
            template_id="tmpl_123",
            asset_ids=["a1", "a2"],
            title_ids=["t1"],
            voice_ids=["v1"],
            video_title="测试预览视频",
            duration=30.0,
            video_ratio="9:16",
            bgm_config={"enabled": True, "volume": 0.5},
        )
        assert req.video_title == "测试预览视频"
        assert req.duration == 30.0
        assert req.video_ratio == "9:16"
        assert req.bgm_config["enabled"] is True
        assert req.bgm_config["volume"] == 0.5


class TestPreviewGenerationTaskResponse:
    """PreviewGenerationTaskResponse schema 测试"""

    def test_pending_state_response(self):
        """pending 状态的响应"""

        resp = PreviewGenerationTaskResponse(
            task_id="task_123",
            status="pending",
            progress=0.0,
        )
        assert resp.task_id == "task_123"
        assert resp.status == "pending"
        assert resp.progress == 0.0
        assert resp.is_preview is True
        assert resp.resolution == ""
        assert resp.video_url == ""
        assert resp.duration == 0.0
        assert resp.file_size == 0
        assert resp.clip_count == 0
        assert resp.error_message == ""

    def test_completed_state_response(self):
        """completed 状态的响应"""
        from datetime import datetime, timezone

        now = datetime.now(timezone.utc)
        resp = PreviewGenerationTaskResponse(
            task_id="task_123",
            status="completed",
            progress=100.0,
            is_preview=True,
            resolution="854x480",
            video_url="https://example.com/preview.mp4",
            duration=30.5,
            file_size=5_000_000,
            clip_count=5,
            transition_count=4,
            material_usage={"videos": 5, "images": 2},
            created_at=now,
            started_at=now,
            finished_at=now,
            generate_duration=12.5,
        )
        assert resp.status == "completed"
        assert resp.progress == 100.0
        assert resp.resolution == "854x480"
        assert resp.video_url == "https://example.com/preview.mp4"
        assert resp.duration == 30.5
        assert resp.file_size == 5_000_000
        assert resp.clip_count == 5
        assert resp.transition_count == 4
        assert resp.generate_duration == 12.5

    def test_failed_state_response(self):
        """failed 状态的响应"""

        resp = PreviewGenerationTaskResponse(
            task_id="task_123",
            status="failed",
            progress=30.0,
            error_message="渲染失败：素材格式不支持",
        )
        assert resp.status == "failed"
        assert resp.error_message == "渲染失败：素材格式不支持"
        assert resp.video_url == ""


# ── 仓储层兼容测试 ──────────────────────────────────────────────────────────


class TestRepositoryIsPreviewCompatibility:
    """仓储层 is_preview 向后兼容测试"""

    def test_to_domain_with_is_preview_true(self):
        """新数据 is_preview=True 时正确映射"""
        mock_model = MagicMock()
        mock_model.id = "task_123"
        mock_model.project_id = "proj1"
        mock_model.strategy_id = ""
        mock_model.asset_library_id = "lib1"
        mock_model.voice_library_id = ""
        mock_model.template_id = "tmpl1"
        mock_model.asset_ids = ["a1"]
        mock_model.title_ids = []
        mock_model.voice_ids = []
        mock_model.status = "pending"
        mock_model.progress = 0.0
        mock_model.result_count = 0
        mock_model.error_message = ""
        mock_model.error_info = {}
        mock_model.retry_count = 0
        mock_model.auto_retry_enabled = False
        mock_model.auto_retry_max = 0
        mock_model.started_at = None
        mock_model.completed_at = None
        mock_model.created_by_user_id = "user1"
        mock_model.source_edit_plan_id = None
        mock_model.asset_select_mode = ""
        mock_model.batch_id = ""
        mock_model.video_title = ""
        mock_model.resolution = "854x480"
        mock_model.bgm_config = {}
        mock_model.is_preview = True
        mock_model.logs = "[]"
        mock_model.created_at = datetime.now(timezone.utc)
        mock_model.updated_at = datetime.now(timezone.utc)

        from packages.adapters.sqlalchemy_impl.generation_task_repository import _to_domain

        result = _to_domain(mock_model)
        assert result.is_preview is True
        assert result.resolution == "854x480"

    def test_to_domain_with_is_preview_false(self):
        """is_preview=False 时正确映射"""
        mock_model = MagicMock()
        mock_model.id = "task_123"
        mock_model.project_id = "proj1"
        mock_model.strategy_id = ""
        mock_model.asset_library_id = "lib1"
        mock_model.voice_library_id = ""
        mock_model.template_id = ""
        mock_model.asset_ids = []
        mock_model.title_ids = []
        mock_model.voice_ids = []
        mock_model.status = "pending"
        mock_model.progress = 0.0
        mock_model.result_count = 0
        mock_model.error_message = ""
        mock_model.error_info = {}
        mock_model.retry_count = 0
        mock_model.auto_retry_enabled = False
        mock_model.auto_retry_max = 0
        mock_model.started_at = None
        mock_model.completed_at = None
        mock_model.created_by_user_id = "user1"
        mock_model.source_edit_plan_id = None
        mock_model.asset_select_mode = ""
        mock_model.batch_id = ""
        mock_model.video_title = ""
        mock_model.resolution = ""
        mock_model.bgm_config = {}
        mock_model.is_preview = False
        mock_model.logs = "[]"
        mock_model.created_at = datetime.now(timezone.utc)
        mock_model.updated_at = datetime.now(timezone.utc)

        from packages.adapters.sqlalchemy_impl.generation_task_repository import _to_domain

        result = _to_domain(mock_model)
        assert result.is_preview is False

    def test_repository_create_includes_is_preview(self):
        """repository.create() 包含 is_preview 字段"""
        from sqlalchemy import create_engine
        from sqlalchemy.orm import sessionmaker

        from packages.adapters.sqlalchemy_impl.generation_task_repository import (
            SQLAlchemyGenerationTaskRepository,
        )
        from packages.adapters.sqlalchemy_impl.models import Base, GenerationTaskModel

        # 使用内存数据库
        engine = create_engine("sqlite:///:memory:")
        Base.metadata.create_all(engine)
        SessionLocal = sessionmaker(bind=engine)
        session = SessionLocal()

        try:
            repo = SQLAlchemyGenerationTaskRepository(session)
            task = GenerationTask.create(
                project_id="proj1",
                asset_library_id="lib1",
                is_preview=True,
                resolution="854x480",
            )
            repo.create(task)

            # 直接查 model 验证
            model = session.query(GenerationTaskModel).filter(GenerationTaskModel.id == task.id).first()
            assert model is not None
            assert model.is_preview is True
            assert model.resolution == "854x480"
        finally:
            session.close()

    def test_repository_update_preserves_is_preview(self):
        """repository.update() 保留 is_preview 字段"""
        from sqlalchemy import create_engine
        from sqlalchemy.orm import sessionmaker

        from packages.adapters.sqlalchemy_impl.generation_task_repository import (
            SQLAlchemyGenerationTaskRepository,
        )
        from packages.adapters.sqlalchemy_impl.models import Base, GenerationTaskModel

        engine = create_engine("sqlite:///:memory:")
        Base.metadata.create_all(engine)
        SessionLocal = sessionmaker(bind=engine)
        session = SessionLocal()

        try:
            repo = SQLAlchemyGenerationTaskRepository(session)
            task = GenerationTask.create(
                project_id="proj1",
                asset_library_id="lib1",
                is_preview=True,
            )
            repo.create(task)

            # 更新任务状态
            task.mark_processing()
            repo.update(task)

            model = session.query(GenerationTaskModel).filter(GenerationTaskModel.id == task.id).first()
            assert model is not None
            assert model.is_preview is True  # is_preview 应该保持不变
            assert model.status == "running"
        finally:
            session.close()


# ── 状态流转测试 ────────────────────────────────────────────────────────────


class TestPreviewTaskStatusFlow:
    """预览任务状态流转测试"""

    def test_pending_to_running(self):
        """pending → running"""
        task = GenerationTask.create(
            project_id="",
            asset_library_id="",
            template_id="tmpl1",
            asset_ids=["a1"],
            is_preview=True,
        )
        assert task.status == GenerationTaskStatus.PENDING
        assert task.is_preview is True

        task.mark_processing()
        assert task.status == GenerationTaskStatus.RUNNING
        assert task.started_at is not None

    def test_running_to_completed(self):
        """running → completed"""
        task = GenerationTask.create(
            project_id="",
            asset_library_id="",
            template_id="tmpl1",
            asset_ids=["a1"],
            is_preview=True,
        )
        task.mark_processing()
        task.mark_completed()

        assert task.status == GenerationTaskStatus.COMPLETED
        assert task.progress == 100.0
        assert task.completed_at is not None
        assert task.is_preview is True

    def test_running_to_failed(self):
        """running → failed"""
        task = GenerationTask.create(
            project_id="",
            asset_library_id="",
            template_id="tmpl1",
            asset_ids=["a1"],
            is_preview=True,
        )
        task.mark_processing()
        task.mark_failed("渲染错误")

        assert task.status == GenerationTaskStatus.FAILED
        assert task.error_message == "渲染错误"
        assert task.is_preview is True

    def test_pending_to_cancelled(self):
        """pending → cancelled"""
        task = GenerationTask.create(
            project_id="",
            asset_library_id="",
            template_id="tmpl1",
            asset_ids=["a1"],
            is_preview=True,
        )
        task.mark_cancelled()

        assert task.status == GenerationTaskStatus.CANCELLED
        assert task.is_preview is True

    def test_preview_resolution_is_480p(self):
        """预览任务分辨率为 854x480"""
        task = GenerationTask.create(
            project_id="",
            asset_library_id="",
            template_id="tmpl1",
            asset_ids=["a1"],
            resolution="854x480",
            is_preview=True,
        )
        assert task.resolution == "854x480"
        assert task.is_preview is True

    def test_non_preview_task_default_false(self):
        """非预览任务 is_preview 默认 False"""
        task = GenerationTask.create(
            project_id="proj1",
            asset_library_id="lib1",
        )
        assert task.is_preview is False


# ═══════════════════════════════════════════════════════════════════════════════
# 路由层测试 — 覆盖 generation_preview.py 的 HTTP handler 逻辑
# ═══════════════════════════════════════════════════════════════════════════════

from dataclasses import dataclass
from unittest.mock import PropertyMock, patch

# 导入路由模块和被测函数
from app.api.routes.generation_preview import (
    _to_preview_response,
    create_preview_generation_task,
    get_preview_generation_task,
)
from app.core.task_enqueue import GlobalQueueFull, UserPendingLimitExceeded
from app.schemas.generation_task import (
    CreatePreviewGenerationTaskRequest,
    PreviewGenerationTaskResponse,
)


def _make_user(user_id="test_user_001"):
    """构造 mock AuthenticatedUser"""
    mock_user = MagicMock()
    mock_user.id = user_id
    auth = MagicMock()
    auth.user = mock_user
    auth.session_id = "sess_001"
    auth.token_type = "bearer"
    return auth


def _make_task(
    task_id="preview_task_001",
    status=GenerationTaskStatus.PENDING,
    progress=0.0,
    is_preview=True,
    resolution="854x480",
    error_message="",
    created_at=None,
    started_at=None,
    completed_at=None,
    extra_meta=None,
    asset_ids=None,
):
    """构造 mock GenerationTask"""
    task = MagicMock()
    task.id = task_id
    task.status = status
    task.progress = progress
    task.is_preview = is_preview
    task.resolution = resolution
    task.error_message = error_message
    task.created_at = created_at or datetime.now(timezone.utc)
    task.started_at = started_at
    task.completed_at = completed_at
    task.extra_meta = extra_meta or {}
    task.asset_ids = asset_ids or []
    task.created_by_user_id = "test_user_001"
    # 默认无关联编辑计划：涉及克隆变体的测试自行设置并 mock EditPlanService
    task.source_edit_plan_id = None
    return task


class TestToPreviewResponse:
    """_to_preview_response 辅助函数测试"""

    def test_basic_pending_task(self):
        """基本 pending 任务转换"""
        task = _make_task(status=GenerationTaskStatus.PENDING)
        resp = _to_preview_response(task)
        assert resp.task_id == "preview_task_001"
        assert resp.status == "pending"
        assert resp.is_preview is True
        assert resp.video_url == ""
        assert resp.duration == 0.0
        assert resp.file_size == 0

    def test_completed_task_with_videos(self):
        """已完成任务，带视频结果（裸URL直接返回，rendered/*已公开读）"""
        task = _make_task(
            status=GenerationTaskStatus.COMPLETED,
            progress=100.0,
        )
        video = MagicMock()
        video.file_url = "https://cdn.example.com/preview.mp4"
        video.duration = 30.5
        video.file_size = 1024000
        resp = _to_preview_response(task, generated_videos=[video])
        assert resp.video_url == "https://cdn.example.com/preview.mp4"
        assert resp.duration == 30.5
        assert resp.file_size == 1024000

    def test_extra_meta_fields(self):
        """从 extra_meta 提取统计信息"""
        task = _make_task(
            extra_meta={
                "clip_count": 5,
                "transition_count": 4,
                "material_usage": {"video": 3, "audio": 2},
            }
        )
        resp = _to_preview_response(task)
        assert resp.clip_count == 5
        assert resp.transition_count == 4
        assert resp.material_usage == {"video": 3, "audio": 2}

    def test_generate_duration_calculation(self):
        """计算生成耗时"""
        from datetime import timedelta

        started = datetime(2026, 8, 1, 10, 0, 0, tzinfo=timezone.utc)
        completed = started + timedelta(seconds=45)
        task = _make_task(started_at=started, completed_at=completed)
        resp = _to_preview_response(task)
        assert resp.generate_duration == 45.0

    def test_no_started_completed_time(self):
        """无开始/完成时间时 generate_duration=0"""
        task = _make_task(started_at=None, completed_at=None)
        resp = _to_preview_response(task)
        assert resp.generate_duration == 0.0

    def test_empty_generated_videos(self):
        """空视频列表时 video_url 为空"""
        task = _make_task()
        resp = _to_preview_response(task, generated_videos=[])
        assert resp.video_url == ""

    def test_none_generated_videos(self):
        """None 视频列表时 video_url 为空"""
        task = _make_task()
        resp = _to_preview_response(task, generated_videos=None)
        assert resp.video_url == ""


class TestCreatePreviewRoute:
    """POST /preview 路由测试"""

    def _make_request(self, **kwargs):
        defaults = {
            "template_id": "tpl_001",
            "asset_ids": ["asset_1", "asset_2"],
            "title_ids": [],
            "voice_ids": [],
            "video_title": "测试预览",
            "duration": 0.0,
            "video_ratio": "",
            "bgm_config": {},
        }
        defaults.update(kwargs)
        return CreatePreviewGenerationTaskRequest(**defaults)

    def test_create_success(self):
        """正常创建预览任务"""
        repo = MagicMock()
        repo.count_pending_by_user.return_value = 0
        repo.count_pending_total.return_value = 5

        task = _make_task()
        with patch("app.api.routes.generation_preview.CreateGenerationTaskUseCase") as MockUC:
            MockUC.return_value.execute.return_value = task
            with patch(
                "app.api.routes.generation_preview.safe_enqueue_generation_task",
                return_value=True,
            ):
                resp = create_preview_generation_task(
                    self._make_request(),
                    authenticated_user=_make_user(),
                    generation_task_repository=repo,
                    db=MagicMock(),
                )
        # 批量响应：N=1 时 items 长度为 1
        assert resp.total == 1
        assert len(resp.items) == 1
        assert resp.items[0].task_id == "preview_task_001"
        assert resp.items[0].status == "pending"
        assert resp.items[0].variant_index == 0

    def test_user_pending_limit_exceeded(self):
        """用户待处理任务超限 → 429"""
        repo = MagicMock()
        repo.count_pending_by_user.return_value = 3
        repo.count_pending_total.return_value = 5

        from fastapi import HTTPException

        with pytest.raises(HTTPException) as exc_info:
            create_preview_generation_task(
                self._make_request(),
                authenticated_user=_make_user(),
                generation_task_repository=repo,
                db=MagicMock(),
            )
        assert exc_info.value.status_code == 429

    def test_global_queue_full(self):
        """全局队列满 → 503"""
        repo = MagicMock()
        repo.count_pending_by_user.return_value = 0
        repo.count_pending_total.return_value = 20

        from fastapi import HTTPException

        with pytest.raises(HTTPException) as exc_info:
            create_preview_generation_task(
                self._make_request(),
                authenticated_user=_make_user(),
                generation_task_repository=repo,
                db=MagicMock(),
            )
        assert exc_info.value.status_code == 503

    def test_create_value_error(self):
        """UseCase 抛出 ValueError → 400"""
        repo = MagicMock()
        repo.count_pending_by_user.return_value = 0
        repo.count_pending_total.return_value = 0

        from fastapi import HTTPException

        with patch("app.api.routes.generation_preview.CreateGenerationTaskUseCase") as MockUC:
            MockUC.return_value.execute.side_effect = ValueError("template_id 不能为空")
            with pytest.raises(HTTPException) as exc_info:
                create_preview_generation_task(
                    self._make_request(),
                    authenticated_user=_make_user(),
                    generation_task_repository=repo,
                    db=MagicMock(),
                )
            assert exc_info.value.status_code == 400

    def test_create_unexpected_error(self):
        """UseCase 抛出未知异常 → 500"""
        repo = MagicMock()
        repo.count_pending_by_user.return_value = 0
        repo.count_pending_total.return_value = 0

        from fastapi import HTTPException

        with patch("app.api.routes.generation_preview.CreateGenerationTaskUseCase") as MockUC:
            MockUC.return_value.execute.side_effect = RuntimeError("db connection lost")
            with pytest.raises(HTTPException) as exc_info:
                create_preview_generation_task(
                    self._make_request(),
                    authenticated_user=_make_user(),
                    generation_task_repository=repo,
                    db=MagicMock(),
                )
            assert exc_info.value.status_code == 500

    def test_enqueue_returns_false(self):
        """safe_enqueue 返回 False → 500"""
        repo = MagicMock()
        repo.count_pending_by_user.return_value = 0
        repo.count_pending_total.return_value = 0

        task = _make_task()

        # 模拟 mark_failed 真实更新任务状态（_mark_task_failed 内部调用）
        def _set_failed(error_message="", **_kwargs):
            task.status = GenerationTaskStatus.FAILED
            task.error_message = error_message

        task.mark_failed.side_effect = _set_failed

        with patch("app.api.routes.generation_preview.CreateGenerationTaskUseCase") as MockUC:
            MockUC.return_value.execute.return_value = task
            with patch(
                "app.api.routes.generation_preview.safe_enqueue_generation_task",
                return_value=False,
            ):
                resp = create_preview_generation_task(
                    self._make_request(),
                    authenticated_user=_make_user(),
                    generation_task_repository=repo,
                    db=MagicMock(),
                )
            # 入队失败：任务被标记 failed（mark_failed 设置错误信息），响应正常返回
            assert resp.total == 1
            assert resp.items[0].status == "failed"

    def test_enqueue_raises_user_limit(self):
        """safe_enqueue 抛出 UserPendingLimitExceeded → 429"""
        repo = MagicMock()
        repo.count_pending_by_user.return_value = 0
        repo.count_pending_total.return_value = 0

        task = _make_task()
        from fastapi import HTTPException

        def _set_failed_limit(error_message="", **_kwargs):
            task.status = GenerationTaskStatus.FAILED
            task.error_message = error_message or "待处理任务超限"

        task.mark_failed.side_effect = _set_failed_limit

        with patch("app.api.routes.generation_preview.CreateGenerationTaskUseCase") as MockUC:
            MockUC.return_value.execute.return_value = task
            with patch(
                "app.api.routes.generation_preview.safe_enqueue_generation_task",
                side_effect=UserPendingLimitExceeded(user_id="u1", pending_count=4, limit=3),
            ):
                with pytest.raises(HTTPException) as exc_info:
                    create_preview_generation_task(
                        self._make_request(),
                        authenticated_user=_make_user(),
                        generation_task_repository=repo,
                        db=MagicMock(),
                    )
                # 全部变体入队失败且错误消息含"待处理任务" → 429
                assert exc_info.value.status_code == 429

    def test_enqueue_raises_global_queue_full(self):
        """safe_enqueue 抛出 GlobalQueueFull → 503"""
        repo = MagicMock()
        repo.count_pending_by_user.return_value = 0
        repo.count_pending_total.return_value = 0

        task = _make_task()
        from fastapi import HTTPException

        def _set_failed_queue(error_message="", **_kwargs):
            task.status = GenerationTaskStatus.FAILED
            task.error_message = error_message or "系统队列已满"

        task.mark_failed.side_effect = _set_failed_queue

        with patch("app.api.routes.generation_preview.CreateGenerationTaskUseCase") as MockUC:
            MockUC.return_value.execute.return_value = task
            with patch(
                "app.api.routes.generation_preview.safe_enqueue_generation_task",
                side_effect=GlobalQueueFull(pending_count=21, limit=20),
            ):
                with pytest.raises(HTTPException) as exc_info:
                    create_preview_generation_task(
                        self._make_request(),
                        authenticated_user=_make_user(),
                        generation_task_repository=repo,
                        db=MagicMock(),
                    )
                # 全部变体入队失败且错误消息含"队列" → 503
                assert exc_info.value.status_code == 503


class TestGetPreviewRoute:
    """GET /preview/{task_id} 路由测试"""

    def test_task_not_found(self):
        """任务不存在 → 404"""
        repo = MagicMock()
        vid_repo = MagicMock()

        from fastapi import HTTPException

        with patch("app.api.routes.generation_preview.GetGenerationTaskUseCase") as MockUC:
            MockUC.return_value.execute.return_value = None
            with pytest.raises(HTTPException) as exc_info:
                get_preview_generation_task(
                    task_id="nonexistent",
                    authenticated_user=_make_user(),
                    generation_task_repository=repo,
                    generated_video_repository=vid_repo,
                )
            assert exc_info.value.status_code == 404

    def test_permission_denied(self):
        """任务不属于当前用户 → 403"""
        repo = MagicMock()
        vid_repo = MagicMock()
        task = _make_task()
        task.created_by_user_id = "other_user_999"

        from fastapi import HTTPException

        with patch("app.api.routes.generation_preview.GetGenerationTaskUseCase") as MockUC:
            MockUC.return_value.execute.return_value = task
            with pytest.raises(HTTPException) as exc_info:
                get_preview_generation_task(
                    task_id="preview_task_001",
                    authenticated_user=_make_user("test_user_001"),
                    generation_task_repository=repo,
                    generated_video_repository=vid_repo,
                )
            assert exc_info.value.status_code == 403

    def test_not_preview_task(self):
        """查询的不是预览任务 → 404"""
        repo = MagicMock()
        vid_repo = MagicMock()
        task = _make_task(is_preview=False)
        task.created_by_user_id = "test_user_001"

        from fastapi import HTTPException

        with patch("app.api.routes.generation_preview.GetGenerationTaskUseCase") as MockUC:
            MockUC.return_value.execute.return_value = task
            with pytest.raises(HTTPException) as exc_info:
                get_preview_generation_task(
                    task_id="preview_task_001",
                    authenticated_user=_make_user(),
                    generation_task_repository=repo,
                    generated_video_repository=vid_repo,
                )
            assert exc_info.value.status_code == 404

    def test_get_pending_task(self):
        """查询 pending 状态的预览任务"""
        repo = MagicMock()
        vid_repo = MagicMock()
        task = _make_task(status=GenerationTaskStatus.PENDING, progress=0.0)

        with patch("app.api.routes.generation_preview.GetGenerationTaskUseCase") as MockUC:
            MockUC.return_value.execute.return_value = task
            resp = get_preview_generation_task(
                task_id="preview_task_001",
                authenticated_user=_make_user(),
                generation_task_repository=repo,
                generated_video_repository=vid_repo,
            )
        assert resp.status == "pending"
        assert resp.is_preview is True

    def test_empty_user_id_denied(self):
        """任务 created_by_user_id 为空 → 403（防止越权）"""
        repo = MagicMock()
        vid_repo = MagicMock()
        task = _make_task()
        task.created_by_user_id = ""  # 空字符串

        from fastapi import HTTPException

        with patch("app.api.routes.generation_preview.GetGenerationTaskUseCase") as MockUC:
            MockUC.return_value.execute.return_value = task
            with pytest.raises(HTTPException) as exc_info:
                get_preview_generation_task(
                    task_id="preview_task_001",
                    authenticated_user=_make_user(),
                    generation_task_repository=repo,
                    generated_video_repository=vid_repo,
                )
            assert exc_info.value.status_code == 403

    def test_get_completed_task_with_videos(self):
        """查询 completed 状态任务，返回视频列表（URL 已签名）"""
        repo = MagicMock()
        vid_repo = MagicMock()
        task = _make_task(status=GenerationTaskStatus.COMPLETED, progress=100.0)

        video = MagicMock()
        video.file_url = "https://cdn.example.com/preview_final.mp4"
        video.duration = 25.0
        video.file_size = 512000

        # Mock URL 签名（返回带签名的 URL）
        mock_storage = MagicMock()
        mock_storage.get_download_url.return_value = "https://cdn.example.com/preview_final.mp4"

        with patch("app.api.routes.generation_preview.GetGenerationTaskUseCase") as MockGet:
            MockGet.return_value.execute.return_value = task
            with patch("app.api.routes.generation_preview.ListGeneratedVideosByTaskUseCase") as MockList:
                MockList.return_value.execute.return_value = [video]
                with patch("app.api.routes.generation_preview.get_storage_service", return_value=mock_storage):
                    resp = get_preview_generation_task(
                        task_id="preview_task_001",
                        authenticated_user=_make_user(),
                        generation_task_repository=repo,
                        generated_video_repository=vid_repo,
                    )
        assert resp.video_url == "https://cdn.example.com/preview_final.mp4"
        assert resp.duration == 25.0


# ═══════════════════════════════════════════════════════════════════════════════
# Worker 预览模式测试 — 覆盖 generation.py 预览分辨率强制逻辑
# ═══════════════════════════════════════════════════════════════════════════════


class TestWorkerPreviewResolution:
    """Worker 层预览模式分辨率强制逻辑测试"""

    def test_preview_mode_respects_resolution(self):
        """预览模式尊重传入的 resolution 参数（如竖屏 480x854）"""
        is_preview = True
        resolution = "480x854"  # 竖屏模板
        original_config = {"export": {"resolution": "1280x720", "bitrate": "5M"}}
        plan_cfg = dict(original_config)
        export_cfg = dict(plan_cfg.get("export", {}) or {})

        if is_preview:
            preview_res = resolution if resolution else "854x480"
            export_cfg["resolution"] = preview_res
            export_cfg["bitrate"] = "1M"
        elif resolution:
            export_cfg["resolution"] = resolution

        plan_cfg["export"] = export_cfg

        assert export_cfg["resolution"] == "480x854"  # 竖屏
        assert export_cfg["bitrate"] == "1M"
        # 验证原始配置未被污染
        assert original_config["export"]["resolution"] == "1280x720"

    def test_non_preview_uses_user_resolution(self):
        """非预览模式使用用户指定分辨率（逻辑不变）"""
        is_preview = False
        resolution = "1920x1080"
        plan_cfg = dict({"export": {"resolution": "1280x720"}})
        export_cfg = dict(plan_cfg.get("export", {}) or {})

        if is_preview:
            preview_res = resolution if resolution else "854x480"
            export_cfg["resolution"] = preview_res
            export_cfg["bitrate"] = "1M"
        elif resolution:
            export_cfg["resolution"] = resolution

        assert export_cfg["resolution"] == "1920x1080"

    def test_non_preview_no_resolution_uses_template(self):
        """非预览模式且用户未指定分辨率，使用模板配置（逻辑不变）"""
        is_preview = False
        resolution = ""
        plan_cfg = dict({"export": {"resolution": "1280x720"}})
        export_cfg = dict(plan_cfg.get("export", {}) or {})

        if is_preview:
            preview_res = resolution if resolution else "854x480"
            export_cfg["resolution"] = preview_res
            export_cfg["bitrate"] = "1M"
        elif resolution:
            export_cfg["resolution"] = resolution
        elif not export_cfg.get("resolution"):
            export_cfg["resolution"] = "1280x720"

        assert export_cfg["resolution"] == "1280x720"

    def test_preview_mode_empty_export_cfg(self):
        """预览模式且模板无 export 配置，无传入 resolution 时默认 854x480"""
        is_preview = True
        resolution = ""
        plan_cfg = dict({})
        export_cfg = dict(plan_cfg.get("export", {}) or {})

        if is_preview:
            preview_res = resolution if resolution else "854x480"
            export_cfg["resolution"] = preview_res
            export_cfg["bitrate"] = "1M"

        assert export_cfg["resolution"] == "854x480"
        assert export_cfg["bitrate"] == "1M"


# ═══════════════════════════════════════════════════════════════════════════════
# 新增：URL 签名 + 模板时长约束测试
# ═══════════════════════════════════════════════════════════════════════════════


class TestInferVideoRatioFromTemplate:
    """_infer_video_ratio_from_template 单元测试。"""

    def test_pip_mode_returns_9_16(self):
        """模板 mode=pip → 返回 '9:16'"""
        from app.api.routes.generation_preview import _infer_video_ratio_from_template

        mock_template = MagicMock()
        mock_template.mode = "pip"
        mock_db = MagicMock()

        with patch("app.api.routes.generation_preview.SQLAlchemyTemplateRepository") as MockRepo:
            MockRepo.return_value.get.return_value = mock_template
            result = _infer_video_ratio_from_template("tpl_001", mock_db, "user_1")
        assert result == "9:16"

    def test_standard_mode_returns_16_9(self):
        """模板 mode=standard → 返回 '16:9'"""
        from app.api.routes.generation_preview import _infer_video_ratio_from_template

        mock_template = MagicMock()
        mock_template.mode = "standard"
        mock_db = MagicMock()

        with patch("app.api.routes.generation_preview.SQLAlchemyTemplateRepository") as MockRepo:
            MockRepo.return_value.get.return_value = mock_template
            result = _infer_video_ratio_from_template("tpl_001", mock_db, "user_1")
        assert result == "16:9"

    def test_unknown_mode_returns_empty(self):
        """模板 mode 未知 → 返回空字符串"""
        from app.api.routes.generation_preview import _infer_video_ratio_from_template

        mock_template = MagicMock()
        mock_template.mode = "unknown_mode"
        mock_db = MagicMock()

        with patch("app.api.routes.generation_preview.SQLAlchemyTemplateRepository") as MockRepo:
            MockRepo.return_value.get.return_value = mock_template
            result = _infer_video_ratio_from_template("tpl_001", mock_db, "user_1")
        assert result == ""

    def test_template_not_found_returns_empty(self):
        """模板不存在 → 返回空字符串"""
        from app.api.routes.generation_preview import _infer_video_ratio_from_template

        mock_db = MagicMock()

        with patch("app.api.routes.generation_preview.SQLAlchemyTemplateRepository") as MockRepo:
            MockRepo.return_value.get.return_value = None
            result = _infer_video_ratio_from_template("nonexistent", mock_db, "user_1")
        assert result == ""

    def test_empty_template_id_returns_empty(self):
        """空 template_id → 直接返回空字符串"""
        from app.api.routes.generation_preview import _infer_video_ratio_from_template

        mock_db = MagicMock()
        result = _infer_video_ratio_from_template("", mock_db, "user_1")
        assert result == ""

    def test_db_exception_returns_empty(self):
        """DB 异常 → 返回空字符串，不抛出"""
        from app.api.routes.generation_preview import _infer_video_ratio_from_template

        mock_db = MagicMock()

        with patch("app.api.routes.generation_preview.SQLAlchemyTemplateRepository") as MockRepo:
            MockRepo.side_effect = Exception("db connection error")
            result = _infer_video_ratio_from_template("tpl_001", mock_db, "user_1")
        assert result == ""


class TestPreviewRouteAutoInfersVideoRatio:
    """验证预览路由在前端未传 video_ratio 时自动从模板推断。"""

    def test_auto_infer_pip_resolution(self):
        """前端传 video_ratio=''，模板 mode=pip → resolution 为空（使用用户默认导出配置）"""
        from app.api.routes.generation_preview import create_preview_generation_task
        from app.schemas.generation_task import CreatePreviewGenerationTaskRequest

        repo = MagicMock()
        repo.count_pending_by_user.return_value = 0
        repo.count_pending_total.return_value = 0
        mock_db = MagicMock()

        mock_template = MagicMock()
        mock_template.mode = "pip"

        task = _make_task()

        request = CreatePreviewGenerationTaskRequest(
            template_id="tpl_pip",
            asset_ids=["a1"],
            title_ids=[],
            voice_ids=[],
            video_title="test",
            duration=0.0,
            video_ratio="",
            bgm_config={},
        )

        with patch("app.api.routes.generation_preview.SQLAlchemyEditTemplateRepository") as MockNewRepo:
            MockNewRepo.return_value.get.return_value = None  # 新模板系统无数据，fallback 旧系统
            with patch("app.api.routes.generation_preview.SQLAlchemyTemplateRepository") as MockRepo:
                MockRepo.return_value.get.return_value = mock_template
                with patch("app.api.routes.generation_preview.CreateGenerationTaskUseCase") as MockUC:
                    MockUC.return_value.execute.return_value = task
                    with patch(
                        "app.api.routes.generation_preview.safe_enqueue_generation_task",
                        return_value=True,
                    ):
                        resp = create_preview_generation_task(
                            request,
                            authenticated_user=_make_user(),
                            generation_task_repository=repo,
                            db=mock_db,
                        )

        # Verify the resolution passed to CreateGenerationTaskCommand
        call_args = MockUC.return_value.execute.call_args
        cmd = call_args[0][0]
        # video_ratio inferred from pip → 9:16 → resolution=1080x1920
        assert cmd.resolution == "1080x1920", f"Expected 1080x1920, got {cmd.resolution}"
        assert cmd.output_width == 1080, f"Expected output_width=1080, got {cmd.output_width}"
        assert cmd.output_height == 1920, f"Expected output_height=1920, got {cmd.output_height}"


# ═══════════════════════════════════════════════════════════════════════════════
# _resolve_strategy_id_from_template 单元测试
# ═══════════════════════════════════════════════════════════════════════════════


class TestResolveStrategyIdFromTemplate:
    """_resolve_strategy_id_from_template 单元测试：预览生成从模板读取 editing_mode。"""

    def test_new_template_found_returns_editing_mode(self):
        """新模板系统找到模板 → 返回 editing_mode。"""
        from app.api.routes.generation_preview import _resolve_strategy_id_from_template

        mock_new_template = MagicMock()
        mock_new_template.editing_mode = "standard"
        mock_db = MagicMock()

        with patch("app.api.routes.generation_preview.SQLAlchemyEditTemplateRepository") as MockNewRepo:
            MockNewRepo.return_value.get.return_value = mock_new_template
            result = _resolve_strategy_id_from_template("tpl_001", mock_db, "user_1")

        assert result == "standard"

    def test_new_template_not_found_fallback_to_old(self):
        """新模板系统未找到 → fallback 旧模板系统返回 mode。"""
        from app.api.routes.generation_preview import _resolve_strategy_id_from_template

        mock_old_template = MagicMock()
        mock_old_template.mode = "standard"
        mock_db = MagicMock()

        with patch("app.api.routes.generation_preview.SQLAlchemyEditTemplateRepository") as MockNewRepo:
            MockNewRepo.return_value.get.return_value = None
            with patch("app.api.routes.generation_preview.SQLAlchemyTemplateRepository") as MockOldRepo:
                MockOldRepo.return_value.get.return_value = mock_old_template
                result = _resolve_strategy_id_from_template("tpl_001", mock_db, "user_1")

        assert result == "standard"

    def test_both_not_found_returns_empty(self):
        """新旧模板系统都未找到 → 返回空字符串。"""
        from app.api.routes.generation_preview import _resolve_strategy_id_from_template

        mock_db = MagicMock()

        with patch("app.api.routes.generation_preview.SQLAlchemyEditTemplateRepository") as MockNewRepo:
            MockNewRepo.return_value.get.return_value = None
            with patch("app.api.routes.generation_preview.SQLAlchemyTemplateRepository") as MockOldRepo:
                MockOldRepo.return_value.get.return_value = None
                result = _resolve_strategy_id_from_template("tpl_nonexistent", mock_db, "user_1")

        assert result == ""

    def test_empty_template_id_returns_empty(self):
        """空 template_id → 直接返回空字符串。"""
        from app.api.routes.generation_preview import _resolve_strategy_id_from_template

        mock_db = MagicMock()
        result = _resolve_strategy_id_from_template("", mock_db, "user_1")
        assert result == ""

    def test_new_template_exception_fallback_to_old(self):
        """新模板系统异常 → 降级到旧模板系统。"""
        from app.api.routes.generation_preview import _resolve_strategy_id_from_template

        mock_old_template = MagicMock()
        mock_old_template.mode = "voice_over"
        mock_db = MagicMock()

        with patch("app.api.routes.generation_preview.SQLAlchemyEditTemplateRepository") as MockNewRepo:
            MockNewRepo.side_effect = RuntimeError("db error")
            with patch("app.api.routes.generation_preview.SQLAlchemyTemplateRepository") as MockOldRepo:
                MockOldRepo.return_value.get.return_value = mock_old_template
                result = _resolve_strategy_id_from_template("tpl_001", mock_db, "user_1")

        assert result == "voice_over"

    def test_both_exception_returns_empty(self):
        """新旧模板系统都异常 → 返回空字符串。"""
        from app.api.routes.generation_preview import _resolve_strategy_id_from_template

        mock_db = MagicMock()

        with patch("app.api.routes.generation_preview.SQLAlchemyEditTemplateRepository") as MockNewRepo:
            MockNewRepo.side_effect = RuntimeError("new db error")
            with patch("app.api.routes.generation_preview.SQLAlchemyTemplateRepository") as MockOldRepo:
                MockOldRepo.side_effect = RuntimeError("old db error")
                result = _resolve_strategy_id_from_template("tpl_001", mock_db, "user_1")

        assert result == ""

    def test_new_template_empty_editing_mode_fallback(self):
        """新模板找到但 editing_mode 为空 → fallback 旧模板。"""
        from app.api.routes.generation_preview import _resolve_strategy_id_from_template

        mock_new_template = MagicMock()
        mock_new_template.editing_mode = ""
        mock_old_template = MagicMock()
        mock_old_template.mode = "one_take"
        mock_db = MagicMock()

        with patch("app.api.routes.generation_preview.SQLAlchemyEditTemplateRepository") as MockNewRepo:
            MockNewRepo.return_value.get.return_value = mock_new_template
            with patch("app.api.routes.generation_preview.SQLAlchemyTemplateRepository") as MockOldRepo:
                MockOldRepo.return_value.get.return_value = mock_old_template
                result = _resolve_strategy_id_from_template("tpl_001", mock_db, "user_1")

        assert result == "one_take"

    def test_voice_pip_mode(self):
        """模板 editing_mode=voice_pip → 返回 one_take（画中画已下线，统一映射为one_take）。"""
        from app.api.routes.generation_preview import _resolve_strategy_id_from_template

        mock_new_template = MagicMock()
        mock_new_template.editing_mode = "voice_pip"
        mock_db = MagicMock()

        with patch("app.api.routes.generation_preview.SQLAlchemyEditTemplateRepository") as MockNewRepo:
            MockNewRepo.return_value.get.return_value = mock_new_template
            result = _resolve_strategy_id_from_template("tpl_voice", mock_db, "user_1")

        assert result == "one_take"


class TestPreviewRoutePassesStrategyId:
    """验证预览路由正确传递 strategy_id 到 CreateGenerationTaskCommand。"""

    def test_strategy_id_from_new_template(self):
        """预览路由从新模板读取 strategy_id=standard。"""
        from app.api.routes.generation_preview import create_preview_generation_task
        from app.schemas.generation_task import CreatePreviewGenerationTaskRequest

        repo = MagicMock()
        repo.count_pending_by_user.return_value = 0
        repo.count_pending_total.return_value = 0
        mock_db = MagicMock()

        mock_new_template = MagicMock()
        mock_new_template.editing_mode = "standard"

        task = _make_task()

        request = CreatePreviewGenerationTaskRequest(
            template_id="tpl_pip",
            asset_ids=["a1"],
            title_ids=[],
            voice_ids=[],
            video_title="test",
            duration=0.0,
            video_ratio="",
            bgm_config={},
        )

        with patch("app.api.routes.generation_preview.SQLAlchemyEditTemplateRepository") as MockNewRepo:
            MockNewRepo.return_value.get.return_value = mock_new_template
            with patch("app.api.routes.generation_preview.CreateGenerationTaskUseCase") as MockUC:
                MockUC.return_value.execute.return_value = task
                with patch(
                    "app.api.routes.generation_preview.safe_enqueue_generation_task",
                    return_value=True,
                ):
                    resp = create_preview_generation_task(
                        request,
                        authenticated_user=_make_user(),
                        generation_task_repository=repo,
                        db=mock_db,
                    )

        call_args = MockUC.return_value.execute.call_args
        cmd = call_args[0][0]
        assert cmd.strategy_id == "standard", f"Expected strategy_id='standard', got '{cmd.strategy_id}'"

    def test_strategy_id_fallback_to_old_template(self):
        """新模板无数据时，从旧模板读取 strategy_id=standard。"""
        from app.api.routes.generation_preview import create_preview_generation_task
        from app.schemas.generation_task import CreatePreviewGenerationTaskRequest

        repo = MagicMock()
        repo.count_pending_by_user.return_value = 0
        repo.count_pending_total.return_value = 0
        mock_db = MagicMock()

        mock_old_template = MagicMock()
        mock_old_template.mode = "standard"

        task = _make_task()

        request = CreatePreviewGenerationTaskRequest(
            template_id="tpl_standard",
            asset_ids=["a1"],
            title_ids=[],
            voice_ids=[],
            video_title="test",
            duration=0.0,
            video_ratio="",
            bgm_config={},
        )

        with patch("app.api.routes.generation_preview.SQLAlchemyEditTemplateRepository") as MockNewRepo:
            MockNewRepo.return_value.get.return_value = None
            with patch("app.api.routes.generation_preview.SQLAlchemyTemplateRepository") as MockOldRepo:
                MockOldRepo.return_value.get.return_value = mock_old_template
                with patch("app.api.routes.generation_preview.CreateGenerationTaskUseCase") as MockUC:
                    MockUC.return_value.execute.return_value = task
                    with patch(
                        "app.api.routes.generation_preview.safe_enqueue_generation_task",
                        return_value=True,
                    ):
                        resp = create_preview_generation_task(
                            request,
                            authenticated_user=_make_user(),
                            generation_task_repository=repo,
                            db=mock_db,
                        )

        call_args = MockUC.return_value.execute.call_args
        cmd = call_args[0][0]
        assert cmd.strategy_id == "standard", f"Expected strategy_id='standard', got '{cmd.strategy_id}'"
