# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0

"""Unit tests for OpenAI Responses API image_generation tool wiring.

The tool is a server-side Responses-API tool (``{type: "image_generation"}``);
the result comes back as an ``image_generation_call`` output item, which Studio
translates into ``_toolEvent`` chunks so the chat adapter renders it inline.
Tests pin: the tool is added to the body only on a cloud OpenAI base when asked
for, the done event produces the expected chunks, and non-cloud bases drop it.
"""

import asyncio
import json

import httpx

from core.inference import external_provider as ep_mod
from core.inference.external_provider import ExternalProviderClient


def _drive(coro):
    return asyncio.new_event_loop().run_until_complete(coro)


def _capture_body(monkeypatch, *, base_url: str, enabled_tools) -> dict:
    captured: dict = {}

    def handler(request: httpx.Request) -> httpx.Response:
        captured["body"] = json.loads(request.content.decode("utf-8"))
        return httpx.Response(
            200,
            content = (
                b"event: response.completed\n"
                b'data: {"type":"response.completed",'
                b'"response":{"output":[],"usage":{"input_tokens":0,'
                b'"output_tokens":0}}}\n\n'
            ),
            headers = {"content-type": "text/event-stream"},
        )

    monkeypatch.setattr(
        ep_mod,
        "_http_client",
        httpx.AsyncClient(transport = httpx.MockTransport(handler)),
    )

    async def run():
        client = ExternalProviderClient(
            provider_type = "openai",
            base_url = base_url,
            api_key = "sk-test",
        )
        async for _ in client.stream_chat_completion(
            messages = [{"role": "user", "content": "draw a cat"}],
            model = "gpt-5.5",
            temperature = 0.7,
            top_p = 0.95,
            max_tokens = 32,
            reasoning_effort = "medium",
            enabled_tools = enabled_tools,
        ):
            pass
        await client.close()

    _drive(run())
    return captured


def _collect_tool_events(monkeypatch) -> list[dict]:
    """Drive a Responses stream with one image_generation_call done event and
    return the parsed _toolEvent chunks."""

    sse = (
        b"event: response.output_item.done\n"
        b'data: {"type":"response.output_item.done",'
        b'"item":{"type":"image_generation_call",'
        b'"id":"img_abc",'
        b'"revised_prompt":"A photorealistic cat sitting",'
        b'"result":"AAAA",'
        b'"output_format":"png",'
        b'"size":"1024x1024",'
        b'"quality":"high",'
        b'"background":"opaque"}}\n\n'
        b"event: response.completed\n"
        b'data: {"type":"response.completed",'
        b'"response":{"output":[],"usage":{"input_tokens":0,'
        b'"output_tokens":0}}}\n\n'
    )

    def handler(request: httpx.Request) -> httpx.Response:
        return httpx.Response(
            200,
            content = sse,
            headers = {"content-type": "text/event-stream"},
        )

    monkeypatch.setattr(
        ep_mod,
        "_http_client",
        httpx.AsyncClient(transport = httpx.MockTransport(handler)),
    )

    events: list[dict] = []

    async def run():
        client = ExternalProviderClient(
            provider_type = "openai",
            base_url = "https://api.openai.com/v1",
            api_key = "sk-test",
        )
        async for line in client.stream_chat_completion(
            messages = [{"role": "user", "content": "draw a cat"}],
            model = "gpt-5.5",
            temperature = 0.7,
            top_p = 0.95,
            max_tokens = 32,
            reasoning_effort = "medium",
            enabled_tools = ["image_generation"],
        ):
            if not line or not line.startswith("data:"):
                continue
            payload = line[5:].strip()
            if payload == "[DONE]":
                continue
            try:
                obj = json.loads(payload)
            except json.JSONDecodeError:
                continue
            if "_toolEvent" in obj:
                events.append(obj["_toolEvent"])
        await client.close()

    _drive(run())
    return events


# ── tool entry appended to outbound body on cloud OpenAI ─────────────


def test_cloud_openai_appends_image_generation_tool(monkeypatch):
    captured = _capture_body(
        monkeypatch,
        base_url = "https://api.openai.com/v1",
        enabled_tools = ["image_generation"],
    )
    tools = captured["body"].get("tools") or []
    assert {"type": "image_generation"} in tools, tools


def test_combined_with_web_search_and_code_execution(monkeypatch):
    captured = _capture_body(
        monkeypatch,
        base_url = "https://api.openai.com/v1",
        enabled_tools = ["web_search", "code_execution", "image_generation"],
    )
    tools = captured["body"].get("tools") or []
    tool_types = {t["type"] for t in tools if isinstance(t, dict)}
    assert tool_types == {"web_search", "shell", "image_generation"}, tools


# ── non-cloud base silently drops the tool ──────────────────────────


def test_non_cloud_base_drops_image_generation(monkeypatch):
    captured = _capture_body(
        monkeypatch,
        base_url = "http://127.0.0.1:11434/v1",
        enabled_tools = ["image_generation"],
    )
    tools = captured["body"].get("tools") or []
    assert {"type": "image_generation"} not in tools, tools


# ── omitted pill leaves body untouched ──────────────────────────────


def test_omitted_image_generation_pill_no_tool(monkeypatch):
    captured = _capture_body(
        monkeypatch,
        base_url = "https://api.openai.com/v1",
        enabled_tools = ["web_search"],
    )
    tools = captured["body"].get("tools") or []
    assert all(t.get("type") != "image_generation" for t in tools)


# ── output translation surfaces tool_start + tool_end ────────────────


def test_image_generation_done_emits_tool_event_chunks(monkeypatch):
    events = _collect_tool_events(monkeypatch)
    image_events = [
        e
        for e in events
        if e.get("tool_name") == "image_generation"
        or (e.get("type") == "tool_end" and e.get("image_b64"))
    ]
    starts = [e for e in image_events if e.get("type") == "tool_start"]
    ends = [e for e in image_events if e.get("type") == "tool_end"]
    assert len(starts) == 1, image_events
    assert len(ends) == 1, image_events
    # `_server_tool: True` marks this as a provider-side synthetic tool card
    # for the frontend's history serializer.
    assert starts[0]["arguments"] == {
        "kind": "image",
        "prompt": "A photorealistic cat sitting",
        "_server_tool": True,
        "openai_image_generation_call_id": "img_abc",
    }
    assert ends[0]["image_b64"] == "AAAA"
    assert ends[0]["image_mime"] == "image/png"
    assert ends[0]["size"] == "1024x1024"
    assert ends[0]["quality"] == "high"
    assert ends[0]["background"] == "opaque"
