"""Application 层零测试模块合集 — 第100波里程碑。

覆盖:
- packages/application/generated_videos.py (8个UseCase)
- packages/application/assets.py (ListAssets + CreateAsset)
- packages/application/asset_libraries.py (ListLibraries + CreateLibrary)

策略: Mock repository，测参数校验 + 委托行为
"""

from unittest.mock import MagicMock

import pytest

from packages.application.asset_libraries import (
    CreateAssetLibraryCommand,
    CreateAssetLibraryUseCase,
    ListAssetLibrariesUseCase,
)
from packages.application.assets import (
    CreateAssetCommand,
    CreateAssetUseCase,
    ListAssetsUseCase,
)
from packages.application.generated_videos import (
    GetGeneratedVideoDownloadUrlUseCase,
    GetGeneratedVideoUseCase,
    GetVideosByIdsUseCase,
    ListGeneratedVideosByTaskUseCase,
    ListGeneratedVideosPaginatedUseCase,
    ListGeneratedVideosUseCase,
    UpdateVideoReviewStatusUseCase,
)
from packages.domain import AssetLibraryKind, AssetStatus, ClassificationStatus, GeneratedVideo

# ── generated_videos.py ────────────────────────────────────────────────────────


class TestListGeneratedVideosUseCase:
    def test_success(self):
        mock_repo = MagicMock()
        mock_repo.list_by_project.return_value = [MagicMock(spec=GeneratedVideo)]
        use_case = ListGeneratedVideosUseCase(mock_repo)

        result = use_case.execute("proj1")

        assert len(result) == 1
        mock_repo.list_by_project.assert_called_once_with("proj1")

    def test_strips_project_id(self):
        mock_repo = MagicMock()
        use_case = ListGeneratedVideosUseCase(mock_repo)

        use_case.execute("  proj1  ")

        mock_repo.list_by_project.assert_called_once_with("proj1")

    def test_empty_project_id_raises(self):
        mock_repo = MagicMock()
        use_case = ListGeneratedVideosUseCase(mock_repo)

        with pytest.raises(ValueError, match="project_id 不能为空"):
            use_case.execute("")

    def test_whitespace_project_id_raises(self):
        mock_repo = MagicMock()
        use_case = ListGeneratedVideosUseCase(mock_repo)

        with pytest.raises(ValueError, match="project_id 不能为空"):
            use_case.execute("  \t  ")


class TestListGeneratedVideosPaginatedUseCase:
    def test_default_params(self):
        mock_repo = MagicMock()
        mock_repo.list_paginated.return_value = ([], 0)
        use_case = ListGeneratedVideosPaginatedUseCase(mock_repo)

        result, total = use_case.execute()

        assert total == 0
        assert result == []
        mock_repo.list_paginated.assert_called_once_with(
            user_id=None,
            project_id=None,
            status=None,
            review_status=None,
            page=1,
            page_size=20,
        )

    def test_page_below_1_clamps_to_1(self):
        mock_repo = MagicMock()
        mock_repo.list_paginated.return_value = ([], 0)
        use_case = ListGeneratedVideosPaginatedUseCase(mock_repo)

        use_case.execute(page=0)

        mock_repo.list_paginated.assert_called_once()
        call_kwargs = mock_repo.list_paginated.call_args.kwargs
        assert call_kwargs["page"] == 1

    def test_negative_page_clamps(self):
        mock_repo = MagicMock()
        mock_repo.list_paginated.return_value = ([], 0)
        use_case = ListGeneratedVideosPaginatedUseCase(mock_repo)

        use_case.execute(page=-5)

        assert mock_repo.list_paginated.call_args.kwargs["page"] == 1

    def test_page_size_zero_clamps(self):
        mock_repo = MagicMock()
        mock_repo.list_paginated.return_value = ([], 0)
        use_case = ListGeneratedVideosPaginatedUseCase(mock_repo)

        use_case.execute(page_size=0)

        assert mock_repo.list_paginated.call_args.kwargs["page_size"] == 20

    def test_page_size_over_100_clamps(self):
        mock_repo = MagicMock()
        mock_repo.list_paginated.return_value = ([], 0)
        use_case = ListGeneratedVideosPaginatedUseCase(mock_repo)

        use_case.execute(page_size=200)

        assert mock_repo.list_paginated.call_args.kwargs["page_size"] == 20

    def test_page_size_50_ok(self):
        mock_repo = MagicMock()
        mock_repo.list_paginated.return_value = ([], 0)
        use_case = ListGeneratedVideosPaginatedUseCase(mock_repo)

        use_case.execute(page_size=50)

        assert mock_repo.list_paginated.call_args.kwargs["page_size"] == 50

    def test_with_all_filters(self):
        mock_repo = MagicMock()
        mock_repo.list_paginated.return_value = ([], 0)
        use_case = ListGeneratedVideosPaginatedUseCase(mock_repo)

        use_case.execute(
            user_id="u1",
            project_id="p1",
            status="completed",
            review_status="approved",
            page=2,
            page_size=10,
        )

        mock_repo.list_paginated.assert_called_once_with(
            user_id="u1",
            project_id="p1",
            status="completed",
            review_status="approved",
            page=2,
            page_size=10,
        )


class TestGetGeneratedVideoUseCase:
    def test_found(self):
        mock_repo = MagicMock()
        expected = MagicMock(spec=GeneratedVideo)
        mock_repo.get.return_value = expected
        use_case = GetGeneratedVideoUseCase(mock_repo)

        result = use_case.execute("vid1")

        assert result == expected
        mock_repo.get.assert_called_once_with("vid1")

    def test_not_found(self):
        mock_repo = MagicMock()
        mock_repo.get.return_value = None
        use_case = GetGeneratedVideoUseCase(mock_repo)

        result = use_case.execute("vid1")

        assert result is None


class TestListGeneratedVideosByTaskUseCase:
    def test_success(self):
        mock_repo = MagicMock()
        mock_repo.list_by_generation_task.return_value = [MagicMock()]
        use_case = ListGeneratedVideosByTaskUseCase(mock_repo)

        result = use_case.execute("task1")

        assert len(result) == 1
        mock_repo.list_by_generation_task.assert_called_once_with("task1")

    def test_strips_task_id(self):
        mock_repo = MagicMock()
        use_case = ListGeneratedVideosByTaskUseCase(mock_repo)

        use_case.execute("  task1  ")

        mock_repo.list_by_generation_task.assert_called_once_with("task1")

    def test_empty_task_id_raises(self):
        mock_repo = MagicMock()
        use_case = ListGeneratedVideosByTaskUseCase(mock_repo)

        with pytest.raises(ValueError, match="generation_task_id 不能为空"):
            use_case.execute("")


class TestGetGeneratedVideoDownloadUrlUseCase:
    def test_found(self):
        mock_repo = MagicMock()
        mock_item = MagicMock()
        mock_item.file_url = "https://cdn/v.mp4"
        mock_repo.get.return_value = mock_item
        use_case = GetGeneratedVideoDownloadUrlUseCase(mock_repo)

        result = use_case.execute("vid1")

        assert result == "https://cdn/v.mp4"

    def test_not_found_returns_none(self):
        mock_repo = MagicMock()
        mock_repo.get.return_value = None
        use_case = GetGeneratedVideoDownloadUrlUseCase(mock_repo)

        result = use_case.execute("vid1")

        assert result is None


class TestUpdateVideoReviewStatusUseCase:
    def test_pending_review(self):
        mock_repo = MagicMock()
        mock_repo.update_review_status.return_value = MagicMock()
        use_case = UpdateVideoReviewStatusUseCase(mock_repo)

        use_case.execute("vid1", "pending_review")

        mock_repo.update_review_status.assert_called_once_with("vid1", "pending_review")

    def test_approved(self):
        mock_repo = MagicMock()
        use_case = UpdateVideoReviewStatusUseCase(mock_repo)

        use_case.execute("vid1", "approved")

        mock_repo.update_review_status.assert_called_once_with("vid1", "approved")

    def test_rejected(self):
        mock_repo = MagicMock()
        use_case = UpdateVideoReviewStatusUseCase(mock_repo)

        use_case.execute("vid1", "rejected")

        mock_repo.update_review_status.assert_called_once_with("vid1", "rejected")

    def test_strips_video_id(self):
        mock_repo = MagicMock()
        use_case = UpdateVideoReviewStatusUseCase(mock_repo)

        use_case.execute("  vid1  ", "approved")

        mock_repo.update_review_status.assert_called_once_with("vid1", "approved")

    def test_empty_video_id_raises(self):
        mock_repo = MagicMock()
        use_case = UpdateVideoReviewStatusUseCase(mock_repo)

        with pytest.raises(ValueError, match="video_id 不能为空"):
            use_case.execute("", "approved")

    def test_invalid_status_raises(self):
        mock_repo = MagicMock()
        use_case = UpdateVideoReviewStatusUseCase(mock_repo)

        with pytest.raises(ValueError, match="无效的 review_status"):
            use_case.execute("vid1", "invalid_status")

    def test_not_found_returns_none(self):
        mock_repo = MagicMock()
        mock_repo.update_review_status.return_value = None
        use_case = UpdateVideoReviewStatusUseCase(mock_repo)

        result = use_case.execute("vid1", "approved")

        assert result is None


class TestGetVideosByIdsUseCase:
    def test_success(self):
        mock_repo = MagicMock()
        mock_repo.get_by_ids.return_value = [MagicMock(), MagicMock()]
        use_case = GetVideosByIdsUseCase(mock_repo)

        result = use_case.execute(["id1", "id2", "id3"])

        assert len(result) == 2
        mock_repo.get_by_ids.assert_called_once_with(["id1", "id2", "id3"])

    def test_empty_list(self):
        mock_repo = MagicMock()
        mock_repo.get_by_ids.return_value = []
        use_case = GetVideosByIdsUseCase(mock_repo)

        result = use_case.execute([])

        assert result == []
        mock_repo.get_by_ids.assert_called_once_with([])


# ── assets.py ──────────────────────────────────────────────────────────────────


class TestCreateAssetCommand:
    def test_minimal(self):
        cmd = CreateAssetCommand(
            project_id="p1",
            library_id="l1",
            name="test.mp4",
            storage_key="k",
            mime_type="video/mp4",
        )
        assert cmd.project_id == "p1"
        assert cmd.library_id == "l1"
        assert cmd.name == "test.mp4"
        assert cmd.storage_key == "k"
        assert cmd.mime_type == "video/mp4"
        assert cmd.file_size == 0
        assert cmd.status == AssetStatus.UPLOADING
        assert cmd.classification_status == ClassificationStatus.PENDING

    def test_full(self):
        cmd = CreateAssetCommand(
            project_id="p1",
            library_id="l1",
            name="test.mp4",
            storage_key="k",
            mime_type="video/mp4",
            metadata={"k": "v"},
            file_size=1024,
            duration=10.0,
            width=1920,
            height=1080,
            fps=30.0,
            codec="h264",
            status=AssetStatus.READY,
            quality_score=0.9,
            uploaded_by_user_id="u1",
        )
        assert cmd.file_size == 1024
        assert cmd.duration == 10.0
        assert cmd.status == AssetStatus.READY
        assert cmd.quality_score == 0.9


class TestListAssetsUseCase:
    def test_success(self):
        mock_repo = MagicMock()
        mock_repo.find_by_library.return_value = []
        use_case = ListAssetsUseCase(mock_repo)

        result = use_case.execute("lib1")

        assert result == []
        mock_repo.find_by_library.assert_called_once_with("lib1")

    def test_strips_library_id(self):
        mock_repo = MagicMock()
        use_case = ListAssetsUseCase(mock_repo)

        use_case.execute("  lib1  ")

        mock_repo.find_by_library.assert_called_once_with("lib1")

    def test_empty_library_id_raises(self):
        mock_repo = MagicMock()
        use_case = ListAssetsUseCase(mock_repo)

        with pytest.raises(ValueError, match="library_id 不能为空"):
            use_case.execute("")


class TestCreateAssetUseCase:
    def test_creates_asset_via_repo(self):
        mock_repo = MagicMock()
        mock_repo.create.return_value = MagicMock()
        use_case = CreateAssetUseCase(mock_repo)

        cmd = CreateAssetCommand(
            project_id="p1",
            library_id="l1",
            name="test.mp4",
            storage_key="videos/t.mp4",
            mime_type="video/mp4",
            file_size=1024,
        )
        result = use_case.execute(cmd)

        assert result is not None
        mock_repo.create.assert_called_once()
        created_asset = mock_repo.create.call_args[0][0]
        assert created_asset.project_id == "p1"
        assert created_asset.name == "test.mp4"
        assert created_asset.file_size == 1024
        assert created_asset.status == AssetStatus.UPLOADING

    def test_asset_create_validation_propagates(self):
        mock_repo = MagicMock()
        use_case = CreateAssetUseCase(mock_repo)

        cmd = CreateAssetCommand(
            project_id="p1",
            library_id="l1",
            name="",
            storage_key="k",
            mime_type="video/mp4",
        )

        with pytest.raises(ValueError, match="素材名称不能为空"):
            use_case.execute(cmd)


# ── asset_libraries.py ────────────────────────────────────────────────────────


class TestCreateAssetLibraryCommand:
    def test_creation(self):
        cmd = CreateAssetLibraryCommand(
            project_id="p1",
            name="我的库",
            kind=AssetLibraryKind.VIDEO,
        )
        assert cmd.project_id == "p1"
        assert cmd.name == "我的库"
        assert cmd.kind == AssetLibraryKind.VIDEO


class TestListAssetLibrariesUseCase:
    def test_success(self):
        mock_repo = MagicMock()
        mock_repo.find_by_project.return_value = []
        use_case = ListAssetLibrariesUseCase(mock_repo)

        result = use_case.execute("p1")

        assert result == []
        mock_repo.find_by_project.assert_called_once_with("p1")

    def test_strips_project_id(self):
        mock_repo = MagicMock()
        use_case = ListAssetLibrariesUseCase(mock_repo)

        use_case.execute("  p1  ")

        mock_repo.find_by_project.assert_called_once_with("p1")

    def test_empty_project_id_raises(self):
        mock_repo = MagicMock()
        use_case = ListAssetLibrariesUseCase(mock_repo)

        with pytest.raises(ValueError, match="project_id 不能为空"):
            use_case.execute("")


class TestCreateAssetLibraryUseCase:
    def test_creates_library_via_repo(self):
        mock_repo = MagicMock()
        mock_repo.create.return_value = MagicMock()
        use_case = CreateAssetLibraryUseCase(mock_repo)

        cmd = CreateAssetLibraryCommand(
            project_id="p1",
            name="视频库",
            kind=AssetLibraryKind.VIDEO,
        )
        result = use_case.execute(cmd)

        assert result is not None
        mock_repo.create.assert_called_once()
        created = mock_repo.create.call_args[0][0]
        assert created.project_id == "p1"
        assert created.name == "视频库"
        assert created.kind == AssetLibraryKind.VIDEO

    def test_validation_propagates(self):
        mock_repo = MagicMock()
        use_case = CreateAssetLibraryUseCase(mock_repo)

        cmd = CreateAssetLibraryCommand(
            project_id="p1",
            name="",
            kind=AssetLibraryKind.VIDEO,
        )

        with pytest.raises(ValueError, match="素材库名称不能为空"):
            use_case.execute(cmd)
