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

"""Main script for testing the Interactions API integration.

This script tests the following features:
1. Basic text generation
2. Google Search tool (via bypass_multi_tools_limit)
3. Multi-turn conversations with stateful interactions
4. Google Search tool (additional coverage)
5. Custom function tool (get_current_weather)

NOTE: The Interactions API does NOT support mixing custom function calling tools
with built-in tools. To work around this, we use bypass_multi_tools_limit=True
on GoogleSearchTool, which converts it to a function calling tool (via
GoogleSearchAgentTool). The bypass only triggers when len(agent.tools) > 1,
so we include both GoogleSearchTool and get_current_weather in the agent.

NOTE: Code execution via UnsafeLocalCodeExecutor is not compatible with function
calling mode because the model tries to call a function instead of outputting
code in markdown.

Run with:
  cd contributing/samples
  python -m interactions_api_test.main
"""

import argparse
import asyncio
import logging
from pathlib import Path
import time
from typing import Optional

from dotenv import load_dotenv
from google.adk.agents.run_config import RunConfig
from google.adk.cli.utils import logs
from google.adk.runners import InMemoryRunner
from google.adk.runners import Runner
from google.genai import types

from .agent import root_agent

# Load .env from the samples directory (parent of this module's directory)
_env_path = Path(__file__).parent.parent / ".env"
load_dotenv(_env_path)

APP_NAME = "interactions_api_test_app"
USER_ID = "test_user"


async def call_agent_async(
    runner: Runner,
    user_id: str,
    session_id: str,
    prompt: str,
    agent_name: str = "",
    show_interaction_id: bool = True,
) -> tuple[str, Optional[str]]:
  """Call the agent asynchronously with the user's prompt.

  Args:
    runner: The agent runner
    user_id: The user ID
    session_id: The session ID
    prompt: The prompt to send
    agent_name: The expected agent name for filtering responses
    show_interaction_id: Whether to show interaction IDs in output

  Returns:
    A tuple of (response_text, interaction_id)
  """
  content = types.Content(
      role="user", parts=[types.Part.from_text(text=prompt)]
  )

  final_response_text = ""
  last_interaction_id = None

  print(f"\n>> User: {prompt}")

  async for event in runner.run_async(
      user_id=user_id,
      session_id=session_id,
      new_message=content,
      run_config=RunConfig(save_input_blobs_as_artifacts=False),
  ):
    # Track interaction ID if available
    if event.interaction_id:
      last_interaction_id = event.interaction_id

    # Show function calls
    if event.get_function_calls():
      for fc in event.get_function_calls():
        print(f"   [Tool Call] {fc.name}({fc.args})")

    # Show function responses
    if event.get_function_responses():
      for fr in event.get_function_responses():
        print(f"   [Tool Result] {fr.name}: {fr.response}")

    # Collect text responses from the agent (not user, not partial)
    if (
        event.content
        and event.content.parts
        and event.author != "user"
        and not event.partial
    ):
      for part in event.content.parts:
        if part.text:
          # Filter by agent name if provided, otherwise accept any non-user
          if not agent_name or event.author == agent_name:
            final_response_text += part.text

  print(f"<< Agent: {final_response_text}")
  if show_interaction_id and last_interaction_id:
    print(f"   [Interaction ID: {last_interaction_id}]")

  return final_response_text, last_interaction_id


async def test_basic_text_generation(runner: Runner, session_id: str):
  """Test basic text generation without tools."""
  print("\n" + "=" * 60)
  print("TEST 1: Basic Text Generation")
  print("=" * 60)

  response, interaction_id = await call_agent_async(
      runner, USER_ID, session_id, "Hello! What can you help me with?"
  )

  assert response, "Expected a non-empty response"
  print("PASSED: Basic text generation works")
  return interaction_id


async def test_function_calling(runner: Runner, session_id: str):
  """Test function calling with the google_search tool."""
  print("\n" + "=" * 60)
  print("TEST 2: Function Calling (Google Search Tool)")
  print("=" * 60)

  response, interaction_id = await call_agent_async(
      runner,
      USER_ID,
      session_id,
      "Search for the capital of France.",
  )

  assert response, "Expected a non-empty response"
  assert "paris" in response.lower(), f"Expected Paris in response: {response}"
  print("PASSED: Google search tool works")
  return interaction_id


async def test_multi_turn_conversation(runner: Runner, session_id: str):
  """Test multi-turn conversation to verify stateful interactions."""
  print("\n" + "=" * 60)
  print("TEST 3: Multi-Turn Conversation (Stateful)")
  print("=" * 60)

  # Turn 1: Tell the agent a fact directly (test conversation memory)
  response1, id1 = await call_agent_async(
      runner,
      USER_ID,
      session_id,
      "My favorite color is blue. Just acknowledge this, don't use any tools.",
  )
  assert response1, "Expected a response for turn 1"
  print(f"   Turn 1 interaction_id: {id1}")

  # Turn 2: Ask about something else (use weather tool to add variety)
  response2, id2 = await call_agent_async(
      runner,
      USER_ID,
      session_id,
      "What's the weather like in London?",
  )
  assert response2, "Expected a response for turn 2"
  assert (
      "59" in response2
      or "london" in response2.lower()
      or "cloudy" in response2.lower()
  ), f"Expected London weather info in response: {response2}"
  print(f"   Turn 2 interaction_id: {id2}")

  # Turn 3: Ask the agent to recall conversation context
  response3, id3 = await call_agent_async(
      runner,
      USER_ID,
      session_id,
      "What is my favorite color that I mentioned earlier in our conversation?",
  )
  assert response3, "Expected a response for turn 3"
  assert (
      "blue" in response3.lower()
  ), f"Expected agent to remember the color 'blue': {response3}"
  print(f"   Turn 3 interaction_id: {id3}")

  # Verify interaction IDs are different (new interactions) but chained
  if id1 and id2 and id3:
    print(f"   Interaction chain: {id1} -> {id2} -> {id3}")

  print("PASSED: Multi-turn conversation works with context retention")


async def test_google_search_tool(runner: Runner, session_id: str):
  """Test the google_search built-in tool."""
  print("\n" + "=" * 60)
  print("TEST 4: Google Search Tool (Additional)")
  print("=" * 60)

  response, interaction_id = await call_agent_async(
      runner,
      USER_ID,
      session_id,
      "Use google search to find out who wrote the novel '1984'.",
  )

  assert response, "Expected a non-empty response"
  assert (
      "orwell" in response.lower() or "george" in response.lower()
  ), f"Expected George Orwell in response: {response}"
  print("PASSED: Google search built-in tool works")


async def test_custom_function_tool(runner: Runner, session_id: str):
  """Test the custom function tool alongside google_search.

  The root_agent has both GoogleSearchTool (with bypass_multi_tools_limit=True)
  and get_current_weather. This tests that function calling tools work with
  the Interactions API when all tools are function calling types.
  """
  print("\n" + "=" * 60)
  print("TEST 5: Custom Function Tool (get_current_weather)")
  print("=" * 60)

  response, interaction_id = await call_agent_async(
      runner,
      USER_ID,
      session_id,
      "What's the weather like in Tokyo?",
  )

  assert response, "Expected a non-empty response"
  # The mock weather data for Tokyo has temperature 68, condition "Partly Cloudy"
  assert (
      "68" in response
      or "partly" in response.lower()
      or "tokyo" in response.lower()
  ), f"Expected weather info for Tokyo in response: {response}"
  print("PASSED: Custom function tool works with bypass_multi_tools_limit")
  return interaction_id


def check_interactions_api_available() -> bool:
  """Check if the interactions API is available in the SDK."""
  try:
    from google.genai import Client

    client = Client()
    # Check if interactions attribute exists
    return hasattr(client.aio, "interactions")
  except Exception:
    return False


async def run_all_tests():
  """Run all tests with the Interactions API."""
  print("\n" + "#" * 70)
  print("# Running tests with Interactions API")
  print("#" * 70)

  # Check if interactions API is available
  if not check_interactions_api_available():
    print("\nERROR: Interactions API is not available in the current SDK.")
    print("The interactions API requires a SDK version with this feature.")
    print("To use the interactions API, ensure you have the SDK with")
    print("interactions support installed (e.g., from private-python-genai).")
    return False

  test_agent = root_agent

  runner = InMemoryRunner(
      agent=test_agent,
      app_name=APP_NAME,
  )

  # Create a new session
  session = await runner.session_service.create_session(
      user_id=USER_ID,
      app_name=APP_NAME,
  )
  print(f"\nSession created: {session.id}")

  try:
    # Run all tests
    await test_basic_text_generation(runner, session.id)
    await test_function_calling(runner, session.id)
    await test_multi_turn_conversation(runner, session.id)
    await test_google_search_tool(runner, session.id)
    await test_custom_function_tool(runner, session.id)

    print("\n" + "=" * 60)
    print("ALL TESTS PASSED (Interactions API)")
    print("=" * 60)
    return True

  except AssertionError as e:
    print(f"\nTEST FAILED: {e}")
    return False
  except Exception as e:
    print(f"\nERROR: {e}")
    import traceback

    traceback.print_exc()
    return False


async def interactive_mode():
  """Run in interactive mode for manual testing."""
  # Check if interactions API is available
  if not check_interactions_api_available():
    print("\nERROR: Interactions API is not available in the current SDK.")
    print("To use the interactions API, ensure you have the SDK with")
    print("interactions support installed (e.g., from private-python-genai).")
    return

  print("\nInteractive mode with Interactions API")
  print("Type 'quit' to exit, 'new' for a new session\n")

  test_agent = agent.root_agent

  runner = InMemoryRunner(
      agent=test_agent,
      app_name=APP_NAME,
  )

  session = await runner.session_service.create_session(
      user_id=USER_ID,
      app_name=APP_NAME,
  )
  print(f"Session created: {session.id}\n")

  while True:
    try:
      user_input = input("You: ").strip()
      if not user_input:
        continue
      if user_input.lower() == "quit":
        break
      if user_input.lower() == "new":
        session = await runner.session_service.create_session(
            user_id=USER_ID,
            app_name=APP_NAME,
        )
        print(f"New session created: {session.id}\n")
        continue

      await call_agent_async(runner, USER_ID, session.id, user_input)

    except KeyboardInterrupt:
      break

  print("\nGoodbye!")


def main():
  parser = argparse.ArgumentParser(
      description="Test the Interactions API integration"
  )
  parser.add_argument(
      "--mode",
      choices=["test", "interactive"],
      default="test",
      help=(
          "Run mode: 'test' runs automated tests, 'interactive' for manual"
          " testing"
      ),
  )
  parser.add_argument(
      "--debug",
      action="store_true",
      help="Enable debug logging",
  )

  args = parser.parse_args()

  if args.debug:
    logs.setup_adk_logger(level=logging.DEBUG)
  else:
    logs.setup_adk_logger(level=logging.INFO)

  start_time = time.time()

  if args.mode == "test":
    success = asyncio.run(run_all_tests())
    if not success:
      exit(1)

  elif args.mode == "interactive":
    asyncio.run(interactive_mode())

  end_time = time.time()
  print(f"\nTotal execution time: {end_time - start_time:.2f} seconds")


if __name__ == "__main__":
  main()
