# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from google.adk import models
from google.adk.models.gemma_llm import Gemma
from google.adk.models.google_llm import Gemini
from google.adk.models.llm_request import LlmRequest
from google.adk.models.llm_response import LlmResponse
from google.genai import types
from google.genai.types import Content
from google.genai.types import Part
import pytest


@pytest.fixture
def llm_request():
  return LlmRequest(
      model="gemma-3-4b-it",
      contents=[Content(role="user", parts=[Part.from_text(text="Hello")])],
      config=types.GenerateContentConfig(
          temperature=0.1,
          response_modalities=[types.Modality.TEXT],
          system_instruction="You are a helpful assistant",
      ),
  )


@pytest.fixture
def llm_request_with_duplicate_instruction():
  return LlmRequest(
      model="gemma-3-1b-it",
      contents=[
          Content(
              role="user",
              parts=[Part.from_text(text="Talk like a pirate.")],
          ),
          Content(role="user", parts=[Part.from_text(text="Hello")]),
      ],
      config=types.GenerateContentConfig(
          response_modalities=[types.Modality.TEXT],
          system_instruction="Talk like a pirate.",
      ),
  )


@pytest.fixture
def llm_request_with_tools():
  return LlmRequest(
      model="gemma-3-1b-it",
      contents=[Content(role="user", parts=[Part.from_text(text="Hello")])],
      config=types.GenerateContentConfig(
          tools=[
              types.Tool(
                  function_declarations=[
                      types.FunctionDeclaration(
                          name="search_web",
                          description="Search the web for a query.",
                          parameters=types.Schema(
                              type=types.Type.OBJECT,
                              properties={
                                  "query": types.Schema(type=types.Type.STRING)
                              },
                              required=["query"],
                          ),
                      ),
                      types.FunctionDeclaration(
                          name="get_current_time",
                          description="Gets the current time.",
                          parameters=types.Schema(
                              type=types.Type.OBJECT, properties={}
                          ),
                      ),
                  ]
              )
          ],
      ),
  )


def test_supported_models_matches_gemma4():
  """Gemma 4 model strings must resolve to the Gemini class via the registry."""
  assert models.LLMRegistry.resolve("gemma-4-31b-it") is Gemini


def test_supported_models_matches_gemma3():
  """Gemma 3 model strings must continue to resolve to the Gemma class."""
  assert models.LLMRegistry.resolve("gemma-3-27b-it") is Gemma


@pytest.mark.asyncio
async def test_not_gemma_model():
  llm = Gemma()
  llm_request_bad_model = LlmRequest(
      model="not-a-gemma-model",
  )
  with pytest.raises(AssertionError, match=r".*model.*"):
    async for _ in llm.generate_content_async(llm_request_bad_model):
      pass


@pytest.mark.asyncio
@pytest.mark.parametrize(
    "llm_request",
    ["llm_request", "llm_request_with_duplicate_instruction"],
    indirect=True,
)
async def test_preprocess_request(llm_request):
  llm = Gemma()
  want_content_text = llm_request.config.system_instruction

  await llm._preprocess_request(llm_request)

  # system instruction should be cleared
  assert not llm_request.config.system_instruction
  # should be two content bits now (deduped, if needed)
  assert len(llm_request.contents) == 2
  # first message in contents should be "user": <original sys instruction>
  assert llm_request.contents[0].role == "user"
  assert llm_request.contents[0].parts[0].text == want_content_text


@pytest.mark.asyncio
async def test_preprocess_request_with_tools(llm_request_with_tools):

  gemma = Gemma()
  await gemma._preprocess_request(llm_request_with_tools)

  assert not llm_request_with_tools.config.tools

  # The original user content should now be the second item
  assert llm_request_with_tools.contents[1].role == "user"
  assert llm_request_with_tools.contents[1].parts[0].text == "Hello"

  sys_instruct_text = llm_request_with_tools.contents[0].parts[0].text
  assert sys_instruct_text is not None
  assert "You have access to the following functions" in sys_instruct_text
  assert (
      """{"description":"Search the web for a query.","name":"search_web","""
      in sys_instruct_text
  )
  assert (
      """{"description":"Gets the current time.","name":"get_current_time","parameters":{"properties":{}"""
      in sys_instruct_text
  )


@pytest.mark.asyncio
async def test_preprocess_request_with_function_response():
  # Simulate an LlmRequest with a function response
  func_response_data = types.FunctionResponse(
      name="search_web", response={"results": [{"title": "ADK"}]}
  )
  llm_request = LlmRequest(
      model="gemma-3-1b-it",
      contents=[
          types.Content(
              role="model",
              parts=[types.Part(function_response=func_response_data)],
          )
      ],
      config=types.GenerateContentConfig(),
  )

  gemma = Gemma()
  await gemma._preprocess_request(llm_request)

  # Assertions: function response converted to user role text content
  assert llm_request.contents
  assert len(llm_request.contents) == 1
  assert llm_request.contents[0].role == "user"
  assert llm_request.contents[0].parts
  assert (
      llm_request.contents[0].parts[0].text
      == 'Invoking tool `search_web` produced: `{"results": [{"title":'
      ' "ADK"}]}`.'
  )
  assert llm_request.contents[0].parts[0].function_response is None
  assert llm_request.contents[0].parts[0].function_call is None


@pytest.mark.asyncio
async def test_preprocess_request_with_function_call():
  func_call_data = types.FunctionCall(name="get_current_time", args={})
  llm_request = LlmRequest(
      model="gemma-3-1b-it",
      contents=[
          types.Content(
              role="user", parts=[types.Part(function_call=func_call_data)]
          )
      ],
  )

  gemma = Gemma()
  await gemma._preprocess_request(llm_request)

  assert len(llm_request.contents) == 1
  assert llm_request.contents[0].role == "model"
  expected_text = func_call_data.model_dump_json(exclude_none=True)
  assert llm_request.contents[0].parts
  got_part = llm_request.contents[0].parts[0]
  assert got_part.text == expected_text
  assert got_part.function_call is None
  assert got_part.function_response is None


@pytest.mark.asyncio
async def test_preprocess_request_with_mixed_content():
  func_call = types.FunctionCall(name="get_weather", args={"city": "London"})
  func_response = types.FunctionResponse(
      name="get_weather", response={"temp": "15C"}
  )

  llm_request = LlmRequest(
      model="gemma-3-1b-it",
      contents=[
          types.Content(
              role="user", parts=[types.Part.from_text(text="Hello!")]
          ),
          types.Content(
              role="model", parts=[types.Part(function_call=func_call)]
          ),
          types.Content(
              role="some_function",
              parts=[types.Part(function_response=func_response)],
          ),
          types.Content(
              role="user", parts=[types.Part.from_text(text="How are you?")]
          ),
      ],
  )

  gemma = Gemma()
  await gemma._preprocess_request(llm_request)

  # Assertions
  assert len(llm_request.contents) == 4

  # First part: original user text
  assert llm_request.contents[0].role == "user"
  assert llm_request.contents[0].parts
  assert llm_request.contents[0].parts[0].text == "Hello!"

  # Second part: function call converted to model text
  assert llm_request.contents[1].role == "model"
  assert llm_request.contents[1].parts
  assert llm_request.contents[1].parts[0].text == func_call.model_dump_json(
      exclude_none=True
  )

  # Third part: function response converted to user text
  assert llm_request.contents[2].role == "user"
  assert llm_request.contents[2].parts
  assert (
      llm_request.contents[2].parts[0].text
      == 'Invoking tool `get_weather` produced: `{"temp": "15C"}`.'
  )

  # Fourth part: original user text
  assert llm_request.contents[3].role == "user"
  assert llm_request.contents[3].parts
  assert llm_request.contents[3].parts[0].text == "How are you?"


def test_process_response():
  # Simulate a response from Gemma that should be converted to a FunctionCall
  json_function_call_str = (
      '{"name": "search_web", "parameters": {"query": "latest news"}}'
  )
  llm_response = LlmResponse(
      content=Content(
          role="model", parts=[Part.from_text(text=json_function_call_str)]
      )
  )

  gemma = Gemma()
  gemma._extract_function_calls_from_response(llm_response=llm_response)

  # Assert that the content was transformed into a FunctionCall
  assert llm_response.content
  assert llm_response.content.parts
  assert len(llm_response.content.parts) == 1
  part = llm_response.content.parts[0]
  assert part.function_call is not None
  assert part.function_call.name == "search_web"
  assert part.function_call.args == {"query": "latest news"}
  # Assert that the entire part matches the expected structure
  expected_function_call = types.FunctionCall(
      name="search_web", args={"query": "latest news"}
  )
  expected_part = Part(function_call=expected_function_call)
  assert part == expected_part
  assert part.text is None  # Ensure text part is cleared


def test_process_response_invalid_json_text():
  # Simulate a response with plain text that is not JSON
  original_text = "This is a regular text response."
  llm_response = LlmResponse(
      content=Content(role="model", parts=[Part.from_text(text=original_text)])
  )

  gemma = Gemma()
  gemma._extract_function_calls_from_response(llm_response=llm_response)

  # Assert that the content remains unchanged
  assert llm_response.content
  assert llm_response.content.parts
  assert len(llm_response.content.parts) == 1
  assert llm_response.content.parts[0].text == original_text
  assert llm_response.content.parts[0].function_call is None


def test_process_response_malformed_json():
  # Simulate a response with valid JSON but not in the function call format
  malformed_json_str = '{"not_a_function": "value", "another_field": 123}'
  llm_response = LlmResponse(
      content=Content(
          role="model", parts=[Part.from_text(text=malformed_json_str)]
      )
  )
  gemma = Gemma()
  gemma._extract_function_calls_from_response(llm_response=llm_response)

  # Assert that the content remains unchanged because it doesn't match the expected schema
  assert llm_response.content
  assert llm_response.content.parts
  assert len(llm_response.content.parts) == 1
  assert llm_response.content.parts[0].text == malformed_json_str
  assert llm_response.content.parts[0].function_call is None


def test_process_response_empty_content_or_multiple_parts():
  gemma = Gemma()

  # Test case 1: LlmResponse with no content
  llm_response_no_content = LlmResponse(content=None)
  gemma._extract_function_calls_from_response(
      llm_response=llm_response_no_content
  )
  assert llm_response_no_content.content is None

  # Test case 2: LlmResponse with empty parts list
  llm_response_empty_parts = LlmResponse(
      content=Content(role="model", parts=[])
  )
  gemma._extract_function_calls_from_response(
      llm_response=llm_response_empty_parts
  )
  assert llm_response_empty_parts.content
  assert not llm_response_empty_parts.content.parts

  # Test case 3: LlmResponse with multiple parts
  llm_response_multiple_parts = LlmResponse(
      content=Content(
          role="model",
          parts=[
              Part.from_text(text="part one"),
              Part.from_text(text="part two"),
          ],
      )
  )
  original_parts = list(
      llm_response_multiple_parts.content.parts
  )  # Copy for comparison
  gemma._extract_function_calls_from_response(
      llm_response=llm_response_multiple_parts
  )
  assert llm_response_multiple_parts.content
  assert (
      llm_response_multiple_parts.content.parts == original_parts
  )  # Should remain unchanged

  # Test case 4: LlmResponse with one part, but empty text
  llm_response_empty_text_part = LlmResponse(
      content=Content(role="model", parts=[Part.from_text(text="")])
  )
  gemma._extract_function_calls_from_response(
      llm_response=llm_response_empty_text_part
  )
  assert llm_response_empty_text_part.content
  assert llm_response_empty_text_part.content.parts
  assert llm_response_empty_text_part.content.parts[0].text == ""
  assert llm_response_empty_text_part.content.parts[0].function_call is None


def test_process_response_with_markdown_json_block():
  # Simulate a response from Gemma with a JSON function call in a markdown block
  json_function_call_str = """
```json
{"name": "search_web", "parameters": {"query": "latest news"}}
```"""
  llm_response = LlmResponse(
      content=Content(
          role="model", parts=[Part.from_text(text=json_function_call_str)]
      )
  )

  gemma = Gemma()
  gemma._extract_function_calls_from_response(llm_response)

  assert llm_response.content
  assert llm_response.content.parts
  assert len(llm_response.content.parts) == 1
  part = llm_response.content.parts[0]
  assert part.function_call is not None
  assert part.function_call.name == "search_web"
  assert part.function_call.args == {"query": "latest news"}
  assert part.text is None


def test_process_response_with_markdown_tool_code_block():
  # Simulate a response from Gemma with a JSON function call in a 'tool_code' markdown block
  json_function_call_str = """
Some text before.
```tool_code
{"name": "get_current_time", "parameters": {}}
```
And some text after."""
  llm_response = LlmResponse(
      content=Content(
          role="model", parts=[Part.from_text(text=json_function_call_str)]
      )
  )

  gemma = Gemma()
  gemma._extract_function_calls_from_response(llm_response)

  assert llm_response.content
  assert llm_response.content.parts
  assert len(llm_response.content.parts) == 1
  part = llm_response.content.parts[0]
  assert part.function_call is not None
  assert part.function_call.name == "get_current_time"
  assert part.function_call.args == {}
  assert part.text is None


def test_process_response_with_embedded_json():
  # Simulate a response with valid JSON embedded in text
  embedded_json_str = (
      'Please call the tool: {"name": "search_web", "parameters": {"query":'
      ' "new features"}} thanks!'
  )
  llm_response = LlmResponse(
      content=Content(
          role="model", parts=[Part.from_text(text=embedded_json_str)]
      )
  )

  gemma = Gemma()
  gemma._extract_function_calls_from_response(llm_response)

  assert llm_response.content
  assert llm_response.content.parts
  assert len(llm_response.content.parts) == 1
  part = llm_response.content.parts[0]
  assert part.function_call is not None
  assert part.function_call.name == "search_web"
  assert part.function_call.args == {"query": "new features"}
  assert part.text is None


def test_process_response_flexible_parsing():
  # Test with "function" and "args" keys as supported by GemmaFunctionCallModel
  flexible_json_str = '{"function": "do_something", "args": {"value": 123}}'
  llm_response = LlmResponse(
      content=Content(
          role="model", parts=[Part.from_text(text=flexible_json_str)]
      )
  )

  gemma = Gemma()
  gemma._extract_function_calls_from_response(llm_response)

  assert llm_response.content
  assert llm_response.content.parts
  assert len(llm_response.content.parts) == 1
  part = llm_response.content.parts[0]
  assert part.function_call is not None
  assert part.function_call.name == "do_something"
  assert part.function_call.args == {"value": 123}
  assert part.text is None


def test_process_response_last_json_object():
  # Simulate a response with multiple JSON objects, ensuring the last valid one is picked
  multiple_json_str = (
      'I thought about {"name": "first_call", "parameters": {"a": 1}} but then'
      ' decided to call: {"name": "second_call", "parameters": {"b": 2}}'
  )
  llm_response = LlmResponse(
      content=Content(
          role="model", parts=[Part.from_text(text=multiple_json_str)]
      )
  )

  gemma = Gemma()
  gemma._extract_function_calls_from_response(llm_response)

  assert llm_response.content
  assert llm_response.content.parts
  assert len(llm_response.content.parts) == 1
  part = llm_response.content.parts[0]
  assert part.function_call is not None
  assert part.function_call.name == "second_call"
  assert part.function_call.args == {"b": 2}
  assert part.text is None


# Tests for Gemma 4 registry routing
def test_gemma4_resolves_to_gemini_not_gemma():
  """Gemma 4 models should resolve to Gemini, not the Gemma workaround class."""
  resolved = models.LLMRegistry.resolve("gemma-4-31b-it")
  assert resolved is not Gemma
  assert resolved is Gemini


# Tests for Gemma3Ollama (only run when LiteLLM is installed)
try:
  from google.adk.models.gemma_llm import Gemma3Ollama
  from google.adk.models.lite_llm import LiteLlm

  def test_gemma3_ollama_supported_models():
    assert Gemma3Ollama.supported_models() == [r"ollama/gemma3.*"]

  def test_gemma3_ollama_registry_resolution():
    assert models.LLMRegistry.resolve("ollama/gemma3:12b") is Gemma3Ollama

  def test_non_gemma_ollama_registry_resolution():
    assert models.LLMRegistry.resolve("ollama/llama3.2") is LiteLlm

  @pytest.mark.parametrize(
      "model_arg,expected_model",
      [
          (None, "ollama/gemma3:12b"),
          ("ollama/gemma3:27b", "ollama/gemma3:27b"),
      ],
  )
  def test_gemma3_ollama_model(model_arg, expected_model):
    model = (
        Gemma3Ollama() if model_arg is None else Gemma3Ollama(model=model_arg)
    )
    assert model.model == expected_model

except ImportError:
  # LiteLLM not installed, skip Gemma3Ollama tests
  pass
