Feature: LSP Client coverage
  As a developer maintaining LspClient
  I want thorough BDD tests for every code path
  So that coverage for client.py exceeds 90%

  # ── Property: server_capabilities (line 73) ────────────────────────

  Scenario: lccov access server_capabilities before initialize
    Given lccov I create an LspClient with a mock transport
    Then lccov server_capabilities should be an empty dict

  # ── _send_request: success with params (lines 100-109, 111, 114-117, 130, 141) ──

  Scenario: lccov send_request with params returns result
    Given lccov I create an LspClient with a mock transport
    And lccov the transport echoes back a successful response
    When lccov I call _send_request with method "test/method" and params
    Then lccov the request result should contain the expected data
    And lccov the sent message should include params

  # ── _send_request: success without params (lines 100-104, 106 skip) ──

  Scenario: lccov send_request without params omits params key
    Given lccov I create an LspClient with a mock transport
    And lccov the transport echoes back a successful response
    When lccov I call _send_request with method "test/noparams" and no params
    Then lccov the request result should contain the expected data
    And lccov the sent message should not include params key

  # ── _send_request: server returns JSON-RPC error (lines 131-138) ──

  Scenario: lccov send_request raises LspError on JSON-RPC error
    Given lccov I create an LspClient with a mock transport
    And lccov the transport returns a JSON-RPC error response
    When lccov I call _send_request expecting an error
    Then lccov an LspError should be stored with message containing "LSP error"

  # ── _send_request: server closed connection (lines 117, 121-126) ──

  Scenario: lccov send_request raises LspError when server dies
    Given lccov I create an LspClient with a mock transport
    And lccov the transport returns None and is not alive
    When lccov I call _send_request expecting an error
    Then lccov an LspError should be stored with message containing "closed connection"

  # ── _send_request: transport alive but no response yet (lines 117, 121-122 continue loop) ──

  Scenario: lccov send_request retries when transport alive but no data
    Given lccov I create an LspClient with a mock transport
    And lccov the transport returns None then a valid response while alive
    When lccov I call _send_request with method "test/retry" and no params
    Then lccov the request result should contain the expected data

  # ── _send_request: timeout (lines 146-149) ────────────────────────

  Scenario: lccov send_request raises LspError on timeout
    Given lccov I create an LspClient with a mock transport
    And lccov the transport always returns None but stays alive
    When lccov I call _send_request with a very short timeout expecting error
    Then lccov an LspError should be stored with message containing "Timed out"

  # ── _send_request: notification while waiting (line 144) ───────────

  Scenario: lccov send_request queues unrelated messages as notifications
    Given lccov I create an LspClient with a mock transport
    And lccov the transport returns a notification then the response
    When lccov I call _send_request with method "test/mixed" and no params
    Then lccov the request result should contain the expected data
    And lccov the pending notifications should contain the queued message

  # ── _send_notification: with params (lines 163-169) ────────────────

  Scenario: lccov send_notification with params
    Given lccov I create an LspClient with a mock transport
    When lccov I call _send_notification with method "test/note" and params
    Then lccov the transport should have received a notification with params

  # ── _send_notification: without params (lines 163-165, 167 skip) ──

  Scenario: lccov send_notification without params
    Given lccov I create an LspClient with a mock transport
    When lccov I call _send_notification with method "test/note" and no params
    Then lccov the transport should have received a notification without params key

  # ── _handle_notification: publishDiagnostics (lines 173-184) ───────

  Scenario: lccov handle_notification stores publishDiagnostics
    Given lccov I create an LspClient with a mock transport
    When lccov I handle a publishDiagnostics notification for "file:///test.py"
    Then lccov the diagnostics cache for "file:///test.py" should have 2 items

  # ── _handle_notification: other notification (lines 173-174, 187) ──

  Scenario: lccov handle_notification queues non-diagnostic notifications
    Given lccov I create an LspClient with a mock transport
    When lccov I handle a window/logMessage notification
    Then lccov the pending notifications deque should have 1 item

  # ── initialize: success without file:// prefix (lines 208, 215-268, 272 skip, 275-294) ──

  Scenario: lccov initialize with plain path succeeds
    Given lccov I create an LspClient with a mock transport
    And lccov the transport echoes back an initialize result with capabilities
    When lccov I call initialize with workspace "/tmp/project"
    Then lccov the client should be initialized
    And lccov server_capabilities should include "textDocumentSync"
    And lccov the workspace URI sent should start with "file://"

  # ── initialize: workspace already has file:// prefix (lines 215-216) ──

  Scenario: lccov initialize with file URI keeps prefix
    Given lccov I create an LspClient with a mock transport
    And lccov the transport echoes back an initialize result with capabilities
    When lccov I call initialize with workspace "file:///already/uri"
    Then lccov the client should be initialized
    And lccov the workspace URI sent should be "file:///already/uri"

  # ── initialize: with initialization_options (lines 272-273) ──────

  Scenario: lccov initialize with initialization_options includes them
    Given lccov I create an LspClient with a mock transport
    And lccov the transport echoes back an initialize result with capabilities
    When lccov I call initialize with workspace "/tmp/project" and init options
    Then lccov the client should be initialized
    And lccov the sent params should include initializationOptions

  # ── initialize: already initialized (lines 208-211) ────────────────

  Scenario: lccov initialize raises error when already initialized
    Given lccov I create an LspClient with a mock transport
    And lccov the client is already marked as initialized
    When lccov I call initialize expecting an error
    Then lccov an LspError should be stored with message containing "already initialized"

  # ── shutdown: not initialized (lines 302-307) ──────────────────────

  Scenario: lccov shutdown when not initialized returns early
    Given lccov I create an LspClient with a mock transport
    When lccov I call shutdown
    Then lccov no request should have been sent to the transport

  # ── shutdown: success path (lines 302, 309, 311-312, 320-321, 323) ──

  Scenario: lccov shutdown sends shutdown request then exit notification
    Given lccov I create an LspClient with a mock transport
    And lccov the client is already marked as initialized
    And lccov the transport echoes back a shutdown response
    When lccov I call shutdown
    Then lccov the client should not be initialized after shutdown
    And lccov the transport should have received shutdown and exit messages

  # ── shutdown: shutdown request raises LspError (lines 313-317) ─────

  Scenario: lccov shutdown handles LspError from shutdown request gracefully
    Given lccov I create an LspClient with a mock transport
    And lccov the client is already marked as initialized
    And lccov the transport will raise LspError on shutdown request
    When lccov I call shutdown
    Then lccov the client should not be initialized after shutdown

  # ── shutdown: exit notification raises BrokenPipeError (line 320) ──

  Scenario: lccov shutdown suppresses BrokenPipeError on exit
    Given lccov I create an LspClient with a mock transport
    And lccov the client is already marked as initialized
    And lccov the transport echoes back a shutdown response but raises BrokenPipeError on exit
    When lccov I call shutdown
    Then lccov the client should not be initialized after shutdown

  # ── did_open (lines 342-349) ───────────────────────────────────────

  Scenario: lccov did_open sends textDocument/didOpen notification
    Given lccov I create an LspClient with a mock transport
    When lccov I call did_open for "file:///test.py" with language "python"
    Then lccov the transport should have received a didOpen notification

  # ── did_close (lines 360-365) ──────────────────────────────────────

  Scenario: lccov did_close sends notification and clears diagnostics
    Given lccov I create an LspClient with a mock transport
    And lccov there are cached diagnostics for "file:///test.py"
    When lccov I call did_close for "file:///test.py"
    Then lccov the transport should have received a didClose notification
    And lccov the diagnostics cache for "file:///test.py" should be empty

  # ── get_diagnostics (lines 382-389) ────────────────────────────────

  Scenario: lccov get_diagnostics drains messages and returns cached
    Given lccov I create an LspClient with a mock transport
    And lccov the transport has queued diagnostic notifications for drain
    When lccov I call get_diagnostics for "file:///test.py"
    Then lccov the returned diagnostics list should have 1 item

  Scenario: lccov get_diagnostics returns empty for unknown URI
    Given lccov I create an LspClient with a mock transport
    And lccov the transport returns None immediately for drain
    When lccov I call get_diagnostics for "file:///unknown.py"
    Then lccov the returned diagnostics list should have 0 items

  # ── get_completions: CompletionList (lines 407-411, 416-417) ───────

  Scenario: lccov get_completions returns items from CompletionList
    Given lccov I create an LspClient with a mock transport
    And lccov the transport returns a CompletionList response
    When lccov I call get_completions for "file:///test.py" at line 0 char 5
    Then lccov the completions should have 2 items

  # ── get_completions: plain list (lines 418-419) ────────────────────

  Scenario: lccov get_completions returns items from plain list
    Given lccov I create an LspClient with a mock transport
    And lccov the transport returns a plain list completion response
    When lccov I call get_completions for "file:///test.py" at line 0 char 5
    Then lccov the completions should have 1 items

  # ── get_completions: unexpected result type (line 420) ─────────────

  Scenario: lccov get_completions returns empty for unexpected result
    Given lccov I create an LspClient with a mock transport
    And lccov the transport returns a scalar completion response
    When lccov I call get_completions for "file:///test.py" at line 0 char 5
    Then lccov the completions should have 0 items
