"""领域实体测试 — Project + Asset + AssetStatus + IngestJob."""

from __future__ import annotations

import pytest

from packages.domain.entities import (
    Asset,
    AssetLibrary,
    AssetStatus,
    ClassificationStatus,
    IngestJob,
    Project,
    User,
)


class TestUser:
    """User 实体测试."""

    def test_create_user(self):
        """创建用户."""
        user = User(
            id="u1",
            email="test@example.com",
            display_name="测试用户",
            username="testuser",
        )
        assert user.id == "u1"
        assert user.email == "test@example.com"
        assert user.display_name == "测试用户"
        assert user.username == "testuser"

    def test_default_subscription_free(self):
        """默认订阅免费版."""
        user = User(id="u1", email="a@b.com", display_name="A")
        assert user.subscription_plan == "free"
        assert user.subscription_status == "active"
        assert user.max_projects == 3
        assert user.max_storage_gb == 10

    def test_default_not_admin(self):
        """默认不是管理员."""
        user = User(id="u1", email="a@b.com", display_name="A")
        assert user.is_admin is False


class TestProject:
    """Project 实体测试."""

    def test_create_project_success(self):
        """创建项目成功."""
        project = Project.create(
            owner_user_id="user_1",
            name="我的项目",
            description="测试项目描述",
        )
        assert project.id is not None
        assert len(project.id) == 32  # uuid hex
        assert project.owner_user_id == "user_1"
        assert project.name == "我的项目"
        assert project.description == "测试项目描述"
        assert project.shared_users == []

    def test_create_project_name_stripped(self):
        """项目名称首尾空白被去除."""
        project = Project.create(owner_user_id="u1", name="  测试项目  ")
        assert project.name == "测试项目"

    def test_create_project_empty_name_raises(self):
        """空项目名抛异常."""
        with pytest.raises(ValueError, match="项目名称不能为空"):
            Project.create(owner_user_id="u1", name="")

    def test_create_project_whitespace_name_raises(self):
        """全空白项目名抛异常."""
        with pytest.raises(ValueError, match="项目名称不能为空"):
            Project.create(owner_user_id="u1", name="   ")

    def test_is_owner_true(self):
        """是项目所有者."""
        project = Project.create(owner_user_id="owner_1", name="项目")
        assert project.is_owner("owner_1") is True

    def test_is_owner_false(self):
        """不是项目所有者."""
        project = Project.create(owner_user_id="owner_1", name="项目")
        assert project.is_owner("other_user") is False

    def test_is_shared_with_true(self):
        """项目已共享给用户."""
        project = Project.create(owner_user_id="u1", name="项目")
        project.shared_users.append("user_2")
        assert project.is_shared_with("user_2") is True

    def test_is_shared_with_false(self):
        """项目未共享给用户."""
        project = Project.create(owner_user_id="u1", name="项目")
        assert project.is_shared_with("nobody") is False

    def test_can_access_owner(self):
        """所有者可以访问."""
        project = Project.create(owner_user_id="u1", name="项目")
        assert project.can_access("u1") is True

    def test_can_access_shared_user(self):
        """共享用户可以访问."""
        project = Project.create(owner_user_id="u1", name="项目")
        project.shared_users.append("u2")
        assert project.can_access("u2") is True

    def test_can_access_outsider(self):
        """无关用户不能访问."""
        project = Project.create(owner_user_id="u1", name="项目")
        assert project.can_access("stranger") is False


class TestAssetLibrary:
    """AssetLibrary 实体测试."""

    def test_create_library(self):
        """创建素材库."""
        from packages.domain.classification import AssetLibraryKind

        lib = AssetLibrary.create(
            project_id="p1",
            name="默认库",
            kind=AssetLibraryKind.VIDEO,
        )
        assert lib.id is not None
        assert lib.project_id == "p1"
        assert lib.name == "默认库"
        assert lib.kind == AssetLibraryKind.VIDEO
        assert lib.asset_count == 0

    def test_create_default_counts(self):
        """默认素材数量和大小为0."""
        from packages.domain.classification import AssetLibraryKind

        lib = AssetLibrary.create(
            project_id="p1",
            name="我的素材",
            kind=AssetLibraryKind.VOICE,
        )
        assert lib.asset_count == 0
        assert lib.total_size == 0

    def test_empty_name_raises(self):
        """空名称抛异常."""
        from packages.domain.classification import AssetLibraryKind

        with pytest.raises(ValueError, match="素材库名称不能为空"):
            AssetLibrary.create(project_id="p1", name="", kind=AssetLibraryKind.VIDEO)


class TestAssetStatus:
    """AssetStatus 枚举兼容测试."""

    def test_direct_values(self):
        """直接枚举值."""
        assert AssetStatus.UPLOADING.value == "uploading"
        assert AssetStatus.READY.value == "ready"
        assert AssetStatus.PROCESSING.value == "processing"
        assert AssetStatus.ERROR.value == "error"
        assert AssetStatus.DELETED.value == "deleted"

    def test_missing_uploaded_maps_to_ready(self):
        """历史值 uploaded → READY."""
        assert AssetStatus("uploaded") == AssetStatus.READY

    def test_missing_success_maps_to_ready(self):
        """success → READY."""
        assert AssetStatus("success") == AssetStatus.READY

    def test_missing_ok_maps_to_ready(self):
        """ok → READY."""
        assert AssetStatus("ok") == AssetStatus.READY

    def test_missing_done_maps_to_ready(self):
        """done → READY."""
        assert AssetStatus("done") == AssetStatus.READY

    def test_missing_upload_maps_to_uploading(self):
        """upload → UPLOADING."""
        assert AssetStatus("upload") == AssetStatus.UPLOADING

    def test_missing_uploading_start_maps_to_uploading(self):
        """uploading_start → UPLOADING."""
        assert AssetStatus("uploading_start") == AssetStatus.UPLOADING

    def test_missing_failed_maps_to_error(self):
        """failed → ERROR."""
        assert AssetStatus("failed") == AssetStatus.ERROR

    def test_missing_fail_maps_to_error(self):
        """fail → ERROR."""
        assert AssetStatus("fail") == AssetStatus.ERROR

    def test_missing_process_maps_to_processing(self):
        """process → PROCESSING."""
        assert AssetStatus("process") == AssetStatus.PROCESSING

    def test_missing_running_maps_to_processing(self):
        """running → PROCESSING."""
        assert AssetStatus("running") == AssetStatus.PROCESSING

    def test_unknown_value_fallback_to_ready(self):
        """完全未知值 → READY兜底."""
        assert AssetStatus("completely_unknown") == AssetStatus.READY

    def test_case_insensitive(self):
        """大小写不敏感."""
        assert AssetStatus("UPLOADED") == AssetStatus.READY
        assert AssetStatus("Failed") == AssetStatus.ERROR

    def test_whitespace_stripped(self):
        """首尾空白被去除."""
        assert AssetStatus("  ready  ") == AssetStatus.READY

    def test_none_value_fallback(self):
        """None值 → READY兜底（不抛异常）."""
        assert AssetStatus(None) == AssetStatus.READY


class TestAsset:
    """Asset 实体测试."""

    def test_create_asset_success(self):
        """创建素材成功."""
        asset = Asset.create(
            project_id="p1",
            library_id="lib1",
            name="测试视频.mp4",
            storage_key="projects/p1/assets/test.mp4",
            mime_type="video/mp4",
            file_size=1024000,
            duration=30.5,
            width=1920,
            height=1080,
        )
        assert asset.id is not None
        assert len(asset.id) == 32
        assert asset.project_id == "p1"
        assert asset.name == "测试视频.mp4"
        assert asset.storage_key == "projects/p1/assets/test.mp4"
        assert asset.mime_type == "video/mp4"
        assert asset.file_size == 1024000
        assert asset.duration == pytest.approx(30.5)
        assert asset.width == 1920
        assert asset.height == 1080
        assert asset.status == AssetStatus.UPLOADING
        assert asset.tag_ids == []

    def test_file_type_video(self):
        """video类型从mime_type推导."""
        asset = Asset.create(
            project_id="p1",
            library_id="lib1",
            name="v.mp4",
            storage_key="k",
            mime_type="video/mp4",
        )
        assert asset.file_type == "video"

    def test_file_type_image(self):
        """image类型."""
        asset = Asset.create(
            project_id="p1",
            library_id="lib1",
            name="i.jpg",
            storage_key="k",
            mime_type="image/jpeg",
        )
        assert asset.file_type == "image"

    def test_file_type_audio(self):
        """audio类型."""
        asset = Asset.create(
            project_id="p1",
            library_id="lib1",
            name="a.mp3",
            storage_key="k",
            mime_type="audio/mpeg",
        )
        assert asset.file_type == "audio"

    def test_file_type_no_slash(self):
        """mime_type没有斜杠时返回原值."""
        asset = Asset.create(
            project_id="p1",
            library_id="lib1",
            name="f.txt",
            storage_key="k",
            mime_type="text",
        )
        assert asset.file_type == "text"

    def test_empty_name_raises(self):
        """空名称抛异常."""
        with pytest.raises(ValueError, match="素材名称不能为空"):
            Asset.create(
                project_id="p1",
                library_id="lib1",
                name="",
                storage_key="k",
                mime_type="video/mp4",
            )

    def test_empty_storage_key_raises(self):
        """空storage_key抛异常."""
        with pytest.raises(ValueError, match="storage_key"):
            Asset.create(
                project_id="p1",
                library_id="lib1",
                name="v.mp4",
                storage_key="   ",
                mime_type="video/mp4",
            )

    def test_empty_mime_type_raises(self):
        """空mime_type抛异常."""
        with pytest.raises(ValueError, match="mime_type"):
            Asset.create(
                project_id="p1",
                library_id="lib1",
                name="v.mp4",
                storage_key="k",
                mime_type="",
            )

    def test_name_stripped(self):
        """名称首尾空白被去除."""
        asset = Asset.create(
            project_id="p1",
            library_id="lib1",
            name="  视频.mp4  ",
            storage_key="k",
            mime_type="video/mp4",
        )
        assert asset.name == "视频.mp4"

    def test_add_tag_success(self):
        """添加标签成功."""
        asset = Asset.create(
            project_id="p1",
            library_id="lib1",
            name="v.mp4",
            storage_key="k",
            mime_type="video/mp4",
        )
        asset.add_tag("tag_1")
        assert "tag_1" in asset.tag_ids
        assert len(asset.tag_ids) == 1

    def test_add_duplicate_tag_deduped(self):
        """重复添加标签自动去重."""
        asset = Asset.create(
            project_id="p1",
            library_id="lib1",
            name="v.mp4",
            storage_key="k",
            mime_type="video/mp4",
        )
        asset.add_tag("tag_1")
        asset.add_tag("tag_1")
        assert asset.tag_ids.count("tag_1") == 1

    def test_add_empty_tag_raises(self):
        """空标签ID抛异常."""
        asset = Asset.create(
            project_id="p1",
            library_id="lib1",
            name="v.mp4",
            storage_key="k",
            mime_type="video/mp4",
        )
        with pytest.raises(ValueError, match="标签 ID 不能为空"):
            asset.add_tag("   ")

    def test_remove_tag_success(self):
        """删除标签成功."""
        asset = Asset.create(
            project_id="p1",
            library_id="lib1",
            name="v.mp4",
            storage_key="k",
            mime_type="video/mp4",
        )
        asset.add_tag("tag_1")
        asset.remove_tag("tag_1")
        assert "tag_1" not in asset.tag_ids

    def test_remove_nonexistent_tag_no_error(self):
        """删除不存在的标签不报错（幂等）."""
        asset = Asset.create(
            project_id="p1",
            library_id="lib1",
            name="v.mp4",
            storage_key="k",
            mime_type="video/mp4",
        )
        # 不抛异常
        asset.remove_tag("nonexistent_tag")

    def test_default_status_uploading(self):
        """默认状态UPLOADING."""
        asset = Asset.create(
            project_id="p1",
            library_id="lib1",
            name="v.mp4",
            storage_key="k",
            mime_type="video/mp4",
        )
        assert asset.status == AssetStatus.UPLOADING

    def test_custom_status(self):
        """自定义状态."""
        asset = Asset.create(
            project_id="p1",
            library_id="lib1",
            name="v.mp4",
            storage_key="k",
            mime_type="video/mp4",
            status=AssetStatus.READY,
        )
        assert asset.status == AssetStatus.READY

    def test_metadata_default_empty_dict(self):
        """metadata默认为空dict."""
        asset = Asset.create(
            project_id="p1",
            library_id="lib1",
            name="v.mp4",
            storage_key="k",
            mime_type="video/mp4",
        )
        assert asset.metadata == {}

    def test_metadata_none_becomes_empty(self):
        """metadata=None → {}."""
        asset = Asset.create(
            project_id="p1",
            library_id="lib1",
            name="v.mp4",
            storage_key="k",
            mime_type="video/mp4",
            metadata=None,
        )
        assert asset.metadata == {}


class TestIngestJob:
    """IngestJob 实体测试."""

    def test_create_ingest_job(self):
        """创建入库任务."""
        job = IngestJob.create(
            project_id="p1",
            library_id="lib1",
            storage_key="projects/p1/uploads/temp.mp4",
            file_hash="abc123",
        )
        assert job.id is not None
        assert job.project_id == "p1"
        assert job.library_id == "lib1"
        assert job.storage_key == "projects/p1/uploads/temp.mp4"
        assert job.file_hash == "abc123"

    def test_default_status_pending(self):
        """默认状态PENDING."""
        from packages.domain.classification import IngestJobStatus

        job = IngestJob.create(
            project_id="p1",
            library_id="lib1",
            storage_key="projects/p1/v.mp4",
        )
        assert job.status == IngestJobStatus.PENDING

    def test_empty_project_id_raises(self):
        """空project_id抛异常."""
        with pytest.raises(ValueError, match="project_id"):
            IngestJob.create(
                project_id="  ",
                library_id="lib1",
                storage_key="k",
            )

    def test_empty_library_id_raises(self):
        """空library_id抛异常."""
        with pytest.raises(ValueError, match="library_id"):
            IngestJob.create(
                project_id="p1",
                library_id="",
                storage_key="k",
            )

    def test_empty_storage_key_raises(self):
        """空storage_key抛异常."""
        with pytest.raises(ValueError, match="storage_key"):
            IngestJob.create(
                project_id="p1",
                library_id="lib1",
                storage_key="   ",
            )

    def test_default_result_asset_id_empty(self):
        """默认result_asset_id为空."""
        job = IngestJob.create(
            project_id="p1",
            library_id="lib1",
            storage_key="k",
        )
        assert job.result_asset_id == ""

    def test_error_message_empty_by_default(self):
        """默认error_message为空."""
        job = IngestJob.create(
            project_id="p1",
            library_id="lib1",
            storage_key="k",
        )
        assert job.error_message == ""
