"""Tests for rbsp.indexer.positions -- PositionIndex."""

import os
import sys
import unittest

sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))

from rbsp.indexer.positions import PositionIndex


# ---------------------------------------------------------------------------
# Basic add / remove / lookup
# ---------------------------------------------------------------------------

class TestPositionIndexAddRemove(unittest.TestCase):
    """Test add_document, remove_document, and basic lookups."""

    def setUp(self):
        self.pidx = PositionIndex()

    def test_add_document_and_get_positions(self):
        self.pidx.add_document("doc1", ["the", "quick", "brown", "fox"])
        self.assertEqual(self.pidx.get_positions("doc1", "quick"), [1])
        self.assertEqual(self.pidx.get_positions("doc1", "fox"), [3])

    def test_get_positions_missing_doc(self):
        self.assertEqual(self.pidx.get_positions("missing", "word"), [])

    def test_get_positions_missing_term(self):
        self.pidx.add_document("doc1", ["hello"])
        self.assertEqual(self.pidx.get_positions("doc1", "missing"), [])

    def test_duplicate_term_positions(self):
        self.pidx.add_document("doc1", ["go", "go", "go"])
        self.assertEqual(self.pidx.get_positions("doc1", "go"), [0, 1, 2])

    def test_add_document_replaces_old_data(self):
        self.pidx.add_document("doc1", ["old", "data"])
        self.pidx.add_document("doc1", ["new", "content"])
        self.assertEqual(self.pidx.get_positions("doc1", "new"), [0])
        self.assertEqual(self.pidx.get_positions("doc1", "old"), [])

    def test_remove_document_returns_true(self):
        self.pidx.add_document("doc1", ["hello"])
        self.assertTrue(self.pidx.remove_document("doc1"))

    def test_remove_nonexistent_document_returns_false(self):
        self.assertFalse(self.pidx.remove_document("nope"))

    def test_remove_clears_positions(self):
        self.pidx.add_document("doc1", ["hello", "world"])
        self.pidx.remove_document("doc1")
        self.assertEqual(self.pidx.get_positions("doc1", "hello"), [])

    def test_remove_cleans_up_term_entries(self):
        self.pidx.add_document("doc1", ["unique_term"])
        self.pidx.remove_document("doc1")
        self.assertEqual(self.pidx.term_count(), 0)


# ---------------------------------------------------------------------------
# Phrase match
# ---------------------------------------------------------------------------

class TestPositionIndexPhraseMatch(unittest.TestCase):
    """Test phrase_match with and without slop."""

    def setUp(self):
        self.pidx = PositionIndex()
        self.pidx.add_document("doc1", ["the", "quick", "brown", "fox", "jumps"])

    def test_exact_phrase_match(self):
        self.assertTrue(self.pidx.phrase_match("doc1", ["quick", "brown"]))

    def test_exact_phrase_match_three_terms(self):
        self.assertTrue(self.pidx.phrase_match("doc1", ["quick", "brown", "fox"]))

    def test_reversed_phrase_no_match(self):
        self.assertFalse(self.pidx.phrase_match("doc1", ["brown", "quick"]))

    def test_non_adjacent_phrase_no_match(self):
        self.assertFalse(self.pidx.phrase_match("doc1", ["quick", "fox"]))

    def test_phrase_with_slop_1(self):
        # "quick" at 1, "fox" at 3 -- gap of 1 beyond adjacency
        self.assertTrue(self.pidx.phrase_match("doc1", ["quick", "fox"], slop=1))

    def test_phrase_with_insufficient_slop(self):
        # "quick" at 1, "jumps" at 4 -- gap of 2 beyond adjacency
        self.assertFalse(self.pidx.phrase_match("doc1", ["quick", "jumps"], slop=1))

    def test_phrase_with_slop_2(self):
        self.assertTrue(self.pidx.phrase_match("doc1", ["quick", "jumps"], slop=2))

    def test_empty_phrase_always_matches(self):
        self.assertTrue(self.pidx.phrase_match("doc1", []))

    def test_single_term_phrase(self):
        self.assertTrue(self.pidx.phrase_match("doc1", ["quick"]))
        self.assertFalse(self.pidx.phrase_match("doc1", ["missing"]))

    def test_phrase_match_missing_doc(self):
        self.assertFalse(self.pidx.phrase_match("missing", ["quick", "brown"]))

    def test_phrase_match_missing_term(self):
        self.assertFalse(self.pidx.phrase_match("doc1", ["quick", "nonexistent"]))


# ---------------------------------------------------------------------------
# Phrase search
# ---------------------------------------------------------------------------

class TestPositionIndexPhraseSearch(unittest.TestCase):
    """Test phrase_search across multiple documents."""

    def setUp(self):
        self.pidx = PositionIndex()
        self.pidx.add_document("doc1", ["the", "quick", "brown", "fox"])
        self.pidx.add_document("doc2", ["quick", "brown", "dog"])
        self.pidx.add_document("doc3", ["brown", "quick", "cat"])

    def test_phrase_found_in_multiple_docs(self):
        results = self.pidx.phrase_search(["quick", "brown"])
        self.assertEqual(results, {"doc1", "doc2"})

    def test_phrase_found_in_one_doc(self):
        results = self.pidx.phrase_search(["brown", "fox"])
        self.assertEqual(results, {"doc1"})

    def test_phrase_not_found(self):
        results = self.pidx.phrase_search(["fox", "dog"])
        self.assertEqual(results, set())

    def test_empty_phrase_returns_empty(self):
        results = self.pidx.phrase_search([])
        self.assertEqual(results, set())

    def test_phrase_search_with_slop(self):
        # doc3 has "brown" at 0, "cat" at 2 -- slop=1 should match
        results = self.pidx.phrase_search(["brown", "cat"], slop=1)
        self.assertEqual(results, {"doc3"})


# ---------------------------------------------------------------------------
# Proximity score
# ---------------------------------------------------------------------------

class TestPositionIndexProximityScore(unittest.TestCase):
    """Test proximity_score calculations."""

    def setUp(self):
        self.pidx = PositionIndex()
        self.pidx.add_document("doc1", ["the", "quick", "brown", "fox"])

    def test_adjacent_terms_high_score(self):
        # "quick" at 1, "brown" at 2 -> span = 2, score = 0.5
        score = self.pidx.proximity_score("doc1", ["quick", "brown"])
        self.assertAlmostEqual(score, 0.5)

    def test_distant_terms_lower_score(self):
        # "the" at 0, "fox" at 3 -> span = 4, score = 0.25
        score = self.pidx.proximity_score("doc1", ["the", "fox"])
        self.assertAlmostEqual(score, 0.25)

    def test_single_term_returns_one(self):
        score = self.pidx.proximity_score("doc1", ["quick"])
        self.assertAlmostEqual(score, 1.0)

    def test_missing_term_returns_zero(self):
        score = self.pidx.proximity_score("doc1", ["quick", "missing"])
        self.assertAlmostEqual(score, 0.0)

    def test_missing_doc_returns_zero(self):
        score = self.pidx.proximity_score("missing", ["quick"])
        self.assertAlmostEqual(score, 0.0)

    def test_empty_terms_returns_zero(self):
        score = self.pidx.proximity_score("doc1", [])
        self.assertAlmostEqual(score, 0.0)


# ---------------------------------------------------------------------------
# Info methods
# ---------------------------------------------------------------------------

class TestPositionIndexInfo(unittest.TestCase):
    """Test doc_count, term_count, __contains__, __len__, clear."""

    def setUp(self):
        self.pidx = PositionIndex()

    def test_empty_counts(self):
        self.assertEqual(self.pidx.doc_count(), 0)
        self.assertEqual(self.pidx.term_count(), 0)
        self.assertEqual(len(self.pidx), 0)

    def test_doc_count_after_add(self):
        self.pidx.add_document("d1", ["a"])
        self.pidx.add_document("d2", ["b"])
        self.assertEqual(self.pidx.doc_count(), 2)

    def test_term_count_after_add(self):
        self.pidx.add_document("d1", ["alpha", "beta", "gamma"])
        self.assertEqual(self.pidx.term_count(), 3)

    def test_term_count_shared_terms(self):
        self.pidx.add_document("d1", ["hello", "world"])
        self.pidx.add_document("d2", ["hello", "there"])
        self.assertEqual(self.pidx.term_count(), 3)  # hello, world, there

    def test_contains_present_doc(self):
        self.pidx.add_document("doc1", ["x"])
        self.assertIn("doc1", self.pidx)

    def test_contains_absent_doc(self):
        self.assertNotIn("doc1", self.pidx)

    def test_len_equals_doc_count(self):
        self.pidx.add_document("d1", ["a"])
        self.pidx.add_document("d2", ["b"])
        self.assertEqual(len(self.pidx), self.pidx.doc_count())

    def test_clear_removes_everything(self):
        self.pidx.add_document("d1", ["x", "y"])
        self.pidx.add_document("d2", ["z"])
        self.pidx.clear()
        self.assertEqual(self.pidx.doc_count(), 0)
        self.assertEqual(self.pidx.term_count(), 0)
        self.assertNotIn("d1", self.pidx)


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