# 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 unittest.mock import MagicMock

import pytest

# Skip the module when the optional crewai dependency is not installed. Guard on
# the third-party dep itself rather than the adk wrapper, so a real import bug in
# crewai_tool surfaces as a failure instead of being silently skipped.
pytest.importorskip(
    "crewai.tools", reason="Requires crewai (google-adk[extensions])"
)

from google.adk.agents.context import Context
from google.adk.agents.invocation_context import InvocationContext
from google.adk.integrations.crewai import CrewaiTool
from google.adk.sessions.session import Session
from google.adk.tools.tool_context import ToolContext


@pytest.fixture
def mock_tool_context() -> ToolContext:
  """Fixture that provides a mock ToolContext for testing."""
  mock_invocation_context = MagicMock(spec=InvocationContext)
  mock_invocation_context._state_schema = None
  mock_invocation_context.session = MagicMock(spec=Session)
  mock_invocation_context.session.state = MagicMock()
  return ToolContext(invocation_context=mock_invocation_context)


def _simple_crewai_tool(*args, **kwargs):
  """Simple CrewAI-style tool that accepts any keyword arguments."""
  return {
      "search_query": kwargs.get("search_query"),
      "other_param": kwargs.get("other_param"),
  }


def _crewai_tool_with_context(tool_context: ToolContext, *args, **kwargs):
  """CrewAI tool with explicit tool_context parameter."""
  return {
      "search_query": kwargs.get("search_query"),
      "tool_context_present": bool(tool_context),
  }


def _crewai_tool_with_context_type(ctx: Context, *args, **kwargs):
  """CrewAI tool with Context type annotation."""
  return {
      "search_query": kwargs.get("search_query"),
      "context_present": bool(ctx),
  }


class MockCrewaiBaseTool:
  """Mock CrewAI BaseTool for testing."""

  def __init__(self, run_func, name="mock_tool", description="Mock tool"):
    self.run = run_func
    self.name = name
    self.description = description
    self.args_schema = MagicMock()
    self.args_schema.model_json_schema.return_value = {
        "type": "object",
        "properties": {
            "search_query": {"type": "string", "description": "Search query"}
        },
    }


def test_crewai_tool_initialization():
  """Test CrewaiTool initialization with various parameters."""
  mock_crewai_tool = MockCrewaiBaseTool(_simple_crewai_tool)

  # Test with custom name and description
  tool = CrewaiTool(
      mock_crewai_tool,
      name="custom_search_tool",
      description="Custom search tool description",
  )

  assert tool.name == "custom_search_tool"
  assert tool.description == "Custom search tool description"
  assert tool.tool == mock_crewai_tool


def test_crewai_tool_initialization_with_tool_defaults():
  """Test CrewaiTool initialization using tool's default name and description."""
  mock_crewai_tool = MockCrewaiBaseTool(
      _simple_crewai_tool,
      name="Serper Dev Tool",
      description="Search the internet with Serper",
  )

  # Test with empty name and description (should use tool defaults)
  tool = CrewaiTool(mock_crewai_tool, name="", description="")

  assert (
      tool.name == "serper_dev_tool"
  )  # Spaces replaced with underscores, lowercased
  assert tool.description == "Search the internet with Serper"


@pytest.mark.asyncio
async def test_crewai_tool_basic_functionality(mock_tool_context):
  """Test basic CrewaiTool functionality with **kwargs parameter passing."""
  mock_crewai_tool = MockCrewaiBaseTool(_simple_crewai_tool)
  tool = CrewaiTool(mock_crewai_tool, name="test_tool", description="Test tool")

  # Test that **kwargs parameters are passed through correctly
  result = await tool.run_async(
      args={"search_query": "test query", "other_param": "test value"},
      tool_context=mock_tool_context,
  )

  assert result["search_query"] == "test query"
  assert result["other_param"] == "test value"


@pytest.mark.asyncio
async def test_crewai_tool_with_tool_context(mock_tool_context):
  """Test CrewaiTool with a tool that has explicit tool_context parameter."""
  mock_crewai_tool = MockCrewaiBaseTool(_crewai_tool_with_context)
  tool = CrewaiTool(
      mock_crewai_tool, name="context_tool", description="Context tool"
  )

  # Test that tool_context is properly injected
  result = await tool.run_async(
      args={"search_query": "test query"},
      tool_context=mock_tool_context,
  )

  assert result["search_query"] == "test query"
  assert result["tool_context_present"] is True


@pytest.mark.asyncio
async def test_crewai_tool_parameter_filtering(mock_tool_context):
  """Test that CrewaiTool filters parameters for non-**kwargs functions."""

  def explicit_params_func(arg1: str, arg2: int):
    """Function with explicit parameters (no **kwargs)."""
    return {"arg1": arg1, "arg2": arg2}

  mock_crewai_tool = MockCrewaiBaseTool(explicit_params_func)
  tool = CrewaiTool(
      mock_crewai_tool, name="explicit_tool", description="Explicit tool"
  )

  # Test that unexpected parameters are filtered out
  result = await tool.run_async(
      args={
          "arg1": "test",
          "arg2": 42,
          "unexpected_param": "should_be_filtered",
      },
      tool_context=mock_tool_context,
  )

  assert result == {"arg1": "test", "arg2": 42}
  # Verify unexpected parameter was filtered out
  assert "unexpected_param" not in result


@pytest.mark.asyncio
async def test_crewai_tool_get_declaration():
  """Test that CrewaiTool properly builds function declarations."""
  mock_crewai_tool = MockCrewaiBaseTool(_simple_crewai_tool)
  tool = CrewaiTool(mock_crewai_tool, name="test_tool", description="Test tool")

  # Test function declaration generation
  declaration = tool._get_declaration()

  # Verify the declaration object structure and content
  assert declaration is not None
  assert declaration.name == "test_tool"
  assert declaration.description == "Test tool"
  assert declaration.parameters is not None

  # Verify that the args_schema was used to build the declaration
  mock_crewai_tool.args_schema.model_json_schema.assert_called_once()


@pytest.mark.asyncio
async def test_crewai_tool_with_context_type_annotation(mock_tool_context):
  """Test CrewaiTool with Context type annotation and custom parameter name."""
  mock_crewai_tool = MockCrewaiBaseTool(_crewai_tool_with_context_type)
  tool = CrewaiTool(
      mock_crewai_tool,
      name="context_type_tool",
      description="Context type tool",
  )

  # Verify the context parameter is detected by type
  assert tool._context_param_name == "ctx"

  # Test that context is properly injected
  result = await tool.run_async(
      args={"search_query": "test query"},
      tool_context=mock_tool_context,
  )

  assert result["search_query"] == "test query"
  assert result["context_present"]
