"""Step definitions for LSP Actor Service wiring tests."""

from __future__ import annotations

import tempfile
from typing import Any

from behave import given, then, when

from cleveragents.application.services.lsp_actor_service import LspActorService
from cleveragents.lsp.client import LspClient
from cleveragents.lsp.lifecycle import LspLifecycleManager
from cleveragents.lsp.models import LspBinding, LspCapability, LspServerConfig
from cleveragents.lsp.registry import LspRegistry
from cleveragents.lsp.runtime import LspRuntime


class _MockLifecycleManager(LspLifecycleManager):
    """Stub lifecycle manager that does not spawn real processes."""

    def __init__(self) -> None:
        self._servers: dict[str, object] = {}
        self._started: set[str] = set()

    def start_server(self, config: LspServerConfig, workspace_path: str) -> LspClient:
        """Record the server as started without spawning a process."""
        self._started.add(config.name)
        mock_client: LspClient = LspClient.__new__(LspClient)
        return mock_client

    def stop_server(self, name: str) -> None:
        """Record the server as stopped."""
        self._started.discard(name)

    def health_check(self, name: str) -> bool:
        """Return True if the server was started."""
        return name in self._started

    def stop_all(self) -> None:
        """Clear all started servers."""
        self._started.clear()


def _make_mock_lifecycle() -> LspLifecycleManager:
    """Create a mock lifecycle manager that does not spawn real processes."""
    return _MockLifecycleManager()


@given("a clean LSP actor service registry")
def step_clean_lsp_registry(context: Any) -> None:
    """Initialize a clean LSP registry."""
    context.registry = LspRegistry()
    # Build the runtime with test doubles via the public LspRuntime
    # constructor, then inject it through LspActorService's public
    # constructor parameter — no private-attribute access required.
    runtime = LspRuntime(
        registry=context.registry,
        lifecycle_manager=_make_mock_lifecycle(),
    )
    context.service = LspActorService(runtime=runtime)


@given("a test workspace directory")
def step_test_workspace_directory(context: Any) -> None:
    """Create a temporary workspace directory."""
    context.workspace_dir = tempfile.mkdtemp()


@given('an LSP server "{server_name}" is registered with capabilities:')
def step_register_lsp_server(context: Any, server_name: str) -> None:
    """Register an LSP server with specified capabilities."""
    capabilities = []
    for row in context.table:
        cap_name = row["CAPABILITIES"] if "CAPABILITIES" in row.headings else row[0]
        try:
            capabilities.append(LspCapability[cap_name])
        except KeyError:
            raise ValueError(f"Unknown capability: {cap_name}") from None

    config = LspServerConfig(
        name=server_name,
        command="echo",
        capabilities=capabilities,
    )
    context.registry.register(config)


@given("an actor with LSP bindings:")
def step_actor_with_lsp_bindings(context: Any) -> None:
    """Create an actor with LSP bindings."""
    bindings = []
    for row in context.table:
        server_name = row["lsp_server_name"]
        binding = LspBinding(
            node_name="test_node",
            lsp_server_name=server_name,
            languages=[],
            auto_detect=True,
        )
        bindings.append(binding)

    context.actor_bindings = bindings
    context.actor_name = "local/test-actor"


@given('an actor "{actor_name}" with LSP bindings:')
def step_named_actor_with_lsp_bindings(context: Any, actor_name: str) -> None:
    """Create a named actor with LSP bindings."""
    bindings = []
    for row in context.table:
        server_name = row["lsp_server_name"]
        binding = LspBinding(
            node_name="test_node",
            lsp_server_name=server_name,
            languages=[],
            auto_detect=True,
        )
        bindings.append(binding)

    context.actor_bindings = bindings
    context.actor_name = actor_name


@given("the actor bindings are activated with workspace path")
@when("the actor bindings are activated with workspace path")
def step_activate_actor_bindings(context: Any) -> None:
    """Activate the actor bindings."""
    try:
        context.tool_specs = context.service.activate_actor_bindings(
            context.actor_name,
            context.actor_bindings,
            context.workspace_dir,
        )
        context.activation_error = None
    except Exception as exc:
        context.activation_error = exc
        context.tool_specs = []


@when("the actor bindings are deactivated")
def step_deactivate_actor_bindings(context: Any) -> None:
    """Deactivate the actor bindings."""
    context.service.deactivate_actor_bindings(context.actor_name)


@when("activation is attempted with empty actor name")
def step_activate_empty_actor_name(context: Any) -> None:
    """Attempt activation with empty actor name."""
    try:
        context.service.activate_actor_bindings(
            "",
            context.actor_bindings,
            context.workspace_dir,
        )
        context.activation_error = None
    except ValueError as exc:
        context.activation_error = exc


@when("activation is attempted with empty workspace path")
def step_activate_empty_workspace_path(context: Any) -> None:
    """Attempt activation with empty workspace path."""
    try:
        context.service.activate_actor_bindings(
            context.actor_name,
            context.actor_bindings,
            "",
        )
        context.activation_error = None
    except ValueError as exc:
        context.activation_error = exc


@when("the actor is loaded and activated")
def step_actor_loaded_and_activated(context: Any) -> None:
    """Load and activate the actor."""
    context.tool_specs = context.service.activate_actor_bindings(
        context.actor_name,
        context.actor_bindings,
        context.workspace_dir,
    )


@then('the LSP runtime has started the "{server_name}" server')
def step_lsp_server_started(context: Any, server_name: str) -> None:
    """Verify that the LSP server has been started."""
    # Check that the server is in the lifecycle manager
    assert context.service.runtime.lifecycle.health_check(server_name), (
        f"Server {server_name} is not running"
    )


@then("tool specs are generated for the actor:")
def step_tool_specs_generated(context: Any) -> None:
    """Verify that tool specs are generated."""
    expected_tools = [row[0] for row in context.table]
    actual_tools = [spec["name"] for spec in context.tool_specs]

    for expected in expected_tools:
        assert expected in actual_tools, (
            f"Expected tool {expected} not found in {actual_tools}"
        )


@then("tool specs are generated for both servers")
def step_tool_specs_both_servers(context: Any) -> None:
    """Verify that tool specs are generated for both servers."""
    assert len(context.tool_specs) > 0, "No tool specs generated"
    tool_names = [spec["name"] for spec in context.tool_specs]
    assert any("pyright" in name for name in tool_names), "No pyright tools found"
    assert any("eslint" in name for name in tool_names), "No eslint tools found"


@then('the LSP runtime has released the "{server_name}" server')
def step_lsp_server_released(context: Any, server_name: str) -> None:
    """Verify that the LSP server has been released."""
    # After deactivation, the server should no longer be in the lifecycle manager
    # (or at least the reference count should be decremented)
    # This is a simplified check; in reality, the server might still be running
    # if other actors reference it
    # The lifecycle manager exposes a health_check method to verify server state
    assert not context.service.runtime.lifecycle.health_check(server_name), (
        f"Server {server_name} was not released after deactivation"
    )


@then("the activation completes with warnings")
def step_activation_with_warnings(context: Any) -> None:
    """Verify that activation completes despite warnings."""
    assert context.activation_error is None, (
        f"Activation failed: {context.activation_error}"
    )


@then("no tool specs are generated for the missing server")
def step_no_tool_specs_missing_server(context: Any) -> None:
    """Verify that no tool specs are generated for missing servers."""
    # Tool specs should only be generated for servers that were successfully started
    tool_names = [spec["name"] for spec in context.tool_specs]
    assert not any("missing" in name for name in tool_names), (
        "Tool specs for missing server found"
    )


@then('a ValueError is raised with message "{message}"')
def step_value_error_raised(context: Any, message: str) -> None:
    """Verify that a ValueError is raised with the expected message."""
    assert isinstance(context.activation_error, ValueError), (
        f"Expected ValueError, got {type(context.activation_error)}"
    )
    assert str(context.activation_error) == message, (
        f"Expected message '{message}', got '{context.activation_error}'"
    )


@then("the actor has access to LSP tools:")
def step_actor_has_lsp_tools(context: Any) -> None:
    """Verify that the actor has access to LSP tools."""
    expected_tools = [row[0] for row in context.table]
    actual_tools = [spec["name"] for spec in context.tool_specs]

    for expected in expected_tools:
        assert expected in actual_tools, (
            f"Expected tool {expected} not found in {actual_tools}"
        )


@then("the LSP server is running in the background")
def step_lsp_server_running(context: Any) -> None:
    """Verify that the LSP server is running."""
    for binding in context.actor_bindings:
        server_name = binding.lsp_server_name
        assert context.service.runtime.lifecycle.health_check(server_name), (
            f"Server {server_name} is not running"
        )


@then("each tool spec has required fields:")
def step_tool_spec_required_fields(context: Any) -> None:
    """Verify that each tool spec has required fields."""
    required_fields = [row[0] for row in context.table]

    for spec in context.tool_specs:
        for field in required_fields:
            assert field in spec, f"Tool spec {spec['name']} missing field {field}"


@then('the input_schema for "{tool_name}" requires "{fields}"')
def step_input_schema_requires_fields(
    context: Any, tool_name: str, fields: str
) -> None:
    """Verify that the input schema requires specific fields."""
    # The fields placeholder captures everything between the first and last
    # quote on the line, so a list like '"file_path", "line", "column"' comes
    # through as the literal string 'file_path", "line", "column'. Strip
    # surrounding whitespace and quotes from each comma-separated entry.
    field_list = [f.strip().strip('"') for f in fields.split(",")]

    spec = next((s for s in context.tool_specs if s["name"] == tool_name), None)
    assert spec is not None, f"Tool spec {tool_name} not found"

    schema = spec.get("input_schema", {})
    required = schema.get("required", [])

    for field in field_list:
        assert field in required, (
            f"Field {field} not required in {tool_name} schema. Required: {required}"
        )
