"""Step definitions for LSP Registry, Runtime, and Tool Adapter tests.

Covers LspRegistry, LspRuntime, LspToolAdapter, LspBinding,
LspServerConfig, and the LSP error hierarchy.
"""

from __future__ import annotations

from typing import Any

from behave import given, then, when
from behave.runner import Context
from pydantic import ValidationError as PydanticValidationError

from cleveragents.core.exceptions import CleverAgentsError
from cleveragents.lsp.errors import (
    LspError,
    LspNotAvailableError,
    LspServerNotFoundError,
)
from cleveragents.lsp.models import (
    LspBinding,
    LspCapability,
    LspServerConfig,
)
from cleveragents.lsp.registry import LspRegistry
from cleveragents.lsp.runtime import LspRuntime
from cleveragents.lsp.tool_adapter import LspToolAdapter

# ---------------------------------------------------------------------------
# Capability name -> enum mapping
# ---------------------------------------------------------------------------
_CAP_MAP: dict[str, LspCapability] = {c.value: c for c in LspCapability}


# ---------------------------------------------------------------------------
# Background
# ---------------------------------------------------------------------------


@given("a clean LSP registry")
def step_clean_lsp_registry(context: Context) -> None:
    context.lsp_registry = LspRegistry()
    context.lsp_config: LspServerConfig | None = None
    context.lsp_result: Any = None
    context.lsp_error: Exception | None = None
    context.lsp_tool_specs: list[dict[str, Any]] = []


# ---------------------------------------------------------------------------
# Config creation
# ---------------------------------------------------------------------------


@given(
    'an LSP server config named "{name}" for language "{language}" '
    'with command "{command}"'
)
def step_lsp_config_single_lang(
    context: Context, name: str, language: str, command: str
) -> None:
    context.lsp_config = LspServerConfig(
        name=name,
        languages=[language.strip().lower()],
        command=command,
    )


@given(
    'an LSP server config named "{name}" for languages "{languages}" '
    'with command "{command}"'
)
def step_lsp_config_multi_lang(
    context: Context, name: str, languages: str, command: str
) -> None:
    lang_list = [lang.strip().lower() for lang in languages.split(",")]
    context.lsp_config = LspServerConfig(
        name=name,
        languages=lang_list,
        command=command,
    )


@given('the LSP server config has capabilities "{caps_str}"')
def step_lsp_config_capabilities(context: Context, caps_str: str) -> None:
    caps = [_CAP_MAP[c.strip()] for c in caps_str.split(",")]
    assert context.lsp_config is not None
    context.lsp_config = context.lsp_config.model_copy(update={"capabilities": caps})


# ---------------------------------------------------------------------------
# Register
# ---------------------------------------------------------------------------


@when("the LSP server is registered")
@given("the LSP server is registered")
def step_register_lsp_server(context: Context) -> None:
    assert context.lsp_config is not None
    context.lsp_registry.register(context.lsp_config)


@when('a second LSP server with the same name "{name}" is registered')
def step_register_duplicate(context: Context, name: str) -> None:
    dup = LspServerConfig(name=name, command="dup-cmd", languages=["python"])
    try:
        context.lsp_registry.register(dup)
        context.lsp_error = None
    except ValueError as exc:
        context.lsp_error = exc


@given("the following LSP servers are registered:")
def step_register_table(context: Context) -> None:
    for row in context.table:
        config = LspServerConfig(
            name=row["name"],
            languages=[row["language"].strip().lower()],
            command=row["command"],
        )
        context.lsp_registry.register(config)


# ---------------------------------------------------------------------------
# Lookup
# ---------------------------------------------------------------------------


@when('I look up LSP server "{name}"')
def step_lookup(context: Context, name: str) -> None:
    context.lsp_result = context.lsp_registry.get(name)


@when('I look up LSP server "{name}" with get_or_raise')
def step_lookup_or_raise(context: Context, name: str) -> None:
    try:
        context.lsp_result = context.lsp_registry.get_or_raise(name)
        context.lsp_error = None
    except LspServerNotFoundError as exc:
        context.lsp_error = exc


# ---------------------------------------------------------------------------
# List
# ---------------------------------------------------------------------------


@when("I list all LSP servers")
def step_list_all(context: Context) -> None:
    context.lsp_result = context.lsp_registry.list_servers()


@when('I list LSP servers with namespace "{namespace}"')
def step_list_ns(context: Context, namespace: str) -> None:
    context.lsp_result = context.lsp_registry.list_servers(namespace=namespace)


@when('I list LSP servers with language "{language}"')
def step_list_lang(context: Context, language: str) -> None:
    context.lsp_result = context.lsp_registry.list_servers(language=language)


# ---------------------------------------------------------------------------
# Remove
# ---------------------------------------------------------------------------


@when('I remove LSP server "{name}"')
def step_remove(context: Context, name: str) -> None:
    context.lsp_result = context.lsp_registry.remove(name)


# ---------------------------------------------------------------------------
# Runtime
# ---------------------------------------------------------------------------


@given("a local-mode LSP runtime")
def step_lsp_runtime(context: Context) -> None:
    context.lsp_runtime = LspRuntime()


@when('I call start_server on the runtime with name "{name}" and workspace "{ws}"')
def step_runtime_start(context: Context, name: str, ws: str) -> None:
    try:
        context.lsp_runtime.start_server(name, ws)
        context.lsp_error = None
    except (LspNotAvailableError, LspServerNotFoundError, LspError) as exc:
        context.lsp_error = exc


@when('I call stop_server on the runtime with name "{name}"')
def step_runtime_stop(context: Context, name: str) -> None:
    try:
        context.lsp_runtime.stop_server(name)
        context.lsp_error = None
    except (LspNotAvailableError, LspServerNotFoundError, LspError) as exc:
        context.lsp_error = exc


@when('I call get_diagnostics on the runtime with name "{name}" and file "{file_path}"')
def step_runtime_diagnostics(context: Context, name: str, file_path: str) -> None:
    try:
        context.lsp_result = context.lsp_runtime.get_diagnostics(name, file_path)
        context.lsp_error = None
    except (LspNotAvailableError, LspServerNotFoundError, LspError) as exc:
        context.lsp_error = exc


@when(
    'I call get_completions on the runtime with name "{name}" '
    'file "{file_path}" line {line:d} column {col:d}'
)
def step_runtime_completions(
    context: Context, name: str, file_path: str, line: int, col: int
) -> None:
    try:
        context.lsp_result = context.lsp_runtime.get_completions(
            name, file_path, line, col
        )
        context.lsp_error = None
    except (LspNotAvailableError, LspServerNotFoundError, LspError) as exc:
        context.lsp_error = exc


# ---------------------------------------------------------------------------
# Tool adapter
# ---------------------------------------------------------------------------


@when("I generate tool specs from the adapter")
def step_generate_specs(context: Context) -> None:
    adapter = LspToolAdapter()
    assert context.lsp_config is not None
    context.lsp_tool_specs = adapter.generate_tool_specs(context.lsp_config)


@when("I invoke the first tool spec handler")
def step_invoke_handler(context: Context) -> None:
    assert context.lsp_tool_specs, "No tool specs generated"
    handler = context.lsp_tool_specs[0]["handler"]
    try:
        handler()
        context.lsp_error = None
    except LspNotAvailableError as exc:
        context.lsp_error = exc


# ---------------------------------------------------------------------------
# Binding
# ---------------------------------------------------------------------------


@when('I create a binding for node "{node}" to server "{server}"')
def step_create_binding(context: Context, node: str, server: str) -> None:
    try:
        context.lsp_binding = LspBinding(
            node_name=node,
            lsp_server_name=server,
        )
        context.lsp_error = None
    except PydanticValidationError as exc:
        context.lsp_error = exc


# ---------------------------------------------------------------------------
# Model validation
# ---------------------------------------------------------------------------


@when('I try to create an LSP server config with name "{name}" and command "{command}"')
def step_invalid_config(context: Context, name: str, command: str) -> None:
    try:
        context.lsp_config = LspServerConfig(name=name, command=command)
        context.lsp_error = None
    except PydanticValidationError as exc:
        context.lsp_error = exc


# ---------------------------------------------------------------------------
# Error creation
# ---------------------------------------------------------------------------


@when("I create an LspNotAvailableError with default message")
def step_create_not_available_error(context: Context) -> None:
    context.lsp_error = LspNotAvailableError()


@when('I create an LspServerNotFoundError for server "{name}"')
def step_create_not_found_error(context: Context, name: str) -> None:
    context.lsp_error = LspServerNotFoundError(name)


@when("I try to create an LspServerNotFoundError with empty name")
def step_create_not_found_empty(context: Context) -> None:
    try:
        LspServerNotFoundError("")
        context.lsp_error = None
    except ValueError as exc:
        context.lsp_error = exc


# ---------------------------------------------------------------------------
# Assertions
# ---------------------------------------------------------------------------


@then('the LSP registry should contain "{name}"')
def step_registry_contains(context: Context, name: str) -> None:
    assert name in context.lsp_registry, f"Registry should contain '{name}'"


@then("the LSP registry should have {count:d} server")
@then("the LSP registry should have {count:d} servers")
def step_registry_count(context: Context, count: int) -> None:
    assert len(context.lsp_registry) == count, (
        f"Expected {count} servers, got {len(context.lsp_registry)}"
    )


@then('the LSP server "{name}" should support language "{lang}"')
def step_server_supports_lang(context: Context, name: str, lang: str) -> None:
    config = context.lsp_registry.get(name)
    assert config is not None, f"Server '{name}' not found"
    assert lang.lower() in config.languages, f"Server '{name}' should support '{lang}'"


@then('the LSP server "{name}" should have {count:d} capabilities')
def step_server_cap_count(context: Context, name: str, count: int) -> None:
    config = context.lsp_registry.get(name)
    assert config is not None, f"Server '{name}' not found"
    assert len(config.capabilities) == count, (
        f"Expected {count} capabilities, got {len(config.capabilities)}"
    )


@then("the LSP lookup result should not be None")
def step_result_not_none(context: Context) -> None:
    assert context.lsp_result is not None


@then("the LSP lookup result should be None")
def step_result_none(context: Context) -> None:
    assert context.lsp_result is None


@then('the LSP lookup result name should be "{name}"')
def step_result_name(context: Context, name: str) -> None:
    assert context.lsp_result.name == name


@then("the LSP server list should contain {count:d} servers")
def step_list_count(context: Context, count: int) -> None:
    assert len(context.lsp_result) == count, (
        f"Expected {count}, got {len(context.lsp_result)}"
    )


@then("the LSP removal result should be True")
def step_removal_true(context: Context) -> None:
    assert context.lsp_result is True


@then("the LSP removal result should be False")
def step_removal_false(context: Context) -> None:
    assert context.lsp_result is False


# NOTE: The @then step 'a ValueError should be raised mentioning ...'
# is defined in config_service_coverage_steps.py (shared step).


@then("an LspNotAvailableError should be raised")
def step_not_available_error(context: Context) -> None:
    assert context.lsp_error is not None, "Expected an error"
    assert isinstance(context.lsp_error, LspNotAvailableError), (
        f"Expected LspNotAvailableError, got {type(context.lsp_error).__name__}"
    )


@then("an LSP error should be raised")
def step_any_lsp_error(context: Context) -> None:
    assert context.lsp_error is not None, "Expected an LSP error"
    assert isinstance(context.lsp_error, LspError), (
        f"Expected LspError subclass, got {type(context.lsp_error).__name__}"
    )


@then('the LSP error message should mention "{text}"')
@then('the LSP error message should contain "{text}"')
def step_lsp_error_mentions(context: Context, text: str) -> None:
    assert context.lsp_error is not None
    assert text in str(context.lsp_error), (
        f"Error '{context.lsp_error}' should mention '{text}'"
    )


@then('an LspServerNotFoundError should be raised for "{name}"')
def step_not_found_error(context: Context, name: str) -> None:
    assert context.lsp_error is not None, "Expected an error"
    assert isinstance(context.lsp_error, LspServerNotFoundError), (
        f"Expected LspServerNotFoundError, got {type(context.lsp_error).__name__}"
    )
    assert name in str(context.lsp_error)


@then("the tool spec list should contain {count:d} specs")
def step_spec_count(context: Context, count: int) -> None:
    assert len(context.lsp_tool_specs) == count, (
        f"Expected {count} specs, got {len(context.lsp_tool_specs)}"
    )


@then('the tool spec names should include "{name}"')
def step_spec_name_includes(context: Context, name: str) -> None:
    names = [s["name"] for s in context.lsp_tool_specs]
    assert name in names, f"Tool spec '{name}' not in {names}"


@then("the binding should be valid")
def step_binding_valid(context: Context) -> None:
    assert context.lsp_error is None, f"Unexpected error: {context.lsp_error}"
    assert hasattr(context, "lsp_binding"), "No binding created"


@then('the binding node_name should be "{name}"')
def step_binding_node(context: Context, name: str) -> None:
    assert context.lsp_binding.node_name == name


@then('the binding lsp_server_name should be "{name}"')
def step_binding_server(context: Context, name: str) -> None:
    assert context.lsp_binding.lsp_server_name == name


@then("the binding auto_detect should be True")
def step_binding_auto_detect(context: Context) -> None:
    assert context.lsp_binding.auto_detect is True


# NOTE: The @then step 'a ValidationError should be raised mentioning ...'
# is defined in config_service_coverage_steps.py (shared step).


@then('the LSP server config language "{lang}" should be present')
def step_config_lang_present(context: Context, lang: str) -> None:
    assert context.lsp_config is not None
    assert lang in context.lsp_config.languages


@then('the LSP server config namespace should be "{ns}"')
def step_config_namespace(context: Context, ns: str) -> None:
    assert context.lsp_config is not None
    assert context.lsp_config.namespace == ns


@then('the LSP server config short_name should be "{short}"')
def step_config_short_name(context: Context, short: str) -> None:
    assert context.lsp_config is not None
    assert context.lsp_config.short_name == short


@then("LspError should be a subclass of CleverAgentsError")
def step_lsp_error_hierarchy(context: Context) -> None:
    assert issubclass(LspError, CleverAgentsError)


@then('the error server_name should be "{name}"')
def step_error_server_name(context: Context, name: str) -> None:
    assert hasattr(context.lsp_error, "server_name"), "Error has no server_name"
    assert context.lsp_error.server_name == name


@then("a ValueError should be raised")
def step_value_error_raised(context: Context) -> None:
    assert context.lsp_error is not None, "Expected a ValueError"
    assert isinstance(context.lsp_error, ValueError), (
        f"Expected ValueError, got {type(context.lsp_error).__name__}"
    )
