"""Tests for rbsp.indexer.docvalues -- Columnar document values."""

import os
import sys
import threading
import unittest

sys.path.insert(0, os.path.dirname(__file__))
from conftest import RBSPTestCase

from rbsp.indexer.docvalues import (
    DocValueType,
    NumericDocValues,
    BinaryDocValues,
    SortedDocValues,
    SortedSetDocValues,
    DocValuesStore,
)


# ---------------------------------------------------------------------------
# NumericDocValues tests
# ---------------------------------------------------------------------------

class TestNumericDocValues(unittest.TestCase):
    """Tests for NumericDocValues."""

    def setUp(self):
        self.nv = NumericDocValues()

    def test_type(self):
        self.assertEqual(self.nv.type, DocValueType.NUMERIC)

    def test_set_and_get(self):
        self.nv.set("doc1", 42)
        self.assertEqual(self.nv.get("doc1"), 42)

    def test_get_missing(self):
        self.assertIsNone(self.nv.get("missing"))

    def test_overwrite(self):
        self.nv.set("doc1", 10)
        self.nv.set("doc1", 20)
        self.assertEqual(self.nv.get("doc1"), 20)

    def test_float_values(self):
        self.nv.set("doc1", 3.14)
        self.assertAlmostEqual(self.nv.get("doc1"), 3.14)

    def test_negative_values(self):
        self.nv.set("doc1", -100)
        self.assertEqual(self.nv.get("doc1"), -100)

    def test_delete(self):
        self.nv.set("doc1", 42)
        self.assertTrue(self.nv.delete("doc1"))
        self.assertIsNone(self.nv.get("doc1"))

    def test_delete_missing(self):
        self.assertFalse(self.nv.delete("missing"))

    def test_doc_count(self):
        self.assertEqual(self.nv.doc_count(), 0)
        self.nv.set("doc1", 1)
        self.nv.set("doc2", 2)
        self.assertEqual(self.nv.doc_count(), 2)

    def test_sort_docs_ascending(self):
        self.nv.set("doc1", 30)
        self.nv.set("doc2", 10)
        self.nv.set("doc3", 20)
        result = self.nv.sort_docs(["doc1", "doc2", "doc3"])
        self.assertEqual(result, ["doc2", "doc3", "doc1"])

    def test_sort_docs_descending(self):
        self.nv.set("doc1", 30)
        self.nv.set("doc2", 10)
        self.nv.set("doc3", 20)
        result = self.nv.sort_docs(["doc1", "doc2", "doc3"], reverse=True)
        self.assertEqual(result, ["doc1", "doc3", "doc2"])

    def test_sort_docs_missing_at_end(self):
        self.nv.set("doc1", 10)
        self.nv.set("doc3", 5)
        result = self.nv.sort_docs(["doc1", "doc2", "doc3"])
        # doc2 has no value => placed at end
        self.assertEqual(result[:2], ["doc3", "doc1"])
        self.assertEqual(result[2], "doc2")

    def test_sort_empty_list(self):
        self.assertEqual(self.nv.sort_docs([]), [])

    def test_range_query_both_bounds(self):
        self.nv.set("doc1", 10)
        self.nv.set("doc2", 20)
        self.nv.set("doc3", 30)
        self.nv.set("doc4", 40)
        result = self.nv.range_query(15, 35)
        self.assertEqual(result, {"doc2", "doc3"})

    def test_range_query_inclusive(self):
        self.nv.set("doc1", 10)
        self.nv.set("doc2", 20)
        result = self.nv.range_query(10, 20)
        self.assertEqual(result, {"doc1", "doc2"})

    def test_range_query_no_lower(self):
        self.nv.set("doc1", 10)
        self.nv.set("doc2", 20)
        result = self.nv.range_query(None, 15)
        self.assertEqual(result, {"doc1"})

    def test_range_query_no_upper(self):
        self.nv.set("doc1", 10)
        self.nv.set("doc2", 20)
        result = self.nv.range_query(15, None)
        self.assertEqual(result, {"doc2"})

    def test_range_query_unbounded(self):
        self.nv.set("doc1", 10)
        self.nv.set("doc2", 20)
        result = self.nv.range_query(None, None)
        self.assertEqual(result, {"doc1", "doc2"})

    def test_range_query_empty(self):
        self.nv.set("doc1", 10)
        result = self.nv.range_query(100, 200)
        self.assertEqual(result, set())

    def test_clear(self):
        self.nv.set("doc1", 42)
        self.nv.clear()
        self.assertEqual(self.nv.doc_count(), 0)
        self.assertIsNone(self.nv.get("doc1"))

    def test_thread_safety(self):
        errors = []

        def writer(start):
            try:
                for i in range(100):
                    self.nv.set(f"doc_{start + i}", start + i)
            except Exception as e:
                errors.append(e)

        threads = [threading.Thread(target=writer, args=(i * 100,)) for i in range(4)]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        self.assertEqual(errors, [])
        self.assertEqual(self.nv.doc_count(), 400)


# ---------------------------------------------------------------------------
# BinaryDocValues tests
# ---------------------------------------------------------------------------

class TestBinaryDocValues(unittest.TestCase):
    """Tests for BinaryDocValues."""

    def setUp(self):
        self.bv = BinaryDocValues()

    def test_type(self):
        self.assertEqual(self.bv.type, DocValueType.BINARY)

    def test_set_and_get(self):
        self.bv.set("doc1", b"\x00\x01\x02")
        self.assertEqual(self.bv.get("doc1"), b"\x00\x01\x02")

    def test_get_missing(self):
        self.assertIsNone(self.bv.get("missing"))

    def test_overwrite(self):
        self.bv.set("doc1", b"old")
        self.bv.set("doc1", b"new")
        self.assertEqual(self.bv.get("doc1"), b"new")

    def test_empty_bytes(self):
        self.bv.set("doc1", b"")
        self.assertEqual(self.bv.get("doc1"), b"")

    def test_delete(self):
        self.bv.set("doc1", b"data")
        self.assertTrue(self.bv.delete("doc1"))
        self.assertIsNone(self.bv.get("doc1"))

    def test_delete_missing(self):
        self.assertFalse(self.bv.delete("missing"))

    def test_doc_count(self):
        self.assertEqual(self.bv.doc_count(), 0)
        self.bv.set("doc1", b"a")
        self.bv.set("doc2", b"b")
        self.assertEqual(self.bv.doc_count(), 2)

    def test_clear(self):
        self.bv.set("doc1", b"data")
        self.bv.clear()
        self.assertEqual(self.bv.doc_count(), 0)


# ---------------------------------------------------------------------------
# SortedDocValues tests
# ---------------------------------------------------------------------------

class TestSortedDocValues(unittest.TestCase):
    """Tests for SortedDocValues."""

    def setUp(self):
        self.sv = SortedDocValues()

    def test_type(self):
        self.assertEqual(self.sv.type, DocValueType.SORTED)

    def test_set_and_get(self):
        self.sv.set("doc1", "banana")
        self.assertEqual(self.sv.get("doc1"), "banana")

    def test_get_missing(self):
        self.assertIsNone(self.sv.get("missing"))

    def test_overwrite(self):
        self.sv.set("doc1", "old")
        self.sv.set("doc1", "new")
        self.assertEqual(self.sv.get("doc1"), "new")

    def test_delete(self):
        self.sv.set("doc1", "value")
        self.assertTrue(self.sv.delete("doc1"))
        self.assertIsNone(self.sv.get("doc1"))

    def test_delete_missing(self):
        self.assertFalse(self.sv.delete("missing"))

    def test_ordinal_basic(self):
        self.sv.set("doc1", "banana")
        self.sv.set("doc2", "apple")
        self.sv.set("doc3", "cherry")
        # Sorted: apple=0, banana=1, cherry=2
        self.assertEqual(self.sv.ordinal("doc2"), 0)
        self.assertEqual(self.sv.ordinal("doc1"), 1)
        self.assertEqual(self.sv.ordinal("doc3"), 2)

    def test_ordinal_missing_raises(self):
        with self.assertRaises(KeyError):
            self.sv.ordinal("missing")

    def test_ordinal_after_overwrite(self):
        self.sv.set("doc1", "banana")
        self.sv.set("doc2", "apple")
        # Replace banana with cherry; banana is gone
        self.sv.set("doc1", "cherry")
        # Sorted: apple=0, cherry=1
        self.assertEqual(self.sv.ordinal("doc2"), 0)
        self.assertEqual(self.sv.ordinal("doc1"), 1)

    def test_ordinal_duplicate_values(self):
        self.sv.set("doc1", "apple")
        self.sv.set("doc2", "apple")
        self.sv.set("doc3", "banana")
        # Sorted: apple=0, banana=1
        self.assertEqual(self.sv.ordinal("doc1"), 0)
        self.assertEqual(self.sv.ordinal("doc2"), 0)
        self.assertEqual(self.sv.ordinal("doc3"), 1)

    def test_sort_docs_ascending(self):
        self.sv.set("doc1", "cherry")
        self.sv.set("doc2", "apple")
        self.sv.set("doc3", "banana")
        result = self.sv.sort_docs(["doc1", "doc2", "doc3"])
        self.assertEqual(result, ["doc2", "doc3", "doc1"])

    def test_sort_docs_descending(self):
        self.sv.set("doc1", "cherry")
        self.sv.set("doc2", "apple")
        self.sv.set("doc3", "banana")
        result = self.sv.sort_docs(["doc1", "doc2", "doc3"], reverse=True)
        self.assertEqual(result, ["doc1", "doc3", "doc2"])

    def test_sort_docs_missing_at_end(self):
        self.sv.set("doc1", "banana")
        self.sv.set("doc3", "apple")
        result = self.sv.sort_docs(["doc1", "doc2", "doc3"])
        self.assertEqual(result[:2], ["doc3", "doc1"])
        self.assertEqual(result[2], "doc2")

    def test_doc_count(self):
        self.assertEqual(self.sv.doc_count(), 0)
        self.sv.set("doc1", "a")
        self.sv.set("doc2", "b")
        self.assertEqual(self.sv.doc_count(), 2)

    def test_value_count(self):
        self.sv.set("doc1", "apple")
        self.sv.set("doc2", "apple")
        self.sv.set("doc3", "banana")
        self.assertEqual(self.sv.value_count(), 2)

    def test_value_count_after_delete(self):
        self.sv.set("doc1", "apple")
        self.sv.set("doc2", "banana")
        self.sv.delete("doc1")
        # "apple" no longer referenced
        self.assertEqual(self.sv.value_count(), 1)

    def test_clear(self):
        self.sv.set("doc1", "a")
        self.sv.clear()
        self.assertEqual(self.sv.doc_count(), 0)
        self.assertEqual(self.sv.value_count(), 0)


# ---------------------------------------------------------------------------
# SortedSetDocValues tests
# ---------------------------------------------------------------------------

class TestSortedSetDocValues(unittest.TestCase):
    """Tests for SortedSetDocValues."""

    def setUp(self):
        self.ssv = SortedSetDocValues()

    def test_type(self):
        self.assertEqual(self.ssv.type, DocValueType.SORTED_SET)

    def test_add_and_get(self):
        self.ssv.add("doc1", "red")
        self.ssv.add("doc1", "blue")
        self.assertEqual(self.ssv.get("doc1"), frozenset({"red", "blue"}))

    def test_get_missing(self):
        self.assertEqual(self.ssv.get("missing"), frozenset())

    def test_add_duplicate(self):
        self.ssv.add("doc1", "red")
        self.ssv.add("doc1", "red")
        self.assertEqual(self.ssv.get("doc1"), frozenset({"red"}))

    def test_docs_with_value(self):
        self.ssv.add("doc1", "red")
        self.ssv.add("doc2", "blue")
        self.ssv.add("doc3", "red")
        self.assertEqual(self.ssv.docs_with_value("red"), {"doc1", "doc3"})
        self.assertEqual(self.ssv.docs_with_value("blue"), {"doc2"})
        self.assertEqual(self.ssv.docs_with_value("green"), set())

    def test_remove_single_value(self):
        self.ssv.add("doc1", "red")
        self.ssv.add("doc1", "blue")
        self.assertTrue(self.ssv.remove("doc1", "red"))
        self.assertEqual(self.ssv.get("doc1"), frozenset({"blue"}))
        self.assertEqual(self.ssv.docs_with_value("red"), set())

    def test_remove_last_value_deletes_doc(self):
        self.ssv.add("doc1", "red")
        self.ssv.remove("doc1", "red")
        self.assertEqual(self.ssv.get("doc1"), frozenset())
        self.assertEqual(self.ssv.doc_count(), 0)

    def test_remove_missing(self):
        self.assertFalse(self.ssv.remove("doc1", "red"))

    def test_delete(self):
        self.ssv.add("doc1", "red")
        self.ssv.add("doc1", "blue")
        self.assertTrue(self.ssv.delete("doc1"))
        self.assertEqual(self.ssv.get("doc1"), frozenset())
        self.assertEqual(self.ssv.docs_with_value("red"), set())
        self.assertEqual(self.ssv.docs_with_value("blue"), set())

    def test_delete_missing(self):
        self.assertFalse(self.ssv.delete("missing"))

    def test_doc_count(self):
        self.assertEqual(self.ssv.doc_count(), 0)
        self.ssv.add("doc1", "a")
        self.ssv.add("doc2", "b")
        self.assertEqual(self.ssv.doc_count(), 2)

    def test_value_count(self):
        self.ssv.add("doc1", "red")
        self.ssv.add("doc1", "blue")
        self.ssv.add("doc2", "red")
        self.assertEqual(self.ssv.value_count(), 2)  # red, blue

    def test_clear(self):
        self.ssv.add("doc1", "red")
        self.ssv.clear()
        self.assertEqual(self.ssv.doc_count(), 0)
        self.assertEqual(self.ssv.value_count(), 0)

    def test_thread_safety(self):
        errors = []

        def writer(start):
            try:
                for i in range(50):
                    self.ssv.add(f"doc_{start}", f"val_{start + i}")
            except Exception as e:
                errors.append(e)

        threads = [threading.Thread(target=writer, args=(i,)) for i in range(4)]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        self.assertEqual(errors, [])
        self.assertEqual(self.ssv.doc_count(), 4)


# ---------------------------------------------------------------------------
# DocValuesStore tests
# ---------------------------------------------------------------------------

class TestDocValuesStore(unittest.TestCase):
    """Tests for DocValuesStore."""

    def setUp(self):
        self.store = DocValuesStore()

    def test_add_numeric(self):
        nv = self.store.add_numeric("price")
        self.assertIsInstance(nv, NumericDocValues)
        self.assertEqual(nv.type, DocValueType.NUMERIC)

    def test_add_binary(self):
        bv = self.store.add_binary("payload")
        self.assertIsInstance(bv, BinaryDocValues)

    def test_add_sorted(self):
        sv = self.store.add_sorted("category")
        self.assertIsInstance(sv, SortedDocValues)

    def test_add_sorted_set(self):
        ssv = self.store.add_sorted_set("tags")
        self.assertIsInstance(ssv, SortedSetDocValues)

    def test_get_field(self):
        nv = self.store.add_numeric("price")
        retrieved = self.store.get_field("price")
        self.assertIs(retrieved, nv)

    def test_get_field_missing(self):
        self.assertIsNone(self.store.get_field("nonexistent"))

    def test_has_field(self):
        self.assertFalse(self.store.has_field("price"))
        self.store.add_numeric("price")
        self.assertTrue(self.store.has_field("price"))

    def test_duplicate_field_raises(self):
        self.store.add_numeric("price")
        with self.assertRaises(ValueError):
            self.store.add_numeric("price")

    def test_duplicate_field_different_type_raises(self):
        self.store.add_numeric("field")
        with self.assertRaises(ValueError):
            self.store.add_sorted("field")

    def test_field_names(self):
        self.store.add_numeric("z_price")
        self.store.add_sorted("a_category")
        self.store.add_binary("m_payload")
        self.assertEqual(
            self.store.field_names(),
            ["a_category", "m_payload", "z_price"],
        )

    def test_remove_field(self):
        self.store.add_numeric("price")
        self.assertTrue(self.store.remove_field("price"))
        self.assertIsNone(self.store.get_field("price"))

    def test_remove_field_missing(self):
        self.assertFalse(self.store.remove_field("missing"))

    def test_clear(self):
        self.store.add_numeric("price")
        self.store.add_sorted("name")
        self.store.clear()
        self.assertEqual(self.store.field_names(), [])

    def test_end_to_end(self):
        price = self.store.add_numeric("price")
        tags = self.store.add_sorted_set("tags")

        price.set("product_1", 9.99)
        price.set("product_2", 19.99)
        price.set("product_3", 14.99)

        tags.add("product_1", "electronics")
        tags.add("product_1", "sale")
        tags.add("product_2", "electronics")
        tags.add("product_3", "clothing")

        # Sort by price ascending
        sorted_prods = price.sort_docs(["product_1", "product_2", "product_3"])
        self.assertEqual(sorted_prods, ["product_1", "product_3", "product_2"])

        # Find products in price range
        mid_range = price.range_query(10.0, 15.0)
        self.assertEqual(mid_range, {"product_3"})

        # Facet: find all electronics
        electronics = tags.docs_with_value("electronics")
        self.assertEqual(electronics, {"product_1", "product_2"})


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