"""Step definitions for lsp_lifecycle_coverage.feature.

Exercises every uncovered code path in cleveragents.lsp.lifecycle including
_ManagedServer construction, start/stop/restart lifecycle, race conditions,
health checks, error handling, stop_all, and list_running.

Also covers the 3-phase lock pattern in restart_server() to verify that
health_check() and other operations are not blocked during blocking I/O.

All external I/O (StdioTransport, LspClient) is mocked so no real
subprocesses are spawned.
"""

from __future__ import annotations

import threading
from unittest.mock import MagicMock, patch

from behave import given, then, when
from behave.runner import Context

from cleveragents.lsp.errors import LspServerNotFoundError
from cleveragents.lsp.lifecycle import LspLifecycleManager, _ManagedServer
from cleveragents.lsp.models import LspServerConfig

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _make_config(name: str, command: str) -> LspServerConfig:
    """Create a minimal LspServerConfig for testing."""
    return LspServerConfig(name=name, command=command)


def _make_mock_transport(alive: bool = True) -> MagicMock:
    """Return a mock StdioTransport that claims to be alive."""
    t = MagicMock()
    t.is_alive = alive
    t.start = MagicMock()
    t.stop = MagicMock()
    return t


def _make_mock_client(initialized: bool = True) -> MagicMock:
    """Return a mock LspClient."""
    c = MagicMock()
    c.is_initialized = initialized
    c.initialize = MagicMock()
    c.shutdown = MagicMock()
    return c


def _patch_transport_and_client(context: Context, transport=None, client=None):
    """Patch StdioTransport and LspClient constructors.

    Returns (mock_transport_instance, mock_client_instance).
    """
    mock_transport = transport or _make_mock_transport()
    mock_client = client or _make_mock_client()

    p_transport = patch(
        "cleveragents.lsp.lifecycle.StdioTransport",
        return_value=mock_transport,
    )
    p_client = patch(
        "cleveragents.lsp.lifecycle.LspClient",
        return_value=mock_client,
    )
    p_transport.start()
    p_client.start()
    context.add_cleanup(p_transport.stop)
    context.add_cleanup(p_client.stop)

    return mock_transport, mock_client


class _RaceInjectingDict(dict):
    """A dict subclass that injects a race winner on the second .get() call
    for a particular key, simulating another thread registering a server
    between Phase 1 and Phase 3 of start_server.
    """

    def __init__(self, *args, target_key=None, race_winner=None, **kwargs):
        super().__init__(*args, **kwargs)
        self._target_key = target_key
        self._race_winner = race_winner
        self._get_count = 0

    def get(self, key, default=None):
        result = super().get(key, default)
        if key == self._target_key:
            self._get_count += 1
            if self._get_count == 2:
                # Simulate another thread having registered this key
                self[key] = self._race_winner
                return self._race_winner
        return result


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


@given("llcov a fresh LspLifecycleManager instance")
def step_fresh_manager(context: Context) -> None:
    context.llcov_manager = LspLifecycleManager()
    context.llcov_config: LspServerConfig | None = None
    context.llcov_config2: LspServerConfig | None = None
    context.llcov_client = None
    context.llcov_original_client = None
    context.llcov_error = None
    context.llcov_result = None
    context.llcov_health = None
    context.llcov_running_list = None
    context.llcov_mock_transport = None
    context.llcov_mock_client = None
    context.llcov_old_transport = None
    context.llcov_race_transport = None
    context.llcov_race_client = None
    context.llcov_new_transport_stopped = False


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


@given('llcov a server config named "{name}" with command "{command}"')
def step_create_config(context: Context, name: str, command: str) -> None:
    cfg = _make_config(name, command)
    # If first config already set, store as config2
    if context.llcov_config is not None:
        context.llcov_config2 = cfg
    else:
        context.llcov_config = cfg


# ---------------------------------------------------------------------------
# Start server (happy path)
# ---------------------------------------------------------------------------


@given('llcov the server is already started for workspace "{workspace}"')
def step_server_already_started(context: Context, workspace: str) -> None:
    mock_transport, mock_client = _patch_transport_and_client(context)
    context.llcov_mock_transport = mock_transport
    context.llcov_mock_client = mock_client
    context.llcov_original_client = context.llcov_manager.start_server(
        context.llcov_config, workspace
    )


@given('llcov the second server is already started for workspace "{workspace}"')
def step_second_server_already_started(context: Context, workspace: str) -> None:
    mock_transport2 = _make_mock_transport()
    mock_client2 = _make_mock_client()

    p_transport = patch(
        "cleveragents.lsp.lifecycle.StdioTransport",
        return_value=mock_transport2,
    )
    p_client = patch(
        "cleveragents.lsp.lifecycle.LspClient",
        return_value=mock_client2,
    )
    p_transport.start()
    p_client.start()
    context.add_cleanup(p_transport.stop)
    context.add_cleanup(p_client.stop)

    context.llcov_manager.start_server(context.llcov_config2, workspace)


@when('llcov I start the server for workspace "{workspace}"')
def step_start_server(context: Context, workspace: str) -> None:
    mock_transport, mock_client = _patch_transport_and_client(context)
    context.llcov_mock_transport = mock_transport
    context.llcov_mock_client = mock_client
    context.llcov_client = context.llcov_manager.start_server(
        context.llcov_config, workspace
    )


# ---------------------------------------------------------------------------
# Start server: reuse existing alive server
# ---------------------------------------------------------------------------


@given("llcov the server is started again to bump ref count")
def step_bump_ref_count(context: Context) -> None:
    """Start the server a second time to bump ref_count to 2."""
    context.llcov_manager.start_server(context.llcov_config, "/tmp/ws")


@when('llcov I start the server again for workspace "{workspace}"')
def step_start_server_again(context: Context, workspace: str) -> None:
    # The existing transport is still alive, so Phase 1 should return existing client
    context.llcov_client = context.llcov_manager.start_server(
        context.llcov_config, workspace
    )


# ---------------------------------------------------------------------------
# Start server: initialize failure
# ---------------------------------------------------------------------------


@given("llcov the LspClient initialize will raise an error")
def step_client_init_will_fail(context: Context) -> None:
    context.llcov_init_fails = True


@when('llcov I attempt to start the server for workspace "{workspace}"')
def step_attempt_start_server(context: Context, workspace: str) -> None:
    mock_transport = _make_mock_transport()
    mock_client = _make_mock_client()
    mock_client.initialize.side_effect = RuntimeError("init boom")

    p_transport = patch(
        "cleveragents.lsp.lifecycle.StdioTransport",
        return_value=mock_transport,
    )
    p_client = patch(
        "cleveragents.lsp.lifecycle.LspClient",
        return_value=mock_client,
    )
    p_transport.start()
    p_client.start()
    context.add_cleanup(p_transport.stop)
    context.add_cleanup(p_client.stop)

    context.llcov_mock_transport = mock_transport
    context.llcov_mock_client = mock_client

    try:
        context.llcov_client = context.llcov_manager.start_server(
            context.llcov_config, workspace
        )
    except Exception as exc:
        context.llcov_error = exc


# ---------------------------------------------------------------------------
# Start server: race condition
# ---------------------------------------------------------------------------


@given('llcov another thread already registered "{name}" before Phase 3')
def step_setup_race_condition(context: Context, name: str) -> None:
    """Pre-build a race winner that will be injected into _servers."""
    context.llcov_race_transport = _make_mock_transport(alive=True)
    context.llcov_race_client = _make_mock_client()


@when('llcov I start the server for workspace "{workspace}" with race')
def step_start_server_with_race(context: Context, workspace: str) -> None:
    """Simulate start_server where another thread wins the race in Phase 3.

    We replace ``manager._servers`` with a ``_RaceInjectingDict`` that
    injects the race winner on the second ``.get()`` call (Phase 3),
    while the first ``.get()`` (Phase 1) returns None normally.
    """
    phase2_transport = _make_mock_transport(alive=True)
    phase2_client = _make_mock_client()

    race_winner = _ManagedServer(
        config=context.llcov_config,
        transport=context.llcov_race_transport,
        client=context.llcov_race_client,
        workspace_path=workspace,
    )

    # Replace _servers with our race-injecting dict
    race_dict = _RaceInjectingDict(
        target_key=context.llcov_config.name,
        race_winner=race_winner,
    )
    context.llcov_manager._servers = race_dict

    p_transport = patch(
        "cleveragents.lsp.lifecycle.StdioTransport",
        return_value=phase2_transport,
    )
    p_client = patch(
        "cleveragents.lsp.lifecycle.LspClient",
        return_value=phase2_client,
    )
    p_transport.start()
    p_client.start()
    context.add_cleanup(p_transport.stop)
    context.add_cleanup(p_client.stop)

    context.llcov_client = context.llcov_manager.start_server(
        context.llcov_config, workspace
    )
    context.llcov_phase2_transport = phase2_transport


# ---------------------------------------------------------------------------
# Stop server
# ---------------------------------------------------------------------------


@when('llcov I stop the server "{name}"')
def step_stop_server(context: Context, name: str) -> None:
    context.llcov_manager.stop_server(name)


@when('llcov I attempt to stop server "{name}"')
def step_attempt_stop_server(context: Context, name: str) -> None:
    try:
        context.llcov_manager.stop_server(name)
    except Exception as exc:
        context.llcov_error = exc


# ---------------------------------------------------------------------------
# Shutdown (direct call)
# ---------------------------------------------------------------------------


@given("llcov the client shutdown will raise an exception")
def step_client_shutdown_will_fail(context: Context) -> None:
    context.llcov_mock_client.shutdown.side_effect = RuntimeError("shutdown boom")


@given("llcov the transport stop will raise an exception")
def step_transport_stop_will_fail(context: Context) -> None:
    context.llcov_mock_transport.stop.side_effect = RuntimeError("transport stop boom")


@when("llcov I trigger shutdown on the managed server")
def step_trigger_shutdown(context: Context) -> None:
    name = context.llcov_config.name
    managed = context.llcov_manager._servers[name]
    context.llcov_manager._shutdown_server(managed)


# ---------------------------------------------------------------------------
# Get client
# ---------------------------------------------------------------------------


@when('llcov I get the client for "{name}"')
def step_get_client(context: Context, name: str) -> None:
    context.llcov_client = context.llcov_manager.get_client(name)


@when('llcov I attempt to get the client for "{name}"')
def step_attempt_get_client(context: Context, name: str) -> None:
    try:
        context.llcov_client = context.llcov_manager.get_client(name)
    except Exception as exc:
        context.llcov_error = exc


# ---------------------------------------------------------------------------
# Health check
# ---------------------------------------------------------------------------


@when('llcov I health check "{name}"')
def step_health_check(context: Context, name: str) -> None:
    context.llcov_health = context.llcov_manager.health_check(name)


# ---------------------------------------------------------------------------
# Restart server
# ---------------------------------------------------------------------------


@when('llcov I restart the server "{name}"')
def step_restart_server(context: Context, name: str) -> None:
    context.llcov_old_transport = context.llcov_mock_transport

    new_transport = _make_mock_transport()
    new_client = _make_mock_client()
    context.llcov_new_transport = new_transport
    context.llcov_new_client = new_client

    p_transport = patch(
        "cleveragents.lsp.lifecycle.StdioTransport",
        return_value=new_transport,
    )
    p_client = patch(
        "cleveragents.lsp.lifecycle.LspClient",
        return_value=new_client,
    )
    p_transport.start()
    p_client.start()
    context.add_cleanup(p_transport.stop)
    context.add_cleanup(p_client.stop)

    context.llcov_client = context.llcov_manager.restart_server(name)


@when('llcov I attempt to restart server "{name}"')
def step_attempt_restart_server(context: Context, name: str) -> None:
    try:
        # For "not found" case, don't set up mocks
        if not context.llcov_manager._servers.get(name):
            context.llcov_manager.restart_server(name)
        else:
            # For init failure during restart
            context.llcov_old_transport = context.llcov_mock_transport

            new_transport = _make_mock_transport()
            new_client = _make_mock_client()
            new_client.initialize.side_effect = RuntimeError("restart init boom")
            context.llcov_new_restart_transport = new_transport

            p_transport = patch(
                "cleveragents.lsp.lifecycle.StdioTransport",
                return_value=new_transport,
            )
            p_client = patch(
                "cleveragents.lsp.lifecycle.LspClient",
                return_value=new_client,
            )
            p_transport.start()
            p_client.start()
            context.add_cleanup(p_transport.stop)
            context.add_cleanup(p_client.stop)

            context.llcov_client = context.llcov_manager.restart_server(name)
    except Exception as exc:
        context.llcov_error = exc


@given("llcov the LspClient initialize will raise on restart")
def step_init_fails_on_restart(context: Context) -> None:
    context.llcov_init_fails_on_restart = True


# ---------------------------------------------------------------------------
# Stop all
# ---------------------------------------------------------------------------


@when("llcov I stop all servers")
def step_stop_all(context: Context) -> None:
    context.llcov_manager.stop_all()


# ---------------------------------------------------------------------------
# List running
# ---------------------------------------------------------------------------


@when("llcov I list running servers")
def step_list_running(context: Context) -> None:
    context.llcov_running_list = context.llcov_manager.list_running()


# ---------------------------------------------------------------------------
# Then assertions
# ---------------------------------------------------------------------------


@then("llcov the returned client should not be None")
def step_client_not_none(context: Context) -> None:
    assert context.llcov_client is not None, "Expected a client but got None"


@then('llcov the server "{name}" should be registered')
def step_server_registered(context: Context, name: str) -> None:
    assert name in context.llcov_manager._servers, (
        f"Server '{name}' not found in _servers"
    )


@then('llcov the server "{name}" should not be registered')
def step_server_not_registered(context: Context, name: str) -> None:
    assert name not in context.llcov_manager._servers, (
        f"Server '{name}' should not be in _servers"
    )


@then('llcov the server "{name}" should still be registered')
def step_server_still_registered(context: Context, name: str) -> None:
    assert name in context.llcov_manager._servers, (
        f"Server '{name}' should still be in _servers"
    )


@then("llcov the returned client should be the same as the original")
def step_client_same_as_original(context: Context) -> None:
    assert context.llcov_client is context.llcov_original_client, (
        "Expected reused client to be the same object"
    )


@then('llcov the ref count for "{name}" should be {count:d}')
def step_ref_count(context: Context, name: str, count: int) -> None:
    managed = context.llcov_manager._servers[name]
    assert managed.ref_count == count, (
        f"Expected ref_count={count}, got {managed.ref_count}"
    )


@then("llcov a start error should have been raised")
def step_start_error_raised(context: Context) -> None:
    assert context.llcov_error is not None, "Expected an error but none was raised"
    assert isinstance(context.llcov_error, RuntimeError), (
        f"Expected RuntimeError, got {type(context.llcov_error)}"
    )


@then("llcov the transport should have been stopped after init failure")
def step_transport_stopped_after_init(context: Context) -> None:
    context.llcov_mock_transport.stop.assert_called()


@then("llcov the returned client should be the race winner client")
def step_client_is_race_winner(context: Context) -> None:
    assert context.llcov_client is context.llcov_race_client, (
        "Expected the race winner client"
    )


@then("llcov the losing transport should have been stopped")
def step_losing_transport_stopped(context: Context) -> None:
    context.llcov_phase2_transport.stop.assert_called()


@then("llcov shutdown should have been called on the client")
def step_shutdown_called(context: Context) -> None:
    context.llcov_mock_client.shutdown.assert_called()


@then("llcov a LspServerNotFoundError should have been raised")
def step_not_found_error(context: Context) -> None:
    assert context.llcov_error is not None, "Expected an error but none was raised"
    assert isinstance(context.llcov_error, LspServerNotFoundError), (
        f"Expected LspServerNotFoundError, got {type(context.llcov_error)}"
    )


@then("llcov the client shutdown method should have been called")
def step_client_shutdown_called(context: Context) -> None:
    context.llcov_mock_client.shutdown.assert_called()


@then("llcov the transport stop method should have been called")
def step_transport_stop_called(context: Context) -> None:
    context.llcov_mock_transport.stop.assert_called()


@then("llcov the transport stop method should have been called despite error")
def step_transport_stop_called_despite_error(context: Context) -> None:
    context.llcov_mock_transport.stop.assert_called()


@then("llcov the health check result should be true")
def step_health_true(context: Context) -> None:
    assert context.llcov_health is True, f"Expected True, got {context.llcov_health}"


@then("llcov the health check result should be false")
def step_health_false(context: Context) -> None:
    assert context.llcov_health is False, f"Expected False, got {context.llcov_health}"


@then("llcov the old transport should have been stopped")
def step_old_transport_stopped(context: Context) -> None:
    context.llcov_old_transport.stop.assert_called()


@then("llcov the managed server should have the new transport and client")
def step_managed_has_new(context: Context) -> None:
    name = context.llcov_config.name
    managed = context.llcov_manager._servers[name]
    assert managed.transport is context.llcov_new_transport, (
        "Managed server should have the new transport"
    )
    assert managed.client is context.llcov_new_client, (
        "Managed server should have the new client"
    )


@then("llcov a restart error should have been raised")
def step_restart_error_raised(context: Context) -> None:
    assert context.llcov_error is not None, "Expected an error but none was raised"
    assert isinstance(context.llcov_error, RuntimeError), (
        f"Expected RuntimeError, got {type(context.llcov_error)}"
    )


@then("llcov the new transport should have been stopped after restart failure")
def step_new_transport_stopped_after_restart(context: Context) -> None:
    context.llcov_new_restart_transport.stop.assert_called()


@then("llcov no servers should be registered")
def step_no_servers(context: Context) -> None:
    assert len(context.llcov_manager._servers) == 0, (
        f"Expected 0 servers, got {len(context.llcov_manager._servers)}"
    )


@then("llcov the running list should contain {count:d} entry")
def step_running_list_count_singular(context: Context, count: int) -> None:
    assert len(context.llcov_running_list) == count, (
        f"Expected {count} entries, got {len(context.llcov_running_list)}"
    )


@then("llcov the running list should contain {count:d} entries")
def step_running_list_count_plural(context: Context, count: int) -> None:
    assert len(context.llcov_running_list) == count, (
        f"Expected {count} entries, got {len(context.llcov_running_list)}"
    )


@then('llcov the running entry for "{name}" should have workspace "{workspace}"')
def step_running_entry_workspace(context: Context, name: str, workspace: str) -> None:
    entry = next((e for e in context.llcov_running_list if e["name"] == name), None)
    assert entry is not None, f"No entry found for '{name}'"
    assert entry["workspace"] == workspace, (
        f"Expected workspace='{workspace}', got '{entry['workspace']}'"
    )


@then('llcov the running entry for "{name}" should have alive status true')
def step_running_entry_alive(context: Context, name: str) -> None:
    entry = next((e for e in context.llcov_running_list if e["name"] == name), None)
    assert entry is not None, f"No entry found for '{name}'"
    assert entry["alive"] is True, f"Expected alive=True, got {entry['alive']}"


@then('llcov the running entry for "{name}" should have ref count {count:d}')
def step_running_entry_ref_count(context: Context, name: str, count: int) -> None:
    entry = next((e for e in context.llcov_running_list if e["name"] == name), None)
    assert entry is not None, f"No entry found for '{name}'"
    assert entry["ref_count"] == count, (
        f"Expected ref_count={count}, got {entry['ref_count']}"
    )


# ---------------------------------------------------------------------------
# Concurrent-access tests for 3-phase lock pattern in restart_server()
# ---------------------------------------------------------------------------


@when(
    "llcov I restart the server concurrently and health_check runs during blocking IO"
)
def step_restart_concurrent_health_check(context: Context) -> None:
    """Verify health_check() is not blocked while restart_server() is in progress.

    We inject a barrier into the mock ``initialize`` call so that the
    restart_server() thread pauses in Phase 2 (blocking I/O, no lock held).
    A second thread then calls health_check() and records whether it
    completed without blocking.
    """
    # Barrier: both threads must reach it before either proceeds.
    # Thread 1 (restart): reaches barrier inside initialize()
    # Thread 2 (health_check): reaches barrier after acquiring the lock
    barrier = threading.Barrier(2, timeout=5)
    health_check_completed = threading.Event()
    health_check_result: list[bool] = []

    new_transport = _make_mock_transport()
    new_client = _make_mock_client()

    def _blocking_initialize(workspace_path: str) -> None:
        """Simulate a slow LSP handshake; signal the barrier mid-way."""
        barrier.wait()  # Signal: "I'm in Phase 2, lock is released"

    new_client.initialize.side_effect = _blocking_initialize

    def _restart_thread() -> None:
        p_transport = patch(
            "cleveragents.lsp.lifecycle.StdioTransport",
            return_value=new_transport,
        )
        p_client = patch(
            "cleveragents.lsp.lifecycle.LspClient",
            return_value=new_client,
        )
        with p_transport, p_client:
            context.llcov_restarted_client = context.llcov_manager.restart_server(
                context.llcov_config.name
            )

    def _health_check_thread() -> None:
        barrier.wait()  # Wait until restart is in Phase 2 (lock released)
        # health_check() must acquire _lock; if restart held it, this would block
        result = context.llcov_manager.health_check(context.llcov_config.name)
        health_check_result.append(result)
        health_check_completed.set()

    t_restart = threading.Thread(target=_restart_thread, daemon=True)
    t_health = threading.Thread(target=_health_check_thread, daemon=True)

    t_restart.start()
    t_health.start()

    t_restart.join(timeout=10)
    t_health.join(timeout=10)

    context.llcov_health_check_completed = health_check_completed.is_set()
    context.llcov_health_check_result_concurrent = health_check_result
    context.llcov_new_transport = new_transport
    context.llcov_new_client = new_client


@then("llcov health_check should have completed without blocking")
def step_health_check_completed(context: Context) -> None:
    assert context.llcov_health_check_completed, (
        "health_check() did not complete — it was blocked while restart_server() "
        "held the lock during blocking I/O"
    )


@then("llcov the restarted client should not be None")
def step_restarted_client_not_none(context: Context) -> None:
    assert context.llcov_restarted_client is not None, (
        "Expected a restarted client but got None"
    )


@when("llcov I restart the server and verify lock is free during initialize")
def step_restart_verify_lock_free(context: Context) -> None:
    """Assert that _lock is NOT held when client.initialize() is called.

    We inspect the lock state from within the mock initialize() call.
    If the lock is held, ``_lock.acquire(blocking=False)`` returns False.
    """
    lock_was_free_during_init: list[bool] = []
    manager = context.llcov_manager

    new_transport = _make_mock_transport()
    new_client = _make_mock_client()

    def _check_lock_initialize(workspace_path: str) -> None:
        # Try to acquire the lock non-blocking; succeeds only if not held
        acquired = manager._lock.acquire(blocking=False)
        lock_was_free_during_init.append(acquired)
        if acquired:
            manager._lock.release()

    new_client.initialize.side_effect = _check_lock_initialize

    p_transport = patch(
        "cleveragents.lsp.lifecycle.StdioTransport",
        return_value=new_transport,
    )
    p_client = patch(
        "cleveragents.lsp.lifecycle.LspClient",
        return_value=new_client,
    )
    with p_transport, p_client:
        context.llcov_restarted_client = context.llcov_manager.restart_server(
            context.llcov_config.name
        )

    context.llcov_lock_was_free_during_init = lock_was_free_during_init
    context.llcov_new_transport = new_transport
    context.llcov_new_client = new_client


@then("llcov the lock should have been acquirable during initialize")
def step_lock_free_during_init(context: Context) -> None:
    results = context.llcov_lock_was_free_during_init
    assert results, "initialize() was never called — cannot verify lock state"
    assert all(results), (
        "The lock was held during client.initialize() — restart_server() "
        "must not hold _lock during blocking I/O"
    )
