# 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: E741

import numpy as np
import pytest

import tvm
from tvm import relax, tirx
from tvm.ir import IRModule
from tvm.relax.base_py_module import BasePyModule
from tvm.script import ir as I
from tvm.script import relax as R
from tvm.script import tirx as T


def _make_module():
    return IRModule({})


def test_infer_concrete_shape_from_numpy_input():
    mod = _make_module()
    bpm = BasePyModule(mod, device=tvm.cpu(0), target="llvm")

    n = tirx.Var("n", "int64")
    m = tirx.Var("m", "int64")
    sym_shape = [n, m]

    x = np.zeros((3, 4), dtype="float32")
    inferred = bpm._infer_concrete_shape_from_args(sym_shape, [x])
    assert inferred == [3, 4]


def test_infer_concrete_shape_all_concrete_dims():
    mod = _make_module()
    bpm = BasePyModule(mod, device=tvm.cpu(0), target="llvm")

    shape = [tirx.IntImm("int32", 5), 6]
    inferred = bpm._infer_concrete_shape_from_args(shape, in_args=[])
    assert inferred == [5, 6]


def test_infer_concrete_shape_error_when_uninferrable():
    mod = _make_module()
    bpm = BasePyModule(mod, device=tvm.cpu(0), target="llvm")

    k = tirx.Var("k", "int64")
    with pytest.raises(ValueError):
        bpm._infer_concrete_shape_from_args([k, 8], in_args=[])


@I.ir_module
class AddModuleSymbolic(BasePyModule):
    @T.prim_func(s_tir=True)
    def add_tir(var_x: T.handle, var_y: T.handle, var_out: T.handle):
        T.func_attr({"global_symbol": "add_tir"})
        n = T.int64()
        x = T.match_buffer(var_x, (n,), dtype="float32")
        y = T.match_buffer(var_y, (n,), dtype="float32")
        out = T.match_buffer(var_out, (n,), dtype="float32")

        for i in T.serial(n):
            out[i] = x[i] + y[i]

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


def test_base_py_module_relax_symbolic_end_to_end():
    bpm = AddModuleSymbolic(device=tvm.cpu(0), target="llvm")

    a = np.random.randn(5).astype("float32")
    b = np.random.randn(5).astype("float32")
    out = bpm.main_relax(a, b)
    assert isinstance(out, np.ndarray) or hasattr(out, "numpy")
    out_np = out if isinstance(out, np.ndarray) else out.numpy()
    tvm.testing.assert_allclose(out_np, a + b, rtol=1e-6, atol=1e-6)

    a7 = np.random.randn(7).astype("float32")
    b7 = np.random.randn(7).astype("float32")
    out2 = bpm.main_relax(a7, b7)
    out2_np = out2 if isinstance(out2, np.ndarray) else out2.numpy()
    tvm.testing.assert_allclose(out2_np, a7 + b7, rtol=1e-6, atol=1e-6)


def test_base_py_module_tir_symbolic_end_to_end():
    bpm = AddModuleSymbolic(device=tvm.cpu(0), target="llvm")

    a = np.random.randn(5).astype("float32")
    b = np.random.randn(5).astype("float32")

    n = tirx.Var("n", "int64")
    out_ty = relax.TensorType((n,), "float32")

    out = bpm.call_tir("add_tir", [a, b], out_ty)
    out_np = out if isinstance(out, np.ndarray) else out.numpy()
    tvm.testing.assert_allclose(out_np, a + b, rtol=1e-6, atol=1e-6)


def test_infer_concrete_shape_multiple_symbolic_dims():
    """Test shape inference with multiple symbolic dimensions."""
    mod = _make_module()
    bpm = BasePyModule(mod, device=tvm.cpu(0), target="llvm")

    n = tirx.Var("n", "int64")
    m = tirx.Var("m", "int64")
    k = tirx.Var("k", "int64")
    sym_shape = [n, m, k]

    x = np.zeros((2, 3, 4), dtype="float32")
    inferred = bpm._infer_concrete_shape_from_args(sym_shape, [x])
    assert inferred == [2, 3, 4]


def test_infer_concrete_shape_mixed_concrete_symbolic():
    """Test shape inference with mixed concrete and symbolic dimensions."""
    mod = _make_module()
    bpm = BasePyModule(mod, device=tvm.cpu(0), target="llvm")

    n = tirx.Var("n", "int64")
    sym_shape = [n, 5, 10]  # First dim is symbolic, others are concrete

    x = np.zeros((3, 5, 10), dtype="float32")
    inferred = bpm._infer_concrete_shape_from_args(sym_shape, [x])
    assert inferred == [3, 5, 10]


def test_infer_concrete_shape_from_tvm_tensors():
    """Test shape inference from TVM tensors."""
    try:
        # Try to create TVM tensor using new API
        x_np = np.zeros((3, 4), dtype="float32")
        x_tvm = tvm.runtime.tensor(x_np)

        mod = _make_module()
        bpm = BasePyModule(mod, device=tvm.cpu(0), target="llvm")

        n = tirx.Var("n", "int64")
        m = tirx.Var("m", "int64")
        sym_shape = [n, m]

        inferred = bpm._infer_concrete_shape_from_args(sym_shape, [x_tvm])
        assert inferred == [3, 4]
    except AttributeError:
        # Skip if tvm.runtime.tensor is not available
        pytest.skip("tvm.runtime.tensor not available")


def test_infer_concrete_shape_multiple_inputs():
    """Test shape inference when multiple inputs are available."""
    mod = _make_module()
    bpm = BasePyModule(mod, device=tvm.cpu(0), target="llvm")

    n = tirx.Var("n", "int64")
    m = tirx.Var("m", "int64")
    sym_shape = [n, m]

    # Multiple inputs with different shapes - should use first matching one
    x1 = np.zeros((2, 3), dtype="float32")
    x2 = np.zeros((4, 5), dtype="float32")
    inferred = bpm._infer_concrete_shape_from_args(sym_shape, [x1, x2])
    assert inferred == [2, 3]  # Should use first input


def test_infer_concrete_shape_wrong_ndim():
    """Test shape inference when input has wrong number of dimensions."""
    mod = _make_module()
    bpm = BasePyModule(mod, device=tvm.cpu(0), target="llvm")

    n = tirx.Var("n", "int64")
    m = tirx.Var("m", "int64")
    sym_shape = [n, m]  # 2D

    x = np.zeros((3,), dtype="float32")  # 1D - wrong ndim
    with pytest.raises(ValueError, match="Cannot infer concrete output shape"):
        bpm._infer_concrete_shape_from_args(sym_shape, [x])


@I.ir_module
class MatrixModuleSymbolic(BasePyModule):
    @T.prim_func(s_tir=True)
    def matmul_tir(var_a: T.handle, var_b: T.handle, var_c: T.handle):
        T.func_attr({"global_symbol": "matmul_tir"})
        m = T.int64()
        n = T.int64()
        k = T.int64()
        a = T.match_buffer(var_a, (m, k), dtype="float32")
        b = T.match_buffer(var_b, (k, n), dtype="float32")
        c = T.match_buffer(var_c, (m, n), dtype="float32")

        for i in T.serial(m):
            for j in T.serial(n):
                c[i, j] = 0.0
                for l in T.serial(k):
                    c[i, j] = c[i, j] + a[i, l] * b[l, j]

    @R.function
    def matmul_relax(
        a: R.Tensor(("m", "k"), "float32"), b: R.Tensor(("k", "n"), "float32")
    ) -> R.Tensor(("m", "n"), "float32"):
        return R.matmul(a, b)


def test_base_py_module_multiple_symbolic_dims():
    """Test BasePyModule with multiple symbolic dimensions."""
    bpm = MatrixModuleSymbolic(device=tvm.cpu(0), target="llvm")

    # Test Relax function with multiple symbolic dims
    a = np.random.randn(2, 3).astype("float32")
    b = np.random.randn(3, 4).astype("float32")
    out = bpm.matmul_relax(a, b)
    out_np = out if isinstance(out, np.ndarray) else out.numpy()
    expected = np.matmul(a, b)
    tvm.testing.assert_allclose(out_np, expected, rtol=1e-6, atol=1e-6)

    # Test TIR function with multiple symbolic dims
    # Use concrete shapes for TIR function to avoid constraint issues
    out_ty = relax.TensorType((2, 4), "float32")
    out_tir = bpm.call_tir("matmul_tir", [a, b], out_ty)
    out_tir_np = out_tir if isinstance(out_tir, np.ndarray) else out_tir.numpy()
    tvm.testing.assert_allclose(out_tir_np, expected, rtol=1e-6, atol=1e-6)


def test_base_py_module_call_dps_packed_symbolic():
    """Test call_dps_packed with symbolic shapes."""
    try:
        # Register a simple test function
        @tvm.register_global_func("test_add_packed")
        def test_add_packed(a, b, out):
            """Add two tensors element-wise."""
            a_np = a.numpy()
            b_np = b.numpy()
            result = a_np + b_np
            out[:] = result

        mod = _make_module()
        bpm = BasePyModule(mod, device=tvm.cpu(0), target="llvm")

        a = np.random.randn(5).astype("float32")
        b = np.random.randn(5).astype("float32")

        n = tirx.Var("n", "int64")
        out_ty = relax.TensorType((n,), "float32")

        out = bpm.call_dps_packed("test_add_packed", [a, b], out_ty)
        out_np = out if isinstance(out, np.ndarray) else out.numpy()
        tvm.testing.assert_allclose(out_np, a + b, rtol=1e-6, atol=1e-6)

    except AttributeError as e:
        pytest.skip(f"call_dps_packed test requires register_global_func: {e}")


def test_base_py_module_call_dps_packed_multiple_args():
    """Test call_dps_packed with multiple arguments and symbolic shapes."""
    try:
        # Register a function that takes multiple arguments
        @tvm.register_global_func("test_matmul_packed")
        def test_matmul_packed(a, b, out):
            """Matrix multiplication."""
            a_np = a.numpy()
            b_np = b.numpy()
            result = np.matmul(a_np, b_np)
            out[:] = result

        mod = _make_module()
        bpm = BasePyModule(mod, device=tvm.cpu(0), target="llvm")

        a = np.random.randn(2, 3).astype("float32")
        b = np.random.randn(3, 4).astype("float32")

        out_ty = relax.TensorType((2, 4), "float32")

        out = bpm.call_dps_packed("test_matmul_packed", [a, b], out_ty)
        out_np = out if isinstance(out, np.ndarray) else out.numpy()
        expected = np.matmul(a, b)
        tvm.testing.assert_allclose(out_np, expected, rtol=1e-6, atol=1e-6)

    except AttributeError as e:
        pytest.skip(f"call_dps_packed test requires register_global_func: {e}")


def test_base_py_module_call_dps_packed_scalar_args():
    """Test call_dps_packed with scalar arguments and symbolic shapes."""
    try:
        # Register a function that takes scalar arguments
        @tvm.register_global_func("test_add_scalar_packed")
        def test_add_scalar_packed(x, scalar, out):
            """Add scalar to tensor."""
            x_np = x.numpy()
            if hasattr(scalar, "numpy"):
                scalar_val = scalar.numpy()
            else:
                scalar_val = scalar
            result = x_np + scalar_val
            out[:] = result

        mod = _make_module()
        bpm = BasePyModule(mod, device=tvm.cpu(0), target="llvm")

        x = np.random.randn(4).astype("float32")
        scalar = 2.5

        n = tirx.Var("n", "int64")
        out_ty = relax.TensorType((n,), "float32")

        out = bpm.call_dps_packed("test_add_scalar_packed", [x, scalar], out_ty)
        out_np = out if isinstance(out, np.ndarray) else out.numpy()
        expected = x + scalar
        tvm.testing.assert_allclose(out_np, expected, rtol=1e-6, atol=1e-6)

    except AttributeError as e:
        pytest.skip(f"call_dps_packed test requires register_global_func: {e}")


def test_infer_concrete_shape_from_pytorch_tensors():
    """Test shape inference from PyTorch tensors (if available)."""
    try:
        import torch
    except ImportError:
        pytest.skip("PyTorch not available")

    mod = _make_module()
    bpm = BasePyModule(mod, device=tvm.cpu(0), target="llvm")

    n = tirx.Var("n", "int64")
    m = tirx.Var("m", "int64")
    sym_shape = [n, m]

    x_torch = torch.zeros((3, 4), dtype=torch.float32)
    inferred = bpm._infer_concrete_shape_from_args(sym_shape, [x_torch])
    assert inferred == [3, 4]


def test_base_py_module_relax_with_pytorch_tensors():
    """Test Relax functions with PyTorch tensors and symbolic shapes."""
    try:
        import torch
    except ImportError:
        pytest.skip("PyTorch not available")

    bpm = AddModuleSymbolic(device=tvm.cpu(0), target="llvm")

    a_torch = torch.randn(5, dtype=torch.float32)
    b_torch = torch.randn(5, dtype=torch.float32)

    out = bpm.main_relax(a_torch, b_torch)
    out_np = out if isinstance(out, np.ndarray) else out.numpy()
    expected = a_torch.numpy() + b_torch.numpy()
    tvm.testing.assert_allclose(out_np, expected, rtol=1e-6, atol=1e-6)


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