"""Tests for core/transformation/plugin_host.py (Step 9)."""

from pathlib import Path
from types import ModuleType

import pytest

from konkon.core.models import BuildError, ConfigError, QueryError, QueryResult
from konkon.core.transformation.plugin_host import invoke_build, invoke_query, invoke_schema, load_plugin


def _write_plugin(path: Path, code: str) -> Path:
    """Write a konkon.py plugin file and return its path."""
    plugin = path / "konkon.py"
    plugin.write_text(code)
    return plugin


class TestLoadPlugin:
    """load_plugin(path) — dynamic module loading + contract validation."""

    def test_loads_valid_plugin(self, tmp_path: Path):
        """Valid plugin with build() and query() loads successfully."""
        plugin_path = _write_plugin(tmp_path, """\
def schema():
    return {"description": "test", "params": {}}

def build(raw_data, context):
    pass

def query(request):
    return ""
""")
        module = load_plugin(plugin_path)
        assert isinstance(module, ModuleType)
        assert callable(module.build)
        assert callable(module.query)

    def test_raises_if_build_missing(self, tmp_path: Path):
        """Plugin missing build() raises error."""
        plugin_path = _write_plugin(tmp_path, """\
def schema():
    return {"description": "test", "params": {}}

def query(request):
    return ""
""")
        with pytest.raises(ValueError, match="build"):
            load_plugin(plugin_path)

    def test_raises_if_query_missing(self, tmp_path: Path):
        """Plugin missing query() raises error."""
        plugin_path = _write_plugin(tmp_path, """\
def schema():
    return {"description": "test", "params": {}}

def build(raw_data, context):
    pass
""")
        with pytest.raises(ValueError, match="query"):
            load_plugin(plugin_path)

    def test_raises_if_file_not_found(self, tmp_path: Path):
        """Non-existent plugin file raises FileNotFoundError."""
        with pytest.raises(FileNotFoundError):
            load_plugin(tmp_path / "nonexistent.py")

    def test_raises_if_build_not_callable(self, tmp_path: Path):
        """build defined as non-callable raises error."""
        plugin_path = _write_plugin(tmp_path, """\
def schema():
    return {"description": "test", "params": {}}

build = 42

def query(request):
    return ""
""")
        with pytest.raises(ValueError, match="build"):
            load_plugin(plugin_path)


class TestBuildContractValidation:
    """build() signature validation (06_build_context.md §3.2)."""

    def test_rejects_v1_single_arg_build(self, tmp_path: Path):
        """build(raw_data) with 1 required arg raises ConfigError."""
        plugin_path = _write_plugin(tmp_path, """\
def schema():
    return {"description": "test", "params": {}}

def build(raw_data):
    pass

def query(request):
    return ""
""")
        with pytest.raises(ConfigError, match="2 required positional"):
            load_plugin(plugin_path)

    def test_rejects_zero_arg_build(self, tmp_path: Path):
        """build() with 0 required args raises ConfigError."""
        plugin_path = _write_plugin(tmp_path, """\
def schema():
    return {"description": "test", "params": {}}

def build():
    pass

def query(request):
    return ""
""")
        with pytest.raises(ConfigError, match="2 required positional"):
            load_plugin(plugin_path)

    def test_rejects_required_keyword_only(self, tmp_path: Path):
        """build(raw_data, context, *, required_kw) raises ConfigError."""
        plugin_path = _write_plugin(tmp_path, """\
def schema():
    return {"description": "test", "params": {}}

def build(raw_data, context, *, required_kw):
    pass

def query(request):
    return ""
""")
        with pytest.raises(ConfigError, match="keyword-only"):
            load_plugin(plugin_path)

    def test_accepts_extra_optional_params(self, tmp_path: Path):
        """build(raw_data, context, optional=None) is valid."""
        plugin_path = _write_plugin(tmp_path, """\
def schema():
    return {"description": "test", "params": {}}

def build(raw_data, context, optional=None):
    pass

def query(request):
    return ""
""")
        module = load_plugin(plugin_path)
        assert callable(module.build)


class TestInvokeBuild:
    """invoke_build(plugin, raw_data) — call plugin.build() with accessor."""

    def test_calls_build_with_accessor(self, tmp_path: Path):
        """invoke_build passes the accessor to plugin.build()."""
        plugin_path = _write_plugin(tmp_path, """\
received = None

def schema():
    return {"description": "test", "params": {}}

def build(raw_data, context):
    global received
    received = raw_data

def query(request):
    return ""
""")
        module = load_plugin(plugin_path)
        sentinel = object()
        invoke_build(module, sentinel, None)
        assert module.received is sentinel

    def test_build_error_propagates(self, tmp_path: Path):
        """BuildError from plugin.build() propagates unchanged."""
        plugin_path = _write_plugin(tmp_path, """\
from konkon.core.models import BuildError

def schema():
    return {"description": "test", "params": {}}

def build(raw_data, context):
    raise BuildError("vector DB down")

def query(request):
    return ""
""")
        module = load_plugin(plugin_path)
        with pytest.raises(BuildError, match="vector DB down"):
            invoke_build(module, None, None)

    def test_unexpected_exception_wrapped(self, tmp_path: Path):
        """Non-KonkonError from plugin.build() is wrapped as BuildError."""
        plugin_path = _write_plugin(tmp_path, """\
def schema():
    return {"description": "test", "params": {}}

def build(raw_data, context):
    raise KeyError("missing_key")

def query(request):
    return ""
""")
        module = load_plugin(plugin_path)
        with pytest.raises(BuildError, match="missing_key"):
            invoke_build(module, None, None)


class TestInvokeQuery:
    """invoke_query(plugin, request) — call plugin.query() with request."""

    def test_returns_string_result(self, tmp_path: Path):
        """invoke_query returns a string result from plugin.query()."""
        plugin_path = _write_plugin(tmp_path, """\
def schema():
    return {"description": "test", "params": {}}

def build(raw_data, context):
    pass

def query(request):
    return "found: " + request.query
""")
        from konkon.core.models import QueryRequest
        module = load_plugin(plugin_path)
        result = invoke_query(module, QueryRequest(query="hello"))
        assert result == "found: hello"

    def test_returns_query_result(self, tmp_path: Path):
        """invoke_query returns a QueryResult from plugin.query()."""
        plugin_path = _write_plugin(tmp_path, """\
from konkon.core.models import QueryResult

def schema():
    return {"description": "test", "params": {}}

def build(raw_data, context):
    pass

def query(request):
    return QueryResult(content="answer", metadata={"score": 0.9})
""")
        from konkon.core.models import QueryRequest
        module = load_plugin(plugin_path)
        result = invoke_query(module, QueryRequest(query="test"))
        assert isinstance(result, QueryResult)
        assert result.content == "answer"

    def test_query_error_propagates(self, tmp_path: Path):
        """QueryError from plugin.query() propagates unchanged."""
        plugin_path = _write_plugin(tmp_path, """\
from konkon.core.models import QueryError

def schema():
    return {"description": "test", "params": {}}

def build(raw_data, context):
    pass

def query(request):
    raise QueryError("search failed")
""")
        from konkon.core.models import QueryRequest
        module = load_plugin(plugin_path)
        with pytest.raises(QueryError, match="search failed"):
            invoke_query(module, QueryRequest(query="test"))

    def test_unexpected_exception_wrapped(self, tmp_path: Path):
        """Non-KonkonError from plugin.query() is wrapped as QueryError."""
        plugin_path = _write_plugin(tmp_path, """\
def schema():
    return {"description": "test", "params": {}}

def build(raw_data, context):
    pass

def query(request):
    raise RuntimeError("boom")
""")
        from konkon.core.models import QueryRequest
        module = load_plugin(plugin_path)
        with pytest.raises(QueryError, match="boom"):
            invoke_query(module, QueryRequest(query="test"))


class TestInvokeSchema:
    """invoke_schema(plugin) — call plugin.schema() and return dict."""

    def test_returns_schema_dict(self, tmp_path: Path):
        """invoke_schema returns the schema dict from plugin.schema()."""
        plugin_path = _write_plugin(tmp_path, """\
def schema():
    return {"description": "test plugin", "params": {"q": {"type": "string"}}}

def build(raw_data, context):
    pass

def query(request):
    return ""
""")
        module = load_plugin(plugin_path)
        result = invoke_schema(module)
        assert result == {"description": "test plugin", "params": {"q": {"type": "string"}}}

    def test_schema_exception_wrapped_as_value_error(self, tmp_path: Path):
        """Exception in plugin.schema() is wrapped as ValueError."""
        plugin_path = _write_plugin(tmp_path, """\
def schema():
    raise RuntimeError("bad config")

def build(raw_data, context):
    pass

def query(request):
    return ""
""")
        module = load_plugin(plugin_path)
        with pytest.raises(ValueError, match="bad config"):
            invoke_schema(module)

    def test_non_dict_return_raises_value_error(self, tmp_path: Path):
        """schema() returning non-dict raises ValueError."""
        plugin_path = _write_plugin(tmp_path, """\
def schema():
    return "not a dict"

def build(raw_data, context):
    pass

def query(request):
    return ""
""")
        module = load_plugin(plugin_path)
        with pytest.raises(ValueError, match="must return dict"):
            invoke_schema(module)

    def test_none_return_raises_value_error(self, tmp_path: Path):
        """schema() returning None raises ValueError."""
        plugin_path = _write_plugin(tmp_path, """\
def schema():
    return None

def build(raw_data, context):
    pass

def query(request):
    return ""
""")
        module = load_plugin(plugin_path)
        with pytest.raises(ValueError, match="must return dict"):
            invoke_schema(module)
