"""Step definitions for openrouter_provider_registry.feature.

Tests that ProviderRegistry.create_llm and _create_provider_llm correctly
dispatch to the OpenRouter ChatOpenAI backend without raising ValueError.
"""

from __future__ import annotations

import sys
import types
from typing import Any
from unittest.mock import MagicMock

from behave import given, then, use_step_matcher, when  # type: ignore[import-untyped]

from cleveragents.providers.registry import (
    ProviderRegistry,
    ProviderType,
)

_OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1"


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


def _make_or_settings(
    openrouter: str | None = None,
    organization: str | None = None,
) -> MagicMock:
    """Return a minimal fake Settings object for OpenRouter tests."""
    settings = MagicMock()
    settings.openai_api_key = None
    settings.anthropic_api_key = None
    settings.google_api_key = None
    settings.gemini_api_key = None
    settings.azure_api_key = None
    settings.openrouter_api_key = openrouter
    settings.cohere_api_key = None
    settings.groq_api_key = None
    settings.together_api_key = None
    settings.default_provider = None
    settings.default_model = None
    settings.azure_openai_endpoint = None
    settings.azure_openai_api_version = None
    settings.azure_openai_deployment = None
    settings.openrouter_organization = organization
    return settings


def _stub_chat_openai(context: Any) -> None:
    """Replace langchain_openai.ChatOpenAI with a recording stub."""
    original = sys.modules.get("langchain_openai")
    calls: list[dict[str, Any]] = []

    class FakeChatOpenAI:
        def __init__(self, **kwargs: Any) -> None:
            calls.append(kwargs)

    class FakeAzureChatOpenAI:
        def __init__(self, **kwargs: Any) -> None:
            pass

    stub = types.ModuleType("langchain_openai")
    stub.ChatOpenAI = FakeChatOpenAI  # type: ignore[attr-defined]
    stub.AzureChatOpenAI = FakeAzureChatOpenAI  # type: ignore[attr-defined]
    sys.modules["langchain_openai"] = stub

    context._or_chat_openai_calls = calls
    context._or_chat_openai_cls = FakeChatOpenAI

    if not hasattr(context, "_or_cleanup"):
        context._or_cleanup = []

    def _restore() -> None:
        if original is None:
            sys.modules.pop("langchain_openai", None)
        else:
            sys.modules["langchain_openai"] = original

    context._or_cleanup.append(_restore)


def _parse_headers_string(headers_string: str) -> dict[str, str]:
    """Parse 'Key=Value,Key2=Value2' into a dict."""
    result: dict[str, str] = {}
    for entry in headers_string.split(","):
        entry = entry.strip()
        if "=" in entry:
            key, value = entry.split("=", 1)
            result[key.strip()] = value.strip()
    return result


# ---------------------------------------------------------------------------
# Given
# ---------------------------------------------------------------------------

use_step_matcher("re")


@given(
    r'I have an openrouter registry with API key "(?P<or_api_key>[^"]+)" and organization "(?P<or_org>[^"]+)"'
)
def step_or_registry_with_key_and_org(
    context: Any, or_api_key: str, or_org: str
) -> None:
    context.or_registry = ProviderRegistry(
        settings=_make_or_settings(openrouter=or_api_key, organization=or_org)
    )


@given(r'I have an openrouter registry with API key "(?P<or_api_key>[^"]+)"')
def step_or_registry_with_key(context: Any, or_api_key: str) -> None:
    context.or_registry = ProviderRegistry(
        settings=_make_or_settings(openrouter=or_api_key)
    )


@given(r"I have an openrouter registry with no API keys")
def step_or_registry_no_keys(context: Any) -> None:
    context.or_registry = ProviderRegistry(settings=_make_or_settings())


@given(r"I have the openrouter ProviderRegistry class")
def step_or_registry_class(context: Any) -> None:
    context.or_registry_class = ProviderRegistry


@given(r"the openrouter registry LangChain ChatOpenAI client is stubbed")
def step_or_stub_chat_openai(context: Any) -> None:
    _stub_chat_openai(context)


# ---------------------------------------------------------------------------
# When
# ---------------------------------------------------------------------------


@when(
    r'I call create_llm on the openrouter registry for provider "openrouter" with model "(?P<or_model>[^"]+)"'
)
def step_or_create_llm_with_model(context: Any, or_model: str) -> None:
    context.or_error = None
    try:
        context.or_result = context.or_registry.create_llm(
            provider_type="openrouter", model_id=or_model
        )
    except ValueError as exc:
        context.or_error = exc


@when(r"I call create_llm on the openrouter registry without specifying a provider")
def step_or_create_llm_default(context: Any) -> None:
    context.or_error = None
    try:
        context.or_result = context.or_registry.create_llm()
    except ValueError as exc:
        context.or_error = exc


@when(
    r'I call _create_provider_llm on the openrouter registry for provider "openrouter" with model None'
)
def step_or_private_llm_none_model(context: Any) -> None:
    context.or_error = None
    try:
        context.or_result = context.or_registry._create_provider_llm(
            ProviderType.OPENROUTER, None
        )
    except ValueError as exc:
        context.or_error = exc


@when(
    r'I call _create_provider_llm on the openrouter registry for provider "openrouter" with model "(?P<or_model>[^"]+)" and default_headers "(?P<or_headers_string>[^"]+)"'
)
def step_or_private_llm_with_headers(
    context: Any, or_model: str, or_headers_string: str
) -> None:
    headers = _parse_headers_string(or_headers_string)
    context.or_error = None
    try:
        context.or_result = context.or_registry._create_provider_llm(
            ProviderType.OPENROUTER, or_model, default_headers=headers
        )
    except ValueError as exc:
        context.or_error = exc


@when(
    r'I call _create_provider_llm on the openrouter registry for provider "openrouter" with model "(?P<or_model>[^"]+)"'
)
def step_or_private_llm_with_model(context: Any, or_model: str) -> None:
    context.or_error = None
    try:
        context.or_result = context.or_registry._create_provider_llm(
            ProviderType.OPENROUTER, or_model
        )
    except ValueError as exc:
        context.or_error = exc


@when(r'I try to call create_llm on the openrouter registry for provider "openrouter"')
def step_or_try_create_llm_no_key(context: Any) -> None:
    context.or_error = None
    try:
        context.or_registry.create_llm(provider_type="openrouter")
    except ValueError as exc:
        context.or_error = exc


@when(r"I check the openrouter FALLBACK_ORDER")
def step_or_check_fallback_order(context: Any) -> None:
    context.or_fallback_order = context.or_registry_class.FALLBACK_ORDER


@when(r"I check the openrouter DEFAULT_MODELS")
def step_or_check_default_models(context: Any) -> None:
    context.or_default_models = context.or_registry_class.DEFAULT_MODELS


@when(r"I check the openrouter DEFAULT_CAPABILITIES for OPENROUTER")
def step_or_check_capabilities(context: Any) -> None:
    context.or_capabilities = context.or_registry_class.DEFAULT_CAPABILITIES.get(
        ProviderType.OPENROUTER
    )


@when(r"I call get_provider_info for OPENROUTER on the openrouter registry")
def step_or_get_provider_info(context: Any) -> None:
    context.or_provider_info = context.or_registry.get_provider_info(
        ProviderType.OPENROUTER
    )


# ---------------------------------------------------------------------------
# Then
# ---------------------------------------------------------------------------


@then(
    r'the openrouter registry ChatOpenAI should be called with model "(?P<or_expected_model>[^"]+)"'
)
def step_or_assert_model(context: Any, or_expected_model: str) -> None:
    calls = getattr(context, "_or_chat_openai_calls", [])
    assert calls, "Expected ChatOpenAI to be instantiated"
    actual_model = calls[-1].get("model")
    assert actual_model == or_expected_model, (
        f"Expected model={or_expected_model!r}, got {actual_model!r}"
    )


@then(
    r'the openrouter registry ChatOpenAI should use base url "(?P<or_expected_url>[^"]+)"'
)
def step_or_assert_base_url(context: Any, or_expected_url: str) -> None:
    calls = getattr(context, "_or_chat_openai_calls", [])
    assert calls, "Expected ChatOpenAI to be instantiated"
    actual_url = calls[-1].get("openai_api_base")
    assert actual_url == or_expected_url, (
        f"Expected openai_api_base={or_expected_url!r}, got {actual_url!r}"
    )


@then(
    r'the openrouter registry ChatOpenAI should use api key "(?P<or_expected_key>[^"]+)"'
)
def step_or_assert_api_key(context: Any, or_expected_key: str) -> None:
    calls = getattr(context, "_or_chat_openai_calls", [])
    assert calls, "Expected ChatOpenAI to be instantiated"
    actual_key = calls[-1].get("openai_api_key")
    assert actual_key == or_expected_key, (
        f"Expected openai_api_key={or_expected_key!r}, got {actual_key!r}"
    )


@then(
    r'the openrouter registry ChatOpenAI should include header "(?P<or_header_name>[^"]+)" with value "(?P<or_expected_value>[^"]+)"'
)
def step_or_assert_header(
    context: Any, or_header_name: str, or_expected_value: str
) -> None:
    calls = getattr(context, "_or_chat_openai_calls", [])
    assert calls, "Expected ChatOpenAI to be instantiated"
    headers = calls[-1].get("default_headers")
    assert isinstance(headers, dict), (
        f"Expected default_headers to be a dict, got {type(headers)}"
    )
    actual_value = headers.get(or_header_name)
    assert actual_value == or_expected_value, (
        f"Expected header {or_header_name}={or_expected_value!r}, got {actual_value!r}"
    )


@then(r"an openrouter registry ValueError should be raised")
def step_or_assert_value_error(context: Any) -> None:
    assert context.or_error is not None, "Expected a ValueError to be raised"
    assert isinstance(context.or_error, ValueError), (
        f"Expected ValueError, got {type(context.or_error).__name__}"
    )


@then(r'the openrouter registry error should mention "(?P<or_fragment>[^"]+)"')
def step_or_assert_error_contains(context: Any, or_fragment: str) -> None:
    assert context.or_error is not None, "Expected an error to be set"
    message = str(context.or_error)
    assert or_fragment in message, (
        f"Expected error to contain {or_fragment!r}, got: {message!r}"
    )


@then(r"ProviderType\.OPENROUTER should be in the fallback order")
def step_or_assert_in_fallback(context: Any) -> None:
    assert ProviderType.OPENROUTER in context.or_fallback_order, (
        "Expected ProviderType.OPENROUTER to be in FALLBACK_ORDER"
    )


@then(r'the openrouter default model should be "(?P<or_expected_model>[^"]+)"')
def step_or_assert_default_model(context: Any, or_expected_model: str) -> None:
    actual = context.or_default_models.get(ProviderType.OPENROUTER)
    assert actual == or_expected_model, (
        f"Expected default model={or_expected_model!r}, got {actual!r}"
    )


@then(r"the openrouter provider supports_streaming should be (?P<or_expected>\w+)")
def step_or_assert_streaming(context: Any, or_expected: str) -> None:
    assert context.or_capabilities is not None
    assert context.or_capabilities.supports_streaming is (or_expected == "True")


@then(r"the openrouter provider supports_tool_calls should be (?P<or_expected>\w+)")
def step_or_assert_tool_calls(context: Any, or_expected: str) -> None:
    assert context.or_capabilities is not None
    assert context.or_capabilities.supports_tool_calls is (or_expected == "True")


@then(r"the openrouter provider supports_vision should be (?P<or_expected>\w+)")
def step_or_assert_vision(context: Any, or_expected: str) -> None:
    assert context.or_capabilities is not None
    assert context.or_capabilities.supports_vision is (or_expected == "True")


@then(r"the openrouter provider max_context_length should be (?P<or_expected>\d+)")
def step_or_assert_context_length(context: Any, or_expected: str) -> None:
    assert context.or_capabilities is not None
    expected_int = int(or_expected)
    assert context.or_capabilities.max_context_length == expected_int, (
        f"Expected max_context_length={expected_int}, got {context.or_capabilities.max_context_length}"
    )


@then(r"the openrouter provider supports_json_mode should be (?P<or_expected>\w+)")
def step_or_assert_json_mode(context: Any, or_expected: str) -> None:
    assert context.or_capabilities is not None
    assert context.or_capabilities.supports_json_mode is (or_expected == "True")


@then(r"the openrouter provider info should be configured")
def step_or_assert_provider_configured(context: Any) -> None:
    info = context.or_provider_info
    assert info is not None, "Expected provider info to exist"
    assert info.is_configured is True, "Expected provider to be configured"


@then(r"the openrouter provider info should not be configured")
def step_or_assert_provider_not_configured(context: Any) -> None:
    info = context.or_provider_info
    assert info is not None, "Expected provider info to exist"
    assert info.is_configured is False, "Expected provider to not be configured"


# Restore the default step matcher so that step files loaded after this one
# are not affected by the regex matcher set above.
use_step_matcher("parse")
