"""Tests for rbsp.runtime.plugins -- PluginResult, PluginManager, plugin base classes."""

from __future__ import annotations

import os
import sys
import unittest
from typing import Any, Dict, List

sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))

from rbsp.runtime.plugins import (
    IndexerPlugin,
    PluginManager,
    PluginResult,
    RankerPlugin,
    ResolverPlugin,
)


# ---------------------------------------------------------------------------
# Concrete test subclasses
# ---------------------------------------------------------------------------

class EchoResolver(ResolverPlugin):
    """Test resolver that echoes back the query."""

    name = "echo_resolver"
    priority = 50

    def resolve(self, query: str, scope: str, **kwargs: Any) -> List[PluginResult]:
        return [PluginResult(path=f"/{scope}/{query}", score=1.0, snippet=query)]


class FallbackResolver(ResolverPlugin):
    """Lower-priority resolver for testing ordering."""

    name = "fallback_resolver"
    priority = 200

    def resolve(self, query: str, scope: str, **kwargs: Any) -> List[PluginResult]:
        return [PluginResult(path="/fallback", score=0.1)]

    def can_handle(self, query: str) -> bool:
        return query.startswith("fallback:")


class PythonIndexer(IndexerPlugin):
    """Test indexer for Python files."""

    name = "python_indexer"
    extensions = [".py"]

    def index_file(self, path: str, content: str) -> Dict[str, Any]:
        return {"line_count": content.count("\n") + 1}


class GenericIndexer(IndexerPlugin):
    """Test indexer with no extension filter."""

    name = "generic_indexer"
    extensions = []

    def index_file(self, path: str, content: str) -> Dict[str, Any]:
        return {"size": len(content)}


class BoostRanker(RankerPlugin):
    """Test ranker that boosts scores by 2x."""

    name = "boost_ranker"

    def rank(
        self, query: str, candidates: List[PluginResult]
    ) -> List[PluginResult]:
        for c in candidates:
            c.score *= 2.0
        return sorted(candidates, key=lambda r: r.score, reverse=True)


# ---------------------------------------------------------------------------
# PluginResult
# ---------------------------------------------------------------------------

class TestPluginResult(unittest.TestCase):
    """PluginResult is a dataclass for search results."""

    def test_creation_minimal(self):
        r = PluginResult(path="/foo.py", score=0.95)
        self.assertEqual(r.path, "/foo.py")
        self.assertEqual(r.score, 0.95)
        self.assertEqual(r.snippet, "")
        self.assertEqual(r.line, 0)
        self.assertEqual(r.metadata, {})

    def test_creation_full(self):
        r = PluginResult(
            path="/bar.py",
            score=0.8,
            snippet="def bar():",
            line=42,
            metadata={"lang": "python"},
        )
        self.assertEqual(r.path, "/bar.py")
        self.assertEqual(r.score, 0.8)
        self.assertEqual(r.snippet, "def bar():")
        self.assertEqual(r.line, 42)
        self.assertEqual(r.metadata, {"lang": "python"})

    def test_metadata_default_factory(self):
        r1 = PluginResult(path="a", score=1.0)
        r2 = PluginResult(path="b", score=1.0)
        r1.metadata["key"] = "value"
        # r2 should not be affected (separate dict)
        self.assertEqual(r2.metadata, {})


# ---------------------------------------------------------------------------
# PluginManager -- registration
# ---------------------------------------------------------------------------

class TestPluginManagerRegisterResolver(unittest.TestCase):
    """register_resolver and resolvers accessor."""

    def test_register_single(self):
        pm = PluginManager()
        pm.register_resolver(EchoResolver())
        self.assertEqual(len(pm.resolvers), 1)
        self.assertEqual(pm.resolvers[0].name, "echo_resolver")

    def test_resolvers_sorted_by_priority(self):
        pm = PluginManager()
        pm.register_resolver(FallbackResolver())  # priority 200
        pm.register_resolver(EchoResolver())  # priority 50
        names = [r.name for r in pm.resolvers]
        self.assertEqual(names, ["echo_resolver", "fallback_resolver"])

    def test_resolvers_returns_copy(self):
        pm = PluginManager()
        pm.register_resolver(EchoResolver())
        resolvers = pm.resolvers
        resolvers.clear()
        self.assertEqual(len(pm.resolvers), 1)


class TestPluginManagerRegisterIndexer(unittest.TestCase):
    """register_indexer and indexers accessor."""

    def test_register_single(self):
        pm = PluginManager()
        pm.register_indexer(PythonIndexer())
        self.assertEqual(len(pm.indexers), 1)
        self.assertEqual(pm.indexers[0].name, "python_indexer")

    def test_register_multiple(self):
        pm = PluginManager()
        pm.register_indexer(PythonIndexer())
        pm.register_indexer(GenericIndexer())
        self.assertEqual(len(pm.indexers), 2)

    def test_indexers_returns_copy(self):
        pm = PluginManager()
        pm.register_indexer(PythonIndexer())
        indexers = pm.indexers
        indexers.clear()
        self.assertEqual(len(pm.indexers), 1)


class TestPluginManagerRegisterRanker(unittest.TestCase):
    """register_ranker and rankers accessor."""

    def test_register_single(self):
        pm = PluginManager()
        pm.register_ranker(BoostRanker())
        self.assertEqual(len(pm.rankers), 1)
        self.assertEqual(pm.rankers[0].name, "boost_ranker")

    def test_rankers_returns_copy(self):
        pm = PluginManager()
        pm.register_ranker(BoostRanker())
        rankers = pm.rankers
        rankers.clear()
        self.assertEqual(len(pm.rankers), 1)


# ---------------------------------------------------------------------------
# PluginManager -- hooks
# ---------------------------------------------------------------------------

class TestPluginManagerHooks(unittest.TestCase):
    """register_hook and run_hook."""

    def test_register_valid_hook(self):
        pm = PluginManager()
        calls = []
        pm.register_hook("pre_query", lambda **kw: calls.append(kw))
        pm.run_hook("pre_query", query="test")
        self.assertEqual(len(calls), 1)
        self.assertEqual(calls[0]["query"], "test")

    def test_register_invalid_hook_raises(self):
        pm = PluginManager()
        with self.assertRaises(ValueError):
            pm.register_hook("invalid_hook", lambda: None)

    def test_run_hook_returns_values(self):
        pm = PluginManager()
        pm.register_hook("pre_index", lambda **kw: "result_a")
        pm.register_hook("pre_index", lambda **kw: "result_b")
        results = pm.run_hook("pre_index")
        self.assertEqual(results, ["result_a", "result_b"])

    def test_run_hook_empty(self):
        pm = PluginManager()
        results = pm.run_hook("pre_query")
        self.assertEqual(results, [])

    def test_run_hook_exception_silenced(self):
        pm = PluginManager()

        def bad_hook(**kw):
            raise RuntimeError("boom")

        def good_hook(**kw):
            return "ok"

        pm.register_hook("post_query", bad_hook)
        pm.register_hook("post_query", good_hook)
        results = pm.run_hook("post_query")
        # bad_hook silenced; good_hook runs
        self.assertEqual(results, ["ok"])

    def test_all_valid_hook_names(self):
        pm = PluginManager()
        valid = ["pre_index", "post_index", "pre_query", "post_query", "custom_ranker"]
        for hook_name in valid:
            pm.register_hook(hook_name, lambda **kw: None)
        # Should not raise


# ---------------------------------------------------------------------------
# PluginManager -- summary
# ---------------------------------------------------------------------------

class TestPluginManagerSummary(unittest.TestCase):
    """summary() returns a dict of registered plugins."""

    def test_empty_summary(self):
        pm = PluginManager()
        s = pm.summary()
        self.assertEqual(s["resolvers"], [])
        self.assertEqual(s["indexers"], [])
        self.assertEqual(s["rankers"], [])
        self.assertEqual(s["hooks"], {})
        self.assertEqual(s["loaded_files"], [])

    def test_populated_summary(self):
        pm = PluginManager()
        pm.register_resolver(EchoResolver())
        pm.register_indexer(PythonIndexer())
        pm.register_ranker(BoostRanker())
        pm.register_hook("pre_query", lambda **kw: None)

        s = pm.summary()
        self.assertEqual(s["resolvers"], ["echo_resolver"])
        self.assertEqual(s["indexers"], ["python_indexer"])
        self.assertEqual(s["rankers"], ["boost_ranker"])
        self.assertEqual(s["hooks"]["pre_query"], 1)


# ---------------------------------------------------------------------------
# Plugin base class can_handle
# ---------------------------------------------------------------------------

class TestResolverCanHandle(unittest.TestCase):
    """ResolverPlugin.can_handle defaults to True."""

    def test_default_can_handle(self):
        r = EchoResolver()
        self.assertTrue(r.can_handle("anything"))

    def test_custom_can_handle(self):
        r = FallbackResolver()
        self.assertTrue(r.can_handle("fallback:test"))
        self.assertFalse(r.can_handle("other"))


class TestIndexerCanHandle(unittest.TestCase):
    """IndexerPlugin.can_handle filters by extension."""

    def test_matching_extension(self):
        indexer = PythonIndexer()
        self.assertTrue(indexer.can_handle("foo.py"))
        self.assertTrue(indexer.can_handle("/path/to/bar.py"))

    def test_non_matching_extension(self):
        indexer = PythonIndexer()
        self.assertFalse(indexer.can_handle("foo.js"))
        self.assertFalse(indexer.can_handle("foo.txt"))

    def test_no_extensions_handles_all(self):
        indexer = GenericIndexer()
        self.assertTrue(indexer.can_handle("anything.xyz"))
        self.assertTrue(indexer.can_handle("no_extension"))


# ---------------------------------------------------------------------------
# Plugin functionality
# ---------------------------------------------------------------------------

class TestResolverFunctionality(unittest.TestCase):
    """EchoResolver resolve method."""

    def test_resolve_returns_results(self):
        r = EchoResolver()
        results = r.resolve("test_query", "my_scope")
        self.assertEqual(len(results), 1)
        self.assertEqual(results[0].path, "/my_scope/test_query")
        self.assertEqual(results[0].score, 1.0)
        self.assertEqual(results[0].snippet, "test_query")


class TestIndexerFunctionality(unittest.TestCase):
    """PythonIndexer index_file method."""

    def test_index_file(self):
        indexer = PythonIndexer()
        result = indexer.index_file("test.py", "line1\nline2\nline3")
        self.assertEqual(result, {"line_count": 3})


class TestRankerFunctionality(unittest.TestCase):
    """BoostRanker rank method."""

    def test_rank_boosts_scores(self):
        ranker = BoostRanker()
        candidates = [
            PluginResult(path="a", score=0.5),
            PluginResult(path="b", score=1.0),
        ]
        ranked = ranker.rank("query", candidates)
        self.assertEqual(ranked[0].path, "b")
        self.assertEqual(ranked[0].score, 2.0)
        self.assertEqual(ranked[1].path, "a")
        self.assertEqual(ranked[1].score, 1.0)


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