"""去重纯算法测试 — hamming_distance + histogram_similarity + VideoFingerprint."""

from __future__ import annotations

import sys
from unittest.mock import MagicMock

import numpy as np
import pytest


def _mock_module(**attrs):
    """Create a mock module with __spec__ to avoid AttributeError: __spec__."""
    m = MagicMock()
    m.__spec__ = None
    for k, v in attrs.items():
        setattr(m, k, v)
    return m


# ── Module-level setup: mock deps, import dedup, then restore sys.modules ──
# This pattern ensures:
# 1. dedup is imported with mocks active (no db/celery/cv2 side effects)
# 2. sys.modules is restored immediately so other test files are not polluted
# 3. dedup objects are kept in module namespace for tests to use

_SAVED_MODULES_KEYS = set(sys.modules.keys())
_SAVED_MODULES_VALUES = {
    k: sys.modules.get(k)
    for k in [
        "cv2",
        "celery",
        "sqlalchemy",
        "sqlalchemy.orm",
        "sqlalchemy.engine",
        "sqlalchemy.ext",
        "sqlalchemy.ext.declarative",
        "worker_app.db",
        "worker_app.celery_app",
        "worker_app.core.config",
        "packages.adapters.sqlalchemy_impl.session",
        "packages.adapters.sqlalchemy_impl.generated_video_repository",
        "packages.shared.config",
        "packages.shared.storage",
    ]
}

# Set up mocks
sys.modules["cv2"] = _mock_module()

_mock_celery = MagicMock()
_mock_celery.Task = MagicMock
_mock_celery.Celery = MagicMock
_mock_celery.__spec__ = None
sys.modules["celery"] = _mock_celery

_mock_sqla = MagicMock()
_mock_sqla.__path__ = []
_mock_sqla.__spec__ = None
sys.modules["sqlalchemy"] = _mock_sqla

_mock_sqla_orm = MagicMock()
_mock_sqla_orm.__path__ = []
_mock_sqla_orm.__spec__ = None
_mock_sqla_orm.Session = MagicMock
sys.modules["sqlalchemy.orm"] = _mock_sqla_orm
sys.modules["sqlalchemy.engine"] = _mock_module()
sys.modules["sqlalchemy.ext"] = _mock_module()
sys.modules["sqlalchemy.ext.declarative"] = _mock_module()

sys.modules["worker_app.db"] = _mock_module(SessionLocal=MagicMock())
sys.modules["worker_app.celery_app"] = _mock_module(celery_app=MagicMock())
sys.modules["worker_app.core.config"] = _mock_module(get_settings=MagicMock(return_value=MagicMock()))

sys.modules["packages.adapters.sqlalchemy_impl.session"] = _mock_module(
    Base=MagicMock(),
    build_engine=MagicMock(),
    build_session_factory=MagicMock(),
    ensure_database_exists=MagicMock(),
    initialize_database=MagicMock(),
)
sys.modules["packages.adapters.sqlalchemy_impl.generated_video_repository"] = _mock_module()
sys.modules["packages.shared.config"] = _mock_module(get_shared_settings=MagicMock(return_value=MagicMock()))
sys.modules["packages.shared.storage"] = _mock_module()

# Import dedup while mocks are active
from video_processing.dedup import (  # noqa: E402
    VideoDeduplicator,
    VideoFingerprint,
    hamming_distance,
)

# ── Restore sys.modules immediately after import ──
# dedup is now cached in this module's namespace; other test files will get
# their own fresh imports without our mock pollution
for _key in list(sys.modules.keys()):
    if _key not in _SAVED_MODULES_KEYS:
        del sys.modules[_key]
for _key, _value in _SAVED_MODULES_VALUES.items():
    if _value is not None:
        sys.modules[_key] = _value
    elif _key in sys.modules:
        del sys.modules[_key]
del _SAVED_MODULES_KEYS, _SAVED_MODULES_VALUES, _key, _value


class TestHammingDistance:
    """hamming_distance 汉明距离计算测试."""

    def test_identical_hashes_zero(self):
        """相同哈希距离为0."""
        assert hamming_distance("ff", "ff") == 0
        assert hamming_distance("00", "00") == 0

    def test_all_different(self):
        """全不同的8bit哈希距离为8."""
        assert hamming_distance("00", "ff") == 8

    def test_single_bit_diff(self):
        """1个bit不同."""
        # 0x01 = 00000001, 0x00 = 00000000 → 1 bit不同
        assert hamming_distance("01", "00") == 1

    def test_four_bits_diff(self):
        """4个bit不同."""
        # 0x0F = 00001111, 0xF0 = 11110000 → 8 bits都不同
        assert hamming_distance("0f", "f0") == 8

    def test_longer_hashes(self):
        """更长的哈希（如64-bit pHash）."""
        # 两个完全不同的64-bit哈希
        assert hamming_distance("0000000000000000", "ffffffffffffffff") == 64

    def test_partial_difference(self):
        """部分bit不同."""
        # a = 1010, 5 = 0101 → 4 bits不同（每个hex digit）
        assert hamming_distance("aa", "55") == 8

    def test_case_insensitive(self):
        """十六进制不区分大小写."""
        assert hamming_distance("FF", "ff") == 0
        assert hamming_distance("AbC123", "aBc123") == 0

    def test_different_length_hashes(self):
        """不同长度的哈希（短的前补零）."""
        # "ff" = 0xff = 255, "0ff" = 0x0ff = 255
        # int("ff", 16) = 255, int("0ff", 16) = 255
        assert hamming_distance("ff", "0ff") == 0


class TestVideoFingerprint:
    """VideoFingerprint 数据结构测试."""

    def test_to_dict_contains_all_fields(self):
        """to_dict返回完整字典."""
        fp = VideoFingerprint(
            md5="abc123",
            keyframe_phashes=["hash1", "hash2"],
            color_histograms=[[0.1, 0.2], [0.3, 0.4]],
            duration=30.5,
            resolution=(1920, 1080),
        )
        d = fp.to_dict()
        assert d["md5"] == "abc123"
        assert d["keyframe_phashes"] == ["hash1", "hash2"]
        assert d["duration"] == 30.5
        assert d["resolution"] == [1920, 1080]
        assert "color_histograms" in d

    def test_empty_phashes(self):
        """空关键帧列表."""
        fp = VideoFingerprint(
            md5="test",
            keyframe_phashes=[],
            color_histograms=[],
            duration=0.0,
            resolution=(0, 0),
        )
        d = fp.to_dict()
        assert d["keyframe_phashes"] == []
        assert d["color_histograms"] == []


class TestBhattacharyyaCoefficient:
    """_bhattacharyya_coefficient Bhattacharyya 系数测试."""

    def test_identical_histograms(self):
        """完全相同的直方图系数为1.0（#1702：按 Σ 归一，概率分布语义）。"""
        hist = [0.5, 0.5, 0.0, 0.0]  # Σ=1 的概率分布
        bc = VideoDeduplicator._bhattacharyya_coefficient(hist, hist)
        assert bc == pytest.approx(1.0)
        # 非归一化输入也归一到 1.0（三通道拼接 Σ=3 的等价情形）
        hist3 = [0.5, 0.5, 0.0, 0.3]
        bc3 = VideoDeduplicator._bhattacharyya_coefficient(hist3, hist3)
        assert bc3 == pytest.approx(1.0)

    def test_zero_histograms(self):
        """全零直方图系数为0."""
        bc = VideoDeduplicator._bhattacharyya_coefficient([0.0, 0.0], [0.0, 0.0])
        assert bc == 0.0

    def test_orthogonal_histograms(self):
        """正交直方图（无重叠）系数为0."""
        bc = VideoDeduplicator._bhattacharyya_coefficient([1.0, 0.0], [0.0, 1.0])
        assert bc == pytest.approx(0.0)

    def test_different_lengths(self):
        """不同长度直方图取最小长度对齐，并按各自总量归一（#1702 概率分布语义）。"""
        # 对齐到前 2 维：coeff = 2，norm = √(Σa·Σb) = √(2·2) = 2 → 1.0
        bc = VideoDeduplicator._bhattacharyya_coefficient([1.0, 1.0, 0.0, 0.0], [1.0, 1.0])
        assert bc == pytest.approx(1.0)

    def test_known_value(self):
        """已知值验证."""
        # [0.25, 0.25, 0.25, 0.25] vs [0.25, 0.25, 0.25, 0.25]
        # BC = 4 * √(0.25 * 0.25) = 4 * 0.25 = 1.0
        hist = [0.25, 0.25, 0.25, 0.25]
        bc = VideoDeduplicator._bhattacharyya_coefficient(hist, hist)
        assert bc == pytest.approx(1.0)


class TestComputeHistogramSimilarity:
    """_compute_histogram_similarity 多帧直方图相似度测试."""

    def test_identical_histogram_groups(self):
        """完全相同的两组直方图."""
        hist = [[0.5, 0.5], [0.3, 0.4]]
        sim = VideoDeduplicator._compute_histogram_similarity(hist, hist)
        # Each hist finds best match = itself
        assert sim > 0.0

    def test_empty_first(self):
        """第一组为空返回0."""
        assert VideoDeduplicator._compute_histogram_similarity([], [[0.5]]) == 0.0

    def test_empty_second(self):
        """第二组为空返回0."""
        assert VideoDeduplicator._compute_histogram_similarity([[0.5]], []) == 0.0

    def test_both_empty(self):
        """两组都为空返回0."""
        assert VideoDeduplicator._compute_histogram_similarity([], []) == 0.0

    def test_best_match_selection(self):
        """多帧时取最佳匹配."""
        # ha[0] 与 hb[0] 正交，与 hb[1] 完全相同
        a = [[1.0, 0.0]]
        b = [[0.0, 1.0], [1.0, 0.0]]
        sim = VideoDeduplicator._compute_histogram_similarity(a, b)
        # Best match for [1,0]: max(BC([1,0],[0,1]), BC([1,0],[1,0])) = max(0, 1) = 1
        assert sim == pytest.approx(1.0)
