"""Step definitions for lsp_client_coverage.feature.

Covers every uncovered code-path in ``src/cleveragents/lsp/client.py``.
Uses the ``lccov`` prefix on every step to avoid Behave AmbiguousStep errors.
"""

from __future__ import annotations

from typing import Any
from unittest.mock import MagicMock, PropertyMock, create_autospec

from behave import given, then, when

from cleveragents.lsp.client import LspClient
from cleveragents.lsp.errors import LspError
from cleveragents.lsp.transport import StdioTransport

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


def _make_mock_transport() -> MagicMock:
    """Create an ``autospec``-ed mock of :class:`StdioTransport`."""
    mock = create_autospec(StdioTransport, instance=True)
    # Default: transport is alive
    type(mock).is_alive = PropertyMock(return_value=True)
    return mock


# ---------------------------------------------------------------------------
# Given steps
# ---------------------------------------------------------------------------


@given("lccov I create an LspClient with a mock transport")
def step_create_client(context: Any) -> None:
    context.lccov_transport = _make_mock_transport()
    context.lccov_client = LspClient(context.lccov_transport, server_name="test/lccov")
    # Track sent messages for assertions
    context.lccov_sent_messages = []

    def _capture_send(msg: dict) -> None:
        context.lccov_sent_messages.append(msg)

    context.lccov_transport.send_message = MagicMock(side_effect=_capture_send)


@given("lccov the transport echoes back a successful response")
def step_transport_echo_success(context: Any) -> None:
    """Configure read_message to return a response matching the current request id.

    By the time read_message is called, _next_id() has already incremented
    _request_id, so _request_id holds the id used by the in-flight request.
    """

    def _read_side_effect(timeout: float = 30.0) -> dict | None:
        req_id = context.lccov_client._request_id
        return {
            "jsonrpc": "2.0",
            "id": req_id,
            "result": {"status": "ok"},
        }

    context.lccov_transport.read_message = MagicMock(side_effect=_read_side_effect)


@given("lccov the transport returns a JSON-RPC error response")
def step_transport_rpc_error(context: Any) -> None:
    def _read_side_effect(timeout: float = 30.0) -> dict | None:
        req_id = context.lccov_client._request_id
        return {
            "jsonrpc": "2.0",
            "id": req_id,
            "error": {
                "code": -32600,
                "message": "Invalid Request",
                "data": "bad stuff",
            },
        }

    context.lccov_transport.read_message = MagicMock(side_effect=_read_side_effect)


@given("lccov the transport returns None and is not alive")
def step_transport_dead(context: Any) -> None:
    context.lccov_transport.read_message = MagicMock(return_value=None)
    type(context.lccov_transport).is_alive = PropertyMock(return_value=False)


@given("lccov the transport returns None then a valid response while alive")
def step_transport_retry_then_success(context: Any) -> None:
    """First call returns None (alive so retry), second returns valid response."""
    call_count = {"n": 0}

    def _read_side_effect(timeout: float = 30.0) -> dict | None:
        call_count["n"] += 1
        if call_count["n"] == 1:
            return None  # First read: no data, but transport is alive
        req_id = context.lccov_client._request_id
        return {
            "jsonrpc": "2.0",
            "id": req_id,
            "result": {"status": "ok"},
        }

    type(context.lccov_transport).is_alive = PropertyMock(return_value=True)
    context.lccov_transport.read_message = MagicMock(side_effect=_read_side_effect)


@given("lccov the transport always returns None but stays alive")
def step_transport_always_none_alive(context: Any) -> None:
    context.lccov_transport.read_message = MagicMock(return_value=None)
    type(context.lccov_transport).is_alive = PropertyMock(return_value=True)


@given("lccov the transport returns a notification then the response")
def step_transport_notification_then_response(context: Any) -> None:
    call_count = {"n": 0}

    def _read_side_effect(timeout: float = 30.0) -> dict | None:
        call_count["n"] += 1
        if call_count["n"] == 1:
            # A notification (no matching id)
            return {
                "jsonrpc": "2.0",
                "method": "window/logMessage",
                "params": {"type": 3, "message": "hello"},
            }
        # The actual response
        req_id = context.lccov_client._request_id
        return {
            "jsonrpc": "2.0",
            "id": req_id,
            "result": {"status": "ok"},
        }

    context.lccov_transport.read_message = MagicMock(side_effect=_read_side_effect)


@given("lccov the transport echoes back an initialize result with capabilities")
def step_transport_initialize_result(context: Any) -> None:
    def _read_side_effect(timeout: float = 30.0) -> dict | None:
        req_id = context.lccov_client._request_id
        return {
            "jsonrpc": "2.0",
            "id": req_id,
            "result": {
                "capabilities": {
                    "textDocumentSync": 1,
                    "completionProvider": {"triggerCharacters": ["."]},
                },
            },
        }

    context.lccov_transport.read_message = MagicMock(side_effect=_read_side_effect)


@given("lccov the client is already marked as initialized")
def step_client_force_initialized(context: Any) -> None:
    context.lccov_client._initialized = True


@given("lccov the transport echoes back a shutdown response")
def step_transport_shutdown_response(context: Any) -> None:
    """Shutdown returns a null result; exit is a notification (no read)."""

    def _read_side_effect(timeout: float = 30.0) -> dict | None:
        req_id = context.lccov_client._request_id
        return {"jsonrpc": "2.0", "id": req_id, "result": None}

    context.lccov_transport.read_message = MagicMock(side_effect=_read_side_effect)


@given("lccov the transport will raise LspError on shutdown request")
def step_transport_shutdown_lsp_error(context: Any) -> None:
    """Make read_message return an error response for the shutdown request."""

    def _read_side_effect(timeout: float = 30.0) -> dict | None:
        req_id = context.lccov_client._request_id
        return {
            "jsonrpc": "2.0",
            "id": req_id,
            "error": {
                "code": -32603,
                "message": "Internal error",
            },
        }

    context.lccov_transport.read_message = MagicMock(side_effect=_read_side_effect)


@given(
    "lccov the transport echoes back a shutdown response but raises BrokenPipeError on exit"
)
def step_transport_broken_pipe_on_exit(context: Any) -> None:
    """read_message returns valid shutdown response, send_message raises on 2nd call."""
    call_count = {"n": 0}

    def _read_side_effect(timeout: float = 30.0) -> dict | None:
        req_id = context.lccov_client._request_id
        return {"jsonrpc": "2.0", "id": req_id, "result": None}

    context.lccov_transport.read_message = MagicMock(side_effect=_read_side_effect)

    original_messages: list[dict] = context.lccov_sent_messages

    def _send_side_effect(msg: dict) -> None:
        call_count["n"] += 1
        original_messages.append(msg)
        # First send = shutdown request, second = exit notification
        if call_count["n"] >= 2:
            raise BrokenPipeError("pipe broken")

    context.lccov_transport.send_message = MagicMock(side_effect=_send_side_effect)


@given('lccov there are cached diagnostics for "{uri}"')
def step_cache_diagnostics(context: Any, uri: str) -> None:
    context.lccov_client._diagnostics[uri] = [
        {"range": {}, "message": "old warning", "severity": 2},
    ]


@given("lccov the transport has queued diagnostic notifications for drain")
def step_transport_queued_diagnostics(context: Any) -> None:
    """read_message returns a publishDiagnostics notification, then None."""
    call_count = {"n": 0}

    def _read_side_effect(timeout: float = 0.05) -> dict | None:
        call_count["n"] += 1
        if call_count["n"] == 1:
            return {
                "jsonrpc": "2.0",
                "method": "textDocument/publishDiagnostics",
                "params": {
                    "uri": "file:///test.py",
                    "diagnostics": [
                        {"range": {}, "message": "err", "severity": 1},
                    ],
                },
            }
        return None

    context.lccov_transport.read_message = MagicMock(side_effect=_read_side_effect)


@given("lccov the transport returns None immediately for drain")
def step_transport_drain_none(context: Any) -> None:
    context.lccov_transport.read_message = MagicMock(return_value=None)


@given("lccov the transport returns a CompletionList response")
def step_transport_completion_list(context: Any) -> None:
    def _read_side_effect(timeout: float = 30.0) -> dict | None:
        req_id = context.lccov_client._request_id
        return {
            "jsonrpc": "2.0",
            "id": req_id,
            "result": {
                "isIncomplete": False,
                "items": [
                    {"label": "foo", "kind": 6},
                    {"label": "bar", "kind": 6},
                ],
            },
        }

    context.lccov_transport.read_message = MagicMock(side_effect=_read_side_effect)


@given("lccov the transport returns a plain list completion response")
def step_transport_completion_plain_list(context: Any) -> None:
    def _read_side_effect(timeout: float = 30.0) -> dict | None:
        req_id = context.lccov_client._request_id
        return {
            "jsonrpc": "2.0",
            "id": req_id,
            "result": [{"label": "baz", "kind": 6}],
        }

    context.lccov_transport.read_message = MagicMock(side_effect=_read_side_effect)


@given("lccov the transport returns a scalar completion response")
def step_transport_completion_scalar(context: Any) -> None:
    def _read_side_effect(timeout: float = 30.0) -> dict | None:
        req_id = context.lccov_client._request_id
        return {
            "jsonrpc": "2.0",
            "id": req_id,
            "result": "unexpected_string",
        }

    context.lccov_transport.read_message = MagicMock(side_effect=_read_side_effect)


# ---------------------------------------------------------------------------
# When steps
# ---------------------------------------------------------------------------


@when('lccov I call _send_request with method "{method}" and params')
def step_send_request_with_params(context: Any, method: str) -> None:
    context.lccov_error = None
    try:
        context.lccov_result = context.lccov_client._send_request(
            method, {"key": "value"}, timeout=2.0
        )
    except Exception as exc:
        context.lccov_error = exc


@when('lccov I call _send_request with method "{method}" and no params')
def step_send_request_no_params(context: Any, method: str) -> None:
    context.lccov_error = None
    try:
        context.lccov_result = context.lccov_client._send_request(method, timeout=2.0)
    except Exception as exc:
        context.lccov_error = exc


@when("lccov I call _send_request expecting an error")
def step_send_request_expect_error(context: Any) -> None:
    context.lccov_error = None
    try:
        context.lccov_result = context.lccov_client._send_request(
            "test/fail", timeout=2.0
        )
    except LspError as exc:
        context.lccov_error = exc


@when("lccov I call _send_request with a very short timeout expecting error")
def step_send_request_short_timeout(context: Any) -> None:
    context.lccov_error = None
    try:
        context.lccov_result = context.lccov_client._send_request(
            "test/timeout", timeout=0.01
        )
    except LspError as exc:
        context.lccov_error = exc


@when('lccov I call _send_notification with method "{method}" and params')
def step_send_notification_with_params(context: Any, method: str) -> None:
    context.lccov_client._send_notification(method, {"data": "test"})


@when('lccov I call _send_notification with method "{method}" and no params')
def step_send_notification_no_params(context: Any, method: str) -> None:
    context.lccov_client._send_notification(method)


@when('lccov I handle a publishDiagnostics notification for "{uri}"')
def step_handle_publish_diagnostics(context: Any, uri: str) -> None:
    context.lccov_client._handle_notification(
        {
            "jsonrpc": "2.0",
            "method": "textDocument/publishDiagnostics",
            "params": {
                "uri": uri,
                "diagnostics": [
                    {"range": {}, "message": "err1", "severity": 1},
                    {"range": {}, "message": "warn1", "severity": 2},
                ],
            },
        }
    )


@when("lccov I handle a window/logMessage notification")
def step_handle_log_notification(context: Any) -> None:
    context.lccov_client._handle_notification(
        {
            "jsonrpc": "2.0",
            "method": "window/logMessage",
            "params": {"type": 3, "message": "info msg"},
        }
    )


@when('lccov I call initialize with workspace "{path}"')
def step_call_initialize(context: Any, path: str) -> None:
    context.lccov_error = None
    try:
        context.lccov_result = context.lccov_client.initialize(path)
    except LspError as exc:
        context.lccov_error = exc


@when('lccov I call initialize with workspace "{path}" and init options')
def step_call_initialize_with_options(context: Any, path: str) -> None:
    context.lccov_error = None
    try:
        context.lccov_result = context.lccov_client.initialize(
            path, initialization_options={"custom": True}
        )
    except LspError as exc:
        context.lccov_error = exc


@when("lccov I call initialize expecting an error")
def step_call_initialize_error(context: Any) -> None:
    context.lccov_error = None
    try:
        context.lccov_result = context.lccov_client.initialize("/tmp/nope")
    except LspError as exc:
        context.lccov_error = exc


@when("lccov I call shutdown")
def step_call_shutdown(context: Any) -> None:
    context.lccov_error = None
    try:
        context.lccov_client.shutdown()
    except Exception as exc:
        context.lccov_error = exc


@when('lccov I call did_open for "{uri}" with language "{lang}"')
def step_call_did_open(context: Any, uri: str, lang: str) -> None:
    context.lccov_client.did_open(uri, lang, 1, "print('hello')")


@when('lccov I call did_close for "{uri}"')
def step_call_did_close(context: Any, uri: str) -> None:
    context.lccov_client.did_close(uri)


@when('lccov I call get_diagnostics for "{uri}"')
def step_call_get_diagnostics(context: Any, uri: str) -> None:
    context.lccov_diag_result = context.lccov_client.get_diagnostics(uri)


@when('lccov I call get_completions for "{uri}" at line {line:d} char {char:d}')
def step_call_get_completions(context: Any, uri: str, line: int, char: int) -> None:
    context.lccov_completions = context.lccov_client.get_completions(uri, line, char)


# ---------------------------------------------------------------------------
# Then steps
# ---------------------------------------------------------------------------


@then("lccov server_capabilities should be an empty dict")
def step_capabilities_empty(context: Any) -> None:
    assert context.lccov_client.server_capabilities == {}


@then("lccov the request result should contain the expected data")
def step_result_expected(context: Any) -> None:
    assert context.lccov_error is None, f"Unexpected error: {context.lccov_error}"
    assert context.lccov_result == {"status": "ok"}


@then("lccov the sent message should include params")
def step_sent_has_params(context: Any) -> None:
    # The first sent message should be the request
    msg = context.lccov_sent_messages[0]
    assert "params" in msg
    assert msg["params"] == {"key": "value"}


@then("lccov the sent message should not include params key")
def step_sent_no_params(context: Any) -> None:
    msg = context.lccov_sent_messages[0]
    assert "params" not in msg


@then('lccov an LspError should be stored with message containing "{fragment}"')
def step_lsp_error_contains(context: Any, fragment: str) -> None:
    assert context.lccov_error is not None, "Expected an LspError but none was raised"
    assert isinstance(context.lccov_error, LspError), (
        f"Expected LspError, got {type(context.lccov_error)}"
    )
    assert fragment in str(context.lccov_error), (
        f"'{fragment}' not found in '{context.lccov_error}'"
    )


@then("lccov the pending notifications should contain the queued message")
def step_pending_notifications(context: Any) -> None:
    pending = context.lccov_client._pending_notifications
    assert len(pending) >= 1
    found = any(m.get("method") == "window/logMessage" for m in pending)
    assert found, f"Expected window/logMessage in pending, got {list(pending)}"


@then("lccov the transport should have received a notification with params")
def step_notification_with_params(context: Any) -> None:
    assert len(context.lccov_sent_messages) == 1
    msg = context.lccov_sent_messages[0]
    assert msg["method"] == "test/note"
    assert "params" in msg
    assert msg["params"] == {"data": "test"}
    # Notifications have no "id"
    assert "id" not in msg


@then("lccov the transport should have received a notification without params key")
def step_notification_no_params(context: Any) -> None:
    assert len(context.lccov_sent_messages) == 1
    msg = context.lccov_sent_messages[0]
    assert msg["method"] == "test/note"
    assert "params" not in msg
    assert "id" not in msg


@then('lccov the diagnostics cache for "{uri}" should have {count:d} items')
def step_diagnostics_count(context: Any, uri: str, count: int) -> None:
    diags = context.lccov_client._diagnostics.get(uri, [])
    assert len(diags) == count, (
        f"Expected {count} diagnostics for {uri}, got {len(diags)}"
    )


@then("lccov the pending notifications deque should have {count:d} item")
def step_pending_count(context: Any, count: int) -> None:
    pending = context.lccov_client._pending_notifications
    assert len(pending) == count, (
        f"Expected {count} pending notifications, got {len(pending)}"
    )


@then("lccov the client should be initialized")
def step_client_is_initialized(context: Any) -> None:
    assert context.lccov_client.is_initialized


@then('lccov server_capabilities should include "{key}"')
def step_capabilities_include(context: Any, key: str) -> None:
    caps = context.lccov_client.server_capabilities
    assert key in caps, f"Expected '{key}' in capabilities, got {list(caps.keys())}"


@then('lccov the workspace URI sent should start with "file://"')
def step_workspace_uri_prefix(context: Any) -> None:
    # Find the initialize request in sent messages
    init_msg = None
    for msg in context.lccov_sent_messages:
        if msg.get("method") == "initialize":
            init_msg = msg
            break
    assert init_msg is not None, "No initialize message found"
    root_uri = init_msg["params"]["rootUri"]
    assert root_uri.startswith("file://"), (
        f"rootUri does not start with file://: {root_uri}"
    )


@then('lccov the workspace URI sent should be "{expected}"')
def step_workspace_uri_exact(context: Any, expected: str) -> None:
    init_msg = None
    for msg in context.lccov_sent_messages:
        if msg.get("method") == "initialize":
            init_msg = msg
            break
    assert init_msg is not None, "No initialize message found"
    root_uri = init_msg["params"]["rootUri"]
    assert root_uri == expected, f"Expected rootUri={expected}, got {root_uri}"


@then("lccov the sent params should include initializationOptions")
def step_params_have_init_options(context: Any) -> None:
    init_msg = None
    for msg in context.lccov_sent_messages:
        if msg.get("method") == "initialize":
            init_msg = msg
            break
    assert init_msg is not None, "No initialize message found"
    assert "initializationOptions" in init_msg["params"]
    assert init_msg["params"]["initializationOptions"] == {"custom": True}


@then("lccov no request should have been sent to the transport")
def step_no_messages_sent(context: Any) -> None:
    assert len(context.lccov_sent_messages) == 0, (
        f"Expected no messages, got {len(context.lccov_sent_messages)}"
    )


@then("lccov the client should not be initialized after shutdown")
def step_client_not_initialized(context: Any) -> None:
    assert not context.lccov_client.is_initialized


@then("lccov the transport should have received shutdown and exit messages")
def step_shutdown_and_exit_sent(context: Any) -> None:
    methods = [m.get("method") for m in context.lccov_sent_messages]
    assert "shutdown" in methods, f"No shutdown message found in {methods}"
    assert "exit" in methods, f"No exit message found in {methods}"


@then("lccov the transport should have received a didOpen notification")
def step_did_open_sent(context: Any) -> None:
    assert len(context.lccov_sent_messages) >= 1
    msg = context.lccov_sent_messages[-1]
    assert msg["method"] == "textDocument/didOpen"
    assert "params" in msg
    td = msg["params"]["textDocument"]
    assert td["uri"] == "file:///test.py"
    assert td["languageId"] == "python"
    assert td["version"] == 1
    assert td["text"] == "print('hello')"


@then("lccov the transport should have received a didClose notification")
def step_did_close_sent(context: Any) -> None:
    found = any(
        m.get("method") == "textDocument/didClose" for m in context.lccov_sent_messages
    )
    assert found, "No textDocument/didClose message found"


@then('lccov the diagnostics cache for "{uri}" should be empty')
def step_diagnostics_empty(context: Any, uri: str) -> None:
    assert uri not in context.lccov_client._diagnostics


@then("lccov the returned diagnostics list should have {count:d} item")
def step_returned_diag_count_singular(context: Any, count: int) -> None:
    assert len(context.lccov_diag_result) == count, (
        f"Expected {count} diagnostics, got {len(context.lccov_diag_result)}"
    )


@then("lccov the returned diagnostics list should have {count:d} items")
def step_returned_diag_count(context: Any, count: int) -> None:
    assert len(context.lccov_diag_result) == count, (
        f"Expected {count} diagnostics, got {len(context.lccov_diag_result)}"
    )


@then("lccov the completions should have {count:d} items")
def step_completions_count(context: Any, count: int) -> None:
    assert len(context.lccov_completions) == count, (
        f"Expected {count} completions, got {len(context.lccov_completions)}"
    )
