"""#1743 dedup_helpers 批次查重 + worker 重选 plan 重试函数测试。

覆盖：
- 批次任务且无历史重复时走 check_batch_duplicate，batch_similarity 透传到返回 dict
- 批次任务历史已重复 → 不再做批次查重，is_duplicate=True / duplicate_of 透传
- 非批次任务 batch_similarity 恒为 None
- _reselect_plan_for_batch_retry：成功返回新 plan_id；异常返回 None（保留首版）
"""

from __future__ import annotations

import os
import sys
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

REPO_ROOT = Path(__file__).resolve().parents[2]
for sub in ("apps/worker", "apps/api", "packages", ""):
    p = str(REPO_ROOT / sub) if sub else str(REPO_ROOT)
    if p not in sys.path:
        sys.path.insert(0, p)

from sqlalchemy import create_engine  # noqa: E402
from sqlalchemy.orm import sessionmaker  # noqa: E402

os.environ.setdefault("DATABASE_URL", "sqlite:///:memory:")
from packages.adapters.sqlalchemy_impl.models import Base  # noqa: E402


@pytest.fixture
def session():
    engine = create_engine("sqlite:///:memory:")
    Base.metadata.create_all(engine)
    s = sessionmaker(bind=engine)()
    try:
        yield s
    finally:
        s.close()


def _dedup_kwargs(batch_id):
    return dict(
        generation_task_id="task-batch-1",
        project_id="proj-1",
        user_id="user-1",
        batch_id=batch_id,
        file_url="https://oss.example.com/v.mp4",
        file_size=1024,
        duration=10.0,
        video_path="/tmp/fake.mp4",
        mode="edit_plan",
        width=1280,
        height=720,
        fps=25.0,
    )


class TestBatchDedupInHelpers:
    """dedup_helpers 内部 lazy import video_processing.dedup（依赖 cv2），
    复用 test_generated_video_creation_logic 的 mock 模块注册模式。"""

    @classmethod
    def setup_class(cls):
        import sys

        if "cv2" not in sys.modules:
            sys.modules["cv2"] = MagicMock()

        # 保存 setup 前的真实模块引用，teardown 原样还原（沙箱无 cv2 时 reimport 会失败，
        # 若直接 pop 掉 MagicMock 会让后续测试（如 test_dedup_1702）拿到残缺模块）
        import video_processing

        cls._orig_dedup = sys.modules.get("video_processing.dedup")
        cls._orig_dedup_attr = getattr(video_processing, "dedup", None)
        cls._orig_thumb = sys.modules.get("video_processing.thumbnail_generator")
        cls._orig_thumb_attr = getattr(video_processing, "thumbnail_generator", None)

        mock_dedup = MagicMock()
        mock_dedup.VideoDeduplicator = MagicMock()
        sys.modules["video_processing.dedup"] = mock_dedup

        mock_thumb = MagicMock()
        mock_thumb.extract_first_frame = MagicMock()
        sys.modules["video_processing.thumbnail_generator"] = mock_thumb

        video_processing.dedup = mock_dedup
        video_processing.thumbnail_generator = mock_thumb

    @classmethod
    def teardown_class(cls):
        import sys

        import video_processing

        # 还原 setup 前状态：原本有真模块→放回；原本没有→移除 mock
        if cls._orig_dedup is not None:
            sys.modules["video_processing.dedup"] = cls._orig_dedup
        else:
            sys.modules.pop("video_processing.dedup", None)
        if cls._orig_dedup_attr is not None:
            video_processing.dedup = cls._orig_dedup_attr
        elif hasattr(video_processing, "dedup"):
            delattr(video_processing, "dedup")

        if cls._orig_thumb is not None:
            sys.modules["video_processing.thumbnail_generator"] = cls._orig_thumb
        else:
            sys.modules.pop("video_processing.thumbnail_generator", None)
        if cls._orig_thumb_attr is not None:
            video_processing.thumbnail_generator = cls._orig_thumb_attr
        elif hasattr(video_processing, "thumbnail_generator"):
            delattr(video_processing, "thumbnail_generator")

    def test_batch_similarity_returned_when_batch_duplicate_found(self, session):
        """批次任务 + 无历史重复 + 批次查重命中 → 返回 batch_similarity 与 is_duplicate。"""
        from video_processing.dedup_helpers import create_video_record_and_dedup

        with patch("video_processing.dedup.VideoDeduplicator") as mock_cls:
            dedup = mock_cls.return_value
            dedup.compute_fingerprint.return_value = MagicMock(to_dict=lambda: {})
            dedup.check_duplicate.return_value = None  # 历史无重复
            dedup.check_batch_duplicate.return_value = {
                "duplicate_of": "video-existing",
                "reason": "batch_similar",
                "similarity": 0.601,
            }
            dedup.compute_duplicate_rate.return_value = {
                "duplicate_rate": 0.0,
                "visual_similarity": 0.0,
                "match_count": 0,
            }

            result = create_video_record_and_dedup(session=session, **_dedup_kwargs("batch-abc"))

        assert result["video_count"] == 1
        assert result["batch_similarity"] == pytest.approx(0.601)
        assert result["is_duplicate"] is True
        assert result["duplicate_of"] == "video-existing"
        # 批次查重确实被调用（历史查重为 None 才走批次）
        dedup.check_batch_duplicate.assert_called_once()

    def test_batch_check_skipped_when_historical_duplicate(self, session):
        """历史查重已命中 → 不再批次查重，batch_similarity 为 None。"""
        from video_processing.dedup_helpers import create_video_record_and_dedup

        with patch("video_processing.dedup.VideoDeduplicator") as mock_cls:
            dedup = mock_cls.return_value
            dedup.compute_fingerprint.return_value = MagicMock(to_dict=lambda: {})
            dedup.check_duplicate.return_value = {
                "duplicate_of": "video-old",
                "reason": "global",
                "similarity": 0.85,
            }
            dedup.compute_duplicate_rate.return_value = {
                "duplicate_rate": 85.0,
                "visual_similarity": 0.85,
                "match_count": 3,
            }

            result = create_video_record_and_dedup(session=session, **_dedup_kwargs("batch-abc"))

        assert result["is_duplicate"] is True
        assert result["duplicate_of"] == "video-old"
        assert result["batch_similarity"] is None
        dedup.check_batch_duplicate.assert_not_called()

    def test_non_batch_never_runs_batch_check(self, session):
        """非批次任务（batch_id 为空）→ 不调用批次查重，batch_similarity None。"""
        from video_processing.dedup_helpers import create_video_record_and_dedup

        with patch("video_processing.dedup.VideoDeduplicator") as mock_cls:
            dedup = mock_cls.return_value
            dedup.compute_fingerprint.return_value = MagicMock(to_dict=lambda: {})
            dedup.check_duplicate.return_value = None
            dedup.check_batch_duplicate.return_value = {"similarity": 0.99}
            dedup.compute_duplicate_rate.return_value = {
                "duplicate_rate": 0.0,
                "visual_similarity": 0.0,
                "match_count": 0,
            }

            result = create_video_record_and_dedup(session=session, **_dedup_kwargs(""))

        assert result["video_count"] == 1
        assert result["batch_similarity"] is None
        assert result["is_duplicate"] is False
        dedup.check_batch_duplicate.assert_not_called()

    def test_batch_no_duplicate_returns_none_similarity(self, session):
        """批次任务但批次查重也未命中 → batch_similarity None（驱动不重渲）。"""
        from video_processing.dedup_helpers import create_video_record_and_dedup

        with patch("video_processing.dedup.VideoDeduplicator") as mock_cls:
            dedup = mock_cls.return_value
            dedup.compute_fingerprint.return_value = MagicMock(to_dict=lambda: {})
            dedup.check_duplicate.return_value = None
            dedup.check_batch_duplicate.return_value = None
            dedup.compute_duplicate_rate.return_value = {
                "duplicate_rate": 0.0,
                "visual_similarity": 0.0,
                "match_count": 0,
            }

            result = create_video_record_and_dedup(session=session, **_dedup_kwargs("batch-xyz"))

        assert result["batch_similarity"] is None
        assert result["is_duplicate"] is False


class TestReselectPlanForBatchRetry:
    def test_success_returns_new_plan_id(self):
        """重选成功 → 返回新 plan_id。"""
        from worker_app.tasks.generation import _reselect_plan_for_batch_retry

        fake_svc = MagicMock()
        fake_svc.reselect_plan_for_variant.return_value = MagicMock(id="new-plan-999")
        with patch("app.services.edit_plan_service.EditPlanService", return_value=fake_svc):
            with patch("worker_app.tasks.generation.SessionLocal") as mock_session_local:
                mock_session_local.return_value = MagicMock()
                new_id = _reselect_plan_for_batch_retry(
                    "task-1",
                    "old-plan",
                    {"task_asset_ids": ["a1", "a2"], "user_id": "u-1"},
                )
        assert new_id == "new-plan-999"
        fake_svc.reselect_plan_for_variant.assert_called_once()
        args, kwargs = fake_svc.reselect_plan_for_variant.call_args
        assert args[0] == "old-plan"
        assert args[1] == ["a1", "a2"]
        assert kwargs["created_by_user_id"] == "u-1"

    def test_failure_returns_none_and_keeps_first_version(self):
        """重选抛异常 → 返回 None（调用方放弃重渲、保留首版），不抛出。"""
        from worker_app.tasks.generation import _reselect_plan_for_batch_retry

        fake_svc = MagicMock()
        fake_svc.reselect_plan_for_variant.side_effect = RuntimeError("db down")
        with patch("app.services.edit_plan_service.EditPlanService", return_value=fake_svc):
            with patch("worker_app.tasks.generation.SessionLocal") as mock_session_local:
                mock_session_local.return_value = MagicMock()
                result = _reselect_plan_for_batch_retry("task-1", "old-plan", {"task_asset_ids": [], "user_id": "u-1"})
        assert result is None
