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

from typing import Any
from typing import Dict
from typing import List

from fastapi.openapi.models import Response, Schema
from google.adk.tools.openapi_tool.common.common import ApiParameter
from google.adk.tools.openapi_tool.common.common import PydocHelper
from google.adk.tools.openapi_tool.common.common import rename_python_keywords
from google.adk.tools.openapi_tool.common.common import to_snake_case
from google.adk.tools.openapi_tool.common.common import TypeHintHelper
import pytest


def dict_to_responses(input: Dict[str, Any]) -> Dict[str, Response]:
  return {k: Response.model_validate(input[k]) for k in input}


class TestToSnakeCase:

  @pytest.mark.parametrize(
      'input_str, expected_output',
      [
          ('lowerCamelCase', 'lower_camel_case'),
          ('UpperCamelCase', 'upper_camel_case'),
          ('space separated', 'space_separated'),
          ('REST API', 'rest_api'),
          ('Mixed_CASE with_Spaces', 'mixed_case_with_spaces'),
          ('__init__', 'init'),
          ('APIKey', 'api_key'),
          ('SomeLongURL', 'some_long_url'),
          ('CONSTANT_CASE', 'constant_case'),
          ('already_snake_case', 'already_snake_case'),
          ('single', 'single'),
          ('', ''),
          ('  spaced  ', 'spaced'),
          ('with123numbers', 'with123numbers'),
          ('With_Mixed_123_and_SPACES', 'with_mixed_123_and_spaces'),
          ('HTMLParser', 'html_parser'),
          ('HTTPResponseCode', 'http_response_code'),
          ('a_b_c', 'a_b_c'),
          ('A_B_C', 'a_b_c'),
          ('fromAtoB', 'from_ato_b'),
          ('XMLHTTPRequest', 'xmlhttp_request'),
          ('_leading', 'leading'),
          ('trailing_', 'trailing'),
          ('  leading_and_trailing_  ', 'leading_and_trailing'),
          ('Multiple___Underscores', 'multiple_underscores'),
          ('  spaces_and___underscores  ', 'spaces_and_underscores'),
          ('  _mixed_Case  ', 'mixed_case'),
          ('123Start', '123_start'),
          ('End123', 'end123'),
          ('Mid123dle', 'mid123dle'),
      ],
  )
  def test_to_snake_case(self, input_str, expected_output):
    assert to_snake_case(input_str) == expected_output


class TestRenamePythonKeywords:

  @pytest.mark.parametrize(
      'input_str, expected_output',
      [
          ('in', 'param_in'),
          ('for', 'param_for'),
          ('class', 'param_class'),
          ('normal', 'normal'),
          ('param_if', 'param_if'),
          ('', ''),
      ],
  )
  def test_rename_python_keywords(self, input_str, expected_output):
    assert rename_python_keywords(input_str) == expected_output


class TestApiParameter:

  def test_api_parameter_initialization(self):
    schema = Schema(type='string', description='A string parameter')
    param = ApiParameter(
        original_name='testParam',
        description='A string description',
        param_location='query',
        param_schema=schema,
    )
    assert param.original_name == 'testParam'
    assert param.param_location == 'query'
    assert param.param_schema.type == 'string'
    assert param.param_schema.description == 'A string parameter'
    assert param.py_name == 'test_param'
    assert param.type_hint == 'str'
    assert param.type_value == str
    assert param.description == 'A string description'

  def test_api_parameter_keyword_rename(self):
    schema = Schema(type='string')
    param = ApiParameter(
        original_name='in',
        param_location='query',
        param_schema=schema,
    )
    assert param.py_name == 'param_in'

  def test_api_parameter_custom_py_name(self):
    schema = Schema(type='integer')
    param = ApiParameter(
        original_name='testParam',
        param_location='query',
        param_schema=schema,
        py_name='custom_name',
    )
    assert param.py_name == 'custom_name'

  def test_api_parameter_str_representation(self):
    schema = Schema(type='number')
    param = ApiParameter(
        original_name='testParam',
        param_location='query',
        param_schema=schema,
    )
    assert str(param) == 'test_param: float'

  def test_api_parameter_to_arg_string(self):
    schema = Schema(type='boolean')
    param = ApiParameter(
        original_name='testParam',
        param_location='query',
        param_schema=schema,
    )
    assert param.to_arg_string() == 'test_param=test_param'

  def test_api_parameter_to_dict_property(self):
    schema = Schema(type='string')
    param = ApiParameter(
        original_name='testParam',
        param_location='path',
        param_schema=schema,
    )
    assert param.to_dict_property() == '"test_param": test_param'

  def test_api_parameter_model_serializer(self):
    schema = Schema(type='string', description='test description')
    param = ApiParameter(
        original_name='TestParam',
        param_location='path',
        param_schema=schema,
        py_name='test_param_custom',
        description='test description',
    )

    serialized_param = param.model_dump(mode='json', exclude_none=True)

    assert serialized_param == {
        'original_name': 'TestParam',
        'param_location': 'path',
        'param_schema': {'type': 'string', 'description': 'test description'},
        'description': 'test description',
        'py_name': 'test_param_custom',
    }

  @pytest.mark.parametrize(
      'schema, expected_type_value, expected_type_hint',
      [
          ({'type': 'integer'}, int, 'int'),
          ({'type': 'number'}, float, 'float'),
          ({'type': 'boolean'}, bool, 'bool'),
          ({'type': 'string'}, str, 'str'),
          (
              {'type': 'string', 'format': 'date'},
              str,
              'str',
          ),
          (
              {'type': 'string', 'format': 'date-time'},
              str,
              'str',
          ),
          (
              {'type': 'array', 'items': {'type': 'integer'}},
              List[int],
              'List[int]',
          ),
          (
              {'type': 'array', 'items': {'type': 'string'}},
              List[str],
              'List[str]',
          ),
          (
              {
                  'type': 'array',
                  'items': {'type': 'object'},
              },
              List[Dict[str, Any]],
              'List[Dict[str, Any]]',
          ),
          ({'type': 'object'}, Dict[str, Any], 'Dict[str, Any]'),
          ({'type': 'unknown'}, Any, 'Any'),
          ({}, Any, 'Any'),
      ],
  )
  def test_api_parameter_type_hint_helper(
      self, schema, expected_type_value, expected_type_hint
  ):
    param = ApiParameter(
        original_name='test', param_location='query', param_schema=schema
    )
    assert param.type_value == expected_type_value
    assert param.type_hint == expected_type_hint
    assert (
        TypeHintHelper.get_type_hint(param.param_schema) == expected_type_hint
    )
    assert (
        TypeHintHelper.get_type_value(param.param_schema) == expected_type_value
    )

  def test_api_parameter_description(self):
    schema = Schema(type='string')
    param = ApiParameter(
        original_name='param1',
        param_location='query',
        param_schema=schema,
        description='The description',
    )
    assert param.description == 'The description'

  def test_api_parameter_description_use_schema_fallback(self):
    schema = Schema(type='string', description='The description')
    param = ApiParameter(
        original_name='param1',
        param_location='query',
        param_schema=schema,
    )
    assert param.description == 'The description'


class TestTypeHintHelper:

  @pytest.mark.parametrize(
      'schema, expected_type_value, expected_type_hint',
      [
          ({'type': 'integer'}, int, 'int'),
          ({'type': 'number'}, float, 'float'),
          ({'type': 'string'}, str, 'str'),
          (
              {
                  'type': 'array',
                  'items': {'type': 'string'},
              },
              List[str],
              'List[str]',
          ),
      ],
  )
  def test_get_type_value_and_hint(
      self, schema, expected_type_value, expected_type_hint
  ):

    param = ApiParameter(
        original_name='test_param',
        param_location='query',
        param_schema=schema,
        description='Test parameter',
    )
    assert (
        TypeHintHelper.get_type_value(param.param_schema) == expected_type_value
    )
    assert (
        TypeHintHelper.get_type_hint(param.param_schema) == expected_type_hint
    )


class TestPydocHelper:

  def test_generate_param_doc_simple(self):
    schema = Schema(type='string')
    param = ApiParameter(
        original_name='test_param',
        param_location='query',
        param_schema=schema,
        description='Test description',
    )

    expected_doc = 'test_param (str): Test description'
    assert PydocHelper.generate_param_doc(param) == expected_doc

  def test_generate_param_doc_no_description(self):
    schema = Schema(type='integer')
    param = ApiParameter(
        original_name='test_param',
        param_location='query',
        param_schema=schema,
    )
    expected_doc = 'test_param (int): '
    assert PydocHelper.generate_param_doc(param) == expected_doc

  def test_generate_param_doc_object(self):
    schema = Schema(
        type='object',
        properties={
            'prop1': {'type': 'string', 'description': 'Prop1 desc'},
            'prop2': {'type': 'integer'},
        },
    )
    param = ApiParameter(
        original_name='test_param',
        param_location='query',
        param_schema=schema,
        description='Test object parameter',
    )
    expected_doc = (
        'test_param (Dict[str, Any]): Test object parameter Object'
        ' properties:\n       prop1 (str): Prop1 desc\n       prop2'
        ' (int): \n'
    )
    assert PydocHelper.generate_param_doc(param) == expected_doc

  def test_generate_param_doc_object_no_properties(self):
    schema = Schema(type='object', description='A test schema')
    param = ApiParameter(
        original_name='test_param',
        param_location='query',
        param_schema=schema,
        description='The description.',
    )
    expected_doc = 'test_param (Dict[str, Any]): The description.'
    assert PydocHelper.generate_param_doc(param) == expected_doc

  def test_generate_return_doc_simple(self):
    responses = {
        '200': {
            'description': 'Successful response',
            'content': {'application/json': {'schema': {'type': 'string'}}},
        }
    }
    expected_doc = 'Returns (str): Successful response'
    assert (
        PydocHelper.generate_return_doc(dict_to_responses(responses))
        == expected_doc
    )

  def test_generate_return_doc_no_content(self):
    responses = {'204': {'description': 'No content'}}
    assert not PydocHelper.generate_return_doc(dict_to_responses(responses))

  def test_generate_return_doc_object(self):
    responses = {
        '200': {
            'description': 'Successful object response',
            'content': {
                'application/json': {
                    'schema': {
                        'type': 'object',
                        'properties': {
                            'prop1': {
                                'type': 'string',
                                'description': 'Prop1 desc',
                            },
                            'prop2': {'type': 'integer'},
                        },
                    }
                }
            },
        }
    }

    return_doc = PydocHelper.generate_return_doc(dict_to_responses(responses))

    assert 'Returns (Dict[str, Any]): Successful object response' in return_doc
    assert 'prop1 (str): Prop1 desc' in return_doc
    assert 'prop2 (int):' in return_doc

  def test_generate_return_doc_multiple_success(self):
    responses = {
        '200': {
            'description': 'Successful response',
            'content': {'application/json': {'schema': {'type': 'string'}}},
        },
        '400': {'description': 'Bad request'},
    }
    expected_doc = 'Returns (str): Successful response'
    assert (
        PydocHelper.generate_return_doc(dict_to_responses(responses))
        == expected_doc
    )

  def test_generate_return_doc_2xx_smallest_status_code_response(self):
    responses = {
        '201': {
            'description': '201 response',
            'content': {'application/json': {'schema': {'type': 'integer'}}},
        },
        '200': {
            'description': '200 response',
            'content': {'application/json': {'schema': {'type': 'string'}}},
        },
        '400': {'description': 'Bad request'},
    }

    expected_doc = 'Returns (str): 200 response'
    assert (
        PydocHelper.generate_return_doc(dict_to_responses(responses))
        == expected_doc
    )

  def test_generate_return_doc_contentful_response(self):
    responses = {
        '200': {'description': 'No content response'},
        '201': {
            'description': '201 response',
            'content': {'application/json': {'schema': {'type': 'string'}}},
        },
        '400': {'description': 'Bad request'},
    }
    expected_doc = 'Returns (str): 201 response'
    assert (
        PydocHelper.generate_return_doc(dict_to_responses(responses))
        == expected_doc
    )


if __name__ == '__main__':
  pytest.main([__file__])
