import json
from unittest.mock import Mock, patch

from api.llm.google_client import (
    GoogleGeminiClient,
    _openai_tools_to_gemini_payload,
    _sanitize_tool_params_for_gemini,
)
from api.services.message_service import MessageService
from api.models import CompleteRequest


def test_sanitize_tool_params_adds_file_type_hint_for_grep_type_param():
    """Grep (and similar) 'type' param gets hint so model does not pass JSON type names like object."""
    params = {
        "type": "object",
        "properties": {
            "pattern": {"type": "string"},
            "type": {"type": "string", "description": "Filter by file type"},
        },
        "required": ["pattern"],
    }
    out = _sanitize_tool_params_for_gemini(params, "Grep")
    assert out["properties"]["type"]["description"] == (
        "Filter by file type"
        " File type = extension only (e.g. py, ts, js). Do NOT use JSON type names: object, string, array, number, integer, boolean."
    )
    # Other params unchanged
    assert out["properties"]["pattern"] == {"type": "string"}


def test_openai_tools_translate_to_gemini_parameters_json_schema():
    payload = _openai_tools_to_gemini_payload(
        openai_tools=[
            {
                "type": "function",
                "function": {
                    "name": "Shell",
                    "description": "Run a shell command",
                    "parameters": {
                        "type": "object",
                        "properties": {"command": {"type": "string"}},
                        "required": ["command"],
                        "additionalProperties": False,
                    },
                },
            }
        ],
        tool_choice="auto",
    )

    decls = payload["tools"][0]["functionDeclarations"]
    assert decls[0]["name"] == "Shell"
    assert "parametersJsonSchema" in decls[0]
    assert "parameters" not in decls[0]


def test_message_service_google_encodes_tool_calls_and_results_as_parts():
    svc = MessageService()
    msgs = [
        CompleteRequest.Message(role="system", content="You are Cursor."),
        CompleteRequest.Message(
            role="assistant",
            content=None,
            tool_calls=[
                {
                    "id": "call_1",
                    "type": "function",
                    "function": {"name": "Read", "arguments": json.dumps({"path": "README.md"})},
                }
            ],
        ),
        CompleteRequest.Message(role="tool", content="file contents", tool_call_id="call_1"),
    ]

    out = svc.translate_to_provider(msgs, "google")
    assert out.system and "You are Cursor." in out.system
    assert isinstance(out.messages, list)
    assert out.messages[0]["role"] == "model"
    parts0 = out.messages[0]["parts"]
    assert any("functionCall" in p and p["functionCall"]["name"] == "Read" for p in parts0)

    assert out.messages[1]["role"] == "user"
    parts1 = out.messages[1]["parts"]
    fr = parts1[0]["functionResponse"]
    assert fr["name"] == "Read"
    assert isinstance(fr["response"], dict)


def test_gemini_stream_emits_tool_calls_once_and_finishes_with_tool_calls_reason():
    # Simulate Gemini SSE frames: one functionCall, then finishReason STOP.
    frames = [
        'data: {"candidates":[{"content":{"parts":[{"functionCall":{"name":"Shell","args":{"command":"echo hi"}}}]}}]}\n',
        'data: {"candidates":[{"finishReason":"STOP","content":{"parts":[]}}],"usageMetadata":{"promptTokenCount":1,"candidatesTokenCount":1,"totalTokenCount":2}}\n',
    ]

    class FakeResp:
        status_code = 200
        ok = True
        headers = {}
        text = ""
        content = b"x"

        def iter_lines(self, decode_unicode=True):
            for f in frames:
                yield f if decode_unicode else f.encode("utf-8")

        def close(self):
            return None

    client = GoogleGeminiClient()
    extra = {
        "tools": [
            {
                "type": "function",
                "function": {
                    "name": "Shell",
                    "description": "Run",
                    "parameters": {"type": "object", "properties": {"command": {"type": "string"}}},
                },
            }
        ],
        "tool_choice": "auto",
    }

    with patch("api.llm.google_client.requests.post", return_value=FakeResp()) as p:
        events = list(
            client.stream_generate_content(
                model="gemini-2.0-flash",
                api_key="fake",
                messages=[{"role": "user", "content": "run a cmd"}],
                extra_payload=extra,
                system_prompt="Cursor system",
                use_enhanced_system_prompt=True,
            )
        )

    # Ensure we sent both system_prompt + enhanced prompt combined.
    sent_payload = p.call_args[1]["json"]
    assert "systemInstruction" in sent_payload
    sys_text = sent_payload["systemInstruction"]["parts"][0]["text"]
    assert "Cursor system" in sys_text
    assert "CRITICAL BEHAVIOR FOR TOOL USE" in sys_text

    tool_events = [e for e in events if e[3]]
    assert len(tool_events) == 1
    tool_calls = tool_events[0][3]
    assert tool_calls[0]["type"] == "function"
    assert tool_calls[0]["function"]["name"] == "Shell"
    assert "index" in tool_calls[0]

    # Final finish reason should be tool_calls.
    end_events = [e for e in events if e[1]]
    assert end_events[-1][1] == "tool_calls"


def test_gemini_stream_parses_multiline_sse_events_without_dropping():
    # Simulate SSE with explicit event boundaries and multiline data framing.
    frames = [
        "event: message",
        'data: {"candidates":[{"content":{"parts":[{"text":"Hello "},',
        'data: {"functionCall":{"name":"Shell","args":{"command":"echo hi"}}}]}}]}',
        "",
        'data: {"candidates":[{"content":{"parts":[{"text":"world"}]},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":1,"candidatesTokenCount":2,"totalTokenCount":3}}',
        "",
    ]

    class FakeResp:
        status_code = 200
        ok = True
        headers = {}
        text = ""
        content = b"x"

        def iter_lines(self, decode_unicode=True):
            for f in frames:
                yield f if decode_unicode else f.encode("utf-8")

        def close(self):
            return None

    client = GoogleGeminiClient()
    extra = {
        "tools": [
            {
                "type": "function",
                "function": {
                    "name": "Shell",
                    "description": "Run",
                    "parameters": {"type": "object", "properties": {"command": {"type": "string"}}},
                },
            }
        ],
        "tool_choice": "auto",
    }

    with patch("api.llm.google_client.requests.post", return_value=FakeResp()):
        events = list(
            client.stream_generate_content(
                model="gemini-2.0-flash",
                api_key="fake",
                messages=[{"role": "user", "content": "run a cmd"}],
                extra_payload=extra,
                system_prompt="Cursor system",
                use_enhanced_system_prompt=True,
            )
        )

    text = "".join([e[0] for e in events if e[0]])
    assert text == "Hello world"
    tool_events = [e for e in events if e[3]]
    assert len(tool_events) == 1
    end_events = [e for e in events if e[1]]
    assert end_events[-1][1] == "tool_calls"


def test_gemini_stream_prefers_stop_when_text_follows_tool_call():
    # If provider emits text after a tool-call delta in the same stream, final reason
    # should be treated as text completion, not forced tool_calls.
    frames = [
        'data: {"candidates":[{"content":{"parts":[{"functionCall":{"name":"Shell","args":{"command":"echo hi"}}}]}}]}\n',
        'data: {"candidates":[{"content":{"parts":[{"text":"Final answer after tool."}]},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":1,"candidatesTokenCount":4,"totalTokenCount":5}}\n',
    ]

    class FakeResp:
        status_code = 200
        ok = True
        headers = {}
        text = ""
        content = b"x"

        def iter_lines(self, decode_unicode=True):
            for f in frames:
                yield f if decode_unicode else f.encode("utf-8")

        def close(self):
            return None

    client = GoogleGeminiClient()
    extra = {
        "tools": [
            {
                "type": "function",
                "function": {
                    "name": "Shell",
                    "description": "Run",
                    "parameters": {"type": "object", "properties": {"command": {"type": "string"}}},
                },
            }
        ],
        "tool_choice": "auto",
    }

    with patch("api.llm.google_client.requests.post", return_value=FakeResp()):
        events = list(
            client.stream_generate_content(
                model="gemini-2.0-flash",
                api_key="fake",
                messages=[{"role": "user", "content": "run a cmd"}],
                extra_payload=extra,
                system_prompt="Cursor system",
                use_enhanced_system_prompt=True,
            )
        )

    text = "".join([e[0] for e in events if e[0]])
    assert text == "Final answer after tool."
    end_events = [e for e in events if e[1]]
    assert end_events[-1][1] == "stop"

