"""config_schemas + AI 推荐/封面生成 单元测试.

覆盖（20+ 测试用例）：
- config_schemas: normalize_plan_config / normalize_template_config 默认值填充、部分覆盖、非标准字段保留
- config_schemas: Pydantic 枚举校验（CoverType / TextPosition / BGMSource）
- ai_tasks: run_ai_recommend / run_generate_cover stub 返回结构
- edit_plans API: POST /{plan_id}/ai-recommend 正常/404/400
- edit_plans API: POST /{plan_id}/generate-cover 正常/404
- edit_plans API: create_plan config 标准化
- edit_plans API: update_plan config 标准化
- edit_templates API: create_template / update_template config 标准化
"""

from __future__ import annotations

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

os.environ.setdefault("JWT_SECRET_KEY", "unit-test-secret-key-for-testing")
os.environ.setdefault("DATABASE_URL", "sqlite:///test.db")

import pytest

sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "apps" / "api"))


# ---------------------------------------------------------------------------
# config_schemas 单元测试
# ---------------------------------------------------------------------------


class TestNormalizePlanConfig:
    def test_none_returns_full_defaults(self):
        from packages.domain.config_schemas import DEFAULT_EDIT_PLAN_CONFIG, normalize_plan_config

        result = normalize_plan_config(None)
        assert result == DEFAULT_EDIT_PLAN_CONFIG.copy()

    def test_empty_dict_returns_full_defaults(self):
        from packages.domain.config_schemas import DEFAULT_EDIT_PLAN_CONFIG, normalize_plan_config

        result = normalize_plan_config({})
        assert result["cover"]["type"] == "ai_frame"
        assert result["title"]["enabled"] is True
        assert result["subtitle"]["position"] == "bottom"
        assert result["bgm"]["volume"] == 0.3

    def test_partial_cover_override(self):
        from packages.domain.config_schemas import normalize_plan_config

        result = normalize_plan_config({"cover": {"type": "upload", "image_url": "https://example.com/cover.jpg"}})
        assert result["cover"]["type"] == "upload"
        assert result["cover"]["image_url"] == "https://example.com/cover.jpg"
        # frame_time 保留默认值 None
        assert result["cover"]["frame_time"] is None

    def test_partial_title_override(self):
        from packages.domain.config_schemas import normalize_plan_config

        result = normalize_plan_config({"title": {"text": "我的标题", "ai_auto": False}})
        assert result["title"]["text"] == "我的标题"
        assert result["title"]["ai_auto"] is False
        # 其他字段保留默认值
        assert result["title"]["font"] == "思源黑体"
        assert result["title"]["size"] == 48

    def test_non_standard_fields_preserved(self):
        from packages.domain.config_schemas import normalize_plan_config

        result = normalize_plan_config({"generation_task_id": "task-123", "custom_key": "value"})
        assert result["generation_task_id"] == "task-123"
        assert result["custom_key"] == "value"
        # 标准字段仍然完整
        assert "cover" in result
        assert "bgm" in result

    def test_bgm_partial_override(self):
        from packages.domain.config_schemas import normalize_plan_config

        result = normalize_plan_config({"bgm": {"volume": 0.8}})
        assert result["bgm"]["volume"] == 0.8
        assert result["bgm"]["source"] == "library"
        assert result["bgm"]["asset_id"] == ""

    def test_multiple_sections_override(self):
        from packages.domain.config_schemas import normalize_plan_config

        result = normalize_plan_config(
            {
                "cover": {"type": "manual", "frame_time": 5.0},
                "title": {"enabled": False},
                "subtitle": {"size": 36},
                "bgm": {"source": "ai_recommend"},
            }
        )
        assert result["cover"]["type"] == "manual"
        assert result["cover"]["frame_time"] == 5.0
        assert result["title"]["enabled"] is False
        assert result["subtitle"]["size"] == 36
        assert result["bgm"]["source"] == "ai_recommend"


class TestNormalizeTemplateConfig:
    def test_same_as_plan_config(self):
        from packages.domain.config_schemas import normalize_plan_config, normalize_template_config

        raw = {"title": {"text": "模板标题"}}
        assert normalize_template_config(raw) == normalize_plan_config(raw)

    def test_none_returns_defaults(self):
        from packages.domain.config_schemas import DEFAULT_EDIT_TEMPLATE_CONFIG, normalize_template_config

        result = normalize_template_config(None)
        assert result == DEFAULT_EDIT_TEMPLATE_CONFIG.copy()


class TestConfigSchemas:
    def test_cover_type_enum_values(self):
        from packages.domain.config_schemas import CoverType

        assert CoverType.AI_FRAME.value == "ai_frame"
        assert CoverType.MANUAL.value == "manual"
        assert CoverType.UPLOAD.value == "upload"
        assert CoverType.AI_REGENERATE.value == "ai_regenerate"

    def test_text_position_enum_values(self):
        from packages.domain.config_schemas import TextPosition

        assert TextPosition.TOP.value == "top"
        assert TextPosition.CENTER.value == "center"
        assert TextPosition.BOTTOM.value == "bottom"

    def test_bgm_source_enum_values(self):
        from packages.domain.config_schemas import BGMSource

        assert BGMSource.LIBRARY.value == "library"
        assert BGMSource.UPLOAD.value == "upload"
        assert BGMSource.AI_RECOMMEND.value == "ai_recommend"

    def test_cover_config_model(self):
        from packages.domain.config_schemas import CoverConfig, CoverType

        cfg = CoverConfig(type=CoverType.MANUAL, frame_time=3.5)
        assert cfg.type == CoverType.MANUAL
        assert cfg.frame_time == 3.5
        assert cfg.image_url == ""

    def test_title_config_defaults(self):
        from packages.domain.config_schemas import TitleConfig

        cfg = TitleConfig()
        assert cfg.enabled is True
        assert cfg.ai_auto is True
        assert cfg.font == "思源黑体"
        assert cfg.size == 48
        assert cfg.bold is True
        assert cfg.stroke.enabled is False

    def test_bgm_config_validation(self):
        from packages.domain.config_schemas import BGMConfig

        cfg = BGMConfig(volume=0.5)
        assert cfg.volume == 0.5

        with pytest.raises(Exception):
            BGMConfig(volume=1.5)  # > 1.0 应该校验失败

    def test_edit_plan_config_schema_full(self):
        from packages.domain.config_schemas import EditPlanConfigSchema

        schema = EditPlanConfigSchema()
        assert schema.cover.type.value == "ai_frame"
        assert schema.title.enabled is True
        assert schema.subtitle.position.value == "bottom"
        assert schema.bgm.source.value == "library"


# ---------------------------------------------------------------------------
# ai_tasks 单元测试
# ---------------------------------------------------------------------------


class TestAIRunTasks:
    def test_run_ai_recommend_returns_expected_structure(self):
        from apps.worker.worker_app.tasks.ai_tasks import run_ai_recommend

        result = run_ai_recommend(
            plan_id="plan-001",
            template_id="tpl-001",
            asset_ids=["asset-1", "asset-2"],
            editing_mode="one_take",
            target_duration=30.0,
        )
        assert "clips" in result
        assert "config" in result
        assert "total_duration" in result
        assert "confidence" in result
        assert len(result["clips"]) >= 2  # 至少 intro + outro
        assert result["total_duration"] > 0
        assert 0 <= result["confidence"] <= 1

    def test_run_ai_recommend_clip_structure(self):
        from apps.worker.worker_app.tasks.ai_tasks import run_ai_recommend

        result = run_ai_recommend(
            plan_id="plan-001",
            template_id="tpl-001",
            asset_ids=["asset-1"],
        )
        for clip in result["clips"]:
            assert "clip_type" in clip
            assert "order" in clip
            assert "duration" in clip
            assert clip["duration"] > 0

    def test_run_ai_recommend_empty_assets(self):
        from apps.worker.worker_app.tasks.ai_tasks import run_ai_recommend

        result = run_ai_recommend(
            plan_id="plan-001",
            template_id="tpl-001",
            asset_ids=[],
        )
        # 即使没有素材，也应该有 intro + outro
        assert len(result["clips"]) >= 2

    def test_run_generate_cover_ai_frame(self):
        from apps.worker.worker_app.tasks.ai_tasks import run_generate_cover

        result = run_generate_cover(
            plan_id="plan-001",
            asset_ids=["asset-1"],
            cover_type="ai_frame",
        )
        assert result["type"] == "ai_frame"
        assert "image_url" in result
        assert "frame_time" in result
        assert "confidence" in result

    def test_run_generate_cover_manual(self):
        from apps.worker.worker_app.tasks.ai_tasks import run_generate_cover

        result = run_generate_cover(
            plan_id="plan-001",
            asset_ids=["asset-1"],
            cover_type="manual",
            frame_time=5.0,
        )
        assert result["type"] == "manual"
        assert result["frame_time"] == 5.0

    def test_run_generate_cover_upload(self):
        from apps.worker.worker_app.tasks.ai_tasks import run_generate_cover

        result = run_generate_cover(
            plan_id="plan-001",
            asset_ids=[],
            cover_type="upload",
        )
        assert result["type"] == "upload"


# ---------------------------------------------------------------------------
# API 端点测试 — AI 推荐 & 封面生成
# ---------------------------------------------------------------------------


class StubEditPlanRepository:
    """内存中的 EditPlan 仓储 stub（支持 clips）"""

    def __init__(self):
        self._plans: dict[str, Any] = {}
        self._clips: dict[str, list] = {}  # plan_id → [clip]
        self._counter = 0

    def _next_id(self) -> str:
        self._counter += 1
        return f"plan-{self._counter:03d}"

    def get(self, plan_id: str):
        return self._plans.get(plan_id)

    def create(self, plan):
        self._plans[plan.id] = plan
        return plan

    def update(self, plan):
        if plan.id not in self._plans:
            raise ValueError(f"EditPlan {plan.id} not found")
        self._plans[plan.id] = plan
        return plan

    def delete(self, plan_id: str):
        if plan_id not in self._plans:
            return False
        del self._plans[plan_id]
        return True

    def list_all(self, *, status=None, skip=0, limit=50):
        items = list(self._plans.values())
        if status:
            items = [p for p in items if p.status == status]
        return items[skip : skip + limit]

    def count(self, *, template_id=None, status=None):
        return len(list(self._plans.values()))

    def delete_by_plan(self, plan_id: str):
        self._clips.pop(plan_id, None)


def _make_auth_user():
    from app.auth import AuthenticatedUser

    from packages.domain.entities import User

    user = User(id="user-001", email="test@example.com", display_name="测试用户")
    return AuthenticatedUser(user=user)


def _create_ai_test_app():
    """创建带 stub 注入的测试 FastAPI 应用（支持 AI 端点）"""
    import app.services.edit_plan_service as service_module
    from app.api.routes import edit_plans as edit_plans_module
    from app.api.routes.edit_plans import router

    stub_repo = StubEditPlanRepository()

    # Mock service methods that interact with DB
    original_plan_repo = service_module.SQLAlchemyEditPlanRepository
    original_clip_repo = service_module.SQLAlchemyEditPlanClipRepository
    original_gen_repo = service_module.SQLAlchemyGenerationTaskRepository

    service_module.SQLAlchemyEditPlanRepository = lambda db: stub_repo
    service_module.SQLAlchemyEditPlanClipRepository = lambda db: stub_repo
    service_module.SQLAlchemyGenerationTaskRepository = lambda db: stub_repo

    app = FastAPI()
    app.include_router(router, prefix="/api/v1/edit-plans")
    app.dependency_overrides[edit_plans_module.get_current_user] = _make_auth_user
    app.dependency_overrides[edit_plans_module.get_db_session] = lambda: MagicMock()

    def cleanup():
        service_module.SQLAlchemyEditPlanRepository = original_plan_repo
        service_module.SQLAlchemyEditPlanClipRepository = original_clip_repo
        service_module.SQLAlchemyGenerationTaskRepository = original_gen_repo

    return app, stub_repo, cleanup


from fastapi import FastAPI
from fastapi.testclient import TestClient

from packages.domain.edit_plan import EditPlan, EditPlanStatus


@pytest.fixture
def ai_client():
    app, stub_repo, cleanup = _create_ai_test_app()
    yield TestClient(app), stub_repo
    cleanup()


class TestAIRecommendEndpoint:
    def test_ai_recommend_success(self, ai_client):
        c, repo = ai_client
        plan = EditPlan.create("tpl-001", "测试计划")
        repo.create(plan)

        resp = c.post(
            f"/api/v1/edit-plans/{plan.id}/ai-recommend",
            json={"asset_ids": ["asset-1", "asset-2"]},
        )
        assert resp.status_code == 200
        data = resp.json()
        assert data["plan_id"] == plan.id
        assert "clips" in data
        assert len(data["clips"]) >= 2
        assert "config" in data
        assert data["total_duration"] > 0
        assert "confidence" in data

    def test_ai_recommend_not_found(self, ai_client):
        c, repo = ai_client
        resp = c.post(
            "/api/v1/edit-plans/nonexistent/ai-recommend",
            json={"asset_ids": ["asset-1"]},
        )
        assert resp.status_code == 404

    def test_ai_recommend_rejects_rendering_status(self, ai_client):
        c, repo = ai_client
        plan = EditPlan.create("tpl-001", "渲染中计划")
        plan.start_editing()
        plan.start_rendering()
        repo.create(plan)

        resp = c.post(
            f"/api/v1/edit-plans/{plan.id}/ai-recommend",
            json={"asset_ids": ["asset-1"]},
        )
        assert resp.status_code == 400
        assert "draft/editing" in resp.json()["detail"]

    def test_ai_recommend_with_custom_params(self, ai_client):
        c, repo = ai_client
        plan = EditPlan.create("tpl-001", "自定义参数计划")
        repo.create(plan)

        resp = c.post(
            f"/api/v1/edit-plans/{plan.id}/ai-recommend",
            json={
                "asset_ids": ["asset-1"],
                "editing_mode": "pip",
                "target_duration": 15.0,
            },
        )
        assert resp.status_code == 200

    def test_ai_recommend_invalid_duration(self, ai_client):
        c, repo = ai_client
        plan = EditPlan.create("tpl-001", "测试计划")
        repo.create(plan)

        resp = c.post(
            f"/api/v1/edit-plans/{plan.id}/ai-recommend",
            json={"asset_ids": ["asset-1"], "target_duration": -5.0},
        )
        assert resp.status_code == 422


class TestGenerateCoverEndpoint:
    def test_generate_cover_ai_frame(self, ai_client):
        c, repo = ai_client
        plan = EditPlan.create("tpl-001", "封面测试计划")
        repo.create(plan)

        resp = c.post(
            f"/api/v1/edit-plans/{plan.id}/generate-cover",
            json={"asset_ids": ["asset-1"], "cover_type": "ai_frame"},
        )
        assert resp.status_code == 200
        data = resp.json()
        assert data["plan_id"] == plan.id
        assert "cover" in data
        assert data["cover"]["type"] == "ai_frame"

    def test_generate_cover_manual(self, ai_client):
        c, repo = ai_client
        plan = EditPlan.create("tpl-001", "手动封面计划")
        repo.create(plan)

        resp = c.post(
            f"/api/v1/edit-plans/{plan.id}/generate-cover",
            json={"asset_ids": ["asset-1"], "cover_type": "manual", "frame_time": 3.5},
        )
        assert resp.status_code == 200
        data = resp.json()
        assert data["cover"]["type"] == "manual"
        assert data["cover"]["frame_time"] == 3.5

    def test_generate_cover_not_found(self, ai_client):
        c, repo = ai_client
        resp = c.post(
            "/api/v1/edit-plans/nonexistent/generate-cover",
            json={"asset_ids": []},
        )
        assert resp.status_code == 404

    def test_generate_cover_default_type(self, ai_client):
        c, repo = ai_client
        plan = EditPlan.create("tpl-001", "默认封面计划")
        repo.create(plan)

        # 不传 cover_type，默认 ai_frame
        resp = c.post(
            f"/api/v1/edit-plans/{plan.id}/generate-cover",
            json={"asset_ids": ["asset-1"]},
        )
        assert resp.status_code == 200
        assert resp.json()["cover"]["type"] == "ai_frame"


# ---------------------------------------------------------------------------
# Config 标准化集成测试（create/update plan & template）
# ---------------------------------------------------------------------------


class TestConfigNormalizationInAPI:
    """验证 create/update 端点自动标准化 config"""

    def test_create_plan_normalizes_config(self, ai_client):
        c, repo = ai_client
        resp = c.post(
            "/api/v1/edit-plans",
            json={
                "template_id": "tpl-001",
                "name": "标准化测试",
                "config": {"title": {"text": "自定义标题"}},
            },
        )
        assert resp.status_code == 201
        config = resp.json()["config"]
        # 传入的 title.text 被保留
        assert config["title"]["text"] == "自定义标题"
        # 未传入的 title 字段填充默认值
        assert config["title"]["font"] == "思源黑体"
        # cover/bgm/subtitle 全部填充默认值
        assert config["cover"]["type"] == "ai_frame"
        assert config["bgm"]["volume"] == 0.3
        assert config["subtitle"]["position"] == "bottom"

    def test_update_plan_normalizes_config(self, ai_client):
        c, repo = ai_client
        plan = EditPlan.create("tpl-001", "更新标准化测试")
        repo.create(plan)

        resp = c.put(
            f"/api/v1/edit-plans/{plan.id}",
            json={"config": {"bgm": {"volume": 0.9}}},
        )
        assert resp.status_code == 200
        config = resp.json()["config"]
        assert config["bgm"]["volume"] == 0.9
        assert config["bgm"]["source"] == "library"
        assert config["cover"]["type"] == "ai_frame"
        assert config["title"]["enabled"] is True
