"""Tests for rbsp.query.ranker -- ResultRanker, RankingWeights, RankedResult, rerank_with_filters."""

import os
import sys
import unittest

sys.path.insert(0, os.path.dirname(__file__))
from conftest import RBSPTestCase, FIXED_TIMESTAMP, SAMPLE_TEXTS, SAMPLE_CODE, SAMPLE_PROSE

from rbsp.query.ranker import ResultRanker, RankingWeights, RankedResult
from rbsp.query.resonance import ResonanceHit
from rbsp.core.encoding import NaturalSignature


def _make_hit(item_id, exact=0.0, semantic=0.0, structural=0.0, timestamp=FIXED_TIMESTAMP, metadata=None):
    """Helper to create a ResonanceHit."""
    sig = NaturalSignature(content_hash=hash(item_id) & 0xFFFFFFFFFFFFFFFF, timestamp=timestamp)
    resonance = 0.6 * exact + 0.3 * semantic + 0.1 * structural
    return ResonanceHit(
        item_id=item_id,
        signature=sig,
        resonance_score=resonance,
        exact_score=exact,
        semantic_score=semantic,
        structural_score=structural,
        metadata=metadata,
    )


# ---------------------------------------------------------------------------
# RankingWeights
# ---------------------------------------------------------------------------

class TestRankingWeightsDefault(unittest.TestCase):
    """Test default weight configuration."""

    def test_default_values(self):
        w = RankingWeights()
        self.assertAlmostEqual(w.exact, 0.6)
        self.assertAlmostEqual(w.semantic, 0.3)
        self.assertAlmostEqual(w.recency, 0.1)

    def test_total(self):
        w = RankingWeights()
        self.assertAlmostEqual(w.total, 1.0)


class TestRankingWeightsCustom(unittest.TestCase):
    """Test custom weights."""

    def test_custom_values(self):
        w = RankingWeights(exact=0.5, semantic=0.3, recency=0.2)
        self.assertAlmostEqual(w.exact, 0.5)
        self.assertAlmostEqual(w.total, 1.0)

    def test_non_unit_total(self):
        w = RankingWeights(exact=1.0, semantic=1.0, recency=1.0)
        self.assertAlmostEqual(w.total, 3.0)


class TestRankingWeightsValidation(unittest.TestCase):
    """Test weight validation."""

    def test_all_zero_raises(self):
        with self.assertRaises(ValueError):
            RankingWeights(exact=0.0, semantic=0.0, recency=0.0)

    def test_negative_total_raises(self):
        with self.assertRaises(ValueError):
            RankingWeights(exact=-1.0, semantic=0.0, recency=0.0)


# ---------------------------------------------------------------------------
# ResultRanker.rank()
# ---------------------------------------------------------------------------

class TestResultRankerRank(unittest.TestCase):
    """Test the rank() method."""

    def setUp(self):
        self.ranker = ResultRanker()

    def test_rank_single_hit(self):
        hits = [_make_hit("a", exact=1.0)]
        results = self.ranker.rank(hits, now=FIXED_TIMESTAMP)
        self.assertEqual(len(results), 1)
        self.assertEqual(results[0].rank, 1)
        self.assertEqual(results[0].item_id, "a")

    def test_rank_ordering_by_score(self):
        hits = [
            _make_hit("low", exact=0.2),
            _make_hit("high", exact=1.0),
            _make_hit("mid", exact=0.5),
        ]
        results = self.ranker.rank(hits, now=FIXED_TIMESTAMP)
        self.assertEqual(results[0].item_id, "high")
        self.assertEqual(results[-1].item_id, "low")

    def test_rank_numbers_are_sequential(self):
        hits = [_make_hit(f"item_{i}", exact=1.0 - i * 0.1) for i in range(5)]
        results = self.ranker.rank(hits, now=FIXED_TIMESTAMP)
        for i, r in enumerate(results, start=1):
            self.assertEqual(r.rank, i)

    def test_top_k_limits_results(self):
        hits = [_make_hit(f"item_{i}", exact=0.5) for i in range(10)]
        results = self.ranker.rank(hits, top_k=3, now=FIXED_TIMESTAMP)
        self.assertEqual(len(results), 3)

    def test_score_threshold(self):
        ranker = ResultRanker(score_threshold=0.5)
        hits = [
            _make_hit("above", exact=1.0),
            _make_hit("below", exact=0.1, semantic=0.0),
        ]
        results = ranker.rank(hits, now=FIXED_TIMESTAMP)
        item_ids = [r.item_id for r in results]
        self.assertIn("above", item_ids)
        # "below" has very low score: exact=0.1 -> 0.6*0.1/1.0 + recency contribution
        # The recency is 1.0 if now == timestamp, so: (0.6*0.1 + 0.3*0 + 0.1*1.0)/1.0 = 0.16
        # Should be below 0.5 threshold
        self.assertNotIn("below", item_ids)


class TestResultRankerRecency(unittest.TestCase):
    """Test recency score decay."""

    def test_recent_item_high_recency(self):
        ranker = ResultRanker()
        now = FIXED_TIMESTAMP
        hits = [_make_hit("recent", exact=0.5, timestamp=now)]
        results = ranker.rank(hits, now=now)
        self.assertAlmostEqual(results[0].recency_score, 1.0)

    def test_old_item_low_recency(self):
        ranker = ResultRanker(recency_halflife=86400.0)
        now = FIXED_TIMESTAMP + 86400.0 * 10  # 10 half-lives later
        hits = [_make_hit("old", exact=0.5, timestamp=FIXED_TIMESTAMP)]
        results = ranker.rank(hits, now=now)
        self.assertLess(results[0].recency_score, 0.01)

    def test_halflife_decay(self):
        ranker = ResultRanker(recency_halflife=86400.0)
        now = FIXED_TIMESTAMP + 86400.0  # Exactly one half-life
        hits = [_make_hit("half", exact=0.5, timestamp=FIXED_TIMESTAMP)]
        results = ranker.rank(hits, now=now)
        self.assertAlmostEqual(results[0].recency_score, 0.5, places=5)


class TestResultRankerBoost(unittest.TestCase):
    """Test boost functions."""

    def test_boost_fn_applied(self):
        def double_boost(item_id, metadata):
            return 2.0

        ranker = ResultRanker(boost_fn=double_boost)
        hits = [_make_hit("boosted", exact=0.4)]
        results = ranker.rank(hits, now=FIXED_TIMESTAMP)
        self.assertGreater(results[0].boost, 1.0)

    def test_boost_capped_at_1(self):
        def huge_boost(item_id, metadata):
            return 100.0

        ranker = ResultRanker(boost_fn=huge_boost)
        hits = [_make_hit("boosted", exact=0.5)]
        results = ranker.rank(hits, now=FIXED_TIMESTAMP)
        self.assertLessEqual(results[0].final_score, 1.0)

    def test_no_boost_fn_default_1(self):
        ranker = ResultRanker()
        hits = [_make_hit("normal", exact=0.5)]
        results = ranker.rank(hits, now=FIXED_TIMESTAMP)
        self.assertAlmostEqual(results[0].boost, 1.0)


# ---------------------------------------------------------------------------
# RankedResult
# ---------------------------------------------------------------------------

class TestRankedResult(unittest.TestCase):
    """Test RankedResult dataclass."""

    def test_fields(self):
        r = RankedResult(
            rank=1,
            item_id="test",
            final_score=0.9,
            exact_score=0.8,
            semantic_score=0.1,
            recency_score=0.5,
            boost=1.2,
            metadata={"key": "val"},
        )
        self.assertEqual(r.rank, 1)
        self.assertEqual(r.item_id, "test")
        self.assertAlmostEqual(r.final_score, 0.9)
        self.assertEqual(r.metadata, {"key": "val"})

    def test_default_values(self):
        r = RankedResult(rank=1, item_id="x", final_score=0.5)
        self.assertEqual(r.exact_score, 0.0)
        self.assertEqual(r.semantic_score, 0.0)
        self.assertEqual(r.recency_score, 0.0)
        self.assertAlmostEqual(r.boost, 1.0)
        self.assertIsNone(r.metadata)


# ---------------------------------------------------------------------------
# rerank_with_filters
# ---------------------------------------------------------------------------

class TestRerankWithFilters(unittest.TestCase):
    """Test rerank_with_filters."""

    def setUp(self):
        self.ranker = ResultRanker()

    def _make_ranked(self, item_id, rank, metadata=None):
        return RankedResult(
            rank=rank,
            item_id=item_id,
            final_score=1.0 - rank * 0.1,
            metadata=metadata,
        )

    def test_type_filter(self):
        results = [
            self._make_ranked("a", 1, {"type": "py", "path": "src/a.py"}),
            self._make_ranked("b", 2, {"type": "js", "path": "src/b.js"}),
            self._make_ranked("c", 3, {"type": "py", "path": "src/c.py"}),
        ]
        filtered = self.ranker.rerank_with_filters(results, type_filters=("py",))
        self.assertEqual(len(filtered), 2)
        self.assertEqual(filtered[0].item_id, "a")
        self.assertEqual(filtered[1].item_id, "c")

    def test_path_filter(self):
        results = [
            self._make_ranked("a", 1, {"type": "py", "path": "src/main.py"}),
            self._make_ranked("b", 2, {"type": "py", "path": "tests/test.py"}),
        ]
        filtered = self.ranker.rerank_with_filters(results, path_filters=("src/",))
        self.assertEqual(len(filtered), 1)
        self.assertEqual(filtered[0].item_id, "a")

    def test_excluded_terms_filter(self):
        results = [
            self._make_ranked("a", 1, {"content": "hello world"}),
            self._make_ranked("b", 2, {"content": "test data"}),
        ]
        filtered = self.ranker.rerank_with_filters(results, excluded_terms=("test",))
        self.assertEqual(len(filtered), 1)
        self.assertEqual(filtered[0].item_id, "a")

    def test_renumbered_ranks(self):
        results = [
            self._make_ranked("a", 1, {"type": "py"}),
            self._make_ranked("b", 2, {"type": "js"}),
            self._make_ranked("c", 3, {"type": "py"}),
        ]
        filtered = self.ranker.rerank_with_filters(results, type_filters=("py",))
        self.assertEqual(filtered[0].rank, 1)
        self.assertEqual(filtered[1].rank, 2)

    def test_no_filters_returns_same(self):
        results = [self._make_ranked("a", 1, {"type": "py"})]
        filtered = self.ranker.rerank_with_filters(results)
        self.assertEqual(len(filtered), 1)
        self.assertEqual(filtered[0].item_id, "a")

    def test_all_filtered_out(self):
        results = [
            self._make_ranked("a", 1, {"type": "py"}),
        ]
        filtered = self.ranker.rerank_with_filters(results, type_filters=("rs",))
        self.assertEqual(len(filtered), 0)


# ---------------------------------------------------------------------------
# Edge cases
# ---------------------------------------------------------------------------

class TestResultRankerEdgeCases(unittest.TestCase):
    """Test edge cases."""

    def test_empty_hits(self):
        ranker = ResultRanker()
        results = ranker.rank([], now=FIXED_TIMESTAMP)
        self.assertEqual(results, [])

    def test_all_below_threshold(self):
        ranker = ResultRanker(score_threshold=0.99)
        hits = [_make_hit("a", exact=0.1, semantic=0.1)]
        results = ranker.rank(hits, now=FIXED_TIMESTAMP)
        self.assertEqual(len(results), 0)


if __name__ == "__main__":
    unittest.main()
