from __future__ import annotations

import json
import sys
import types
from dataclasses import dataclass
from typing import Any, Dict, List, Optional

from behave import given, then


@dataclass
class _FakeOpenAiTranscriptionBehavior:
    transcript: Optional[str]
    result_kind: str = "object"
    no_speech_probabilities: Optional[List[float]] = None
    segments_payload: Optional[List[Any]] = None


@dataclass
class _FakeOpenAiChatBehavior:
    response: str
    match_text: Optional[str] = None


@dataclass
class _FakeOpenAiEmbeddingBehavior:
    vector: List[float]


class _ManualToolFunction:
    def __init__(self, name: str, arguments: str) -> None:
        self.name = name
        self.arguments = arguments


class _ManualToolCall:
    def __init__(self, tool_id: str, name: str, arguments: str) -> None:
        self.id = tool_id
        self.type = "function"
        self.function = _ManualToolFunction(name=name, arguments=arguments)


def _ensure_fake_openai_transcription_behaviors(
    context,
) -> Dict[str, _FakeOpenAiTranscriptionBehavior]:
    behaviors = getattr(context, "fake_openai_transcriptions", None)
    if behaviors is None:
        behaviors = {}
        context.fake_openai_transcriptions = behaviors
    return behaviors


def _ensure_fake_openai_chat_behaviors(context) -> List[_FakeOpenAiChatBehavior]:
    behaviors = getattr(context, "fake_openai_chat_behaviors", None)
    if behaviors is None:
        behaviors = []
        context.fake_openai_chat_behaviors = behaviors
    return behaviors


def _ensure_fake_openai_embedding_behaviors(context) -> Dict[str, _FakeOpenAiEmbeddingBehavior]:
    behaviors = getattr(context, "fake_openai_embeddings", None)
    if behaviors is None:
        behaviors = {}
        context.fake_openai_embeddings = behaviors
    return behaviors


def _install_fake_dspy_module(context) -> None:
    already_installed = getattr(context, "_fake_dspy_installed", False)
    if already_installed:
        return

    original_modules: Dict[str, object] = {}
    if "dspy" in sys.modules:
        original_modules["dspy"] = sys.modules["dspy"]

    chat_behaviors = _ensure_fake_openai_chat_behaviors(context)
    embedding_behaviors = _ensure_fake_openai_embedding_behaviors(context)

    class _DspyLM:  # noqa: N801 - external dependency uses PascalCase
        def __init__(self, model: str, **kwargs):  # type: ignore[no-untyped-def]
            dspy_module.last_lm_model = model  # type: ignore[attr-defined]
            dspy_module.last_lm_kwargs = dict(kwargs)  # type: ignore[attr-defined]

        def __call__(self, *, messages, **kwargs):  # type: ignore[no-untyped-def]
            dspy_module.last_chat_messages = list(messages)  # type: ignore[attr-defined]
            dspy_module.last_chat_kwargs = dict(kwargs)  # type: ignore[attr-defined]
            prompt_text = "\n".join(str(message.get("content", "")) for message in messages)
            dspy_module.last_chat_prompt = prompt_text  # type: ignore[attr-defined]
            response_text = ""
            for behavior in chat_behaviors:
                if behavior.match_text is None or behavior.match_text in prompt_text:
                    response_text = behavior.response
                    break

            tool_queue = getattr(dspy_module, "fake_tool_queue", [])
            if tool_queue:
                return [{"text": "", "tool_calls": [tool_queue.pop(0)]}]

            try:
                payload = json.loads(response_text)
            except json.JSONDecodeError:
                return [response_text]

            if not isinstance(payload, dict):
                return [response_text]

            operations = payload.get("operations") or []
            tool_calls: List[Any] = []
            tool_index = getattr(dspy_module, "fake_tool_call_index", 0)
            for operation in operations:
                tool_index += 1
                tool_calls.append(
                    {
                        "id": f"toolu_{tool_index}",
                        "type": "function",
                        "function": {
                            "name": "str_replace",
                            "arguments": json.dumps(
                                {
                                    "old_str": operation.get("old_str", ""),
                                    "new_str": operation.get("new_str", ""),
                                }
                            ),
                        },
                    }
                )
            if payload.get("done"):
                tool_index += 1
                tool_calls.append(
                    {
                        "id": f"toolu_{tool_index}",
                        "type": "function",
                        "function": {"name": "done", "arguments": json.dumps({})},
                    }
                )
            dspy_module.fake_tool_call_index = tool_index
            if payload.get("batch"):
                return [{"text": "", "tool_calls": tool_calls}]
            dspy_module.fake_tool_queue = tool_calls[1:]
            if tool_calls:
                return [{"text": "", "tool_calls": [tool_calls[0]]}]
            return [response_text]

    class _FakeArray:
        def __init__(self, values: Any) -> None:
            self._values = values

        def tolist(self) -> Any:
            return self._values

    class _DspyEmbedder:  # noqa: N801 - external dependency uses PascalCase
        def __init__(self, model: str, batch_size: int = 200, caching: bool = True, **kwargs):  # type: ignore[no-untyped-def]
            dspy_module.last_embedding_model = model  # type: ignore[attr-defined]
            dspy_module.last_embedding_kwargs = dict(kwargs)  # type: ignore[attr-defined]
            dspy_module.last_embedding_batch_size = batch_size  # type: ignore[attr-defined]
            dspy_module.last_embedding_caching = caching  # type: ignore[attr-defined]

        def __call__(self, inputs, batch_size=None, caching=None, **kwargs):  # type: ignore[no-untyped-def]
            inputs_list = inputs if isinstance(inputs, list) else [inputs]
            dspy_module.last_embedding_inputs = list(inputs_list)  # type: ignore[attr-defined]
            if batch_size is not None:
                dspy_module.last_embedding_batch_size = batch_size  # type: ignore[attr-defined]
            if caching is not None:
                dspy_module.last_embedding_caching = caching  # type: ignore[attr-defined]
            if kwargs:
                merged = dict(getattr(dspy_module, "last_embedding_kwargs", {}))
                merged.update(kwargs)
                dspy_module.last_embedding_kwargs = merged  # type: ignore[attr-defined]
            vectors: List[List[float]] = []
            for text in inputs_list:
                key = str(text)
                behavior = embedding_behaviors.get(key)
                if behavior is not None:
                    vectors.append([float(value) for value in behavior.vector])
                else:
                    vectors.append([float(len(key))])
            if isinstance(inputs, list):
                return _FakeArray(vectors)
            return _FakeArray(vectors[0])

    dspy_module = types.ModuleType("dspy")
    dspy_module.LM = _DspyLM
    dspy_module.Embedder = _DspyEmbedder
    dspy_module.last_lm_model = None
    dspy_module.last_lm_kwargs = {}
    dspy_module.last_chat_messages = []
    dspy_module.last_chat_kwargs = {}
    dspy_module.last_chat_prompt = None
    dspy_module.last_embedding_model = None
    dspy_module.last_embedding_inputs = []
    dspy_module.last_embedding_kwargs = {}
    dspy_module.last_embedding_batch_size = None
    dspy_module.last_embedding_caching = None
    dspy_module.fake_tool_queue = []
    dspy_module.fake_tool_call_index = 0

    sys.modules["dspy"] = dspy_module
    context._fake_dspy_installed = True
    context._fake_dspy_original_modules = original_modules


def _install_fake_litellm_module(context) -> None:
    already_installed = getattr(context, "_fake_litellm_installed", False)
    if already_installed:
        return

    original_modules: Dict[str, object] = {}
    if "litellm" in sys.modules:
        original_modules["litellm"] = sys.modules["litellm"]

    embedding_behaviors = _ensure_fake_openai_embedding_behaviors(context)

    def embedding(*, model: str, input: Any, **kwargs) -> Dict[str, Any]:  # type: ignore[no-untyped-def]
        litellm_module.last_embedding_model = model  # type: ignore[attr-defined]
        litellm_module.last_embedding_kwargs = dict(kwargs)  # type: ignore[attr-defined]
        inputs = input if isinstance(input, list) else [input]
        litellm_module.last_embedding_inputs = list(inputs)  # type: ignore[attr-defined]
        data: List[Dict[str, Any]] = []
        for text in inputs:
            key = str(text)
            behavior = embedding_behaviors.get(key)
            if behavior is not None:
                vector = [float(value) for value in behavior.vector]
            else:
                vector = [float(len(key))]
            data.append({"embedding": vector})
        return {"data": data}

    litellm_module = types.ModuleType("litellm")
    litellm_module.embedding = embedding
    litellm_module.last_embedding_model = None
    litellm_module.last_embedding_inputs = []
    litellm_module.last_embedding_kwargs = {}

    sys.modules["litellm"] = litellm_module
    context._fake_litellm_installed = True
    context._fake_litellm_original_modules = original_modules


def _install_fake_openai_module(context) -> None:
    already_installed = getattr(context, "_fake_openai_installed", False)
    if already_installed:
        return

    _install_fake_dspy_module(context)
    _install_fake_litellm_module(context)

    original_modules: Dict[str, object] = {}
    module_names = [
        "openai",
    ]
    for name in module_names:
        if name in sys.modules:
            original_modules[name] = sys.modules[name]

    behaviors = _ensure_fake_openai_transcription_behaviors(context)
    chat_behaviors = _ensure_fake_openai_chat_behaviors(context)
    embedding_behaviors = _ensure_fake_openai_embedding_behaviors(context)

    class _TranscriptionResult:
        def __init__(self, text: str) -> None:
            self.text = text

    class _TranscriptionsApi:
        def create(self, *, file, model: str, **kwargs):  # type: ignore[no-untyped-def]
            openai_module.last_transcription_model = model  # type: ignore[attr-defined]
            openai_module.last_transcription_kwargs = dict(kwargs)  # type: ignore[attr-defined]
            filename = getattr(file, "name", "unknown")
            base_name = filename.rsplit("/", 1)[-1]
            normalized_name = base_name.split("--", 1)[-1] if "--" in base_name else base_name
            openai_module.last_transcription_filename = normalized_name  # type: ignore[attr-defined]
            behavior = behaviors.get(normalized_name)
            transcript = behavior.transcript if behavior is not None else ""
            transcript_text = str(transcript or "")
            if behavior is not None and behavior.result_kind == "dict":
                return {"text": transcript_text}
            if behavior is not None and behavior.result_kind == "verbose_json":
                if behavior.segments_payload is not None:
                    return {"text": transcript_text, "segments": behavior.segments_payload}
                probabilities = behavior.no_speech_probabilities or []
                segments = [{"no_speech_prob": float(value)} for value in probabilities]
                return {"text": transcript_text, "segments": segments}
            return _TranscriptionResult(transcript_text)

    class _AudioApi:
        def __init__(self) -> None:
            self.transcriptions = _TranscriptionsApi()

    class _ChatCompletionMessage:
        def __init__(self, content: str, tool_calls: Optional[List[Any]] = None) -> None:
            self.content = content
            self.tool_calls = tool_calls or []

    class _ChatCompletionChoice:
        def __init__(self, content: str, tool_calls: Optional[List[Any]] = None) -> None:
            self.message = _ChatCompletionMessage(content, tool_calls=tool_calls)

    class _ChatCompletionResult:
        def __init__(self, content: str, tool_calls: Optional[List[Any]] = None) -> None:
            self.choices = [_ChatCompletionChoice(content, tool_calls=tool_calls)]

    class _ToolFunction:
        def __init__(self, name: str, arguments: str) -> None:
            self.name = name
            self.arguments = arguments

    class _ToolCall:
        def __init__(self, tool_id: str, name: str, arguments: str) -> None:
            self.id = tool_id
            self.type = "function"
            self.function = _ToolFunction(name=name, arguments=arguments)

    class _ChatCompletionsApi:
        def create(self, *, model: str, messages: List[Dict[str, Any]], **kwargs):  # type: ignore[no-untyped-def]
            openai_module.last_chat_model = model  # type: ignore[attr-defined]
            openai_module.last_chat_messages = list(messages)  # type: ignore[attr-defined]
            openai_module.last_chat_kwargs = dict(kwargs)  # type: ignore[attr-defined]
            prompt_text = "\n".join(str(message.get("content", "")) for message in messages)
            openai_module.last_chat_prompt = prompt_text  # type: ignore[attr-defined]
            response_text = ""
            for behavior in chat_behaviors:
                if behavior.match_text is None or behavior.match_text in prompt_text:
                    response_text = behavior.response
                    break

            tool_queue = getattr(openai_module, "fake_tool_queue", [])
            if tool_queue:
                return _ChatCompletionResult("", tool_calls=[tool_queue.pop(0)])

            try:
                payload = json.loads(response_text)
            except json.JSONDecodeError:
                return _ChatCompletionResult(response_text)

            if not isinstance(payload, dict):
                return _ChatCompletionResult(response_text)

            operations = payload.get("operations") or []
            tool_calls: List[Any] = []
            tool_index = getattr(openai_module, "fake_tool_call_index", 0)
            for operation in operations:
                tool_index += 1
                tool_calls.append(
                    _ToolCall(
                        tool_id=f"toolu_{tool_index}",
                        name="str_replace",
                        arguments=json.dumps(
                            {
                                "old_str": operation.get("old_str", ""),
                                "new_str": operation.get("new_str", ""),
                            }
                        ),
                    )
                )
            if payload.get("done"):
                tool_index += 1
                tool_calls.append(
                    _ToolCall(
                        tool_id=f"toolu_{tool_index}",
                        name="done",
                        arguments=json.dumps({}),
                    )
                )
            openai_module.fake_tool_call_index = tool_index
            if payload.get("batch"):
                return _ChatCompletionResult("", tool_calls=tool_calls)
            openai_module.fake_tool_queue = tool_calls[1:]
            if tool_calls:
                return _ChatCompletionResult("", tool_calls=[tool_calls[0]])
            return _ChatCompletionResult(response_text)

    class _ChatApi:
        def __init__(self) -> None:
            self.completions = _ChatCompletionsApi()

    class _EmbeddingRow:
        def __init__(self, embedding: List[float]) -> None:
            self.embedding = embedding

    class _EmbeddingsResult:
        def __init__(self, vectors: List[List[float]]) -> None:
            self.data = [_EmbeddingRow(vector) for vector in vectors]

    class _EmbeddingsApi:
        def create(self, *, model: str, input: Any, **kwargs):  # type: ignore[no-untyped-def]
            openai_module.last_embedding_model = model  # type: ignore[attr-defined]
            openai_module.last_embedding_kwargs = dict(kwargs)  # type: ignore[attr-defined]
            inputs = input if isinstance(input, list) else [input]
            openai_module.last_embedding_inputs = list(inputs)  # type: ignore[attr-defined]
            vectors: List[List[float]] = []
            for text in inputs:
                key = str(text)
                behavior = embedding_behaviors.get(key)
                if behavior is not None:
                    vectors.append([float(v) for v in behavior.vector])
                else:
                    vectors.append([float(len(key))])
            return _EmbeddingsResult(vectors)

    class OpenAI:  # noqa: N801 - external dependency uses PascalCase
        def __init__(self, **kwargs):  # type: ignore[no-untyped-def]
            openai_module.last_api_key = kwargs.get("api_key")  # type: ignore[attr-defined]
            self.audio = _AudioApi()
            self.chat = _ChatApi()
            self.embeddings = _EmbeddingsApi()

    openai_module = types.ModuleType("openai")
    openai_module.OpenAI = OpenAI
    openai_module.last_api_key = None
    openai_module.last_transcription_filename = None
    openai_module.last_transcription_model = None
    openai_module.last_transcription_kwargs = {}
    openai_module.last_chat_model = None
    openai_module.last_chat_messages = []
    openai_module.last_chat_kwargs = {}
    openai_module.last_chat_prompt = None
    openai_module.fake_tool_queue = []
    openai_module.fake_tool_call_index = 0
    openai_module.last_embedding_model = None
    openai_module.last_embedding_inputs = []
    openai_module.last_embedding_kwargs = {}

    sys.modules["openai"] = openai_module

    context._fake_openai_installed = True
    context._fake_openai_original_modules = original_modules


def _install_openai_unavailable_module(context) -> None:
    already_installed = getattr(context, "_fake_openai_unavailable_installed", False)
    if already_installed:
        return

    original_modules: Dict[str, object] = {}
    module_names = [
        "openai",
    ]
    for name in module_names:
        if name in sys.modules:
            original_modules[name] = sys.modules[name]

    openai_module = types.ModuleType("openai")
    sys.modules["openai"] = openai_module

    context._fake_openai_unavailable_installed = True
    context._fake_openai_unavailable_original_modules = original_modules


def _install_dspy_unavailable_module(context) -> None:
    already_installed = getattr(context, "_fake_dspy_unavailable_installed", False)
    if already_installed:
        return

    original_modules: Dict[str, object] = {}
    for name in ["dspy", "litellm"]:
        if name in sys.modules:
            original_modules[name] = sys.modules[name]

    dspy_module = types.ModuleType("dspy")
    sys.modules["dspy"] = dspy_module

    litellm_module = types.ModuleType("litellm")
    sys.modules["litellm"] = litellm_module

    context._fake_dspy_unavailable_installed = True
    context._fake_dspy_unavailable_original_modules = original_modules


def _install_dspy_missing_module(context) -> None:
    already_installed = getattr(context, "_fake_dspy_missing_installed", False)
    if already_installed:
        return

    original_modules: Dict[str, object] = {}
    if "dspy" in sys.modules:
        original_modules["dspy"] = sys.modules["dspy"]

    sys.modules["dspy"] = None

    context._fake_dspy_missing_installed = True
    context._fake_dspy_missing_original_modules = original_modules


@given("a fake OpenAI library is available")
def step_fake_openai_available(context) -> None:
    _install_fake_openai_module(context)


@given(
    'a fake OpenAI library is available that returns chat completion "{response}" for any prompt'
)
def step_fake_openai_returns_chat_completion(context, response: str) -> None:
    _install_fake_openai_module(context)
    behaviors = _ensure_fake_openai_chat_behaviors(context)
    behaviors.append(_FakeOpenAiChatBehavior(response=response))


@given('a fake OpenAI library is available that returns chat completion "" for any prompt')
def step_fake_openai_returns_empty_chat_completion(context) -> None:
    _install_fake_openai_module(context)
    behaviors = _ensure_fake_openai_chat_behaviors(context)
    behaviors.append(_FakeOpenAiChatBehavior(response=""))


@given(
    'a fake OpenAI library is available that returns chat completion "{response}" '
    'for prompt containing "{match_text}"'
)
def step_fake_openai_returns_chat_completion_for_prompt(
    context, response: str, match_text: str
) -> None:
    _install_fake_openai_module(context)
    behaviors = _ensure_fake_openai_chat_behaviors(context)
    behaviors.append(_FakeOpenAiChatBehavior(response=response, match_text=match_text))


@given(
    'a fake OpenAI library is available that returns chat completion for prompt containing "{match_text}":'
)
def step_fake_openai_returns_chat_completion_for_prompt_block(context, match_text: str) -> None:
    _install_fake_openai_module(context)
    behaviors = _ensure_fake_openai_chat_behaviors(context)
    response_text = str(getattr(context, "text", "") or "")
    behaviors.append(_FakeOpenAiChatBehavior(response=response_text, match_text=match_text))


@given(
    'a fake OpenAI library is available that returns embedding vector "{vector}" for input text "{text}"'
)
def step_fake_openai_returns_embedding_for_text(context, vector: str, text: str) -> None:
    _install_fake_openai_module(context)
    behaviors = _ensure_fake_openai_embedding_behaviors(context)
    parsed: List[float] = []
    for token in vector.split(","):
        token = token.strip()
        if not token:
            continue
        parsed.append(float(token))
    behaviors[text] = _FakeOpenAiEmbeddingBehavior(vector=parsed)


@given(
    'a fake OpenAI library is available that returns transcript "{transcript}" for filename "{filename}"'
)
def step_fake_openai_returns_transcript(context, transcript: str, filename: str) -> None:
    _install_fake_openai_module(context)
    behaviors = _ensure_fake_openai_transcription_behaviors(context)
    behaviors[filename] = _FakeOpenAiTranscriptionBehavior(transcript=transcript)


@given(
    'a fake OpenAI library is available that returns a dict transcript "{transcript}" for filename "{filename}"'
)
def step_fake_openai_returns_dict_transcript(context, transcript: str, filename: str) -> None:
    _install_fake_openai_module(context)
    behaviors = _ensure_fake_openai_transcription_behaviors(context)
    behaviors[filename] = _FakeOpenAiTranscriptionBehavior(
        transcript=transcript, result_kind="dict"
    )


@given(
    'a fake OpenAI library is available that returns verbose transcript "{transcript}" '
    'with no speech probabilities "{no_speech_probabilities}" for filename "{filename}"'
)
def step_fake_openai_returns_verbose_json_transcript(
    context,
    transcript: str,
    no_speech_probabilities: str,
    filename: str,
) -> None:
    _install_fake_openai_module(context)
    behaviors = _ensure_fake_openai_transcription_behaviors(context)
    parsed: list[float] = []
    for token in (no_speech_probabilities or "").split(","):
        token = token.strip()
        if not token:
            continue
        parsed.append(float(token))
    behaviors[filename] = _FakeOpenAiTranscriptionBehavior(
        transcript=transcript,
        result_kind="verbose_json",
        no_speech_probabilities=parsed,
    )


@given(
    'a fake OpenAI library is available that returns verbose transcript "{transcript}" '
    'with segments "{segments_json}" for filename "{filename}"'
)
def step_fake_openai_returns_verbose_json_with_segments(
    context,
    transcript: str,
    segments_json: str,
    filename: str,
) -> None:
    _install_fake_openai_module(context)
    behaviors = _ensure_fake_openai_transcription_behaviors(context)
    parsed = json.loads(segments_json)
    if not isinstance(parsed, list):
        raise AssertionError("segments_json must decode to a list")
    behaviors[filename] = _FakeOpenAiTranscriptionBehavior(
        transcript=transcript,
        result_kind="verbose_json",
        segments_payload=parsed,
    )


@given(
    'a fake OpenAI library is available that returns verbose transcript "{transcript}" '
    'for filename "{filename}" with segments:'
)
def step_fake_openai_returns_verbose_json_with_segments_block(
    context,
    transcript: str,
    filename: str,
) -> None:
    _install_fake_openai_module(context)
    behaviors = _ensure_fake_openai_transcription_behaviors(context)
    raw = (context.text or "").strip()
    parsed = json.loads(raw)
    if not isinstance(parsed, list):
        raise AssertionError("segments JSON must decode to a list")
    behaviors[filename] = _FakeOpenAiTranscriptionBehavior(
        transcript=transcript,
        result_kind="verbose_json",
        segments_payload=parsed,
    )


@then('the OpenAI transcription request used response format "{response_format}"')
def step_openai_transcription_used_response_format(context, response_format: str) -> None:
    _ = context
    openai_module = sys.modules.get("openai")
    assert openai_module is not None
    kwargs: Dict[str, Any] = getattr(openai_module, "last_transcription_kwargs", {})
    assert kwargs.get("response_format") == response_format


@then('the DSPy chat request used response format "{response_format}"')
def step_dspy_chat_used_response_format(context, response_format: str) -> None:
    _ = context
    dspy_module = sys.modules.get("dspy")
    assert dspy_module is not None
    kwargs: Dict[str, Any] = getattr(dspy_module, "last_chat_kwargs", {})
    assert kwargs.get("response_format") == {"type": response_format}


@given("an OpenAI API key is configured for this scenario")
def step_openai_api_key_configured(context) -> None:
    extra_env = getattr(context, "extra_env", None)
    if extra_env is None:
        extra_env = {}
        context.extra_env = extra_env
    extra_env["OPENAI_API_KEY"] = "test-openai-key"


@given("the OpenAI dependency is unavailable")
def step_openai_dependency_unavailable(context) -> None:
    _install_openai_unavailable_module(context)


@given("the DSPy dependency is unavailable")
def step_dspy_dependency_unavailable(context) -> None:
    _install_dspy_unavailable_module(context)


@given("the DSPy dependency is missing")
def step_dspy_dependency_missing(context) -> None:
    _install_dspy_missing_module(context)


@given('a fake OpenAI tool call is queued for "{tool_name}"')
def step_queue_openai_tool_call(context, tool_name: str) -> None:
    _install_fake_openai_module(context)
    dspy_module = sys.modules.get("dspy")
    assert dspy_module is not None
    tool_index = getattr(dspy_module, "fake_tool_call_index", 0) + 1
    tool_call = _ManualToolCall(
        tool_id=f"toolu_{tool_index}",
        name=tool_name,
        arguments=json.dumps({}),
    )
    dspy_module.fake_tool_call_index = tool_index
    dspy_module.fake_tool_queue.append(tool_call)


@given('a fake OpenAI tool call is queued for "{tool_name}" with arguments:')
def step_queue_openai_tool_call_with_args(context, tool_name: str) -> None:
    _install_fake_openai_module(context)
    dspy_module = sys.modules.get("dspy")
    assert dspy_module is not None
    raw_args = str(getattr(context, "text", "") or "")
    parsed_args = json.loads(raw_args)
    tool_index = getattr(dspy_module, "fake_tool_call_index", 0) + 1
    tool_call = _ManualToolCall(
        tool_id=f"toolu_{tool_index}",
        name=tool_name,
        arguments=json.dumps(parsed_args),
    )
    dspy_module.fake_tool_call_index = tool_index
    dspy_module.fake_tool_queue.append(tool_call)


@given('the OpenAI client was configured with API key "{expected_api_key}"')
@then('the OpenAI client was configured with API key "{expected_api_key}"')
def step_openai_client_configured_with_api_key(context, expected_api_key: str) -> None:
    _ = context
    openai_module = sys.modules.get("openai")
    assert openai_module is not None
    configured = getattr(openai_module, "last_api_key", None)
    assert configured == expected_api_key
