"""Tests for rbsp.query.resolver -- MultiScopeResolver, ScopeStore, Scope."""

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.resolver import MultiScopeResolver, ScopeStore, Scope, ResolvedResult
from rbsp.query.resonance import SignatureRegistry
from rbsp.query.signal import SignalEncoder, Signal
from rbsp.query.parser import QueryParser
from rbsp.core.encoding import NaturalSignature
from rbsp.core.signature import SignatureGenerator
from rbsp.core.lsh import LSHIndex, ngram_shingles, word_shingles


def _make_store(name, scope, texts, timestamp=FIXED_TIMESTAMP, with_lsh=False):
    """Helper: build a ScopeStore from texts, registering signatures."""
    gen = SignatureGenerator(ngram_size=3, num_hashes=128)
    registry = SignatureRegistry()
    lsh = LSHIndex(num_hashes=128, bands=16, rows=8) if with_lsh else None

    for i, text in enumerate(texts):
        sig = gen.generate(text, timestamp=timestamp)
        item_id = f"{name}_{i}"
        metadata = {"type": "py", "path": f"src/{name}/{item_id}.py", "content": text}
        registry.register(item_id, sig, metadata=metadata)
        if lsh and len(text) >= 3:
            shingles = ngram_shingles(text, n=3) | word_shingles(text, n=2)
            if shingles:
                lsh.insert(item_id, shingles)

    return ScopeStore(name=name, scope=scope, registry=registry, lsh_index=lsh)


# ---------------------------------------------------------------------------
# MultiScopeResolver: add_store / remove_store
# ---------------------------------------------------------------------------

class TestMultiScopeResolverStoreManagement(unittest.TestCase):
    """Test add_store and remove_store."""

    def setUp(self):
        self.resolver = MultiScopeResolver()

    def test_add_store(self):
        store = _make_store("proj", Scope.CODEBASE, SAMPLE_TEXTS[:3])
        self.resolver.add_store(store)
        self.assertIn("proj", self.resolver.store_names)

    def test_add_duplicate_raises(self):
        store = _make_store("proj", Scope.CODEBASE, SAMPLE_TEXTS[:1])
        self.resolver.add_store(store)
        with self.assertRaises(ValueError):
            self.resolver.add_store(store)

    def test_remove_store(self):
        store = _make_store("proj", Scope.CODEBASE, SAMPLE_TEXTS[:1])
        self.resolver.add_store(store)
        result = self.resolver.remove_store("proj")
        self.assertTrue(result)
        self.assertNotIn("proj", self.resolver.store_names)

    def test_remove_nonexistent_store(self):
        result = self.resolver.remove_store("nope")
        self.assertFalse(result)

    def test_get_store(self):
        store = _make_store("proj", Scope.CODEBASE, SAMPLE_TEXTS[:1])
        self.resolver.add_store(store)
        got = self.resolver.get_store("proj")
        self.assertIsNotNone(got)
        self.assertEqual(got.name, "proj")

    def test_get_store_not_found(self):
        got = self.resolver.get_store("missing")
        self.assertIsNone(got)

    def test_store_names(self):
        self.resolver.add_store(_make_store("a", Scope.FILE, SAMPLE_TEXTS[:1]))
        self.resolver.add_store(_make_store("b", Scope.DIRECTORY, SAMPLE_TEXTS[:1]))
        self.assertEqual(sorted(self.resolver.store_names), ["a", "b"])


# ---------------------------------------------------------------------------
# MultiScopeResolver: resolve()
# ---------------------------------------------------------------------------

class TestMultiScopeResolverResolve(unittest.TestCase):
    """Test resolve() across scopes."""

    def setUp(self):
        self.resolver = MultiScopeResolver()
        self.store1 = _make_store("project_a", Scope.CODEBASE, SAMPLE_TEXTS[:5])
        self.store2 = _make_store("project_b", Scope.CODEBASE, SAMPLE_TEXTS[5:8])
        self.resolver.add_store(self.store1)
        self.resolver.add_store(self.store2)

    def test_resolve_finds_results(self):
        results = self.resolver.resolve(SAMPLE_TEXTS[0])
        self.assertGreater(len(results), 0)

    def test_resolve_returns_resolved_result(self):
        results = self.resolver.resolve(SAMPLE_TEXTS[0])
        if results:
            self.assertIsInstance(results[0], ResolvedResult)

    def test_resolve_has_scope_name(self):
        results = self.resolver.resolve(SAMPLE_TEXTS[0])
        if results:
            self.assertIn(results[0].scope_name, ["project_a", "project_b"])

    def test_resolve_top_k(self):
        results = self.resolver.resolve(SAMPLE_TEXTS[0], top_k=1)
        self.assertLessEqual(len(results), 1)


class TestMultiScopeResolverScopeFiltering(unittest.TestCase):
    """Test scope-level filtering."""

    def setUp(self):
        self.resolver = MultiScopeResolver()
        self.resolver.add_store(_make_store("file_store", Scope.FILE, SAMPLE_TEXTS[:2]))
        self.resolver.add_store(_make_store("code_store", Scope.CODEBASE, SAMPLE_TEXTS[:2]))

    def test_filter_by_scope(self):
        results = self.resolver.resolve(SAMPLE_TEXTS[0], scopes=[Scope.FILE])
        for r in results:
            self.assertEqual(r.scope, Scope.FILE)

    def test_filter_by_store_name(self):
        results = self.resolver.resolve(SAMPLE_TEXTS[0], store_names=["file_store"])
        for r in results:
            self.assertEqual(r.scope_name, "file_store")


class TestMultiScopeResolverScopeFromQuery(unittest.TestCase):
    """Test that scope:xxx in query is respected."""

    def setUp(self):
        self.resolver = MultiScopeResolver()
        self.resolver.add_store(_make_store("f", Scope.FILE, SAMPLE_TEXTS[:2]))
        self.resolver.add_store(_make_store("c", Scope.CODEBASE, SAMPLE_TEXTS[:2]))

    def test_scope_codebase_filter(self):
        query = SAMPLE_TEXTS[0] + " scope:codebase"
        results = self.resolver.resolve(query)
        for r in results:
            self.assertEqual(r.scope, Scope.CODEBASE)

    def test_scope_file_filter(self):
        query = SAMPLE_TEXTS[0] + " scope:file"
        results = self.resolver.resolve(query)
        for r in results:
            self.assertEqual(r.scope, Scope.FILE)


# ---------------------------------------------------------------------------
# Deduplication
# ---------------------------------------------------------------------------

class TestMultiScopeResolverDeduplication(unittest.TestCase):
    """Test deduplication across scopes."""

    def setUp(self):
        self.resolver = MultiScopeResolver()
        # Both stores have the same texts, so same content hashes
        # but different item_ids since _make_store uses store name as prefix
        self.resolver.add_store(_make_store("s1", Scope.CODEBASE, SAMPLE_TEXTS[:3]))
        self.resolver.add_store(_make_store("s2", Scope.CODEBASE, SAMPLE_TEXTS[:3]))

    def test_deduplicate_on(self):
        results = self.resolver.resolve(SAMPLE_TEXTS[0], deduplicate=True)
        item_ids = [r.item_id for r in results]
        # Each item_id should appear at most once
        self.assertEqual(len(item_ids), len(set(item_ids)))

    def test_deduplicate_off(self):
        results = self.resolver.resolve(SAMPLE_TEXTS[0], deduplicate=False)
        # May have duplicates from both stores
        # Just verify it doesn't crash
        self.assertIsInstance(results, list)


# ---------------------------------------------------------------------------
# _apply_filters with ParsedQuery
# ---------------------------------------------------------------------------

class TestMultiScopeResolverApplyFilters(unittest.TestCase):
    """Test _apply_filters with type, path, and exclusion filters."""

    def setUp(self):
        self.resolver = MultiScopeResolver()
        self.resolver.add_store(_make_store("proj", Scope.CODEBASE, SAMPLE_TEXTS[:5]))

    def test_type_filter_in_query(self):
        # Verify that type filter parsing works by checking the filter excludes non-matching types.
        # Adding type:py changes the signal, so we test the _apply_filters path directly.
        parser = QueryParser()
        parsed = parser.parse(SAMPLE_TEXTS[0] + " type:py")
        self.assertEqual(parsed.type_filters, ("py",))

    def test_type_filter_excludes_nonmatching(self):
        # Use _apply_filters directly to verify filtering logic
        r = ResolvedResult(
            item_id="a", scope_name="proj", scope=Scope.CODEBASE,
            final_score=0.9, metadata={"type": "py"},
        )
        parser = QueryParser()
        parsed = parser.parse("query type:rs")
        filtered = MultiScopeResolver._apply_filters([r], parsed)
        self.assertEqual(len(filtered), 0)

    def test_path_filter_in_query(self):
        # Test _apply_filters with path filter
        r = ResolvedResult(
            item_id="a", scope_name="proj", scope=Scope.CODEBASE,
            final_score=0.9, metadata={"type": "py", "path": "src/proj/a.py"},
        )
        parser = QueryParser()
        parsed = parser.parse("query path:src/proj/")
        filtered = MultiScopeResolver._apply_filters([r], parsed)
        self.assertEqual(len(filtered), 1)

    def test_excluded_terms_in_query(self):
        # NOT should exclude items whose content contains the term
        query = SAMPLE_TEXTS[0] + " NOT programming"
        results = self.resolver.resolve(query)
        for r in results:
            if r.metadata:
                content = r.metadata.get("content", "").lower()
                self.assertNotIn("programming", content)


# ---------------------------------------------------------------------------
# resolve_signal()
# ---------------------------------------------------------------------------

class TestMultiScopeResolverResolveSignal(unittest.TestCase):
    """Test resolve_signal() with a pre-built Signal."""

    def setUp(self):
        self.resolver = MultiScopeResolver()
        self.resolver.add_store(_make_store("proj", Scope.CODEBASE, SAMPLE_TEXTS[:5]))
        self.encoder = SignalEncoder(ngram_size=3, num_hashes=128)

    def test_resolve_signal_returns_results(self):
        signal = self.encoder.encode(SAMPLE_TEXTS[0])
        results = self.resolver.resolve_signal(signal)
        self.assertGreater(len(results), 0)

    def test_resolve_signal_scope_filter(self):
        signal = self.encoder.encode(SAMPLE_TEXTS[0])
        results = self.resolver.resolve_signal(signal, scopes=[Scope.FILE])
        # No FILE scope stores, so no results
        self.assertEqual(len(results), 0)

    def test_resolve_signal_top_k(self):
        signal = self.encoder.encode(SAMPLE_TEXTS[0])
        results = self.resolver.resolve_signal(signal, top_k=1)
        self.assertLessEqual(len(results), 1)


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

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

    def test_resolve_no_stores(self):
        resolver = MultiScopeResolver()
        results = resolver.resolve("hello world")
        self.assertEqual(results, [])

    def test_resolve_empty_query(self):
        resolver = MultiScopeResolver()
        resolver.add_store(_make_store("s", Scope.CODEBASE, SAMPLE_TEXTS[:2]))
        results = resolver.resolve("")
        # Empty query may or may not match; just ensure no crash
        self.assertIsInstance(results, list)


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