"""End-to-end integration tests for the RBSP framework.

Tests the full pipeline from indexing through querying, as well as
round-trip encoding, resonance matching, semantic similarity, and CLI
configuration.
"""

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

import tempfile
import shutil
import unittest

from rbsp.core.signature import SignatureGenerator
from rbsp.core.encoding import NaturalSignature, SIGNATURE_SIZE
from rbsp.core.registry import SignatureRegistry
from rbsp.core.lsh import LSHIndex, MinHasher, ngram_shingles, word_shingles
from rbsp.indexer.inverted import InvertedIndex, tokenize
from rbsp.query.resonance import ResonanceMatcher, SignatureRegistry as ResonanceRegistry
from rbsp.query.signal import SignalEncoder, Signal
from rbsp.query.semantic import SemanticResolver
from rbsp.runtime.engine import RBSPEngine


# ---------------------------------------------------------------------------
# 1. Full Pipeline Test
# ---------------------------------------------------------------------------

class TestFullPipeline(RBSPTestCase):
    """Create temp files, init+index via RBSPEngine, then resolve queries."""

    def setUp(self):
        super().setUp()
        RBSPEngine.reset()

        # Create sample source files in the temp directory
        self._write(
            "auth.py",
            "import hashlib\n\n"
            "def authenticate(user, password):\n"
            "    hashed = hashlib.sha256(password.encode()).hexdigest()\n"
            "    return check_credentials(user, hashed)\n"
            "\n"
            "def check_credentials(user, hashed):\n"
            "    return user == 'admin' and hashed == ADMIN_HASH\n",
        )
        self._write(
            "utils.js",
            "function formatDate(date) {\n"
            "    return date.toISOString().split('T')[0];\n"
            "}\n"
            "\n"
            "function parseJSON(text) {\n"
            "    return JSON.parse(text);\n"
            "}\n",
        )
        self._write(
            "readme.txt",
            "This project provides authentication utilities and date formatting.\n"
            "It includes Python and JavaScript modules for common operations.\n",
        )

    def tearDown(self):
        RBSPEngine.reset()
        super().tearDown()

    def _write(self, name, content):
        path = os.path.join(self.tmp_dir, name)
        os.makedirs(os.path.dirname(path), exist_ok=True)
        with open(path, "w", encoding="utf-8") as f:
            f.write(content)

    def test_init_creates_rbsp_directory(self):
        engine = RBSPEngine.get_instance()
        engine.init(self.tmp_dir)
        rbsp_dir = os.path.join(self.tmp_dir, ".rbsp")
        self.assertTrue(os.path.isdir(rbsp_dir))
        self.assertTrue(os.path.isdir(os.path.join(rbsp_dir, "index")))
        self.assertTrue(os.path.isdir(os.path.join(rbsp_dir, "plugins")))

    def test_index_finds_files(self):
        engine = RBSPEngine.get_instance()
        engine.init(self.tmp_dir)
        stats = engine.index(self.tmp_dir)
        # Should have indexed at least the 3 files we created
        self.assertGreaterEqual(stats.files_indexed, 3)
        self.assertGreater(stats.total_size, 0)

    def test_resolve_returns_ranked_results(self):
        engine = RBSPEngine.get_instance()
        engine.init(self.tmp_dir)
        engine.index(self.tmp_dir)
        results = engine.resolve("authenticate password")
        self.assertGreater(len(results), 0)
        # auth.py should appear in results (highest relevance for 'authenticate')
        paths = [r.path for r in results]
        auth_found = any("auth" in p for p in paths)
        self.assertTrue(auth_found, f"Expected auth.py in results, got: {paths}")

    def test_resolve_scores_are_ordered(self):
        engine = RBSPEngine.get_instance()
        engine.init(self.tmp_dir)
        engine.index(self.tmp_dir)
        results = engine.resolve("authenticate password")
        if len(results) >= 2:
            for i in range(len(results) - 1):
                self.assertGreaterEqual(results[i].score, results[i + 1].score)

    def test_resolve_javascript_query(self):
        engine = RBSPEngine.get_instance()
        engine.init(self.tmp_dir)
        engine.index(self.tmp_dir)
        results = engine.resolve("formatDate parseJSON")
        self.assertGreater(len(results), 0)
        paths = [r.path for r in results]
        js_found = any("utils" in p for p in paths)
        self.assertTrue(js_found, f"Expected utils.js in results, got: {paths}")

    def test_engine_status_after_operations(self):
        engine = RBSPEngine.get_instance()
        engine.init(self.tmp_dir)
        engine.index(self.tmp_dir)
        engine.resolve("test query")
        status = engine.status()
        self.assertTrue(status["initialized"])
        self.assertGreater(status["index_entries"], 0)
        self.assertEqual(status["stats"]["queries"], 1)
        self.assertGreaterEqual(status["stats"]["index_operations"], 1)

    def test_search_alias(self):
        engine = RBSPEngine.get_instance()
        engine.init(self.tmp_dir)
        engine.index(self.tmp_dir)
        results = engine.search("authenticate", scope=self.tmp_dir)
        self.assertGreater(len(results), 0)


# ---------------------------------------------------------------------------
# 2. Core Protocol Round-Trip
# ---------------------------------------------------------------------------

class TestProtocolRoundTrip(RBSPTestCase):
    """Signature generation -> binary encode -> decode -> registry store/retrieve."""

    def test_signature_encode_decode_preserves_fields(self):
        gen = SignatureGenerator(ngram_size=3, num_hashes=128)
        for text in SAMPLE_TEXTS:
            sig = gen.generate(text, timestamp=FIXED_TIMESTAMP)
            encoded = sig.to_bytes()
            self.assertEqual(len(encoded), SIGNATURE_SIZE)
            decoded = NaturalSignature.from_bytes(encoded)
            self.assertEqual(decoded.content_hash, sig.content_hash)
            self.assertEqual(decoded.semantic_hash, sig.semantic_hash)
            self.assertEqual(decoded.structural_hash, sig.structural_hash)
            self.assertAlmostEqual(decoded.timestamp, sig.timestamp, places=3)
            self.assertEqual(decoded.size_class, sig.size_class)
            self.assertEqual(decoded.flags, sig.flags)

    def test_hex_round_trip(self):
        gen = SignatureGenerator()
        sig = gen.generate(SAMPLE_CODE, timestamp=FIXED_TIMESTAMP)
        hex_str = sig.hex()
        decoded = NaturalSignature.from_hex(hex_str)
        self.assertEqual(decoded.content_hash, sig.content_hash)
        self.assertEqual(decoded.semantic_hash, sig.semantic_hash)

    def test_registry_store_and_retrieve(self):
        gen = SignatureGenerator()
        registry = SignatureRegistry()
        sigs = {}
        for i, text in enumerate(SAMPLE_TEXTS):
            sig = gen.generate(text, timestamp=FIXED_TIMESTAMP)
            sid = f"sig_{i}"
            registry.add(sid, sig, source_path=f"/test/{i}.txt")
            sigs[sid] = sig

        self.assertEqual(len(registry), len(SAMPLE_TEXTS))

        for sid, expected in sigs.items():
            retrieved = registry.get(sid)
            self.assertIsNotNone(retrieved)
            self.assertEqual(retrieved.content_hash, expected.content_hash)

    def test_registry_path_index(self):
        gen = SignatureGenerator()
        registry = SignatureRegistry()
        sig = gen.generate("test content", timestamp=FIXED_TIMESTAMP)
        registry.add("id_1", sig, source_path="/foo/bar.py")
        by_path = registry.get_by_path("/foo/bar.py")
        self.assertIsNotNone(by_path)
        self.assertEqual(by_path.content_hash, sig.content_hash)

    def test_registry_remove(self):
        gen = SignatureGenerator()
        registry = SignatureRegistry()
        sig = gen.generate("remove me", timestamp=FIXED_TIMESTAMP)
        registry.add("rm_id", sig)
        self.assertIn("rm_id", registry)
        removed = registry.remove("rm_id")
        self.assertIsNotNone(removed)
        self.assertNotIn("rm_id", registry)
        self.assertIsNone(registry.get("rm_id"))

    def test_registry_bulk_add(self):
        gen = SignatureGenerator()
        registry = SignatureRegistry()
        entries = [
            (f"bulk_{i}", gen.generate(t, timestamp=FIXED_TIMESTAMP))
            for i, t in enumerate(SAMPLE_TEXTS[:5])
        ]
        count = registry.bulk_add(entries)
        self.assertEqual(count, 5)
        self.assertEqual(len(registry), 5)

    def test_registry_persistence_round_trip(self):
        gen = SignatureGenerator()
        log_path = self.tmp_path("registry.log")
        reg1 = SignatureRegistry(backing_path=log_path)
        sig = gen.generate("persistent data", timestamp=FIXED_TIMESTAMP)
        reg1.add("persist_1", sig)
        reg1.flush()
        reg1.close()

        # Reload from the same log
        reg2 = SignatureRegistry(backing_path=log_path)
        retrieved = reg2.get("persist_1")
        self.assertIsNotNone(retrieved)
        self.assertEqual(retrieved.content_hash, sig.content_hash)
        reg2.close()

    def test_perfect_hash_table(self):
        gen = SignatureGenerator()
        registry = SignatureRegistry()
        for i in range(20):
            sig = gen.generate(f"content number {i}", timestamp=FIXED_TIMESTAMP)
            registry.add(f"ph_{i}", sig)

        pht = registry.build_perfect_hash()
        self.assertEqual(len(pht), 20)
        for i in range(20):
            result = pht.get(f"ph_{i}")
            self.assertIsNotNone(result, f"ph_{i} not found in perfect hash table")

    def test_corrupted_bytes_raises(self):
        gen = SignatureGenerator()
        sig = gen.generate("test", timestamp=FIXED_TIMESTAMP)
        data = bytearray(sig.to_bytes())
        # Corrupt a byte in the middle
        data[20] ^= 0xFF
        with self.assertRaises(ValueError):
            NaturalSignature.from_bytes(bytes(data))

    def test_bad_magic_raises(self):
        data = b"\x00" * SIGNATURE_SIZE
        with self.assertRaises(ValueError):
            NaturalSignature.from_bytes(data)


# ---------------------------------------------------------------------------
# 3. Indexer + Query Integration
# ---------------------------------------------------------------------------

class TestIndexerQueryIntegration(RBSPTestCase):
    """InvertedIndex + ResonanceRegistry + ResonanceMatcher working together."""

    def test_inverted_index_add_and_lookup(self):
        idx = InvertedIndex()
        idx.add_document("doc1", ["python", "hash", "index"])
        idx.add_document("doc2", ["python", "search", "query"])
        idx.add_document("doc3", ["java", "hash", "map"])

        self.assertEqual(idx.lookup("python"), {"doc1", "doc2"})
        self.assertEqual(idx.lookup("hash"), {"doc1", "doc3"})
        self.assertEqual(idx.lookup("query"), {"doc2"})
        self.assertEqual(idx.lookup("nonexistent"), set())

    def test_inverted_index_boolean_search(self):
        idx = InvertedIndex()
        idx.add_document("doc1", ["python", "hash", "index"])
        idx.add_document("doc2", ["python", "search", "query"])
        idx.add_document("doc3", ["java", "hash", "map"])

        # AND query
        result = idx.search("python AND hash")
        self.assertEqual(result, {"doc1"})

        # OR query
        result = idx.search("python OR java")
        self.assertEqual(result, {"doc1", "doc2", "doc3"})

    def test_inverted_index_add_text(self):
        idx = InvertedIndex()
        idx.add_text("code1", "def authenticate_user(username, password)")
        self.assertIn("code1", idx.lookup("authenticate"))

    def test_resonance_matcher_exact_match(self):
        gen = SignatureGenerator()
        encoder = SignalEncoder()

        registry = ResonanceRegistry()
        # Register a signature for known content
        content = "The quick brown fox jumps over the lazy dog"
        sig = gen.generate(content, timestamp=FIXED_TIMESTAMP)
        registry.register("item_fox", sig, metadata={"source": "test"})

        # Create a signal for the exact same content
        signal = encoder.encode(content)

        matcher = ResonanceMatcher(registry)
        hits = matcher.match(signal)

        self.assertGreater(len(hits), 0)
        top_hit = hits[0]
        self.assertEqual(top_hit.item_id, "item_fox")
        self.assertGreater(top_hit.resonance_score, 0.0)
        self.assertEqual(top_hit.exact_score, 1.0)

    def test_resonance_matcher_scoring(self):
        gen = SignatureGenerator()
        encoder = SignalEncoder()

        registry = ResonanceRegistry()
        texts = {
            "item_a": "Python programming language for data science",
            "item_b": "Java enterprise application development",
            "item_c": "Python data analysis with pandas and numpy",
        }
        for item_id, text in texts.items():
            sig = gen.generate(text, timestamp=FIXED_TIMESTAMP)
            registry.register(item_id, sig)

        signal = encoder.encode("Python data science programming")
        matcher = ResonanceMatcher(registry)
        hits = matcher.match(signal)

        # Results should be ordered by score
        if len(hits) >= 2:
            for i in range(len(hits) - 1):
                self.assertGreaterEqual(
                    hits[i].resonance_score, hits[i + 1].resonance_score
                )

    def test_resonance_matcher_with_lsh(self):
        lsh = LSHIndex(num_hashes=128, bands=16, rows=8)
        gen = SignatureGenerator()
        encoder = SignalEncoder()
        registry = ResonanceRegistry()

        texts = {
            "doc_sim1": "The quick brown fox jumps over the lazy dog",
            "doc_sim2": "The quick brown fox leaps over the lazy dog",
            "doc_diff": "A completely unrelated sentence about programming",
        }
        for doc_id, text in texts.items():
            sig = gen.generate(text, timestamp=FIXED_TIMESTAMP)
            registry.register(doc_id, sig)
            shingles = ngram_shingles(text, n=3)
            lsh.insert(doc_id, shingles)

        signal = encoder.encode("The quick brown fox jumps over the lazy dog")
        matcher = ResonanceMatcher(registry, lsh_index=lsh)
        hits = matcher.match(signal)

        self.assertGreater(len(hits), 0)

    def test_inverted_index_remove_document(self):
        idx = InvertedIndex()
        idx.add_document("doc1", ["python", "hash"])
        idx.add_document("doc2", ["python", "search"])
        self.assertTrue(idx.remove_document("doc1"))
        self.assertEqual(idx.lookup("python"), {"doc2"})
        self.assertEqual(idx.lookup("hash"), set())


# ---------------------------------------------------------------------------
# 4. Semantic Similarity Pipeline
# ---------------------------------------------------------------------------

class TestSemanticSimilarityPipeline(RBSPTestCase):
    """LSHIndex + SemanticResolver for semantic neighbor search."""

    def setUp(self):
        super().setUp()
        self.lsh = LSHIndex(num_hashes=128, bands=16, rows=8)
        self.documents = {
            "py_auth": "def authenticate(user, password): return check_password(user, password)",
            "py_login": "def login(username, pwd): return verify_credentials(username, pwd)",
            "js_format": "function formatDate(date) { return date.toISOString(); }",
            "js_parse": "function parseJSON(text) { return JSON.parse(text); }",
            "txt_readme": "This is a readme file about the project documentation and setup guide.",
        }
        for doc_id, text in self.documents.items():
            shingles = ngram_shingles(text, n=3) | word_shingles(text, n=2)
            self.lsh.insert(doc_id, shingles)

    def test_similar_docs_rank_higher(self):
        # Query with something similar to the Python auth functions
        query_text = "def authenticate_user(name, password): return verify(name, password)"
        query_shingles = ngram_shingles(query_text, n=3) | word_shingles(query_text, n=2)
        results = self.lsh.query(query_shingles, top_k=5, min_similarity=0.0)

        if len(results) >= 2:
            result_ids = [r[0] for r in results]
            # The Python auth docs should be more similar than JS docs
            py_positions = [i for i, rid in enumerate(result_ids) if rid.startswith("py_")]
            js_positions = [i for i, rid in enumerate(result_ids) if rid.startswith("js_")]
            if py_positions and js_positions:
                self.assertLess(
                    min(py_positions), max(js_positions),
                    "Python auth documents should rank higher than JS documents"
                )

    def test_query_by_id(self):
        results = self.lsh.query_by_id("py_auth", top_k=5, min_similarity=0.0)
        # py_login should be among the neighbors since it's similar
        if results:
            neighbor_ids = [r[0] for r in results]
            # py_login shares many shingles with py_auth
            self.assertIn("py_login", neighbor_ids,
                          f"Expected py_login in neighbors, got: {neighbor_ids}")

    def test_semantic_resolver_resolve_text(self):
        resolver = SemanticResolver(self.lsh, min_similarity=0.0)
        # Use the exact same text as a known document to guarantee hits
        results = resolver.resolve_text(
            "def authenticate(user, password): return check_password(user, password)",
            top_k=5,
            min_similarity=0.0,
        )
        self.assertGreater(len(results), 0)
        # Results should have similarity scores
        for r in results:
            self.assertGreaterEqual(r.similarity, 0.0)
            self.assertLessEqual(r.similarity, 1.0)

    def test_semantic_resolver_expand_neighbors(self):
        resolver = SemanticResolver(self.lsh, min_similarity=0.0)
        neighbors = resolver.expand_neighbors("py_auth", top_k=5, min_similarity=0.0)
        # py_auth and py_login share many shingles, so neighbors should exist
        # However, if LSH bands don't align, this can be empty -- just verify types
        self.assertIsInstance(neighbors, list)
        for n in neighbors:
            # Should not include the query item itself
            self.assertNotEqual(n.item_id, "py_auth")

    def test_semantic_resolver_with_metadata(self):
        resolver = SemanticResolver(self.lsh, min_similarity=0.0)
        resolver.set_metadata("py_auth", {"language": "python", "type": "auth"})
        results = resolver.resolve_text(
            "def authenticate(user, password)",
            top_k=5,
        )
        for r in results:
            if r.item_id == "py_auth":
                self.assertIsNotNone(r.metadata)
                self.assertEqual(r.metadata["language"], "python")

    def test_lsh_insert_and_contains(self):
        self.assertIn("py_auth", self.lsh)
        self.assertIn("js_format", self.lsh)
        self.assertNotIn("nonexistent", self.lsh)
        self.assertEqual(len(self.lsh), 5)

    def test_lsh_remove(self):
        self.assertTrue(self.lsh.remove("txt_readme"))
        self.assertNotIn("txt_readme", self.lsh)
        self.assertEqual(len(self.lsh), 4)
        self.assertFalse(self.lsh.remove("nonexistent"))


# ---------------------------------------------------------------------------
# 5. CLI Integration (light test)
# ---------------------------------------------------------------------------

class TestCLIIntegration(RBSPTestCase):
    """Verify CLI module structure and basic subcommand parsing."""

    def setUp(self):
        super().setUp()
        RBSPEngine.reset()

    def tearDown(self):
        RBSPEngine.reset()
        super().tearDown()

    def test_main_function_exists(self):
        from rbsp.runtime.cli import main
        self.assertTrue(callable(main))

    def test_build_parser_returns_argparse(self):
        import argparse
        from rbsp.runtime.cli import build_parser
        parser = build_parser()
        self.assertIsInstance(parser, argparse.ArgumentParser)

    def test_parser_has_subcommands(self):
        from rbsp.runtime.cli import build_parser
        parser = build_parser()
        # Verify the expected subcommands are registered
        args = parser.parse_args(["init", self.tmp_dir])
        self.assertEqual(args.command, "init")

        args = parser.parse_args(["status"])
        self.assertEqual(args.command, "status")

        args = parser.parse_args(["search", "test", "query"])
        self.assertEqual(args.command, "search")
        self.assertEqual(args.query, ["test", "query"])

        args = parser.parse_args(["index", "--force"])
        self.assertEqual(args.command, "index")
        self.assertTrue(args.force)

    def test_cli_init_subcommand(self):
        from rbsp.runtime.cli import main
        ret = main(["init", self.tmp_dir])
        self.assertEqual(ret, 0)
        self.assertTrue(os.path.isdir(os.path.join(self.tmp_dir, ".rbsp")))

    def test_cli_status_subcommand(self):
        from rbsp.runtime.cli import main
        # Init first so status has something to report
        main(["init", self.tmp_dir])
        RBSPEngine.reset()
        ret = main(["status", self.tmp_dir])
        self.assertEqual(ret, 0)

    def test_cli_no_command_shows_help(self):
        from rbsp.runtime.cli import main
        ret = main([])
        self.assertEqual(ret, 0)


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