# 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 enum import Enum

from google.adk.features import FeatureName
from google.adk.features._feature_registry import temporary_feature_override
from google.adk.tools import _automatic_function_calling_util
from google.adk.tools.tool_context import ToolContext
from google.adk.utils.variant_utils import GoogleLLMVariant
from google.genai import types
# TODO: crewai requires python 3.10 as minimum
# from crewai_tools import FileReadTool
from pydantic import BaseModel
import pytest


class TestBuildFunctionDeclarationLegacy:

  @pytest.fixture(autouse=True)
  def disable_feature_flag(self):
    """Disable the JSON_SCHEMA_FOR_FUNC_DECL feature flag for legacy tests."""
    with temporary_feature_override(
        FeatureName.JSON_SCHEMA_FOR_FUNC_DECL, False
    ):
      yield

  def test_string_input(self):
    def simple_function(input_str: str) -> str:
      return {'result': input_str}

    function_decl = _automatic_function_calling_util.build_function_declaration(
        func=simple_function
    )

    assert function_decl.name == 'simple_function'
    assert function_decl.parameters.type == 'OBJECT'
    assert function_decl.parameters.properties['input_str'].type == 'STRING'

  def test_int_input(self):
    def simple_function(input_str: int) -> str:
      return {'result': input_str}

    function_decl = _automatic_function_calling_util.build_function_declaration(
        func=simple_function
    )

    assert function_decl.name == 'simple_function'
    assert function_decl.parameters.type == 'OBJECT'
    assert function_decl.parameters.properties['input_str'].type == 'INTEGER'

  def test_float_input(self):
    def simple_function(input_str: float) -> str:
      return {'result': input_str}

    function_decl = _automatic_function_calling_util.build_function_declaration(
        func=simple_function
    )

    assert function_decl.name == 'simple_function'
    assert function_decl.parameters.type == 'OBJECT'
    assert function_decl.parameters.properties['input_str'].type == 'NUMBER'

  def test_bool_input(self):
    def simple_function(input_str: bool) -> str:
      return {'result': input_str}

    function_decl = _automatic_function_calling_util.build_function_declaration(
        func=simple_function
    )

    assert function_decl.name == 'simple_function'
    assert function_decl.parameters.type == 'OBJECT'
    assert function_decl.parameters.properties['input_str'].type == 'BOOLEAN'

  def test_array_input(self):
    def simple_function(input_str: list[str]) -> str:
      return {'result': input_str}

    function_decl = _automatic_function_calling_util.build_function_declaration(
        func=simple_function
    )

    assert function_decl.name == 'simple_function'
    assert function_decl.parameters.type == 'OBJECT'
    assert function_decl.parameters.properties['input_str'].type == 'ARRAY'

  def test_dict_input(self):
    def simple_function(input_str: dict[str, str]) -> str:
      return {'result': input_str}

    function_decl = _automatic_function_calling_util.build_function_declaration(
        func=simple_function
    )

    assert function_decl.name == 'simple_function'
    assert function_decl.parameters.type == 'OBJECT'
    assert function_decl.parameters.properties['input_str'].type == 'OBJECT'

  def test_basemodel_input(self):
    class CustomInput(BaseModel):
      input_str: str

    def simple_function(input: CustomInput) -> str:
      return {'result': input}

    function_decl = _automatic_function_calling_util.build_function_declaration(
        func=simple_function
    )

    assert function_decl.name == 'simple_function'
    assert function_decl.parameters.type == 'OBJECT'
    assert function_decl.parameters.properties['input'].type == 'OBJECT'
    assert (
        function_decl.parameters.properties['input']
        .properties['input_str']
        .type
        == 'STRING'
    )

  def test_basemodel_required_fields(self):
    class SearchRequest(BaseModel):
      query: str
      max_results: int
      filter: str = ''

    def search(request: SearchRequest) -> list:
      return []

    function_decl = _automatic_function_calling_util.build_function_declaration(
        func=search
    )

    inner = function_decl.parameters.properties['request']
    assert set(inner.required) == {'query', 'max_results'}
    assert 'filter' not in (inner.required or [])

  def test_basemodel_all_optional_fields_no_required(self):
    class Config(BaseModel):
      timeout: int = 30
      retries: int = 3

    def run(config: Config) -> str:
      return ''

    function_decl = _automatic_function_calling_util.build_function_declaration(
        func=run
    )

    inner = function_decl.parameters.properties['config']
    assert not inner.required

  def test_nested_basemodel_required_fields(self):
    class Inner(BaseModel):
      x: int
      y: int = 0

    class Outer(BaseModel):
      inner: Inner
      label: str = ''

    def process(data: Outer) -> str:
      return ''

    function_decl = _automatic_function_calling_util.build_function_declaration(
        func=process
    )

    outer = function_decl.parameters.properties['data']
    assert set(outer.required) == {'inner'}
    assert 'label' not in (outer.required or [])

    inner = outer.properties['inner']
    assert set(inner.required) == {'x'}
    assert 'y' not in (inner.required or [])

  def test_toolcontext_ignored(self):
    def simple_function(input_str: str, tool_context: ToolContext) -> str:
      return {'result': input_str}

    function_decl = _automatic_function_calling_util.build_function_declaration(
        func=simple_function, ignore_params=['tool_context']
    )

    assert function_decl.name == 'simple_function'
    assert function_decl.parameters.type == 'OBJECT'
    assert function_decl.parameters.properties['input_str'].type == 'STRING'
    assert 'tool_context' not in function_decl.parameters.properties

  def test_basemodel(self):
    class SimpleFunction(BaseModel):
      input_str: str
      custom_input: int

    function_decl = _automatic_function_calling_util.build_function_declaration(
        func=SimpleFunction, ignore_params=['custom_input']
    )

    assert function_decl.name == 'SimpleFunction'
    assert function_decl.parameters.type == 'OBJECT'
    assert function_decl.parameters.properties['input_str'].type == 'STRING'
    assert 'custom_input' not in function_decl.parameters.properties

  def test_nested_basemodel_input(self):
    class ChildInput(BaseModel):
      input_str: str

    class CustomInput(BaseModel):
      child: ChildInput

    def simple_function(input: CustomInput) -> str:
      return {'result': input}

    function_decl = _automatic_function_calling_util.build_function_declaration(
        func=simple_function
    )

    assert function_decl.name == 'simple_function'
    assert function_decl.parameters.type == 'OBJECT'
    assert function_decl.parameters.properties['input'].type == 'OBJECT'
    assert (
        function_decl.parameters.properties['input'].properties['child'].type
        == 'OBJECT'
    )
    assert (
        function_decl.parameters.properties['input']
        .properties['child']
        .properties['input_str']
        .type
        == 'STRING'
    )

  def test_basemodel_with_nested_basemodel(self):
    class ChildInput(BaseModel):
      input_str: str

    class CustomInput(BaseModel):
      child: ChildInput

    function_decl = _automatic_function_calling_util.build_function_declaration(
        func=CustomInput, ignore_params=['custom_input']
    )

    assert function_decl.name == 'CustomInput'
    assert function_decl.parameters.type == 'OBJECT'
    assert function_decl.parameters.properties['child'].type == 'OBJECT'
    assert (
        function_decl.parameters.properties['child']
        .properties['input_str']
        .type
        == 'STRING'
    )
    assert 'custom_input' not in function_decl.parameters.properties

  def test_list(self):
    def simple_function(
        input_str: list[str], input_dir: list[dict[str, str]]
    ) -> str:
      return {'result': input_str}

    function_decl = _automatic_function_calling_util.build_function_declaration(
        func=simple_function
    )

    assert function_decl.name == 'simple_function'
    assert function_decl.parameters.type == 'OBJECT'
    assert function_decl.parameters.properties['input_str'].type == 'ARRAY'
    assert (
        function_decl.parameters.properties['input_str'].items.type == 'STRING'
    )
    assert function_decl.parameters.properties['input_dir'].type == 'ARRAY'
    assert (
        function_decl.parameters.properties['input_dir'].items.type == 'OBJECT'
    )

  def test_enums(self):

    class InputEnum(Enum):
      AGENT = 'agent'
      TOOL = 'tool'

    def simple_function(input: InputEnum = InputEnum.AGENT):
      return input.value

    function_decl = _automatic_function_calling_util.build_function_declaration(
        func=simple_function
    )

    assert function_decl.name == 'simple_function'
    assert function_decl.parameters.type == 'OBJECT'
    assert function_decl.parameters.properties['input'].type == 'STRING'
    assert function_decl.parameters.properties['input'].default == 'agent'
    assert function_decl.parameters.properties['input'].enum == [
        'agent',
        'tool',
    ]

    def simple_function_with_wrong_enum(input: InputEnum = 'WRONG_ENUM'):
      return input.value

    with pytest.raises(ValueError):
      _automatic_function_calling_util.build_function_declaration(
          func=simple_function_with_wrong_enum
      )

  def test_basemodel_list(self):
    class ChildInput(BaseModel):
      input_str: str

    class CustomInput(BaseModel):
      child: ChildInput

    def simple_function(input_str: list[CustomInput]) -> str:
      return {'result': input_str}

    function_decl = _automatic_function_calling_util.build_function_declaration(
        func=simple_function
    )

    assert function_decl.name == 'simple_function'
    assert function_decl.parameters.type == 'OBJECT'
    assert function_decl.parameters.properties['input_str'].type == 'ARRAY'
    assert (
        function_decl.parameters.properties['input_str'].items.type == 'OBJECT'
    )
    assert (
        function_decl.parameters.properties['input_str']
        .items.properties['child']
        .type
        == 'OBJECT'
    )
    assert (
        function_decl.parameters.properties['input_str']
        .items.properties['child']
        .properties['input_str']
        .type
        == 'STRING'
    )

  # TODO: comment out this test for now as crewai requires python 3.10 as minimum
  # def test_crewai_tool():
  #   docs_tool = CrewaiTool(
  #       name='directory_read_tool',
  #       description='use this to find files for you.',
  #       tool=FileReadTool(),
  #   )
  #   function_decl = docs_tool.get_declaration()
  #   assert function_decl.name == 'directory_read_tool'
  #   assert function_decl.parameters.type == 'OBJECT'
  #   assert function_decl.parameters.properties['file_path'].type == 'STRING'

  def test_function_no_return_annotation_gemini_api(self):
    """Test function with no return annotation using GEMINI_API variant."""

    def function_no_return(param: str):
      """A function with no return annotation."""
      return None

    function_decl = _automatic_function_calling_util.build_function_declaration(
        func=function_no_return, variant=GoogleLLMVariant.GEMINI_API
    )

    assert function_decl.name == 'function_no_return'
    assert function_decl.parameters.type == 'OBJECT'
    assert function_decl.parameters.properties['param'].type == 'STRING'
    # GEMINI_API should not have response schema
    assert function_decl.response is None

  def test_function_no_return_annotation_vertex_ai(self):
    """Test function with no return annotation using VERTEX_AI variant."""

    def function_no_return(param: str):
      """A function with no return annotation."""
      return None

    function_decl = _automatic_function_calling_util.build_function_declaration(
        func=function_no_return, variant=GoogleLLMVariant.VERTEX_AI
    )

    assert function_decl.name == 'function_no_return'
    assert function_decl.parameters.type == 'OBJECT'
    assert function_decl.parameters.properties['param'].type == 'STRING'
    # VERTEX_AI should have response schema for functions with no return annotation
    # Changed: Now uses Any type instead of NULL for no return annotation
    assert function_decl.response is not None
    assert (
        function_decl.response.type is None
    )  # Any type maps to None in schema

  def test_function_explicit_none_return_vertex_ai(self):
    """Test function with explicit None return annotation using VERTEX_AI variant."""

    def function_none_return(param: str) -> None:
      """A function that explicitly returns None."""
      pass

    function_decl = _automatic_function_calling_util.build_function_declaration(
        func=function_none_return, variant=GoogleLLMVariant.VERTEX_AI
    )

    assert function_decl.name == 'function_none_return'
    assert function_decl.parameters.type == 'OBJECT'
    assert function_decl.parameters.properties['param'].type == 'STRING'
    # VERTEX_AI should have response schema for explicit None return
    assert function_decl.response is not None
    assert function_decl.response.type == types.Type.NULL

  def test_function_explicit_none_return_gemini_api(self):
    """Test function with explicit None return annotation using GEMINI_API variant."""

    def function_none_return(param: str) -> None:
      """A function that explicitly returns None."""
      pass

    function_decl = _automatic_function_calling_util.build_function_declaration(
        func=function_none_return, variant=GoogleLLMVariant.GEMINI_API
    )

    assert function_decl.name == 'function_none_return'
    assert function_decl.parameters.type == 'OBJECT'
    assert function_decl.parameters.properties['param'].type == 'STRING'
    # GEMINI_API should not have response schema
    assert function_decl.response is None

  def test_function_regular_return_type_vertex_ai(self):
    """Test function with regular return type using VERTEX_AI variant."""

    def function_string_return(param: str) -> str:
      """A function that returns a string."""
      return param

    function_decl = _automatic_function_calling_util.build_function_declaration(
        func=function_string_return, variant=GoogleLLMVariant.VERTEX_AI
    )

    assert function_decl.name == 'function_string_return'
    assert function_decl.parameters.type == 'OBJECT'
    assert function_decl.parameters.properties['param'].type == 'STRING'
    # VERTEX_AI should have response schema for string return
    assert function_decl.response is not None
    assert function_decl.response.type == types.Type.STRING

  def test_function_with_no_response_annotations(self):
    """Test a function that has no response annotations."""

    def transfer_to_agent(agent_name: str, tool_context: ToolContext):
      """Transfer the question to another agent."""
      tool_context.actions.transfer_to_agent = agent_name

    function_decl = _automatic_function_calling_util.build_function_declaration(
        func=transfer_to_agent,
        ignore_params=['tool_context'],
        variant=GoogleLLMVariant.VERTEX_AI,
    )

    assert function_decl.name == 'transfer_to_agent'
    assert function_decl.parameters.type == 'OBJECT'
    assert function_decl.parameters.properties['agent_name'].type == 'STRING'
    assert 'tool_context' not in function_decl.parameters.properties
    # This function has no return annotation, so it gets Any type instead of NULL
    # Changed: Now uses Any type instead of NULL for no return annotation
    assert function_decl.response is not None
    assert (
        function_decl.response.type is None
    )  # Any type maps to None in schema

  def test_transfer_to_agent_tool_with_enum_constraint(self):
    """Test TransferToAgentTool adds enum constraint to agent_name."""
    from google.adk.tools.transfer_to_agent_tool import TransferToAgentTool

    agent_names = ['agent_a', 'agent_b', 'agent_c']
    tool = TransferToAgentTool(agent_names=agent_names)

    function_decl = tool._get_declaration()

    assert function_decl.name == 'transfer_to_agent'
    assert function_decl.parameters.type == 'OBJECT'
    assert function_decl.parameters.properties['agent_name'].type == 'STRING'
    assert function_decl.parameters.properties['agent_name'].enum == agent_names
    assert 'tool_context' not in function_decl.parameters.properties


class TestBuildFunctionDeclarationWithJsonSchema:
  """Tests for build_function_declaration when JSON_SCHEMA_FOR_FUNC_DECL is enabled."""

  @pytest.fixture(autouse=True)
  def enable_feature_flag(self):
    """Enable the JSON_SCHEMA_FOR_FUNC_DECL feature flag for all tests."""
    with temporary_feature_override(
        FeatureName.JSON_SCHEMA_FOR_FUNC_DECL, True
    ):
      yield

  def test_basic_string_parameter(self):
    """Test basic string parameter with feature flag enabled."""

    def greet(name: str) -> str:
      """Greet someone."""
      return f'Hello, {name}!'

    decl = _automatic_function_calling_util.build_function_declaration(greet)

    assert decl.name == 'greet'
    assert decl.description == 'Greet someone.'
    assert decl.parameters_json_schema == {
        'properties': {'name': {'title': 'Name', 'type': 'string'}},
        'required': ['name'],
        'title': 'greetParams',
        'type': 'object',
    }

  def test_multiple_parameter_types(self):
    """Test multiple parameter types with feature flag enabled."""

    def create_user(name: str, age: int, active: bool) -> str:
      """Create a new user."""
      return f'Created {name}'

    decl = _automatic_function_calling_util.build_function_declaration(
        create_user
    )

    schema = decl.parameters_json_schema
    assert schema['properties'] == {
        'name': {'title': 'Name', 'type': 'string'},
        'age': {'title': 'Age', 'type': 'integer'},
        'active': {'title': 'Active', 'type': 'boolean'},
    }
    assert set(schema['required']) == {'name', 'age', 'active'}

  def test_list_parameter(self):
    """Test list parameter with feature flag enabled."""

    def sum_numbers(numbers: list[int]) -> int:
      """Sum a list of numbers."""
      return sum(numbers)

    decl = _automatic_function_calling_util.build_function_declaration(
        sum_numbers
    )

    schema = decl.parameters_json_schema
    assert schema['properties']['numbers'] == {
        'items': {'type': 'integer'},
        'title': 'Numbers',
        'type': 'array',
    }

  def test_dict_parameter(self):
    """Test dict parameter with feature flag enabled."""

    def process_data(data: dict[str, str]) -> str:
      """Process a dictionary."""
      return str(data)

    decl = _automatic_function_calling_util.build_function_declaration(
        process_data
    )

    schema = decl.parameters_json_schema
    assert schema['properties']['data'] == {
        'additionalProperties': {'type': 'string'},
        'title': 'Data',
        'type': 'object',
    }

  def test_optional_parameter(self):
    """Test optional parameter with feature flag enabled."""

    def search(query: str, limit: int | None = None) -> str:
      """Search for something."""
      return query

    decl = _automatic_function_calling_util.build_function_declaration(search)

    schema = decl.parameters_json_schema
    assert schema['required'] == ['query']
    assert 'query' in schema['properties']
    assert 'limit' in schema['properties']

  def test_enum_parameter(self):
    """Test enum parameter with feature flag enabled."""

    class Color(Enum):
      RED = 'red'
      GREEN = 'green'
      BLUE = 'blue'

    def set_color(color: Color) -> str:
      """Set the color."""
      return color.value

    decl = _automatic_function_calling_util.build_function_declaration(
        set_color
    )

    schema = decl.parameters_json_schema
    assert schema['properties']['color'] == {
        '$ref': '#/$defs/Color',
    }
    assert schema['$defs']['Color'] == {
        'enum': ['red', 'green', 'blue'],
        'title': 'Color',
        'type': 'string',
    }

  def test_tool_context_ignored(self):
    """Test that tool_context is ignored."""

    def my_tool(query: str, tool_context: ToolContext) -> str:
      """A tool that uses context."""
      return query

    decl = _automatic_function_calling_util.build_function_declaration(
        my_tool, ignore_params=['tool_context']
    )

    schema = decl.parameters_json_schema
    assert set(schema['properties'].keys()) == {'query'}
    assert 'tool_context' not in schema['properties']

  def test_gemini_api_no_response_schema(self):
    """Test that GEMINI_API variant does not include response schema."""

    def get_data() -> dict[str, int]:
      """Get some data."""
      return {'count': 42}

    decl = _automatic_function_calling_util.build_function_declaration(
        get_data, variant=GoogleLLMVariant.GEMINI_API
    )

    # GEMINI_API should not have response_json_schema due to bug b/421991354
    assert decl.response_json_schema is None

  @pytest.mark.parametrize(
      'variant, expect_response_schema',
      [
          (GoogleLLMVariant.GEMINI_API, False),
          (GoogleLLMVariant.VERTEX_AI, True),
      ],
  )
  def test_response_schema_by_variant(self, variant, expect_response_schema):
    """Test response schema generation based on the LLM variant."""

    def get_data() -> dict[str, int]:
      """Get some data."""
      return {'count': 42}

    decl = _automatic_function_calling_util.build_function_declaration(
        get_data, variant=variant
    )

    assert (decl.response_json_schema is not None) == expect_response_schema

  def test_pydantic_model_parameter(self):
    """Test Pydantic model parameter with feature flag enabled."""

    class Address(BaseModel):
      street: str
      city: str

    def save_address(address: Address) -> str:
      """Save an address."""
      return f'Saved address in {address.city}'

    decl = _automatic_function_calling_util.build_function_declaration(
        save_address
    )

    assert decl.parameters_json_schema is not None
    assert 'address' in decl.parameters_json_schema['properties']

  def test_no_parameters(self):
    """Test function with no parameters."""

    def get_time() -> str:
      """Get current time."""
      return '12:00'

    decl = _automatic_function_calling_util.build_function_declaration(get_time)

    assert decl.name == 'get_time'
    assert decl.parameters_json_schema is None

  def test_docstring_preserved(self):
    """Test that docstring is preserved as description."""

    def well_documented(x: int) -> int:
      """This is a well-documented function.

      It does something useful.
      """
      return x

    decl = _automatic_function_calling_util.build_function_declaration(
        well_documented
    )

    assert 'well-documented function' in decl.description
    assert 'something useful' in decl.description

  def test_default_values(self):
    """Test parameters with default values."""

    def greet(name: str = 'World') -> str:
      """Greet someone."""
      return f'Hello, {name}!'

    decl = _automatic_function_calling_util.build_function_declaration(greet)

    schema = decl.parameters_json_schema
    assert schema['properties']['name']['default'] == 'World'
    assert 'name' not in schema.get('required', [])
