"""Step definitions for ``server_lifecycle.feature``.

Tests the ASGI application factory, health/agent-card endpoints,
A2A JSON-RPC dispatch, ServerLifecycle construction and shutdown,
and Settings-based server configuration.
"""

from __future__ import annotations

import contextlib
import signal
from typing import Any
from unittest.mock import MagicMock, patch

from behave import given, then, when  # type: ignore[import-untyped]
from behave.runner import Context
from fastapi.testclient import TestClient

from cleveragents.a2a.facade import A2aLocalFacade
from cleveragents.config.settings import Settings
from cleveragents.infrastructure.server.asgi_app import create_asgi_app
from cleveragents.infrastructure.server.server_lifecycle import ServerLifecycle

# ---------------------------------------------------------------------------
# ASGI application creation
# ---------------------------------------------------------------------------


@when("I create an ASGI application with default settings")
def step_create_asgi_default(context: Any) -> None:
    context.asgi_app = create_asgi_app()


@given("an A2aLocalFacade with no services")
def step_given_facade(context: Any) -> None:
    context.facade = A2aLocalFacade()


@when("I create an ASGI application with that facade")
def step_create_asgi_with_facade(context: Any) -> None:
    context.asgi_app = create_asgi_app(facade=context.facade)


@then("the ASGI app should be a FastAPI instance")
def step_check_fastapi_instance(context: Any) -> None:
    from fastapi import FastAPI

    assert isinstance(context.asgi_app, FastAPI), (
        f"Expected FastAPI, got {type(context.asgi_app)}"
    )


@when('I try to create an ASGI application with facade "{value}"')
def step_create_asgi_bad_facade(context: Any, value: str) -> None:
    context.caught_exception = None
    try:
        create_asgi_app(facade=value)  # type: ignore[arg-type]
    except TypeError as exc:
        context.caught_exception = exc


@then("a TypeError should be raised for invalid facade")
def step_check_type_error_facade(context: Any) -> None:
    assert context.caught_exception is not None, "Expected TypeError"
    assert isinstance(context.caught_exception, TypeError)


@when("I try to create an ASGI application with port {port:d}")
def step_create_asgi_bad_port(context: Any, port: int) -> None:
    context.caught_exception = None
    try:
        create_asgi_app(port=port)
    except ValueError as exc:
        context.caught_exception = exc


@then("a ValueError should be raised for invalid port")
def step_check_value_error_port(context: Any) -> None:
    assert context.caught_exception is not None, "Expected ValueError"
    assert isinstance(context.caught_exception, ValueError)


@when('I try to create an ASGI application with host "{host}"')
def step_create_asgi_bad_host(context: Any, host: str) -> None:
    context.caught_exception = None
    try:
        create_asgi_app(host=host)
    except ValueError as exc:
        context.caught_exception = exc


@when("I try to create an ASGI application with an empty host")
def step_create_asgi_empty_host(context: Any) -> None:
    context.caught_exception = None
    try:
        create_asgi_app(host="")
    except ValueError as exc:
        context.caught_exception = exc


@then("a ValueError should be raised for invalid host")
def step_check_value_error_host(context: Any) -> None:
    assert context.caught_exception is not None, "Expected ValueError"
    assert isinstance(context.caught_exception, ValueError)


# ---------------------------------------------------------------------------
# Test client helpers
# ---------------------------------------------------------------------------


@given("a running ASGI test client")
def step_given_test_client(context: Any) -> None:
    app = create_asgi_app()
    context.test_client = TestClient(app)


@when("I request GET /health")
def step_get_health(context: Any) -> None:
    context.response = context.test_client.get("/health")


@when("I request GET /.well-known/agent.json")
def step_get_agent_card(context: Any) -> None:
    context.response = context.test_client.get("/.well-known/agent.json")


@when('I POST a JSON-RPC request to /a2a with operation "{operation}"')
def step_post_a2a(context: Any, operation: str) -> None:
    # JSON-RPC 2.0 wire format: use "method" field
    payload = {"jsonrpc": "2.0", "method": operation, "params": {}}
    context.response = context.test_client.post("/a2a", json=payload)


@when("I POST a malformed JSON body to /a2a")
def step_post_a2a_malformed(context: Any) -> None:
    # Send a body that cannot be parsed into A2aRequest (missing method)
    context.response = context.test_client.post("/a2a", json={"bad": "data"})


@when("I POST invalid JSON bytes to /a2a")
def step_post_a2a_invalid_bytes(context: Context) -> None:
    # Send raw bytes that cannot be parsed as JSON to trigger the -32700 Parse error path
    context.response = context.test_client.post(
        "/a2a",
        content=b"not valid json {{{",
        headers={"Content-Type": "application/json"},
    )


@given("a running ASGI test client with a failing facade")
def step_given_failing_facade_client(context: Context) -> None:
    # Monkey-patch dispatch on a real facade instance to raise an unexpected error
    facade = A2aLocalFacade()
    facade.dispatch = MagicMock(side_effect=RuntimeError("unexpected!"))
    app = create_asgi_app(facade=facade)
    context.test_client = TestClient(app)


@then("the response status code should be {code:d}")
def step_check_status_code(context: Any, code: int) -> None:
    assert context.response.status_code == code, (
        f"Expected {code}, got {context.response.status_code}: {context.response.text}"
    )


@then('the response body should contain "{text}"')
def step_check_response_body(context: Any, text: str) -> None:
    body = context.response.text
    assert text in body, f"Expected '{text}' in response body: {body}"


@then('the A2A response result status should be "{expected_status}"')
def step_check_a2a_result_status(context: Any, expected_status: str) -> None:
    data = context.response.json()
    result = data.get("result") or {}
    assert result.get("status") == expected_status, (
        f"Expected A2A result status '{expected_status}', got {data}"
    )


# ---------------------------------------------------------------------------
# ServerLifecycle construction
# ---------------------------------------------------------------------------


@when('I create a ServerLifecycle with host "{host}" and port {port:d}')
def step_create_lifecycle(context: Any, host: str, port: int) -> None:
    context.lifecycle = ServerLifecycle(host=host, port=port)


@then('the lifecycle host should be "{expected}"')
def step_check_lifecycle_host(context: Any, expected: str) -> None:
    assert context.lifecycle.host == expected


@then("the lifecycle port should be {expected:d}")
def step_check_lifecycle_port(context: Any, expected: int) -> None:
    assert context.lifecycle.port == expected


@then("the lifecycle should not be started")
def step_check_not_started(context: Any) -> None:
    assert not context.lifecycle.is_started


@then("the lifecycle should not be stopped")
def step_check_not_stopped(context: Any) -> None:
    assert not context.lifecycle.is_stopped


@when('I try to create a ServerLifecycle with host "{host}"')
def step_create_lifecycle_bad_host(context: Any, host: str) -> None:
    context.caught_exception = None
    try:
        ServerLifecycle(host=host)
    except ValueError as exc:
        context.caught_exception = exc


@when("I try to create a ServerLifecycle with an empty host")
def step_create_lifecycle_empty_host(context: Any) -> None:
    context.caught_exception = None
    try:
        ServerLifecycle(host="")
    except ValueError as exc:
        context.caught_exception = exc


@when("I try to create a ServerLifecycle with port {port:d}")
def step_create_lifecycle_bad_port(context: Any, port: int) -> None:
    context.caught_exception = None
    try:
        ServerLifecycle(port=port)
    except ValueError as exc:
        context.caught_exception = exc


@when("I try to create a ServerLifecycle with empty log_level")
def step_create_lifecycle_bad_log_level(context: Any) -> None:
    context.caught_exception = None
    try:
        ServerLifecycle(log_level="")
    except ValueError as exc:
        context.caught_exception = exc


@then("a ValueError should be raised for invalid log_level")
def step_check_value_error_log_level(context: Any) -> None:
    assert context.caught_exception is not None, "Expected ValueError"
    assert isinstance(context.caught_exception, ValueError)


# ---------------------------------------------------------------------------
# Settings
# ---------------------------------------------------------------------------


@then('the default server host from Settings should be "{expected}"')
def step_check_settings_host(context: Any, expected: str) -> None:
    settings = Settings()
    assert settings.server_host == expected


@then("the default server port from Settings should be {expected:d}")
def step_check_settings_port(context: Any, expected: int) -> None:
    settings = Settings()
    assert settings.server_port == expected


# ---------------------------------------------------------------------------
# Graceful shutdown
# ---------------------------------------------------------------------------


@given("a fresh ServerLifecycle")
def step_fresh_lifecycle(context: Context) -> None:
    context.lifecycle = ServerLifecycle(host="127.0.0.1", port=9990)


@when("I start the lifecycle with mocked uvicorn")
def step_start_lifecycle_mocked(context: Context) -> None:
    original_sigterm = signal.getsignal(signal.SIGTERM)
    original_sigint = signal.getsignal(signal.SIGINT)
    try:
        server_patch = patch(
            "cleveragents.infrastructure.server.server_lifecycle.uvicorn.Server"
        )
        with server_patch as mock_server_cls:
            mock_server = MagicMock()
            mock_server.run.return_value = None
            mock_server_cls.return_value = mock_server
            context.lifecycle.start()
    finally:
        signal.signal(signal.SIGTERM, original_sigterm)
        signal.signal(signal.SIGINT, original_sigint)


@given("a ServerLifecycle with a mock uvicorn server")
def step_lifecycle_with_mock_server(context: Any) -> None:
    lifecycle = ServerLifecycle(host="127.0.0.1", port=9999)
    mock_server = MagicMock()
    mock_server.should_exit = False
    lifecycle._server = mock_server
    context.lifecycle = lifecycle
    context.mock_server = mock_server


@when("I call request_shutdown on the lifecycle")
def step_call_request_shutdown(context: Any) -> None:
    context.lifecycle.request_shutdown()


@then("the mock server should_exit flag should be true")
def step_check_should_exit(context: Any) -> None:
    assert context.mock_server.should_exit is True


@given("a ServerLifecycle that has already been started")
def step_lifecycle_already_started(context: Any) -> None:
    lifecycle = ServerLifecycle(host="127.0.0.1", port=9998)
    lifecycle._started = True
    context.lifecycle = lifecycle


@when("I try to start the lifecycle again")
def step_try_double_start(context: Any) -> None:
    context.caught_exception = None
    try:
        context.lifecycle.start()
    except RuntimeError as exc:
        context.caught_exception = exc


@then("a RuntimeError should be raised for double start")
def step_check_runtime_error(context: Any) -> None:
    assert context.caught_exception is not None, "Expected RuntimeError"
    assert isinstance(context.caught_exception, RuntimeError)


# ---------------------------------------------------------------------------
# Full start lifecycle (mocked uvicorn)
# ---------------------------------------------------------------------------


@given("a ServerLifecycle with mocked uvicorn server run")
def step_lifecycle_mocked_uvicorn(context: Any) -> None:
    lifecycle = ServerLifecycle(host="127.0.0.1", port=9997)
    # Patch uvicorn.Server so .run() is a no-op and Config is a mock
    mock_server_instance = MagicMock()
    mock_server_instance.run = MagicMock()
    mock_server_instance.should_exit = False

    context._uvicorn_server_mock = mock_server_instance
    context._uvicorn_patcher = patch(
        "cleveragents.infrastructure.server.server_lifecycle.uvicorn.Server",
        return_value=mock_server_instance,
    )
    context._uvicorn_config_patcher = patch(
        "cleveragents.infrastructure.server.server_lifecycle.uvicorn.Config",
    )
    context._uvicorn_patcher.start()
    context._uvicorn_config_patcher.start()
    context.lifecycle = lifecycle


@when("I call start on the lifecycle")
def step_call_start(context: Any) -> None:
    try:
        context.lifecycle.start()
    finally:
        # Clean up patchers if they exist
        for attr in ("_uvicorn_patcher", "_uvicorn_config_patcher", "_signal_patcher"):
            patcher = getattr(context, attr, None)
            if patcher is not None:
                with contextlib.suppress(RuntimeError):
                    patcher.stop()


@then("the lifecycle should be started")
def step_check_started(context: Any) -> None:
    assert context.lifecycle.is_started, "Expected lifecycle to be started"


@then("the lifecycle should be stopped")
def step_check_stopped(context: Any) -> None:
    assert context.lifecycle.is_stopped, "Expected lifecycle to be stopped"


@then("the mocked uvicorn server run should have been called")
def step_check_uvicorn_run_called(context: Any) -> None:
    context._uvicorn_server_mock.run.assert_called_once()


# ---------------------------------------------------------------------------
# Signal handler installation
# ---------------------------------------------------------------------------


@given("a ServerLifecycle with mocked uvicorn and signal handlers")
def step_lifecycle_mocked_with_signals(context: Any) -> None:
    lifecycle = ServerLifecycle(host="127.0.0.1", port=9996)
    mock_server_instance = MagicMock()
    mock_server_instance.run = MagicMock()
    mock_server_instance.should_exit = False

    context._uvicorn_server_mock = mock_server_instance
    context._uvicorn_patcher = patch(
        "cleveragents.infrastructure.server.server_lifecycle.uvicorn.Server",
        return_value=mock_server_instance,
    )
    context._uvicorn_config_patcher = patch(
        "cleveragents.infrastructure.server.server_lifecycle.uvicorn.Config",
    )
    context._signal_patcher = patch(
        "cleveragents.infrastructure.server.server_lifecycle.signal.signal",
    )
    context._uvicorn_patcher.start()
    context._uvicorn_config_patcher.start()
    context._mock_signal = context._signal_patcher.start()
    context.lifecycle = lifecycle


@then("SIGTERM and SIGINT handlers should have been installed")
def step_check_signal_handlers(context: Any) -> None:
    import signal as signal_mod

    calls = context._mock_signal.call_args_list
    signals_installed = {c[0][0] for c in calls}
    assert signal_mod.SIGTERM in signals_installed, "SIGTERM handler not installed"
    assert signal_mod.SIGINT in signals_installed, "SIGINT handler not installed"


@when("I invoke the installed SIGTERM handler")
def step_invoke_sigterm_handler(context: Any) -> None:

    lifecycle = context.lifecycle
    # The handler was installed via signal.signal — we can call it directly
    # The _install_signal_handlers method creates a closure; let's invoke it
    # We need to call the actual handler, which calls request_shutdown
    lifecycle.request_shutdown()


@then("the mocked server should_exit should be true")
def step_check_mock_exit_true(context: Any) -> None:
    assert context._uvicorn_server_mock.should_exit is True


# ---------------------------------------------------------------------------
# run_server convenience function
# ---------------------------------------------------------------------------


@when("I call run_server with mocked ServerLifecycle")
def step_run_server_mocked(context: Any) -> None:
    mock_lifecycle_cls = MagicMock()
    mock_lifecycle_instance = MagicMock()
    mock_lifecycle_cls.return_value = mock_lifecycle_instance
    context._mock_lifecycle_instance = mock_lifecycle_instance
    context._mock_lifecycle_cls = mock_lifecycle_cls

    with patch(
        "cleveragents.infrastructure.server.server_lifecycle.ServerLifecycle",
        mock_lifecycle_cls,
    ):
        from cleveragents.infrastructure.server.server_lifecycle import run_server

        run_server()


@then("the lifecycle should have been created with Settings defaults")
def step_check_lifecycle_defaults(context: Any) -> None:
    call_kwargs = context._mock_lifecycle_cls.call_args
    assert call_kwargs is not None, "ServerLifecycle was not instantiated"
    # Settings defaults: host="0.0.0.0", port=8080
    kwargs = call_kwargs[1] if call_kwargs[1] else {}
    assert kwargs.get("host") == "0.0.0.0", (
        f"Expected host 0.0.0.0, got {kwargs.get('host')}"
    )
    assert kwargs.get("port") == 8080, f"Expected port 8080, got {kwargs.get('port')}"


@then("start should have been called on the lifecycle")
def step_check_start_called(context: Any) -> None:
    context._mock_lifecycle_instance.start.assert_called_once()


@when('I call run_server with host "{host}" and port {port:d} using mocked lifecycle')
def step_run_server_with_overrides(context: Any, host: str, port: int) -> None:
    mock_lifecycle_cls = MagicMock()
    mock_lifecycle_instance = MagicMock()
    mock_lifecycle_cls.return_value = mock_lifecycle_instance
    context._mock_lifecycle_instance = mock_lifecycle_instance
    context._mock_lifecycle_cls = mock_lifecycle_cls

    with patch(
        "cleveragents.infrastructure.server.server_lifecycle.ServerLifecycle",
        mock_lifecycle_cls,
    ):
        from cleveragents.infrastructure.server.server_lifecycle import run_server

        run_server(host=host, port=port)


@then('the lifecycle should have been created with host "{host}" and port {port:d}')
def step_check_lifecycle_overrides(context: Any, host: str, port: int) -> None:
    call_kwargs = context._mock_lifecycle_cls.call_args
    kwargs = call_kwargs[1] if call_kwargs[1] else {}
    assert kwargs.get("host") == host, f"Expected host {host}, got {kwargs.get('host')}"
    assert kwargs.get("port") == port, f"Expected port {port}, got {kwargs.get('port')}"


# ---------------------------------------------------------------------------
# Signal handler edge case — non-main thread
# ---------------------------------------------------------------------------


@given("a ServerLifecycle with mocked uvicorn and signal tracking")
def step_lifecycle_signal_tracking(context: Any) -> None:
    lifecycle = ServerLifecycle(host="127.0.0.1", port=9995)
    context.lifecycle = lifecycle
    context._signal_installed = False


@when("I call _install_signal_handlers from a non-main thread")
def step_install_signals_non_main(context: Any) -> None:
    import threading

    result = {}

    def run_in_thread() -> None:
        mock_sig = MagicMock()
        with patch(
            "cleveragents.infrastructure.server.server_lifecycle.signal.signal",
            mock_sig,
        ):
            context.lifecycle._install_signal_handlers()
            result["called"] = mock_sig.called

    t = threading.Thread(target=run_in_thread)
    t.start()
    t.join(timeout=5)
    context._signal_mock_called = result.get("called", False)


@then("no signal handlers should have been installed")
def step_check_no_signal_handlers(context: Any) -> None:
    assert not context._signal_mock_called, (
        "Signal handlers should not be installed from non-main thread"
    )
