"""
Tests for Reciprocal Rank Fusion.

Tests RRF algorithm correctness, deduplication, score calculation,
and edge cases — no external services required.
"""

from django.test import TestCase

from library.services.fusion import SearchCandidate, reciprocal_rank_fusion


def _make_candidate(chunk_uid: str, score: float = 0.5, source: str = "vector", **kwargs):
    """Helper to create a SearchCandidate with defaults."""
    return SearchCandidate(
        chunk_uid=chunk_uid,
        item_uid=kwargs.get("item_uid", f"item_{chunk_uid}"),
        item_title=kwargs.get("item_title", f"Title {chunk_uid}"),
        library_type=kwargs.get("library_type", "technical"),
        text_preview=kwargs.get("text_preview", f"Preview for {chunk_uid}"),
        chunk_s3_key=kwargs.get("chunk_s3_key", f"chunks/{chunk_uid}/chunk_0.txt"),
        chunk_index=kwargs.get("chunk_index", 0),
        score=score,
        source=source,
    )


class ReciprocallRankFusionTest(TestCase):
    """Tests for reciprocal_rank_fusion()."""

    def test_single_list_preserves_order(self):
        """Single result list — RRF should preserve rank ordering."""
        candidates = [
            _make_candidate("a", score=0.9),
            _make_candidate("b", score=0.8),
            _make_candidate("c", score=0.7),
        ]

        fused = reciprocal_rank_fusion([candidates], k=60)

        self.assertEqual(len(fused), 3)
        self.assertEqual(fused[0].chunk_uid, "a")
        self.assertEqual(fused[1].chunk_uid, "b")
        self.assertEqual(fused[2].chunk_uid, "c")

    def test_duplicate_candidate_gets_boosted(self):
        """Candidate appearing in multiple lists gets higher score."""
        list1 = [
            _make_candidate("a", score=0.9, source="vector"),
            _make_candidate("b", score=0.7, source="vector"),
        ]
        list2 = [
            _make_candidate("a", score=0.8, source="fulltext"),
            _make_candidate("c", score=0.6, source="fulltext"),
        ]

        fused = reciprocal_rank_fusion([list1, list2], k=60)

        # 'a' appears in both lists → should have highest RRF score
        self.assertEqual(fused[0].chunk_uid, "a")
        # RRF score for 'a': 1/(60+1) + 1/(60+1) ≈ 0.0328
        # RRF score for 'b': 1/(60+2) ≈ 0.0161
        # RRF score for 'c': 1/(60+2) ≈ 0.0161
        self.assertGreater(fused[0].score, fused[1].score)

    def test_deduplication_by_chunk_uid(self):
        """Same chunk_uid in multiple lists results in one entry."""
        list1 = [_make_candidate("a", source="vector")]
        list2 = [_make_candidate("a", source="fulltext")]
        list3 = [_make_candidate("a", source="graph")]

        fused = reciprocal_rank_fusion([list1, list2, list3], k=60)

        self.assertEqual(len(fused), 1)
        self.assertEqual(fused[0].chunk_uid, "a")

    def test_source_tracking(self):
        """Fused candidate tracks which sources found it."""
        list1 = [_make_candidate("a", source="vector")]
        list2 = [_make_candidate("a", source="fulltext")]

        fused = reciprocal_rank_fusion([list1, list2], k=60)

        self.assertIn("vector", fused[0].source)
        self.assertIn("fulltext", fused[0].source)
        self.assertIn("sources", fused[0].metadata)
        self.assertIn("vector", fused[0].metadata["sources"])
        self.assertIn("fulltext", fused[0].metadata["sources"])

    def test_empty_lists(self):
        """Empty input lists produce empty output."""
        fused = reciprocal_rank_fusion([], k=60)
        self.assertEqual(fused, [])

        fused = reciprocal_rank_fusion([[], []], k=60)
        self.assertEqual(fused, [])

    def test_limit_trims_results(self):
        """Limit parameter trims output size."""
        candidates = [_make_candidate(f"c{i}") for i in range(20)]

        fused = reciprocal_rank_fusion([candidates], k=60, limit=5)

        self.assertEqual(len(fused), 5)

    def test_rrf_score_calculation(self):
        """Verify exact RRF score for a known case."""
        k = 60
        list1 = [_make_candidate("a", source="vector")]  # rank 1
        list2 = [
            _make_candidate("b", source="fulltext"),  # rank 1
            _make_candidate("a", source="fulltext"),  # rank 2
        ]

        fused = reciprocal_rank_fusion([list1, list2], k=k)

        # 'a': 1/(60+1) + 1/(60+2) = 0.01639... + 0.01613... = 0.03253...
        a = next(c for c in fused if c.chunk_uid == "a")
        expected = 1 / (k + 1) + 1 / (k + 2)
        self.assertAlmostEqual(a.score, expected, places=6)

        # 'b': 1/(60+1) = 0.01639...
        b = next(c for c in fused if c.chunk_uid == "b")
        expected_b = 1 / (k + 1)
        self.assertAlmostEqual(b.score, expected_b, places=6)

    def test_higher_k_reduces_rank_emphasis(self):
        """Higher k makes rank differences less significant."""
        list1 = [
            _make_candidate("top", source="vector"),
            _make_candidate("bottom", source="vector"),
        ]

        fused_low_k = reciprocal_rank_fusion([list1], k=1)
        fused_high_k = reciprocal_rank_fusion([list1], k=100)

        # Ratio of rank-1 to rank-2 scores
        ratio_low = fused_low_k[0].score / fused_low_k[1].score
        ratio_high = fused_high_k[0].score / fused_high_k[1].score

        # Higher k → scores closer together → lower ratio
        self.assertGreater(ratio_low, ratio_high)

    def test_keeps_highest_original_score_for_metadata(self):
        """When deduplicating, keeps candidate with highest original score."""
        list1 = [_make_candidate("a", score=0.5, source="vector", item_title="Low")]
        list2 = [_make_candidate("a", score=0.9, source="fulltext", item_title="High")]

        fused = reciprocal_rank_fusion([list1, list2], k=60)

        # Should keep the candidate from list2 (higher original score)
        self.assertEqual(fused[0].item_title, "High")
