# 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.

"""Unit tests for LlmAgent output saving functionality."""

import logging
from unittest.mock import patch

from google.adk.agents.callback_context import CallbackContext
from google.adk.agents.llm_agent import LlmAgent
from google.adk.events.event import Event
from google.adk.events.event_actions import EventActions
from google.genai import types
from pydantic import BaseModel
import pytest

from .. import testing_utils


class MockOutputSchema(BaseModel):
  message: str
  confidence: float


def create_test_event(
    author: str = "test_agent",
    content_text: str = "Hello world",
    is_final: bool = True,
    invocation_id: str = "test_invocation",
) -> Event:
  """Helper to create test events."""
  # Create mock content
  parts = [types.Part.from_text(text=content_text)] if content_text else []
  content = types.Content(role="model", parts=parts) if parts else None

  # Create event
  event = Event(
      invocation_id=invocation_id,
      author=author,
      content=content,
      actions=EventActions(),
  )

  # Mock is_final_response if needed
  if not is_final:
    event.partial = True

  return event


class TestLlmAgentOutputSave:
  """Test suite for LlmAgent output saving functionality."""

  def test_maybe_save_output_to_state_skips_different_author(self, caplog):
    """Test that output is not saved when event author differs from agent name."""
    # Set the LlmAgent logger to DEBUG level
    llm_agent_logger = logging.getLogger(
        "google_adk.google.adk.agents.llm_agent"
    )
    original_level = llm_agent_logger.level
    llm_agent_logger.setLevel(logging.DEBUG)

    try:
      agent = LlmAgent(name="agent_a", output_key="result")
      event = create_test_event(
          author="agent_b", content_text="Response from B"
      )

      with caplog.at_level("DEBUG"):
        agent._LlmAgent__maybe_save_output_to_state(event)

      # Should not add anything to state_delta
      assert len(event.actions.state_delta) == 0

      # Should log the skip
      assert (
          "Skipping output save for agent agent_a: event authored by agent_b"
          in caplog.text
      )
    finally:
      # Restore original logger level
      llm_agent_logger.setLevel(original_level)

  def test_maybe_save_output_to_state_saves_same_author(self):
    """Test that output is saved when event author matches agent name."""
    agent = LlmAgent(name="test_agent", output_key="result")
    event = create_test_event(author="test_agent", content_text="Test response")

    agent._LlmAgent__maybe_save_output_to_state(event)

    # Should save to state_delta
    assert event.actions.state_delta["result"] == "Test response"

  def test_maybe_save_output_to_state_no_output_key(self):
    """Test that nothing is saved when output_key is not set."""
    agent = LlmAgent(name="test_agent")  # No output_key
    event = create_test_event(author="test_agent", content_text="Test response")

    agent._LlmAgent__maybe_save_output_to_state(event)

    # Should not save anything
    assert len(event.actions.state_delta) == 0

  def test_maybe_save_output_to_state_not_final_response(self):
    """Test that output is not saved for non-final responses."""
    agent = LlmAgent(name="test_agent", output_key="result")
    event = create_test_event(
        author="test_agent", content_text="Partial response", is_final=False
    )

    agent._LlmAgent__maybe_save_output_to_state(event)

    # Should not save partial responses
    assert len(event.actions.state_delta) == 0

  def test_maybe_save_output_to_state_no_content(self):
    """Test that nothing is saved when event has no content."""
    agent = LlmAgent(name="test_agent", output_key="result")
    event = create_test_event(author="test_agent", content_text="")

    agent._LlmAgent__maybe_save_output_to_state(event)

    # Should not save empty content
    assert len(event.actions.state_delta) == 0

  def test_maybe_save_output_to_state_with_output_schema(self):
    """Test that output is processed with schema when output_schema is set."""
    agent = LlmAgent(
        name="test_agent", output_key="result", output_schema=MockOutputSchema
    )

    # Create event with JSON content
    json_content = '{"message": "Hello", "confidence": 0.95}'
    event = create_test_event(author="test_agent", content_text=json_content)

    agent._LlmAgent__maybe_save_output_to_state(event)

    # Should save parsed and validated output
    expected_output = {"message": "Hello", "confidence": 0.95}
    assert event.actions.state_delta["result"] == expected_output

  def test_maybe_save_output_to_state_multiple_parts(self):
    """Test that multiple text parts are concatenated."""
    agent = LlmAgent(name="test_agent", output_key="result")

    # Create event with multiple text parts
    parts = [
        types.Part.from_text(text="Hello "),
        types.Part.from_text(text="world"),
        types.Part.from_text(text="!"),
    ]
    content = types.Content(role="model", parts=parts)

    event = Event(
        invocation_id="test_invocation",
        author="test_agent",
        content=content,
        actions=EventActions(),
    )

    agent._LlmAgent__maybe_save_output_to_state(event)

    # Should concatenate all text parts
    assert event.actions.state_delta["result"] == "Hello world!"

  def test_maybe_save_output_to_state_agent_transfer_scenario(self, caplog):
    """Test realistic agent transfer scenario."""
    # Scenario: Agent A transfers to Agent B, Agent B produces output
    # Agent A should not save Agent B's output

    # Set the LlmAgent logger to DEBUG level
    llm_agent_logger = logging.getLogger(
        "google_adk.google.adk.agents.llm_agent"
    )
    original_level = llm_agent_logger.level
    llm_agent_logger.setLevel(logging.DEBUG)

    try:
      agent_a = LlmAgent(name="support_agent", output_key="support_result")
      agent_b_event = create_test_event(
          author="billing_agent", content_text="Your bill is $100"
      )

      with caplog.at_level("DEBUG"):
        agent_a._LlmAgent__maybe_save_output_to_state(agent_b_event)

      # Agent A should not save Agent B's output
      assert len(agent_b_event.actions.state_delta) == 0
      assert (
          "Skipping output save for agent support_agent: event authored by"
          " billing_agent"
          in caplog.text
      )
    finally:
      # Restore original logger level
      llm_agent_logger.setLevel(original_level)

  def test_maybe_save_output_to_state_case_sensitive_names(self, caplog):
    """Test that agent name comparison is case-sensitive."""
    # Set the LlmAgent logger to DEBUG level
    llm_agent_logger = logging.getLogger(
        "google_adk.google.adk.agents.llm_agent"
    )
    original_level = llm_agent_logger.level
    llm_agent_logger.setLevel(logging.DEBUG)

    try:
      agent = LlmAgent(name="TestAgent", output_key="result")
      event = create_test_event(
          author="testagent", content_text="Test response"
      )

      with caplog.at_level("DEBUG"):
        agent._LlmAgent__maybe_save_output_to_state(event)

      # Should not save due to case mismatch
      assert len(event.actions.state_delta) == 0
      assert (
          "Skipping output save for agent TestAgent: event authored by"
          " testagent"
          in caplog.text
      )
    finally:
      # Restore original logger level
      llm_agent_logger.setLevel(original_level)

  @patch("google.adk.agents.llm_agent.logger")
  def test_maybe_save_output_to_state_logging(self, mock_logger):
    """Test that debug logging works correctly."""
    agent = LlmAgent(name="agent1", output_key="result")
    event = create_test_event(author="agent2", content_text="Test response")

    agent._LlmAgent__maybe_save_output_to_state(event)

    # Should call logger.debug with correct parameters
    mock_logger.debug.assert_called_once_with(
        "Skipping output save for agent %s: event authored by %s",
        "agent1",
        "agent2",
    )

  @pytest.mark.parametrize("empty_content", ["", "  ", "\n"])
  def test_maybe_save_output_to_state_handles_empty_final_chunk_with_schema(
      self, empty_content
  ):
    """Tests that the agent correctly handles an empty final streaming chunk

    when an output_schema is specified, preventing a crash.
    """
    # ARRANGE: Create an agent that expects a JSON output matching a schema.
    agent = LlmAgent(
        name="test_agent", output_key="result", output_schema=MockOutputSchema
    )

    # ARRANGE: Create a final event with empty or whitespace-only content.
    # This simulates the final, empty chunk from a model's streaming response.
    event = create_test_event(
        author="test_agent", content_text=empty_content, is_final=True
    )

    # ACT: Call the method. The test's primary goal is to ensure this
    # does NOT raise a pydantic.ValidationError, which it would have before the fix.
    try:
      agent._LlmAgent__maybe_save_output_to_state(event)
    except Exception as e:
      pytest.fail(f"The method unexpectedly raised an exception: {e}")

    # ASSERT: Because the method should return early, the state_delta
    # should remain empty.
    assert len(event.actions.state_delta) == 0

  @pytest.mark.asyncio
  async def test_output_key_saved_when_before_agent_callback_short_circuits(
      self,
  ):
    """Test that output_key is written to session state when
    before_agent_callback short-circuits the agent."""

    def cache_callback(callback_context: CallbackContext) -> types.Content:
      return types.Content(parts=[types.Part.from_text(text="cached answer")])

    agent = LlmAgent(
        name="test_agent",
        output_key="result",
        before_agent_callback=cache_callback,
    )

    runner = testing_utils.InMemoryRunner(agent)
    await runner.run_async("hello")

    assert runner.session.state.get("result") == "cached answer"

  def test_maybe_save_output_to_state_skips_function_response_only_event(self):
    """Test that state_delta set by callback is not overwritten when event

    only has function_response parts and no text.
    """
    agent = LlmAgent(name="test_agent", output_key="result")

    # Simulate a function_response-only event (no text parts)
    parts = [
        types.Part(
            function_response=types.FunctionResponse(
                name="my_tool",
                response={"status": "success", "data": [1, 2, 3]},
            )
        )
    ]
    content = types.Content(role="user", parts=parts)

    event = Event(
        invocation_id="test_invocation",
        author="test_agent",
        content=content,
        actions=EventActions(
            skip_summarization=True,
            state_delta={"result": [1, 2, 3]},
        ),
    )

    agent._LlmAgent__maybe_save_output_to_state(event)

    # The callback-set value should be preserved, not overwritten with ""
    assert event.actions.state_delta["result"] == [1, 2, 3]

  def test_maybe_save_output_to_state_saves_empty_string_when_text_is_empty(
      self,
  ):
    """Test that output is saved as empty string when part.text is explicitly empty."""
    agent = LlmAgent(name="test_agent", output_key="result")

    # Explicitly construct a part with empty string text
    parts = [types.Part(text="")]
    content = types.Content(role="model", parts=parts)
    event = Event(
        invocation_id="test_invocation",
        author="test_agent",
        content=content,
        actions=EventActions(),
    )

    agent._LlmAgent__maybe_save_output_to_state(event)

    # Assert key exists and value is empty string
    assert "result" in event.actions.state_delta
    assert not event.actions.state_delta["result"]
