"""Tests for rbsp.runtime.engine -- singleton engine, tokenizer, scoring."""

import os
import sys
import unittest

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

from rbsp.runtime.engine import (
    RBSPEngine,
    IndexStats,
    Result,
    _IndexEntry,
    _tokenize,
    _trigrams,
    _content_hash,
    _query_score,
)


class TestTokenize(unittest.TestCase):
    """Tests for _tokenize()."""

    def test_basic_words(self):
        tokens = _tokenize("Hello World")
        self.assertEqual(tokens, ["hello", "world"])

    def test_underscores_kept(self):
        tokens = _tokenize("foo_bar baz")
        self.assertEqual(tokens, ["foo_bar", "baz"])

    def test_empty_string(self):
        self.assertEqual(_tokenize(""), [])

    def test_numbers(self):
        tokens = _tokenize("var1 = 42")
        self.assertEqual(tokens, ["var1", "42"])

    def test_special_characters_split(self):
        tokens = _tokenize("a.b.c(d)")
        self.assertEqual(tokens, ["a", "b", "c", "d"])

    def test_case_lowered(self):
        tokens = _tokenize("CamelCase")
        # Porter stemmer: "camelcase" -> "camelcas"
        self.assertEqual(tokens, ["camelcas"])

    def test_trailing_word(self):
        tokens = _tokenize("one two three")
        # Porter stemmer: "one" -> "on"
        self.assertEqual(tokens, ["on", "two", "three"])


class TestTrigrams(unittest.TestCase):
    """Tests for _trigrams()."""

    def test_short_string_no_trigrams(self):
        self.assertEqual(_trigrams("ab"), set())

    def test_exact_three_chars(self):
        self.assertEqual(_trigrams("abc"), {"abc"})

    def test_four_chars(self):
        result = _trigrams("abcd")
        self.assertEqual(result, {"abc", "bcd"})

    def test_empty_string(self):
        self.assertEqual(_trigrams(""), set())

    def test_case_insensitive(self):
        self.assertEqual(_trigrams("ABC"), _trigrams("abc"))


class TestContentHash(unittest.TestCase):
    """Tests for _content_hash()."""

    def test_deterministic(self):
        h1 = _content_hash("hello world")
        h2 = _content_hash("hello world")
        self.assertEqual(h1, h2)

    def test_different_content_different_hash(self):
        h1 = _content_hash("hello")
        h2 = _content_hash("world")
        self.assertNotEqual(h1, h2)

    def test_returns_hex_string(self):
        h = _content_hash("test")
        self.assertEqual(len(h), 32)  # md5 hex length
        int(h, 16)  # should not raise

    def test_empty_string(self):
        h = _content_hash("")
        self.assertIsInstance(h, str)
        self.assertEqual(len(h), 32)


class TestQueryScore(unittest.TestCase):
    """Tests for _query_score()."""

    def _make_entry(self, text):
        tokens = _tokenize(text)
        return _IndexEntry(
            path="/fake",
            relative_path="fake",
            content_hash="aaa",
            size=len(text),
            modified=0.0,
            tokens=tokens,
            trigrams=_trigrams(text),
            token_set=frozenset(tokens),
        )

    def test_perfect_match(self):
        entry = self._make_entry("hello world")
        score = _query_score(["hello", "world"], _trigrams("hello world"), entry)
        self.assertGreater(score, 0.5)

    def test_no_match(self):
        entry = self._make_entry("alpha beta gamma")
        score = _query_score(["zzz", "yyy"], _trigrams("zzz yyy"), entry)
        self.assertAlmostEqual(score, 0.0, places=2)

    def test_empty_query(self):
        entry = self._make_entry("hello world")
        score = _query_score([], set(), entry)
        self.assertEqual(score, 0.0)

    def test_empty_entry(self):
        entry = self._make_entry("")
        score = _query_score(["hello"], _trigrams("hello"), entry)
        self.assertEqual(score, 0.0)

    def test_partial_match(self):
        entry = self._make_entry("hello world foo")
        score = _query_score(
            ["hello", "missing"], _trigrams("hello missing"), entry
        )
        self.assertGreater(score, 0.0)
        self.assertLess(score, 1.0)

    def test_score_in_range(self):
        entry = self._make_entry("python data science")
        score = _query_score(
            ["python", "science"], _trigrams("python science"), entry
        )
        self.assertGreaterEqual(score, 0.0)
        self.assertLessEqual(score, 1.0)


class TestEngineSingleton(RBSPTestCase):
    """Tests for RBSPEngine singleton lifecycle."""

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

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

    def test_get_instance_returns_engine(self):
        engine = RBSPEngine.get_instance()
        self.assertIsInstance(engine, RBSPEngine)

    def test_singleton_same_instance(self):
        e1 = RBSPEngine.get_instance()
        e2 = RBSPEngine.get_instance()
        self.assertIs(e1, e2)

    def test_reset_clears_singleton(self):
        e1 = RBSPEngine.get_instance()
        RBSPEngine.reset()
        e2 = RBSPEngine.get_instance()
        self.assertIsNot(e1, e2)

    def test_not_initialized_by_default(self):
        engine = RBSPEngine.get_instance()
        self.assertFalse(engine.is_initialized)


class TestEngineInit(RBSPTestCase):
    """Tests for RBSPEngine.init()."""

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

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

    def test_init_creates_rbsp_dir(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))

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

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

    def test_init_creates_config_json(self):
        engine = RBSPEngine.get_instance()
        engine.init(self.tmp_dir)
        config_file = os.path.join(self.tmp_dir, ".rbsp", "config.json")
        self.assertTrue(os.path.isfile(config_file))

    def test_init_sets_initialized_flag(self):
        engine = RBSPEngine.get_instance()
        engine.init(self.tmp_dir)
        self.assertTrue(engine.is_initialized)

    def test_init_with_overrides(self):
        engine = RBSPEngine.get_instance()
        engine.init(self.tmp_dir, overrides={"max_results": 10})
        self.assertEqual(engine.config.max_results, 10)

    def test_init_idempotent(self):
        engine = RBSPEngine.get_instance()
        engine.init(self.tmp_dir)
        engine.init(self.tmp_dir)
        self.assertTrue(engine.is_initialized)


class TestEngineIndex(RBSPTestCase):
    """Tests for RBSPEngine.index()."""

    def setUp(self):
        super().setUp()
        RBSPEngine.reset()
        self.engine = RBSPEngine.get_instance()
        self.engine.init(self.tmp_dir)
        # Create some sample files to index
        self._create_sample_files()

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

    def _create_sample_files(self):
        src_dir = os.path.join(self.tmp_dir, "src")
        os.makedirs(src_dir, exist_ok=True)
        self.make_sample_file(os.path.join("src", "hello.py"), "def hello():\n    print('hello world')\n")
        self.make_sample_file(os.path.join("src", "math.py"), "def add(a, b):\n    return a + b\n")
        self.make_sample_file(os.path.join("src", "readme.txt"), "This is a readme file about the project.\n")

    def test_index_returns_stats(self):
        stats = self.engine.index(self.tmp_dir)
        self.assertIsInstance(stats, IndexStats)

    def test_index_counts_files(self):
        stats = self.engine.index(self.tmp_dir)
        self.assertGreaterEqual(stats.files_indexed, 1)

    def test_index_records_total_size(self):
        stats = self.engine.index(self.tmp_dir)
        self.assertGreater(stats.total_size, 0)

    def test_index_records_duration(self):
        stats = self.engine.index(self.tmp_dir)
        self.assertGreater(stats.duration, 0.0)

    def test_index_skips_unchanged_files(self):
        stats1 = self.engine.index(self.tmp_dir)
        stats2 = self.engine.index(self.tmp_dir)
        # Second time around, files should be skipped since unchanged
        self.assertGreaterEqual(stats2.files_skipped, stats1.files_indexed)
        self.assertEqual(stats2.files_indexed, 0)

    def test_index_force_reindexes(self):
        self.engine.index(self.tmp_dir)
        stats = self.engine.index(self.tmp_dir, force=True)
        self.assertGreaterEqual(stats.files_indexed, 1)

    def test_index_updates_engine_stats(self):
        self.engine.index(self.tmp_dir)
        self.assertEqual(self.engine._stats["index_operations"], 1)


class TestEngineResolve(RBSPTestCase):
    """Tests for RBSPEngine.resolve()."""

    def setUp(self):
        super().setUp()
        RBSPEngine.reset()
        self.engine = RBSPEngine.get_instance()
        self.engine.init(self.tmp_dir)
        src_dir = os.path.join(self.tmp_dir, "src")
        os.makedirs(src_dir, exist_ok=True)
        self.make_sample_file(os.path.join("src", "auth.py"), "def authenticate(user, password):\n    return check_credentials(user, password)\n")
        self.make_sample_file(os.path.join("src", "math.py"), "def add(a, b):\n    return a + b\n")
        self.engine.index(self.tmp_dir)

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

    def test_resolve_returns_list(self):
        results = self.engine.resolve("authenticate")
        self.assertIsInstance(results, list)

    def test_resolve_results_are_result_type(self):
        results = self.engine.resolve("authenticate")
        for r in results:
            self.assertIsInstance(r, Result)

    def test_resolve_scores_descending(self):
        results = self.engine.resolve("authenticate")
        if len(results) >= 2:
            scores = [r.score for r in results]
            self.assertEqual(scores, sorted(scores, reverse=True))

    def test_resolve_relevant_results(self):
        results = self.engine.resolve("authenticate user password")
        if results:
            # The auth file should score higher
            paths = [r.path for r in results]
            auth_paths = [p for p in paths if "auth" in p]
            self.assertTrue(len(auth_paths) > 0)

    def test_resolve_increments_query_count(self):
        self.engine.resolve("test query")
        self.assertEqual(self.engine._stats["queries"], 1)

    def test_search_delegates_to_resolve(self):
        results = self.engine.search("add numbers")
        self.assertIsInstance(results, list)


class TestEngineStatus(RBSPTestCase):
    """Tests for RBSPEngine.status()."""

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

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

    def test_status_returns_dict(self):
        engine = RBSPEngine.get_instance()
        engine.init(self.tmp_dir)
        status = engine.status()
        self.assertIsInstance(status, dict)

    def test_status_contains_expected_keys(self):
        engine = RBSPEngine.get_instance()
        engine.init(self.tmp_dir)
        status = engine.status()
        for key in ("initialized", "platform", "index_entries", "watching", "plugins", "stats", "config_root"):
            self.assertIn(key, status)

    def test_status_initialized_true_after_init(self):
        engine = RBSPEngine.get_instance()
        engine.init(self.tmp_dir)
        status = engine.status()
        self.assertTrue(status["initialized"])

    def test_status_watching_false_by_default(self):
        engine = RBSPEngine.get_instance()
        engine.init(self.tmp_dir)
        status = engine.status()
        self.assertFalse(status["watching"])


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