"""Tests for rbsp.query.facets — Faceted search and aggregations."""

from __future__ import annotations

import pytest

from rbsp.query.facets import FacetCollector, FacetResult, FacetValue, HierarchicalFacet


# ===================================================================
# FacetValue / FacetResult dataclasses
# ===================================================================


class TestFacetValue:
    def test_creation(self) -> None:
        fv = FacetValue(value="python", count=5)
        assert fv.value == "python"
        assert fv.count == 5

    def test_frozen(self) -> None:
        fv = FacetValue(value="python", count=5)
        with pytest.raises(AttributeError):
            fv.value = "java"  # type: ignore[misc]

    def test_equality(self) -> None:
        a = FacetValue(value="python", count=5)
        b = FacetValue(value="python", count=5)
        assert a == b

    def test_inequality(self) -> None:
        a = FacetValue(value="python", count=5)
        b = FacetValue(value="python", count=3)
        assert a != b


class TestFacetResult:
    def test_creation(self) -> None:
        vals = [FacetValue("py", 3), FacetValue("rs", 1)]
        fr = FacetResult(field="language", values=vals, total=2)
        assert fr.field == "language"
        assert len(fr.values) == 2
        assert fr.total == 2

    def test_frozen(self) -> None:
        fr = FacetResult(field="x", values=[], total=0)
        with pytest.raises(AttributeError):
            fr.field = "y"  # type: ignore[misc]


# ===================================================================
# FacetCollector
# ===================================================================


class TestFacetCollector:
    def test_empty_collector(self) -> None:
        fc = FacetCollector()
        results = fc.collect({"d1"}, ["language"])
        assert len(results) == 1
        assert results[0].field == "language"
        assert results[0].values == []
        assert results[0].total == 0

    def test_single_doc_single_field(self) -> None:
        fc = FacetCollector()
        fc.add_document("d1", {"language": "python"})
        results = fc.collect({"d1"}, ["language"])
        assert len(results) == 1
        assert results[0].total == 1
        assert results[0].values[0].value == "python"
        assert results[0].values[0].count == 1

    def test_multiple_docs_multiple_fields(self) -> None:
        fc = FacetCollector()
        fc.add_document("d1", {"language": "python", "type": "file"})
        fc.add_document("d2", {"language": "rust", "type": "file"})
        fc.add_document("d3", {"language": "python", "type": "dir"})

        results = fc.collect({"d1", "d2", "d3"}, ["language", "type"])
        lang_result = next(r for r in results if r.field == "language")
        type_result = next(r for r in results if r.field == "type")

        assert lang_result.total == 2  # python, rust
        assert type_result.total == 2  # file, dir

    def test_top_n_limits_buckets(self) -> None:
        fc = FacetCollector()
        fc.add_document("d1", {"lang": "python"})
        fc.add_document("d2", {"lang": "rust"})
        fc.add_document("d3", {"lang": "java"})

        results = fc.collect({"d1", "d2", "d3"}, ["lang"], top_n=2)
        assert len(results[0].values) == 2

    def test_counts_only_matching_docs(self) -> None:
        fc = FacetCollector()
        fc.add_document("d1", {"lang": "python"})
        fc.add_document("d2", {"lang": "rust"})
        fc.add_document("d3", {"lang": "python"})

        # Only query d1 and d2
        results = fc.collect({"d1", "d2"}, ["lang"])
        lang = results[0]
        py_bucket = next(v for v in lang.values if v.value == "python")
        rs_bucket = next(v for v in lang.values if v.value == "rust")
        assert py_bucket.count == 1
        assert rs_bucket.count == 1

    def test_empty_matching_docs(self) -> None:
        fc = FacetCollector()
        fc.add_document("d1", {"lang": "python"})
        results = fc.collect(set(), ["lang"])
        assert results[0].total == 0
        assert results[0].values == []

    def test_unknown_field(self) -> None:
        fc = FacetCollector()
        fc.add_document("d1", {"lang": "python"})
        results = fc.collect({"d1"}, ["nonexistent"])
        assert results[0].total == 0

    def test_multiple_fields_requested(self) -> None:
        fc = FacetCollector()
        fc.add_document("d1", {"a": "1", "b": "2"})
        results = fc.collect({"d1"}, ["a", "b"])
        assert len(results) == 2
        assert results[0].field == "a"
        assert results[1].field == "b"

    def test_ordering_by_count_descending(self) -> None:
        fc = FacetCollector()
        fc.add_document("d1", {"lang": "python"})
        fc.add_document("d2", {"lang": "python"})
        fc.add_document("d3", {"lang": "rust"})
        results = fc.collect({"d1", "d2", "d3"}, ["lang"])
        assert results[0].values[0].value == "python"
        assert results[0].values[0].count == 2


class TestFacetCollectorRange:
    def test_basic_range(self) -> None:
        fc = FacetCollector()
        fc.add_document("d1", {"size": "10"})
        fc.add_document("d2", {"size": "50"})
        fc.add_document("d3", {"size": "100"})

        buckets = fc.collect_range(
            {"d1", "d2", "d3"}, "size", [(0, 25), (26, 75), (76, 200)]
        )
        assert len(buckets) == 3
        assert buckets[0].count == 1  # d1
        assert buckets[1].count == 1  # d2
        assert buckets[2].count == 1  # d3

    def test_range_inclusive_bounds(self) -> None:
        fc = FacetCollector()
        fc.add_document("d1", {"val": "10"})
        buckets = fc.collect_range({"d1"}, "val", [(10, 10)])
        assert buckets[0].count == 1

    def test_range_no_matching_docs(self) -> None:
        fc = FacetCollector()
        fc.add_document("d1", {"val": "5"})
        buckets = fc.collect_range({"d1"}, "val", [(10, 20)])
        assert buckets[0].count == 0

    def test_range_non_numeric_field(self) -> None:
        fc = FacetCollector()
        fc.add_document("d1", {"name": "hello"})
        buckets = fc.collect_range({"d1"}, "name", [(0, 100)])
        assert buckets[0].count == 0

    def test_range_label_format(self) -> None:
        fc = FacetCollector()
        fc.add_document("d1", {"v": "5"})
        buckets = fc.collect_range({"d1"}, "v", [(0.0, 10.0)])
        assert buckets[0].value == "0.0-10.0"

    def test_range_multiple_docs_in_range(self) -> None:
        fc = FacetCollector()
        fc.add_document("d1", {"v": "5"})
        fc.add_document("d2", {"v": "8"})
        fc.add_document("d3", {"v": "15"})
        buckets = fc.collect_range({"d1", "d2", "d3"}, "v", [(0, 10)])
        assert buckets[0].count == 2


# ===================================================================
# HierarchicalFacet
# ===================================================================


class TestHierarchicalFacet:
    def test_basic_depth_1(self) -> None:
        hf = HierarchicalFacet()
        hf.add_path("d1", "src/rbsp/core/bloom.py")
        hf.add_path("d2", "src/rbsp/core/lsh.py")
        hf.add_path("d3", "tests/test_bloom.py")

        buckets = hf.collect({"d1", "d2", "d3"}, depth=1)
        vals = {b.value: b.count for b in buckets}
        assert vals["src"] == 2
        assert vals["tests"] == 1

    def test_basic_depth_2(self) -> None:
        hf = HierarchicalFacet()
        hf.add_path("d1", "src/rbsp/core/bloom.py")
        hf.add_path("d2", "src/rbsp/query/signal.py")

        buckets = hf.collect({"d1", "d2"}, depth=2)
        vals = {b.value: b.count for b in buckets}
        assert vals["src/rbsp"] == 2

    def test_depth_exceeds_components(self) -> None:
        hf = HierarchicalFacet()
        hf.add_path("d1", "a/b")
        buckets = hf.collect({"d1"}, depth=5)
        assert buckets == []

    def test_depth_validation(self) -> None:
        hf = HierarchicalFacet()
        with pytest.raises(ValueError, match="depth must be >= 1"):
            hf.collect(set(), depth=0)

    def test_custom_separator(self) -> None:
        hf = HierarchicalFacet()
        hf.add_path("d1", "a.b.c", separator=".")
        buckets = hf.collect({"d1"}, depth=2)
        assert buckets[0].value == "a.b"

    def test_multiple_paths_per_doc(self) -> None:
        hf = HierarchicalFacet()
        hf.add_path("d1", "a/b/c")
        hf.add_path("d1", "x/y/z")
        buckets = hf.collect({"d1"}, depth=1)
        vals = {b.value: b.count for b in buckets}
        assert vals["a"] == 1
        assert vals["x"] == 1

    def test_dedup_same_prefix_per_doc(self) -> None:
        hf = HierarchicalFacet()
        hf.add_path("d1", "src/a/foo.py")
        hf.add_path("d1", "src/b/bar.py")
        buckets = hf.collect({"d1"}, depth=1)
        # doc d1 should only count once for prefix "src"
        assert buckets[0].count == 1

    def test_empty_matching_docs(self) -> None:
        hf = HierarchicalFacet()
        hf.add_path("d1", "a/b")
        buckets = hf.collect(set(), depth=1)
        assert buckets == []

    def test_doc_not_in_index(self) -> None:
        hf = HierarchicalFacet()
        hf.add_path("d1", "a/b")
        buckets = hf.collect({"d999"}, depth=1)
        assert buckets == []

    def test_sorted_by_count_desc_then_value_asc(self) -> None:
        hf = HierarchicalFacet()
        hf.add_path("d1", "b/x")
        hf.add_path("d2", "a/x")
        hf.add_path("d3", "a/y")
        buckets = hf.collect({"d1", "d2", "d3"}, depth=1)
        # a=2, b=1 → a first
        assert buckets[0].value == "a"
        assert buckets[0].count == 2
        assert buckets[1].value == "b"
        assert buckets[1].count == 1
