# pyright: reportRedeclaration=false
"""Step definitions for A2A stdio transport coverage (coverage boost)."""

from __future__ import annotations

import json
import subprocess
import sys
from typing import Any
from unittest.mock import MagicMock, patch

from behave import given, then, when

from cleveragents.a2a.models import A2aRequest, A2aResponse
from cleveragents.a2a.stdio_transport import A2aStdioTransport

# ── Given steps ──────────────────────────────────────────────────────────


@given("a new A2aStdioTransport instance")
def step_new_transport(context: Any) -> None:
    context.transport = A2aStdioTransport()


@given("a connected A2aStdioTransport with a mock process")
def step_connected_transport(context: Any) -> None:
    transport = A2aStdioTransport()
    mock_proc = MagicMock(spec=subprocess.Popen)
    mock_proc.stdin = MagicMock()
    mock_proc.stdout = MagicMock()
    mock_proc.stderr = MagicMock()
    mock_proc.pid = 12345
    mock_proc.wait = MagicMock(return_value=0)
    transport._process = mock_proc
    transport._is_connected = True
    context.transport = transport
    context.mock_process = mock_proc


@given("the mock process returns a valid JSON-RPC response")
def step_mock_valid_response(context: Any) -> None:
    resp = {"jsonrpc": "2.0", "id": "test-id", "result": {"status": "ok"}}
    context.mock_process.stdout.readline.return_value = json.dumps(resp) + "\n"


@given("the mock process returns invalid JSON")
def step_mock_invalid_json(context: Any) -> None:
    context.mock_process.stdout.readline.return_value = "not-json{{"


@given("the mock process returns empty response")
def step_mock_empty_response(context: Any) -> None:
    context.mock_process.stdout.readline.return_value = ""


@given("the mock process has no stdin")
def step_mock_no_stdin(context: Any) -> None:
    context.mock_process.stdin = None


@given("the mock process has no stdout but valid stdin")
def step_mock_no_stdout(context: Any) -> None:
    context.mock_process.stdin = MagicMock()
    context.mock_process.stdin.write = MagicMock()
    context.mock_process.stdin.flush = MagicMock()
    context.mock_process.stdout = None


@given("the mock process times out on first wait")
def step_mock_timeout(context: Any) -> None:
    context.mock_process.wait.side_effect = [
        subprocess.TimeoutExpired("test", 5.0),
        0,
    ]


@given("subprocess Popen is mocked to succeed")
def step_mock_popen_success(context: Any) -> None:
    mock_proc = MagicMock(spec=subprocess.Popen)
    mock_proc.stdin = MagicMock()
    mock_proc.stdout = MagicMock()
    mock_proc.stderr = MagicMock()
    mock_proc.pid = 99999
    patcher = patch(
        "cleveragents.a2a.stdio_transport.subprocess.Popen", return_value=mock_proc
    )
    context.popen_patcher = patcher
    # patcher.start() returns the mock that replaces subprocess.Popen; call_args
    # are recorded on THIS mock, not on mock_proc (the return value).
    context.popen_mock = patcher.start()

    def cleanup() -> None:
        patcher.stop()

    context.add_cleanup(cleanup)


@given("subprocess Popen raises FileNotFoundError")
def step_mock_popen_fnf(context: Any) -> None:
    patcher = patch(
        "cleveragents.a2a.stdio_transport.subprocess.Popen",
        side_effect=FileNotFoundError("No such file"),
    )
    context.popen_patcher = patcher
    patcher.start()

    def cleanup() -> None:
        patcher.stop()

    context.add_cleanup(cleanup)


# ── When steps ───────────────────────────────────────────────────────────


def _make_request() -> A2aRequest:
    return A2aRequest(method="test.echo", params={"msg": "hello"})


@when("I try to send a request without connecting")
def step_send_without_connect(context: Any) -> None:
    try:
        context.transport.send(_make_request())
        context.raised_error = None
    except (RuntimeError, TypeError) as exc:
        context.raised_error = exc


@when("I try to send a non-A2aRequest object")
def step_send_non_request(context: Any) -> None:
    bad_arg: Any = {"not": "a request"}
    try:
        context.transport.send(bad_arg)
        context.raised_error = None
    except TypeError as exc:
        context.raised_error = exc


@when("I send a valid A2aRequest")
def step_send_valid_request(context: Any) -> None:
    context.response = context.transport.send(_make_request())


@when("I try to send a valid A2aRequest")
def step_try_send_valid_request(context: Any) -> None:
    try:
        context.transport.send(_make_request())
        context.raised_error = None
    except RuntimeError as exc:
        context.raised_error = exc


@when("I try to connect with empty agent path")
def step_connect_empty(context: Any) -> None:
    try:
        context.transport.connect("")
        context.raised_error = None
    except ValueError as exc:
        context.raised_error = exc


@when("I try to connect again with a valid path")
def step_connect_again(context: Any) -> None:
    try:
        context.transport.connect("/some/agent")
        context.raised_error = None
    except RuntimeError as exc:
        context.raised_error = exc


@when("I call disconnect")
def step_disconnect(context: Any) -> None:
    context.transport.disconnect()


@when('I connect with agent path "{path}"')
def step_connect_with_path(context: Any, path: str) -> None:
    context.transport.connect(path)


@when('I try to connect with agent path "{path}"')
def step_try_connect_with_path(context: Any, path: str) -> None:
    try:
        context.transport.connect(path)
        context.raised_error = None
    except (RuntimeError, ValueError) as exc:
        context.raised_error = exc


# ── Then steps ───────────────────────────────────────────────────────────


@then("the stdio transport should not be connected")
def step_stdio_not_connected(context: Any) -> None:
    assert not context.transport.is_connected(), "Expected transport to be disconnected"


@then("the stdio transport should be connected")
def step_stdio_is_connected(context: Any) -> None:
    assert context.transport.is_connected(), "Expected transport to be connected"


@then("the stdio transport process should be None")
def step_stdio_process_none(context: Any) -> None:
    assert context.transport.get_process() is None


@then("a RuntimeError should be raised about not connected")
def step_runtime_not_connected(context: Any) -> None:
    assert isinstance(context.raised_error, RuntimeError)
    assert "not connected" in str(context.raised_error).lower()


@then("a TypeError should be raised about A2aRequest")
def step_type_error_request(context: Any) -> None:
    assert isinstance(context.raised_error, TypeError)
    assert "A2aRequest" in str(context.raised_error)


@then("I should receive an A2aResponse")
def step_received_response(context: Any) -> None:
    assert isinstance(context.response, A2aResponse)


@then("a RuntimeError should be raised about invalid JSON")
def step_runtime_invalid_json(context: Any) -> None:
    assert isinstance(context.raised_error, RuntimeError)
    assert "invalid json" in str(context.raised_error).lower()


@then("a RuntimeError should be raised about closed unexpectedly")
def step_runtime_closed(context: Any) -> None:
    assert isinstance(context.raised_error, RuntimeError)
    assert "closed unexpectedly" in str(context.raised_error).lower()


@then("a RuntimeError should be raised about stdin")
def step_runtime_stdin(context: Any) -> None:
    assert isinstance(context.raised_error, RuntimeError)
    assert "stdin" in str(context.raised_error).lower()


@then("a RuntimeError should be raised about stdout")
def step_runtime_stdout(context: Any) -> None:
    assert isinstance(context.raised_error, RuntimeError)
    assert "stdout" in str(context.raised_error).lower()


@then("a ValueError should be raised about agent_path")
def step_value_error_path(context: Any) -> None:
    assert isinstance(context.raised_error, ValueError)
    assert "agent_path" in str(context.raised_error)


@then("a RuntimeError should be raised about already connected")
def step_runtime_already_connected(context: Any) -> None:
    assert isinstance(context.raised_error, RuntimeError)
    assert "already connected" in str(context.raised_error).lower()


@then("no stdio transport error should be raised")
def step_no_stdio_error(context: Any) -> None:
    # Disconnect on unconnected transport is a no-op
    pass


@then("mock stdin close should have been called")
def step_stdin_closed(context: Any) -> None:
    context.mock_process.stdin.close.assert_called_once()


@then("mock process wait should have been called")
def step_wait_called(context: Any) -> None:
    context.mock_process.wait.assert_called()


@then("mock process terminate should have been called")
def step_terminate_called(context: Any) -> None:
    context.mock_process.terminate.assert_called_once()


@then("a RuntimeError should be raised about agent not found")
def step_runtime_agent_not_found(context: Any) -> None:
    assert isinstance(context.raised_error, RuntimeError)
    assert "agent not found" in str(context.raised_error).lower()


# ── Command construction assertions (reviewer point 5) ─────────────────────


@then("subprocess Popen should have been called with python -m module args")
def step_assert_popen_module_args(context: Any) -> None:
    """Assert that for a module path, Popen received [python, -m, module]."""
    call_args = context.popen_mock.call_args
    cmd_list = call_args[0][0]
    assert len(cmd_list) >= 3
    assert cmd_list[0] == sys.executable
    assert cmd_list[1] == "-m"


@then("subprocess Popen should have been called with python script args")
def step_assert_popen_script_args(context: Any) -> None:
    """Assert that for a .py file path, Popen received [python, file.py]."""
    call_args = context.popen_mock.call_args
    cmd_list = call_args[0][0]
    assert len(cmd_list) >= 2
    assert cmd_list[0] == sys.executable
    assert cmd_list[1].endswith(".py")


@then("subprocess Popen should have been called with direct executable args")
def step_assert_popen_executable_args(context: Any) -> None:
    """Assert that for an executable path, Popen received [executable_path] only."""
    call_args = context.popen_mock.call_args
    cmd_list = call_args[0][0]
    assert len(cmd_list) >= 1
    assert cmd_list[0].startswith("/")
