"""Tests for Issue #1660 — 查重率百分比计算 + 跨项目查重."""

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

import pytest

sys.modules.setdefault("cv2", MagicMock())

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


def _make_fingerprint(md5="abc123", phashes=None, duration_ms=10000):
    from video_processing.dedup import VideoFingerprint

    return VideoFingerprint(
        md5=md5,
        keyframe_phashes=phashes or ["ff00ff00ff00ff00"],
        color_histograms=[],
        duration=duration_ms,
        resolution=(1920, 1080),
    )


def _make_video(vid, fingerprint_dict, project_id="proj1", duration=10.0):
    from packages.domain import GeneratedVideo

    return GeneratedVideo(
        id=vid,
        project_id=project_id,
        generation_task_id="task1",
        name=f"video-{vid}",
        file_url=f"https://example.com/{vid}.mp4",
        file_size=1000,
        duration=duration,
        width=1920,
        height=1080,
        fps=25.0,
        video_fingerprint=fingerprint_dict,
    )


class TestCheckDuplicateScopeProject:
    """test_check_duplicate_scope_project：项目内查重（默认行为）."""

    def test_default_scope_queries_by_project(self):
        from video_processing.dedup import VideoDeduplicator

        deduplicator = VideoDeduplicator()
        fingerprint = _make_fingerprint(md5="unique_md5")
        session = MagicMock()

        with patch("video_processing.dedup.SQLAlchemyGeneratedVideoRepository") as MockRepo:
            mock_repo = MockRepo.return_value
            mock_repo.list_by_project.return_value = []
            result = deduplicator.check_duplicate(fingerprint, "proj1", session)

        mock_repo.list_by_project.assert_called_once_with("proj1")
        assert result is None

    def test_project_scope_finds_duplicate(self):
        from video_processing.dedup import VideoDeduplicator

        deduplicator = VideoDeduplicator()
        fingerprint = _make_fingerprint(md5="same_md5")
        session = MagicMock()

        existing = _make_video("vid2", {"md5": "same_md5", "keyframe_phashes": ["aa"]})

        with patch("video_processing.dedup.SQLAlchemyGeneratedVideoRepository") as MockRepo:
            mock_repo = MockRepo.return_value
            mock_repo.list_by_project.return_value = [existing]
            result = deduplicator.check_duplicate(fingerprint, "proj1", session)

        assert result is not None
        assert result["duplicate"] is True
        assert result["duplicate_of"] == "vid2"


class TestCheckDuplicateScopeUser:
    """test_check_duplicate_scope_user：跨项目查重."""

    def test_user_scope_queries_by_user(self):
        from video_processing.dedup import VideoDeduplicator

        deduplicator = VideoDeduplicator()
        fingerprint = _make_fingerprint(md5="unique_md5")
        session = MagicMock()

        with patch("video_processing.dedup.SQLAlchemyGeneratedVideoRepository") as MockRepo:
            mock_repo = MockRepo.return_value
            mock_repo.list_by_user.return_value = []
            result = deduplicator.check_duplicate(
                fingerprint,
                "proj1",
                session,
                scope="user",
                user_id="user_123",
            )

        mock_repo.list_by_user.assert_called_once()
        assert result is None

    def test_user_scope_finds_cross_project_duplicate(self):
        from video_processing.dedup import VideoDeduplicator

        deduplicator = VideoDeduplicator()
        fingerprint = _make_fingerprint(md5="cross_proj_md5")
        session = MagicMock()

        # Existing video from a different project
        existing = _make_video("vid_other", {"md5": "cross_proj_md5"}, project_id="proj_other")

        with patch("video_processing.dedup.SQLAlchemyGeneratedVideoRepository") as MockRepo:
            mock_repo = MockRepo.return_value
            mock_repo.list_by_user.return_value = [existing]
            result = deduplicator.check_duplicate(
                fingerprint,
                "proj1",
                session,
                scope="user",
                user_id="user_123",
            )

        assert result is not None
        assert result["duplicate"] is True
        assert result["duplicate_of"] == "vid_other"


class TestDurationPrefilter:
    """Issue #1702: scope=user 跨项目查重不做时长预过滤。

    局部片段复用的两个视频时长必然不同（证据视频 20s vs 11s，差 42%），
    旧的 ±15% 窗口会让同源视频互相不可见 → is_duplicate 恒 False。
    全量遍历同用户视频，异源视频由 fusion/temporal_coverage 阈值天然过滤。
    """

    def test_user_scope_no_duration_filter(self):
        from video_processing.dedup import VideoDeduplicator

        deduplicator = VideoDeduplicator()
        fingerprint = _make_fingerprint(duration_ms=30000)  # 30s video
        session = MagicMock()

        with patch("video_processing.dedup.SQLAlchemyGeneratedVideoRepository") as MockRepo:
            mock_repo = MockRepo.return_value
            mock_repo.list_by_user.return_value = []
            deduplicator.check_duplicate(
                fingerprint,
                "proj1",
                session,
                scope="user",
                user_id="user1",
                duration_sec=30.0,
            )

        # scope=user 全量遍历：位置参数只传 user_id，kwargs 不含时长过滤
        call_args = mock_repo.list_by_user.call_args
        assert call_args[0] == ("user1",)
        assert "duration_min" not in call_args[1]
        assert "duration_max" not in call_args[1]

    def test_user_scope_no_duration_filter_when_zero(self):
        from video_processing.dedup import VideoDeduplicator

        deduplicator = VideoDeduplicator()
        fingerprint = _make_fingerprint()
        session = MagicMock()

        with patch("video_processing.dedup.SQLAlchemyGeneratedVideoRepository") as MockRepo:
            mock_repo = MockRepo.return_value
            mock_repo.list_by_user.return_value = []
            deduplicator.check_duplicate(
                fingerprint,
                "proj1",
                session,
                scope="user",
                user_id="user1",
                duration_sec=0,
            )

        call_args = mock_repo.list_by_user.call_args
        assert "duration_min" not in call_args[1]
        assert "duration_max" not in call_args[1]

    def test_project_scope_also_no_duration_filter(self):
        """scope=project 走 list_by_project，本来就不做时长过滤。"""
        from video_processing.dedup import VideoDeduplicator

        deduplicator = VideoDeduplicator()
        fingerprint = _make_fingerprint(duration_ms=30000)
        session = MagicMock()

        with patch("video_processing.dedup.SQLAlchemyGeneratedVideoRepository") as MockRepo:
            mock_repo = MockRepo.return_value
            mock_repo.list_by_project.return_value = []
            deduplicator.check_duplicate(
                fingerprint,
                "proj1",
                session,
                scope="project",
                user_id="user1",
                duration_sec=30.0,
            )

        mock_repo.list_by_project.assert_called_once()
        call_args = mock_repo.list_by_project.call_args
        assert call_args[0] == ("proj1",)
        assert "duration_min" not in call_args[1]
        assert "duration_max" not in call_args[1]


class TestComputeDuplicateRateFormula:
    """test_compute_duplicate_rate_formula：验证 0.4 * frame_match_rate + 0.6 * temporal_coverage_rate."""

    def test_formula_with_matching_frames(self):
        from video_processing.dedup import VideoDeduplicator

        deduplicator = VideoDeduplicator()
        # 10 frames with varied phashes (2 unique) → not bad fingerprint
        # All close in hamming distance to existing → frame_match_rate = 1.0
        phashes = ["aa00aa00aa00aa00", "ab00ab00ab00ab00"] * 5
        fingerprint = _make_fingerprint(md5="new", phashes=phashes, duration_ms=20000)
        session = MagicMock()

        # 5 unique phashes to pass _is_bad_fingerprint check (PR #1688)
        existing = _make_video(
            "vid2",
            {
                "md5": "other",
                "keyframe_phashes": [
                    "aa00aa00aa00aa00",
                    "ab00ab00ab00ab00",
                    "ac00ac00ac00ac00",
                    "aa10aa10aa10aa10",
                    "ba00ba00ba00ba00",
                ],
            },
        )

        with patch("video_processing.dedup.SQLAlchemyGeneratedVideoRepository") as MockRepo:
            mock_repo = MockRepo.return_value
            mock_repo.list_by_project.return_value = [existing]
            deduplicator._get_existing_chunks = MagicMock(return_value=[])
            rate = deduplicator.compute_duplicate_rate(fingerprint, "proj1", "vid1", session)

        # frame_match_rate=1.0, temporal_coverage depends on segments
        # duplicate_rate = (1.0 * 0.4 + temporal_coverage * 0.6) * 100
        assert rate["duplicate_rate"] >= 40.0  # At minimum, frame_match contributes 40%

    def test_no_match_returns_zero(self):
        from video_processing.dedup import VideoDeduplicator

        deduplicator = VideoDeduplicator()
        # Completely different phashes
        fingerprint = _make_fingerprint(md5="new", phashes=["ff00ff00ff00ff00"])
        session = MagicMock()

        existing = _make_video(
            "vid2",
            {"md5": "other", "keyframe_phashes": ["00ff00ff00ff00ff"]},
        )

        with patch("video_processing.dedup.SQLAlchemyGeneratedVideoRepository") as MockRepo:
            mock_repo = MockRepo.return_value
            mock_repo.list_by_project.return_value = [existing]
            deduplicator._get_existing_chunks = MagicMock(return_value=[])
            rate = deduplicator.compute_duplicate_rate(fingerprint, "proj1", "vid1", session)

        # Very different phashes, match_ratio < 0.3 → skipped
        assert rate["duplicate_rate"] == 0.0


class TestComputeDuplicateRateReturnDict:
    """test_compute_duplicate_rate_return_dict：验证返回 dict 含三个字段."""

    def test_return_structure(self):
        from video_processing.dedup import VideoDeduplicator

        deduplicator = VideoDeduplicator()
        fingerprint = _make_fingerprint()
        session = MagicMock()

        with patch("video_processing.dedup.SQLAlchemyGeneratedVideoRepository") as MockRepo:
            mock_repo = MockRepo.return_value
            mock_repo.list_by_project.return_value = []
            result = deduplicator.compute_duplicate_rate(fingerprint, "proj1", "vid1", session)

        assert isinstance(result, dict)
        assert set(result.keys()) == {"duplicate_rate", "visual_similarity", "match_count"}
        assert isinstance(result["duplicate_rate"], float)
        assert isinstance(result["visual_similarity"], float)
        assert isinstance(result["match_count"], int)
        assert 0 <= result["duplicate_rate"] <= 100
        assert 0 <= result["visual_similarity"] <= 1


class TestBackwardCompat:
    """test_backward_compat：不传 scope 时行为不变."""

    def test_default_scope_is_project(self):
        from video_processing.dedup import VideoDeduplicator

        deduplicator = VideoDeduplicator()
        fingerprint = _make_fingerprint()
        session = MagicMock()

        with patch("video_processing.dedup.SQLAlchemyGeneratedVideoRepository") as MockRepo:
            mock_repo = MockRepo.return_value
            mock_repo.list_by_project.return_value = []

            # Call without scope parameter
            result = deduplicator.compute_duplicate_rate(fingerprint, "proj1", "vid1", session)

        # Should use list_by_project (not list_by_user)
        mock_repo.list_by_project.assert_called_once_with("proj1")
        mock_repo.list_by_user.assert_not_called()
        assert result["duplicate_rate"] == 0.0

    def test_check_duplicate_default_scope_backward_compat(self):
        from video_processing.dedup import VideoDeduplicator

        deduplicator = VideoDeduplicator()
        fingerprint = _make_fingerprint()
        session = MagicMock()

        with patch("video_processing.dedup.SQLAlchemyGeneratedVideoRepository") as MockRepo:
            mock_repo = MockRepo.return_value
            mock_repo.list_by_project.return_value = []
            result = deduplicator.check_duplicate(fingerprint, "proj1", session)

        mock_repo.list_by_project.assert_called_once_with("proj1")
        assert result is None


class TestListByUserRepository:
    """直接测试 generated_video_repository.list_by_user() 的真实实现，覆盖 diff 代码行。"""

    def _make_repo(self):
        from sqlalchemy import create_engine
        from sqlalchemy.orm import sessionmaker

        from packages.adapters.sqlalchemy_impl.generated_video_repository import SQLAlchemyGeneratedVideoRepository
        from packages.adapters.sqlalchemy_impl.models import Base, GeneratedVideoModel

        engine = create_engine("sqlite:///:memory:")
        Base.metadata.create_all(engine)
        Session = sessionmaker(bind=engine)
        session = Session()
        repo = SQLAlchemyGeneratedVideoRepository(session)
        return repo, session

    def _insert_video(self, session, video_id, user_id, project_id, duration, **kw):
        from packages.adapters.sqlalchemy_impl.models import GeneratedVideoModel

        row = GeneratedVideoModel(
            id=video_id,
            user_id=user_id,
            project_id=project_id,
            generation_task_id=f"task-{video_id[:8]}",
            name=f"video-{video_id[:8]}.mp4",
            file_url=f"https://example.com/{video_id}.mp4",
            file_size=1024,
            duration=duration,
            width=1280,
            height=720,
            fps=25.0,
            status="completed",
        )
        session.add(row)
        session.flush()
        return row

    def test_list_by_user_returns_cross_project_videos(self):
        """list_by_user 返回该用户所有项目的视频。"""
        repo, session = self._make_repo()
        self._insert_video(session, "v1", "user-a", "proj-1", 30.0)
        self._insert_video(session, "v2", "user-a", "proj-2", 45.0)
        self._insert_video(session, "v3", "user-b", "proj-1", 20.0)

        results = repo.list_by_user("user-a")
        assert len(results) == 2
        ids = {r.id for r in results}
        assert ids == {"v1", "v2"}
        session.close()

    def test_list_by_user_with_duration_filter(self):
        """list_by_user 支持 duration_min/duration_max 过滤。"""
        repo, session = self._make_repo()
        self._insert_video(session, "v1", "user-a", "proj-1", 10.0)
        self._insert_video(session, "v2", "user-a", "proj-1", 30.0)
        self._insert_video(session, "v3", "user-a", "proj-1", 60.0)

        results = repo.list_by_user("user-a", duration_min=20.0, duration_max=50.0)
        assert len(results) == 1
        assert results[0].id == "v2"
        session.close()

    def test_list_by_user_empty_result(self):
        """list_by_user 无匹配时返回空列表。"""
        repo, session = self._make_repo()
        self._insert_video(session, "v1", "user-a", "proj-1", 30.0)

        results = repo.list_by_user("user-nonexistent")
        assert results == []
        session.close()
