# 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.
# ruff: noqa: F401

"""
Comprehensive test cases for Relax to PyFunc converter.
Tests all major features including basic operations, call_tir, call_dps_packed, and symbolic shapes.
"""

import numpy as np
import pytest
import torch
import torch.nn.functional as F

import tvm
from tvm.relax.relax_to_pyfunc_converter import RelaxToPyFuncConverter
from tvm.script import ir as I
from tvm.script import relax as R
from tvm.script import tirx as T


@I.ir_module
class ComprehensiveTestModule:
    """Test module covering all converter features."""

    @T.prim_func(s_tir=True)
    def add_tir(var_x: T.handle, var_y: T.handle, var_out: T.handle):
        """TIR function for addition."""
        x = T.match_buffer(var_x, (5,), "float32")
        y = T.match_buffer(var_y, (5,), "float32")
        out = T.match_buffer(var_out, (5,), "float32")
        for i in range(5):
            out[i] = x[i] + y[i]

    @T.prim_func(s_tir=True)
    def mul_tir(var_x: T.handle, var_y: T.handle, var_out: T.handle):
        """TIR function for multiplication."""
        x = T.match_buffer(var_x, (3, 4), "float32")
        y = T.match_buffer(var_y, (3, 4), "float32")
        out = T.match_buffer(var_out, (3, 4), "float32")
        for i in range(3):
            for j in range(4):
                out[i, j] = x[i, j] * y[i, j]

    @R.function
    def simple_add(x: R.Tensor((5,), "float32"), y: R.Tensor((5,), "float32")) -> R.Tensor(
        (5,), "float32"
    ):
        return R.add(x, y)

    @R.function
    def with_relu(x: R.Tensor((5,), "float32")) -> R.Tensor((5,), "float32"):
        return R.nn.relu(x)

    @R.function
    def with_call_tir(x: R.Tensor((5,), "float32"), y: R.Tensor((5,), "float32")) -> R.Tensor(
        (5,), "float32"
    ):
        cls = ComprehensiveTestModule
        return R.call_tir(cls.add_tir, (x, y), out_ty=R.Tensor((5,), "float32"))

    @R.function
    def with_call_dps_packed(x: R.Tensor((5,), "float32")) -> R.Tensor((5,), "float32"):
        return R.call_dps_packed(
            "my_softmax", (x, R.prim_value(1)), out_ty=R.Tensor((5,), "float32")
        )

    @R.function
    def complex_function(x: R.Tensor((5,), "float32"), y: R.Tensor((5,), "float32")) -> R.Tensor(
        (5,), "float32"
    ):
        added = R.add(x, y)
        relued = R.nn.relu(added)
        cls = ComprehensiveTestModule
        tir_result = R.call_tir(cls.add_tir, (relued, y), out_ty=R.Tensor((5,), "float32"))
        return R.nn.relu(tir_result)

    @R.function
    def symbolic_add(x: R.Tensor(("n",), "float32"), y: R.Tensor(("n",), "float32")) -> R.Tensor(
        ("n",), "float32"
    ):
        return R.add(x, y)

    @R.function
    def symbolic_matmul(
        x: R.Tensor(("batch", "m", "k"), "float32"), y: R.Tensor(("batch", "k", "n"), "float32")
    ) -> R.Tensor(("batch", "m", "n"), "float32"):
        return R.matmul(x, y)

    @R.function
    def symbolic_expand_dims(x: R.Tensor(("batch", "seq_len"), "float32")) -> R.Tensor(
        ("batch", "seq_len", 1), "float32"
    ):
        return R.expand_dims(x, axis=2)

    @R.function
    def multi_ops(x: R.Tensor((3, 4), "float32"), y: R.Tensor((3, 4), "float32")) -> R.Tensor(
        (3, 4), "float32"
    ):
        added = R.add(x, y)
        multiplied = R.multiply(added, y)
        powered = R.power(multiplied, R.const(2.0))
        maxed = R.maximum(powered, x)
        return maxed

    @R.function
    def reduction_ops(x: R.Tensor((5,), "float32")) -> R.Tensor((), "float32"):
        sum_val = R.sum(x)
        mean_val = R.mean(x)
        max_val = R.max(x)
        return R.add(R.add(sum_val, mean_val), max_val)

    @R.function
    def comparison_ops(x: R.Tensor((5,), "float32"), y: R.Tensor((5,), "float32")) -> R.Tensor(
        (5,), "bool"
    ):
        eq_val = R.equal(x, y)
        gt_val = R.greater(x, y)
        return R.logical_and(eq_val, gt_val)

    @R.function
    def test_reshape(x: R.Tensor((2, 3), "float32")) -> R.Tensor((6,), "float32"):
        return R.reshape(x, (6,))

    @R.function
    def test_permute_dims(x: R.Tensor((2, 3, 4), "float32")) -> R.Tensor((4, 2, 3), "float32"):
        return R.permute_dims(x, axes=[2, 0, 1])

    @R.function
    def test_concat(x: R.Tensor((2, 3), "float32"), y: R.Tensor((2, 3), "float32")) -> R.Tensor(
        (4, 3), "float32"
    ):
        return R.concat((x, y), axis=0)

    @R.function
    def test_split(x: R.Tensor((4, 3), "float32")) -> R.Tuple:
        return R.split(x, indices_or_sections=2, axis=0)

    @R.function
    def test_stack(x: R.Tensor((2, 3), "float32"), y: R.Tensor((2, 3), "float32")) -> R.Tensor(
        (2, 2, 3), "float32"
    ):
        return R.stack((x, y), axis=1)

    @R.function
    def test_take(x: R.Tensor((3, 4), "float32"), indices: R.Tensor((2,), "int64")) -> R.Tensor(
        (2,), "float32"
    ):
        return R.take(x, indices, axis=0)

    @R.function
    def test_flip(x: R.Tensor((2, 3), "float32")) -> R.Tensor((2, 3), "float32"):
        return R.flip(x, axis=1)

    @R.function
    def test_tile(x: R.Tensor((2, 3), "float32")) -> R.Tensor((4, 6), "float32"):
        return R.tile(x, (2, 2))

    @R.function
    def test_repeat(x: R.Tensor((2, 3), "float32")) -> R.Tensor((4, 3), "float32"):
        return R.repeat(x, repeats=2, axis=0)

    @R.function
    def test_expand_dims(x: R.Tensor((2, 3), "float32")) -> R.Tensor((2, 3, 1), "float32"):
        return R.expand_dims(x, axis=2)

    @R.function
    def test_squeeze(x: R.Tensor((2, 3, 1), "float32")) -> R.Tensor((2, 3), "float32"):
        return R.squeeze(x, axis=2)

    @R.function
    def test_sum_with_axis(x: R.Tensor((2, 3), "float32")) -> R.Tensor((3,), "float32"):
        return R.sum(x, axis=0)

    @R.function
    def test_max_with_axis(x: R.Tensor((2, 3), "float32")) -> R.Tensor((3,), "float32"):
        return R.max(x, axis=0)


def create_mock_packed_function():
    """Create a mock packed function for testing."""

    def mock_softmax(x, axis):
        """Mock softmax function that just returns the input."""
        return x

    # Register the function globally
    tvm.register_global_func("my_softmax", mock_softmax)


class TestRelaxToPyFuncConverter:
    """Comprehensive test class for Relax to PyFunc converter."""

    @classmethod
    def setup_class(cls):
        """Set up test fixtures."""
        cls.ir_mod = ComprehensiveTestModule
        cls.converter = RelaxToPyFuncConverter(cls.ir_mod)
        create_mock_packed_function()

    def test_basic_operations(self):
        """Test basic arithmetic operations."""
        converted_ir_mod = self.converter.convert(["simple_add", "with_relu"])

        # Test simple_add
        x = torch.tensor([1.0, 2.0, 3.0, 4.0, 5.0], dtype=torch.float32)
        y = torch.tensor([0.1, 0.2, 0.3, 0.4, 0.5], dtype=torch.float32)

        result = converted_ir_mod.pyfuncs["simple_add"](x, y)
        expected = torch.add(x, y)
        assert torch.allclose(result, expected)

        # Test with_relu
        x_neg = torch.tensor([-2.0, -1.0, 0.0, 1.0, 2.0], dtype=torch.float32)
        result = converted_ir_mod.pyfuncs["with_relu"](x_neg)
        expected = torch.nn.functional.relu(x_neg)
        assert torch.allclose(result, expected)

    def test_call_tir(self):
        """Test call_tir functionality with DLPack conversion."""
        converted_ir_mod = self.converter.convert(["with_call_tir"])

        x = torch.tensor([1.0, 2.0, 3.0, 4.0, 5.0], dtype=torch.float32)
        y = torch.tensor([0.1, 0.2, 0.3, 0.4, 0.5], dtype=torch.float32)

        result = converted_ir_mod.pyfuncs["with_call_tir"](x, y)
        expected = torch.add(x, y)
        assert torch.allclose(result, expected)
        assert result.shape == expected.shape

    def test_call_dps_packed(self):
        """Test call_dps_packed functionality."""
        converted_ir_mod = self.converter.convert(["with_call_dps_packed"])

        x = torch.tensor([1.0, 2.0, 3.0, 4.0, 5.0], dtype=torch.float32)

        result = converted_ir_mod.pyfuncs["with_call_dps_packed"](x)
        expected = x
        assert torch.allclose(result, expected)

    def test_complex_function(self):
        """Test complex function with multiple operations."""
        converted_ir_mod = self.converter.convert(["complex_function"])

        x = torch.tensor([1.0, 2.0, 3.0, 4.0, 5.0], dtype=torch.float32)
        y = torch.tensor([0.1, 0.2, 0.3, 0.4, 0.5], dtype=torch.float32)

        result = converted_ir_mod.pyfuncs["complex_function"](x, y)

        # Expected: relu(add(relu(add(x, y)), y))
        step1 = torch.add(x, y)
        step2 = torch.nn.functional.relu(step1)
        step3 = torch.add(step2, y)  # TIR call
        expected = torch.nn.functional.relu(step3)

        assert torch.allclose(result, expected)

    def test_symbolic_shapes(self):
        """Test symbolic shape handling."""
        converted_ir_mod = self.converter.convert(
            ["symbolic_add", "symbolic_matmul", "symbolic_expand_dims"]
        )

        # Test symbolic_add
        x = torch.tensor([1.0, 2.0, 3.0], dtype=torch.float32)
        y = torch.tensor([0.1, 0.2, 0.3], dtype=torch.float32)
        result = converted_ir_mod.pyfuncs["symbolic_add"](x, y)
        expected = torch.add(x, y)
        assert torch.allclose(result, expected)

        # Test symbolic_matmul
        x = torch.randn(2, 3, 4, dtype=torch.float32)  # (batch=2, m=3, k=4)
        y = torch.randn(2, 4, 5, dtype=torch.float32)  # (batch=2, k=4, n=5)
        result = converted_ir_mod.pyfuncs["symbolic_matmul"](x, y)
        expected = torch.matmul(x, y)
        assert torch.allclose(result, expected)
        assert result.shape == (2, 3, 5)

        # Test symbolic_expand_dims
        x = torch.tensor([[1.0, 2.0], [3.0, 4.0]], dtype=torch.float32)
        result = converted_ir_mod.pyfuncs["symbolic_expand_dims"](x)
        expected = torch.unsqueeze(x, dim=2)
        assert torch.allclose(result, expected)
        assert result.shape == (2, 2, 1)

    def test_multi_operations(self):
        """Test multiple operations in sequence."""
        converted_ir_mod = self.converter.convert(["multi_ops"])

        x = torch.tensor(
            [[1.0, 2.0, 3.0, 4.0], [5.0, 6.0, 7.0, 8.0], [9.0, 10.0, 11.0, 12.0]],
            dtype=torch.float32,
        )
        y = torch.tensor(
            [[0.1, 0.2, 0.3, 0.4], [0.5, 0.6, 0.7, 0.8], [0.9, 1.0, 1.1, 1.2]], dtype=torch.float32
        )

        result = converted_ir_mod.pyfuncs["multi_ops"](x, y)

        # Expected: maximum(power(multiply(add(x, y), y), 2), x)
        step1 = torch.add(x, y)
        step2 = torch.mul(step1, y)
        step3 = torch.pow(step2, 2.0)
        expected = torch.maximum(step3, x)

        assert torch.allclose(result, expected)

    def test_reduction_operations(self):
        """Test reduction operations."""
        converted_ir_mod = self.converter.convert(["reduction_ops"])

        x = torch.tensor([1.0, 2.0, 3.0, 4.0, 5.0], dtype=torch.float32)

        result = converted_ir_mod.pyfuncs["reduction_ops"](x)

        # Expected: sum(x) + mean(x) + max(x)
        expected = torch.sum(x) + torch.mean(x) + torch.max(x)

        assert torch.allclose(result, expected)
        assert result.shape == ()

    def test_comparison_operations(self):
        """Test comparison operations."""
        converted_ir_mod = self.converter.convert(["comparison_ops"])

        x = torch.tensor([1.0, 2.0, 3.0, 4.0, 5.0], dtype=torch.float32)
        y = torch.tensor([1.0, 2.5, 3.0, 4.5, 5.0], dtype=torch.float32)

        result = converted_ir_mod.pyfuncs["comparison_ops"](x, y)

        # Expected: logical_and(equal(x, y), greater(x, y))
        eq_val = torch.eq(x, y)
        gt_val = torch.gt(x, y)
        expected = torch.logical_and(eq_val, gt_val)

        assert torch.allclose(result, expected)
        assert result.dtype == torch.bool

    def test_operator_mapping_completeness(self):
        """Test that operator mapping is comprehensive."""
        operator_map = RelaxToPyFuncConverter._get_op_map()

        # Check that we have a good number of operators
        assert len(operator_map) > 100, f"Expected >100 operators, got {len(operator_map)}"

        # Check key operator categories
        binary_ops = [
            op
            for op in operator_map.keys()
            if op.startswith("relax.") and not op.startswith("relax.nn.")
        ]
        nn_ops = [op for op in operator_map.keys() if op.startswith("relax.nn.")]

        assert len(binary_ops) > 20, f"Expected >20 binary ops, got {len(binary_ops)}"
        assert len(nn_ops) > 30, f"Expected >30 nn ops, got {len(nn_ops)}"

        # Check specific important operators
        important_ops = [
            "relax.add",
            "relax.multiply",
            "relax.nn.relu",
            "relax.nn.softmax",
            "relax.matmul",
            "relax.reshape",
            "relax.sum",
            "relax.mean",
        ]

        for op in important_ops:
            assert op in operator_map, f"Missing important operator: {op}"

    def test_error_handling(self):
        """Test error handling for invalid inputs."""
        converted_ir_mod = self.converter.convert(["simple_add"])

        # Test with wrong number of arguments
        x = torch.tensor([1.0, 2.0, 3.0], dtype=torch.float32)

        with pytest.raises(ValueError, match="Expected 2 arguments"):
            converted_ir_mod.pyfuncs["simple_add"](x)  # Missing second argument

        # Test with incompatible shapes - this should raise a RuntimeError
        x = torch.tensor([1.0, 2.0, 3.0], dtype=torch.float32)
        y = torch.tensor([1.0, 2.0], dtype=torch.float32)  # Different shape

        # This should raise a RuntimeError because shapes don't match
        with pytest.raises(RuntimeError, match="The size of tensor a"):
            converted_ir_mod.pyfuncs["simple_add"](x, y)

    def test_conversion_metadata(self):
        """Test that conversion preserves metadata correctly."""
        converted_ir_mod = self.converter.convert(["simple_add"])

        # Check that pyfuncs attribute exists
        assert hasattr(converted_ir_mod, "pyfuncs")
        assert "simple_add" in converted_ir_mod.pyfuncs

        # Check function metadata
        pyfunc = converted_ir_mod.pyfuncs["simple_add"]
        assert hasattr(pyfunc, "__name__")
        assert hasattr(pyfunc, "__doc__")
        assert pyfunc.__name__ == "simple_add"

    def test_tensor_operations(self):
        """Test tensor manipulation operations."""
        converted_ir_mod = self.converter.convert(
            [
                "test_reshape",
                "test_permute_dims",
                "test_concat",
                "test_split",
                "test_stack",
                "test_take",
                "test_flip",
                "test_tile",
                "test_repeat",
                "test_expand_dims",
                "test_squeeze",
                "test_sum_with_axis",
                "test_max_with_axis",
            ]
        )

        # Test reshape
        x1 = torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]], dtype=torch.float32)
        result1 = converted_ir_mod.pyfuncs["test_reshape"](x1)
        expected1 = torch.reshape(x1, (6,))
        assert torch.allclose(result1, expected1), "Reshape operation failed"

        # Test permute_dims
        x2 = torch.randn(2, 3, 4)
        result2 = converted_ir_mod.pyfuncs["test_permute_dims"](x2)
        expected2 = torch.permute(x2, (2, 0, 1))
        assert torch.allclose(result2, expected2), "Permute_dims operation failed"

        # Test concat
        x3 = torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]], dtype=torch.float32)
        y3 = torch.tensor([[7.0, 8.0, 9.0], [10.0, 11.0, 12.0]], dtype=torch.float32)
        result3 = converted_ir_mod.pyfuncs["test_concat"](x3, y3)
        expected3 = torch.cat([x3, y3], dim=0)
        assert torch.allclose(result3, expected3), "Concat operation failed"

        # Test split
        x4 = torch.tensor(
            [[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0], [10.0, 11.0, 12.0]],
            dtype=torch.float32,
        )
        result4 = converted_ir_mod.pyfuncs["test_split"](x4)
        expected4 = torch.split(x4, 2, dim=0)
        assert len(result4) == len(expected4), "Split operation failed - wrong number of tensors"
        for r, e in zip(result4, expected4):
            assert torch.allclose(r, e), "Split operation failed - tensor mismatch"

        # Test stack
        x5 = torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]], dtype=torch.float32)
        y5 = torch.tensor([[7.0, 8.0, 9.0], [10.0, 11.0, 12.0]], dtype=torch.float32)
        result5 = converted_ir_mod.pyfuncs["test_stack"](x5, y5)
        expected5 = torch.stack([x5, y5], dim=1)
        assert torch.allclose(result5, expected5), "Stack operation failed"

        # Test take
        x6 = torch.tensor(
            [[1.0, 2.0, 3.0, 4.0], [5.0, 6.0, 7.0, 8.0], [9.0, 10.0, 11.0, 12.0]],
            dtype=torch.float32,
        )
        indices = torch.tensor([0, 2], dtype=torch.int64)
        result6 = converted_ir_mod.pyfuncs["test_take"](x6, indices)
        expected6 = x6[indices]
        assert torch.allclose(result6, expected6), "Take operation failed"

        # Test flip
        x7 = torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]], dtype=torch.float32)
        result7 = converted_ir_mod.pyfuncs["test_flip"](x7)
        expected7 = torch.flip(x7, dims=[1])
        assert torch.allclose(result7, expected7), "Flip operation failed"

        # Test tile
        x8 = torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]], dtype=torch.float32)
        result8 = converted_ir_mod.pyfuncs["test_tile"](x8)
        expected8 = torch.tile(x8, (2, 2))
        assert torch.allclose(result8, expected8), "Tile operation failed"

        # Test repeat
        x9 = torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]], dtype=torch.float32)
        result9 = converted_ir_mod.pyfuncs["test_repeat"](x9)
        expected9 = torch.repeat_interleave(x9, repeats=2, dim=0)
        assert torch.allclose(result9, expected9), "Repeat operation failed"

        # Test expand_dims
        x10 = torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]], dtype=torch.float32)
        result10 = converted_ir_mod.pyfuncs["test_expand_dims"](x10)
        expected10 = torch.unsqueeze(x10, dim=2)
        assert torch.allclose(result10, expected10), "Expand_dims operation failed"

        # Test squeeze
        x11 = torch.tensor([[[1.0], [2.0], [3.0]], [[4.0], [5.0], [6.0]]], dtype=torch.float32)
        result11 = converted_ir_mod.pyfuncs["test_squeeze"](x11)
        expected11 = torch.squeeze(x11, dim=2)
        assert torch.allclose(result11, expected11), "Squeeze operation failed"

        # Test sum with axis
        x12 = torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]], dtype=torch.float32)
        result12 = converted_ir_mod.pyfuncs["test_sum_with_axis"](x12)
        expected12 = torch.sum(x12, dim=0)
        assert torch.allclose(result12, expected12), "Sum with axis operation failed"

        # Test max with axis
        x13 = torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]], dtype=torch.float32)
        result13 = converted_ir_mod.pyfuncs["test_max_with_axis"](x13)
        expected13 = torch.max(x13, dim=0)[0]  # torch.max returns (values, indices)
        assert torch.allclose(result13, expected13), "Max with axis operation failed"


@I.ir_module
class ExtendedOperatorsModule:
    """Extended test module with additional operators not covered in ComprehensiveTestModule."""

    # Unary operations not covered in ComprehensiveTestModule
    @R.function
    def test_abs(x: R.Tensor((5,), "float32")) -> R.Tensor((5,), "float32"):
        return R.abs(x)

    @R.function
    def test_neg(x: R.Tensor((5,), "float32")) -> R.Tensor((5,), "float32"):
        return R.negative(x)

    @R.function
    def test_exp(x: R.Tensor((5,), "float32")) -> R.Tensor((5,), "float32"):
        return R.exp(x)

    @R.function
    def test_log(x: R.Tensor((5,), "float32")) -> R.Tensor((5,), "float32"):
        return R.log(x)

    @R.function
    def test_sqrt(x: R.Tensor((5,), "float32")) -> R.Tensor((5,), "float32"):
        return R.sqrt(x)

    @R.function
    def test_sin(x: R.Tensor((5,), "float32")) -> R.Tensor((5,), "float32"):
        return R.sin(x)

    @R.function
    def test_cos(x: R.Tensor((5,), "float32")) -> R.Tensor((5,), "float32"):
        return R.cos(x)

    @R.function
    def test_tanh(x: R.Tensor((5,), "float32")) -> R.Tensor((5,), "float32"):
        return R.tanh(x)

    @R.function
    def test_sigmoid(x: R.Tensor((5,), "float32")) -> R.Tensor((5,), "float32"):
        return R.sigmoid(x)

    # Comparison operations not covered in ComprehensiveTestModule
    @R.function
    def test_less(x: R.Tensor((5,), "float32"), y: R.Tensor((5,), "float32")) -> R.Tensor(
        (5,), "bool"
    ):
        return R.less(x, y)

    @R.function
    def test_not_equal(x: R.Tensor((5,), "float32"), y: R.Tensor((5,), "float32")) -> R.Tensor(
        (5,), "bool"
    ):
        return R.not_equal(x, y)

    # Binary operations not covered in ComprehensiveTestModule
    @R.function
    def test_multiply(x: R.Tensor((5,), "float32"), y: R.Tensor((5,), "float32")) -> R.Tensor(
        (5,), "float32"
    ):
        return R.multiply(x, y)

    @R.function
    def test_divide(x: R.Tensor((5,), "float32"), y: R.Tensor((5,), "float32")) -> R.Tensor(
        (5,), "float32"
    ):
        return R.divide(x, y)

    @R.function
    def test_power(x: R.Tensor((5,), "float32"), y: R.Tensor((5,), "float32")) -> R.Tensor(
        (5,), "float32"
    ):
        return R.power(x, y)

    @R.function
    def test_maximum(x: R.Tensor((5,), "float32"), y: R.Tensor((5,), "float32")) -> R.Tensor(
        (5,), "float32"
    ):
        return R.maximum(x, y)

    @R.function
    def test_minimum(x: R.Tensor((5,), "float32"), y: R.Tensor((5,), "float32")) -> R.Tensor(
        (5,), "float32"
    ):
        return R.minimum(x, y)

    @R.function
    def test_subtract(x: R.Tensor((5,), "float32"), y: R.Tensor((5,), "float32")) -> R.Tensor(
        (5,), "float32"
    ):
        return R.subtract(x, y)

    # Additional tensor operations with different parameters
    @R.function
    def test_transpose_2d(x: R.Tensor((2, 4), "float32")) -> R.Tensor((4, 2), "float32"):
        return R.permute_dims(x, axes=[1, 0])

    @R.function
    def test_mean_axis(x: R.Tensor((2, 3), "float32")) -> R.Tensor((3,), "float32"):
        return R.mean(x, axis=0)

    @R.function
    def test_min_axis(x: R.Tensor((2, 3), "float32")) -> R.Tensor((3,), "float32"):
        return R.min(x, axis=0)

    # Neural network operations not covered in ComprehensiveTestModule
    @R.function
    def test_gelu_nn(x: R.Tensor((5,), "float32")) -> R.Tensor((5,), "float32"):
        return R.nn.gelu(x)

    @R.function
    def test_softmax_nn(x: R.Tensor((2, 5), "float32")) -> R.Tensor((2, 5), "float32"):
        return R.nn.softmax(x, axis=1)

    @R.function
    def test_log_softmax_nn(x: R.Tensor((2, 5), "float32")) -> R.Tensor((2, 5), "float32"):
        return R.nn.log_softmax(x, axis=1)

    # Advanced tensor operations with different parameters
    @R.function
    def test_tile_dims(x: R.Tensor((2, 3), "float32")) -> R.Tensor((4, 9), "float32"):
        return R.tile(x, (2, 3))

    @R.function
    def test_repeat_axis(x: R.Tensor((3,), "float32")) -> R.Tensor((6,), "float32"):
        return R.repeat(x, repeats=2, axis=0)


class TestExtendedOperators:
    """Test class for extended operator coverage."""

    @classmethod
    def setup_class(cls):
        """Set up test fixtures."""
        cls.ir_mod = ExtendedOperatorsModule
        cls.converter = RelaxToPyFuncConverter(cls.ir_mod)

    def test_unary_operations(self):
        """Test unary operations."""
        converted_ir_mod = self.converter.convert(
            ["test_abs", "test_neg", "test_exp", "test_log", "test_sqrt"]
        )

        x = torch.tensor([-2.0, -1.0, 0.0, 1.0, 2.0], dtype=torch.float32)

        # Test abs
        result = converted_ir_mod.pyfuncs["test_abs"](x)
        expected = torch.abs(x)
        assert torch.allclose(result, expected)

        # Test negative
        result = converted_ir_mod.pyfuncs["test_neg"](x)
        expected = torch.neg(x)
        assert torch.allclose(result, expected)

        # Test exp
        result = converted_ir_mod.pyfuncs["test_exp"](x)
        expected = torch.exp(x)
        assert torch.allclose(result, expected)

        # Test log (with positive values)
        x_pos = torch.tensor([1.0, 2.0, 3.0, 4.0, 5.0], dtype=torch.float32)
        result = converted_ir_mod.pyfuncs["test_log"](x_pos)
        expected = torch.log(x_pos)
        assert torch.allclose(result, expected)

        # Test sqrt
        result = converted_ir_mod.pyfuncs["test_sqrt"](x_pos)
        expected = torch.sqrt(x_pos)
        assert torch.allclose(result, expected)

    def test_trigonometric_operations(self):
        """Test trigonometric operations."""
        converted_ir_mod = self.converter.convert(
            ["test_sin", "test_cos", "test_tanh", "test_sigmoid"]
        )

        x = torch.tensor([0.0, 0.5, 1.0, 1.5, 2.0], dtype=torch.float32)

        # Test sin
        result = converted_ir_mod.pyfuncs["test_sin"](x)
        expected = torch.sin(x)
        assert torch.allclose(result, expected)

        # Test cos
        result = converted_ir_mod.pyfuncs["test_cos"](x)
        expected = torch.cos(x)
        assert torch.allclose(result, expected)

        # Test tanh
        result = converted_ir_mod.pyfuncs["test_tanh"](x)
        expected = torch.tanh(x)
        assert torch.allclose(result, expected)

        # Test sigmoid
        result = converted_ir_mod.pyfuncs["test_sigmoid"](x)
        expected = torch.sigmoid(x)
        assert torch.allclose(result, expected)

    def test_comparison_operations(self):
        """Test comparison operations not covered in ComprehensiveTestModule."""
        converted_ir_mod = self.converter.convert(["test_less", "test_not_equal"])

        x = torch.tensor([1.0, 2.0, 3.0, 4.0, 5.0], dtype=torch.float32)
        y = torch.tensor([2.0, 2.0, 2.0, 2.0, 2.0], dtype=torch.float32)

        # Test less
        result = converted_ir_mod.pyfuncs["test_less"](x, y)
        expected = torch.lt(x, y)
        assert torch.equal(result, expected)

        # Test not equal
        result = converted_ir_mod.pyfuncs["test_not_equal"](x, y)
        expected = torch.ne(x, y)
        assert torch.equal(result, expected)

    def test_binary_operations(self):
        """Test binary operations."""
        converted_ir_mod = self.converter.convert(
            [
                "test_multiply",
                "test_divide",
                "test_power",
                "test_maximum",
                "test_minimum",
                "test_subtract",
            ]
        )

        x = torch.tensor([1.0, 2.0, 3.0, 4.0, 5.0], dtype=torch.float32)
        y = torch.tensor([2.0, 2.0, 2.0, 2.0, 2.0], dtype=torch.float32)

        # Test multiply
        result = converted_ir_mod.pyfuncs["test_multiply"](x, y)
        expected = torch.mul(x, y)
        assert torch.allclose(result, expected)

        # Test divide
        result = converted_ir_mod.pyfuncs["test_divide"](x, y)
        expected = torch.div(x, y)
        assert torch.allclose(result, expected)

        # Test power
        result = converted_ir_mod.pyfuncs["test_power"](x, y)
        expected = torch.pow(x, y)
        assert torch.allclose(result, expected)

        # Test maximum
        result = converted_ir_mod.pyfuncs["test_maximum"](x, y)
        expected = torch.maximum(x, y)
        assert torch.allclose(result, expected)

        # Test minimum
        result = converted_ir_mod.pyfuncs["test_minimum"](x, y)
        expected = torch.minimum(x, y)
        assert torch.allclose(result, expected)

        # Test subtract
        result = converted_ir_mod.pyfuncs["test_subtract"](x, y)
        expected = torch.sub(x, y)
        assert torch.allclose(result, expected)

    def test_tensor_operations(self):
        """Test tensor operations not covered in ComprehensiveTestModule."""
        converted_ir_mod = self.converter.convert(["test_transpose_2d"])

        x = torch.tensor([[1.0, 2.0, 3.0, 4.0], [5.0, 6.0, 7.0, 8.0]], dtype=torch.float32)

        # Test transpose
        result = converted_ir_mod.pyfuncs["test_transpose_2d"](x)
        expected = torch.transpose(x, 0, 1)
        assert torch.allclose(result, expected)
        assert result.shape == (4, 2)

    def test_reduction_operations(self):
        """Test reduction operations not covered in ComprehensiveTestModule."""
        converted_ir_mod = self.converter.convert(["test_mean_axis", "test_min_axis"])

        x = torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]], dtype=torch.float32)

        # Test mean
        result = converted_ir_mod.pyfuncs["test_mean_axis"](x)
        expected = torch.mean(x, dim=0)
        assert torch.allclose(result, expected)
        assert result.shape == (3,)

        # Test min
        result = converted_ir_mod.pyfuncs["test_min_axis"](x)
        expected = torch.min(x, dim=0)[0]
        assert torch.allclose(result, expected)
        assert result.shape == (3,)

    def test_neural_network_operations(self):
        """Test neural network operations not covered in ComprehensiveTestModule."""
        converted_ir_mod = self.converter.convert(
            ["test_gelu_nn", "test_softmax_nn", "test_log_softmax_nn"]
        )

        x = torch.tensor(
            [[-2.0, -1.0, 0.0, 1.0, 2.0], [0.5, 1.5, 2.5, 3.5, 4.5]], dtype=torch.float32
        )

        # Test gelu
        result = converted_ir_mod.pyfuncs["test_gelu_nn"](x[0])
        expected = F.gelu(x[0])
        assert torch.allclose(result, expected)

        # Test softmax
        result = converted_ir_mod.pyfuncs["test_softmax_nn"](x)
        expected = F.softmax(x, dim=1)
        assert torch.allclose(result, expected)

        # Test log_softmax
        result = converted_ir_mod.pyfuncs["test_log_softmax_nn"](x)
        expected = F.log_softmax(x, dim=1)
        assert torch.allclose(result, expected)

    def test_advanced_tensor_operations(self):
        """Test advanced tensor operations with different parameters."""
        converted_ir_mod = self.converter.convert(["test_tile_dims", "test_repeat_axis"])

        x = torch.tensor([[1.0, 2.0, 3.0, 4.0], [5.0, 6.0, 7.0, 8.0]], dtype=torch.float32)

        # Test tile with different dimensions
        result = converted_ir_mod.pyfuncs["test_tile_dims"](x)
        expected = torch.tile(x, (2, 3))
        assert torch.allclose(result, expected)
        assert result.shape == (4, 12)

        # Test repeat with different parameters
        x_1d = torch.tensor([1.0, 2.0, 3.0], dtype=torch.float32)
        result = converted_ir_mod.pyfuncs["test_repeat_axis"](x_1d)
        expected = torch.repeat_interleave(x_1d, repeats=2, dim=0)
        assert torch.allclose(result, expected)
        assert result.shape == (6,)


class TestDLPackAndTupleSupport:
    """Test DLPack conversion, tuple handling, and API compatibility features."""

    def test_dlpack_conversion_fallback(self):
        """Test DLPack conversion with numpy fallback."""

        @I.ir_module
        class DLPackTestModule:
            @T.prim_func(s_tir=True)
            def test_tir(var_x: T.handle, var_y: T.handle, var_out: T.handle):
                x = T.match_buffer(var_x, (4,), "float32")
                y = T.match_buffer(var_y, (4,), "float32")
                out = T.match_buffer(var_out, (4,), "float32")
                for i in range(4):
                    out[i] = x[i] + y[i]

            @R.function
            def test_func(x: R.Tensor((4,), "float32"), y: R.Tensor((4,), "float32")) -> R.Tensor(
                (4,), "float32"
            ):
                return R.call_tir(
                    DLPackTestModule.test_tir, (x, y), out_ty=R.Tensor((4,), "float32")
                )

        converter = RelaxToPyFuncConverter(DLPackTestModule)
        converted_ir_mod = converter.convert(["test_func"])

        x = torch.tensor([1.0, 2.0, 3.0, 4.0], dtype=torch.float32)
        y = torch.tensor([0.1, 0.2, 0.3, 0.4], dtype=torch.float32)

        result = converted_ir_mod.pyfuncs["test_func"](x, y)
        expected = torch.add(x, y)

        assert torch.allclose(result, expected), "DLPack conversion with numpy fallback failed"

    def test_tuple_return_handling(self):
        """Test proper handling of tuple returns (e.g., split operation)."""

        @I.ir_module
        class TupleTestModule:
            @R.function
            def test_split(x: R.Tensor((6,), "float32")) -> R.Tuple:
                return R.split(x, indices_or_sections=3, axis=0)

        converter = RelaxToPyFuncConverter(TupleTestModule)
        converted_ir_mod = converter.convert(["test_split"])

        x = torch.tensor([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], dtype=torch.float32)
        result = converted_ir_mod.pyfuncs["test_split"](x)
        expected = torch.split(x, 2, dim=0)

        assert isinstance(result, tuple), "Split should return tuple"
        assert len(result) == len(expected), "Split should return correct number of tensors"
        for r, e in zip(result, expected):
            assert torch.allclose(r, e), "Split tensor values should match"

    def test_tvm_runtime_api_compatibility(self):
        """Test compatibility with tvm.runtime API instead of deprecated tvm.nd."""

        @I.ir_module
        class RuntimeAPITestModule:
            @T.prim_func(s_tir=True)
            def test_tir(var_x: T.handle, var_y: T.handle, var_out: T.handle):
                x = T.match_buffer(var_x, (3,), "float32")
                y = T.match_buffer(var_y, (3,), "float32")
                out = T.match_buffer(var_out, (3,), "float32")
                for i in range(3):
                    out[i] = x[i] * y[i]

            @R.function
            def test_func(x: R.Tensor((3,), "float32"), y: R.Tensor((3,), "float32")) -> R.Tensor(
                (3,), "float32"
            ):
                return R.call_tir(
                    RuntimeAPITestModule.test_tir, (x, y), out_ty=R.Tensor((3,), "float32")
                )

        converter = RelaxToPyFuncConverter(RuntimeAPITestModule)
        converted_ir_mod = converter.convert(["test_func"])

        x = torch.tensor([1.0, 2.0, 3.0], dtype=torch.float32)
        y = torch.tensor([2.0, 3.0, 4.0], dtype=torch.float32)

        result = converted_ir_mod.pyfuncs["test_func"](x, y)
        expected = torch.mul(x, y)

        assert torch.allclose(result, expected)

    def test_packed_function_with_primvalue_args(self):
        """Test packed function calls with Expr arguments."""

        # Register a test packed function
        def test_packed_func(x, axis):
            return x  # Simple identity function

        tvm.register_global_func("test_packed_func", test_packed_func)

        @I.ir_module
        class PackedFuncTestModule:
            @R.function
            def test_dps(x: R.Tensor((4,), "float32")) -> R.Tensor((4,), "float32"):
                return R.call_dps_packed(
                    "test_packed_func", (x, R.const(0)), out_ty=R.Tensor((4,), "float32")
                )

        converter = RelaxToPyFuncConverter(PackedFuncTestModule)
        converted_ir_mod = converter.convert(["test_dps"])

        x = torch.tensor([1.0, 2.0, 3.0, 4.0], dtype=torch.float32)
        result = converted_ir_mod.pyfuncs["test_dps"](x)
        expected = x  # Identity function

        assert torch.allclose(result, expected), "Packed function with Expr args failed"

    def test_mixed_tir_and_relax_operations(self):
        """Test mixed TIR and Relax operations in a single function."""

        @I.ir_module
        class MixedOpsTestModule:
            @T.prim_func(s_tir=True)
            def add_tir(var_x: T.handle, var_y: T.handle, var_out: T.handle):
                x = T.match_buffer(var_x, (4,), "float32")
                y = T.match_buffer(var_y, (4,), "float32")
                out = T.match_buffer(var_out, (4,), "float32")
                for i in range(4):
                    out[i] = x[i] + y[i]

            @R.function
            def test_mixed(x: R.Tensor((4,), "float32"), y: R.Tensor((4,), "float32")) -> R.Tensor(
                (4,), "float32"
            ):
                # TIR operation
                tir_result = R.call_tir(
                    MixedOpsTestModule.add_tir, (x, y), out_ty=R.Tensor((4,), "float32")
                )
                # Relax operations
                relued = R.nn.relu(tir_result)
                powered = R.power(relued, R.const(2.0))
                return R.nn.gelu(powered)

        converter = RelaxToPyFuncConverter(MixedOpsTestModule)
        converted_ir_mod = converter.convert(["test_mixed"])

        x = torch.tensor([1.0, 2.0, 3.0, 4.0], dtype=torch.float32)
        y = torch.tensor([0.1, 0.2, 0.3, 0.4], dtype=torch.float32)

        result = converted_ir_mod.pyfuncs["test_mixed"](x, y)

        # Manual computation for expected result
        added = torch.add(x, y)
        relued = F.relu(added)
        powered = torch.pow(relued, 2.0)
        expected = F.gelu(powered)

        assert torch.allclose(result, expected)

    def test_error_handling_improvements(self):
        """Test improved error handling with tensor fallbacks."""

        @I.ir_module
        class ErrorHandlingTestModule:
            @R.function
            def test_error_handling(x: R.Tensor((4,), "float32")) -> R.Tensor((4,), "float32"):
                # This should trigger fallback mechanisms
                return R.nn.relu(x)

        converter = RelaxToPyFuncConverter(ErrorHandlingTestModule)
        converted_ir_mod = converter.convert(["test_error_handling"])

        x = torch.tensor([-2.0, -1.0, 0.0, 1.0], dtype=torch.float32)
        result = converted_ir_mod.pyfuncs["test_error_handling"](x)
        expected = F.relu(x)

        assert torch.allclose(result, expected), "Error handling with tensor fallbacks failed"
        assert isinstance(result, torch.Tensor), "Result should be a tensor, not a string"


if __name__ == "__main__":
    tvm.testing.main()
