# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you 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

import pytest
from tvm_ffi import get_global_func_metadata, register_global_func, remove_global_func
from tvm_ffi.core import TypeInfo, TypeSchema, _lookup_type_attr
from tvm_ffi.testing import _SchemaAllTypes


def _replace_container_types(ty: str) -> str:
    return {
        "Array": "Sequence",
        "List": "MutableSequence",
        "Map": "Mapping",
        "Dict": "MutableMapping",
    }.get(ty, ty)


@pytest.mark.parametrize(
    "func_name,expected",
    [
        ("testing.schema_id_int", "Callable[[int], int]"),
        ("testing.schema_id_float", "Callable[[float], float]"),
        ("testing.schema_id_bool", "Callable[[bool], bool]"),
        ("testing.schema_id_device", "Callable[[Device], Device]"),
        ("testing.schema_id_dtype", "Callable[[dtype], dtype]"),
        ("testing.schema_id_string", "Callable[[str], str]"),
        ("testing.schema_id_bytes", "Callable[[bytes], bytes]"),
        ("testing.schema_id_func", "Callable[[Callable[..., Any]], Callable[..., Any]]"),
        (
            "testing.schema_id_func_typed",
            "Callable[[Callable[[int, float, Callable[..., Any]], None]], Callable[[int, float, Callable[..., Any]], None]]",
        ),
        ("testing.schema_id_any", "Callable[[Any], Any]"),
        ("testing.schema_id_object", "Callable[[Object], Object]"),
        ("testing.schema_id_dltensor", "Callable[[Tensor], Tensor]"),
        ("testing.schema_id_tensor", "Callable[[Tensor], Tensor]"),
        ("testing.schema_tensor_view_input", "Callable[[Tensor], None]"),
        ("testing.schema_id_opt_int", "Callable[[int | None], int | None]"),
        ("testing.schema_id_opt_str", "Callable[[str | None], str | None]"),
        ("testing.schema_id_opt_obj", "Callable[[Object | None], Object | None]"),
        ("testing.schema_id_arr_int", "Callable[[Array[int]], Array[int]]"),
        ("testing.schema_id_arr_str", "Callable[[Array[str]], Array[str]]"),
        ("testing.schema_id_arr_obj", "Callable[[Array[Object]], Array[Object]]"),
        ("testing.schema_id_arr", "Callable[[Array[Any]], Array[Any]]"),
        ("testing.schema_id_map_str_int", "Callable[[Map[str, int]], Map[str, int]]"),
        ("testing.schema_id_map_str_str", "Callable[[Map[str, str]], Map[str, str]]"),
        ("testing.schema_id_map_str_obj", "Callable[[Map[str, Object]], Map[str, Object]]"),
        ("testing.schema_id_map", "Callable[[Map[Any, Any]], Map[Any, Any]]"),
        ("testing.schema_id_dict_str_int", "Callable[[Dict[str, int]], Dict[str, int]]"),
        ("testing.schema_id_dict_str_str", "Callable[[Dict[str, str]], Dict[str, str]]"),
        ("testing.schema_id_variant_int_str", "Callable[[int | str], int | str]"),
        ("testing.schema_packed", "Callable[..., Any]"),
        (
            "testing.schema_arr_map_opt",
            "Callable[[Array[int | None], Map[str, Array[int]], str | None], Map[str, Array[int]]]",
        ),
        (
            "testing.schema_variant_mix",
            "Callable[[int | str | Array[int]], int | str | Array[int]]",
        ),
        ("testing.schema_no_args", "Callable[[], int]"),
        ("testing.schema_no_return", "Callable[[int], None]"),
        ("testing.schema_no_args_no_return", "Callable[[], None]"),
    ],
)
def test_schema_global_func(func_name: str, expected: str) -> None:
    metadata: dict[str, Any] = get_global_func_metadata(func_name)
    actual: TypeSchema = TypeSchema.from_json_str(metadata["type_schema"])
    assert str(actual) == expected, f"{func_name}: {actual}"
    assert actual.repr(_replace_container_types) == (
        expected.replace("Array", "Sequence")
        .replace("List", "MutableSequence")
        .replace("Map", "Mapping")
        .replace("Dict", "MutableMapping")
    )


@pytest.mark.parametrize(
    "field_name,expected",
    [
        ("v_bool", "bool"),
        ("v_int", "int"),
        ("v_float", "float"),
        ("v_device", "Device"),
        ("v_dtype", "dtype"),
        ("v_string", "str"),
        ("v_bytes", "bytes"),
        ("v_opt_int", "int | None"),
        ("v_opt_str", "str | None"),
        ("v_arr_int", "Array[int]"),
        ("v_arr_str", "Array[str]"),
        ("v_map_str_int", "Map[str, int]"),
        ("v_map_str_arr_int", "Map[str, Array[int]]"),
        ("v_variant", "str | Array[int] | Map[str, int]"),
        ("v_opt_arr_variant", "Array[int | str] | None"),
    ],
)
def test_schema_field(field_name: str, expected: str) -> None:
    type_info: TypeInfo = getattr(_SchemaAllTypes, "__tvm_ffi_type_info__")
    for field in type_info.fields:
        if field.name == field_name:
            actual: TypeSchema = TypeSchema.from_json_str(field.metadata["type_schema"])
            assert str(actual) == expected, f"{field_name}: {actual}"
            assert actual.repr(_replace_container_types) == (
                expected.replace("Array", "Sequence")
                .replace("List", "MutableSequence")
                .replace("Map", "Mapping")
                .replace("Dict", "MutableMapping")
            )
            break
    else:
        raise ValueError(f"Field not found: {field_name}")


@pytest.mark.parametrize(
    "method_name,expected",
    [
        ("add_int", "Callable[[testing.SchemaAllTypes, int], int]"),
        ("append_int", "Callable[[testing.SchemaAllTypes, Array[int], int], Array[int]]"),
        ("maybe_concat", "Callable[[testing.SchemaAllTypes, str | None, str | None], str | None]"),
        (
            "merge_map",
            "Callable[[testing.SchemaAllTypes, Map[str, Array[int]], Map[str, Array[int]]], Map[str, Array[int]]]",
        ),
        ("make_with", "Callable[[int, float, str], testing.SchemaAllTypes]"),
    ],
)
def test_schema_member_method(method_name: str, expected: str) -> None:
    type_info: TypeInfo = getattr(_SchemaAllTypes, "__tvm_ffi_type_info__")
    for method in type_info.methods:
        if method.name == method_name:
            actual: TypeSchema = TypeSchema.from_json_str(method.metadata["type_schema"])
            assert str(actual) == expected, f"{method_name}: {actual}"
            assert actual.repr(_replace_container_types) == (
                expected.replace("Array", "Sequence")
                .replace("List", "MutableSequence")
                .replace("Map", "Mapping")
                .replace("Dict", "MutableMapping")
            )
            break
    else:
        raise ValueError(f"Method not found: {method_name}")


def test_metadata_global_func() -> None:
    metadata: dict[str, Any] = get_global_func_metadata("testing.schema_id_int")
    assert len(metadata) == 4
    assert "type_schema" in metadata
    assert metadata["bool_attr"] is True
    assert metadata["int_attr"] == 1
    assert metadata["str_attr"] == "hello"


def test_metadata_empty_for_python_func() -> None:
    @register_global_func("test.python_func_no_metadata")
    def simple_func(x: int) -> int:
        return x + 1

    metadata = get_global_func_metadata("test.python_func_no_metadata")
    assert metadata == {}
    remove_global_func("test.python_func_no_metadata")


def test_metadata_field() -> None:
    type_info: TypeInfo = getattr(_SchemaAllTypes, "__tvm_ffi_type_info__")
    for field in type_info.fields:
        if field.name == "v_bool":
            assert len(field.metadata) == 4
            assert "type_schema" in field.metadata
            assert field.metadata["bool_attr"] is True
            assert field.metadata["int_attr"] == 1
            assert field.metadata["str_attr"] == "hello"
            break
    else:
        raise ValueError("Field not found: v_bool")


def test_metadata_member_method() -> None:
    type_info: TypeInfo = getattr(_SchemaAllTypes, "__tvm_ffi_type_info__")
    for method in type_info.methods:
        if method.name == "add_int":
            assert len(method.metadata) == 4
            assert "type_schema" in method.metadata
            assert method.metadata["bool_attr"] is True
            assert method.metadata["int_attr"] == 1
            assert method.metadata["str_attr"] == "hello"
            break
    else:
        raise ValueError("Method not found: add_int")


def test_mem_fn_as_global_func() -> None:
    metadata: dict[str, Any] = get_global_func_metadata("testing.TestIntPairSum")
    type_schema: TypeSchema = TypeSchema.from_json_str(metadata["type_schema"])
    assert str(type_schema) == "Callable[[testing.TestIntPair], int]"


def test_class_metadata_none() -> None:
    assert _lookup_type_attr(64, "__metadata__") is None
