# 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: F841
"""NNAPI integration operator tests."""

import numpy as np
import pytest

import tvm
import tvm.script
import tvm.script.relax as R
import tvm.script.tirx as T
from test_nnapi.conftest import remote
from test_nnapi.infrastructure import build_and_run


def _build_and_run_network(remote_obj, tracker, mod, input_data):
    """Helper function to build and run a network."""

    def execute_on_host(mod, inputs):
        with tvm.transform.PassContext(opt_level=3):
            ex = tvm.compile(mod, target="llvm")
        dev = tvm.cpu(0)
        vm = tvm.relax.VirtualMachine(ex, device=dev)
        output = vm["main"](*inputs)
        return output.numpy()

    outputs = []
    for nnapi in [True, False]:
        if nnapi:
            outputs.append(
                build_and_run(
                    remote_obj,
                    tracker,
                    mod,
                    input_data,
                    enable_nnapi=nnapi,
                )
            )
        else:
            outputs.append(execute_on_host(mod, input_data))
    return outputs


@pytest.mark.parametrize(
    "op",
    [
        R.exp,
        R.log,
        R.negative,
        R.sqrt,
        R.rsqrt,
        R.floor,
        R.nn.relu,
        R.nn.softmax,
        R.sigmoid,
        R.tanh,
        R.abs,
    ],
)
def test_unary(op, input_shape=(1, 2, 8, 5)):
    remote_obj, tracker = remote()

    def create_model() -> tvm.IRModule:
        @tvm.script.ir_module
        class Module:
            @R.function
            def main(i0: R.Tensor((1, 2, 8, 5), "float32")) -> R.Tensor((1, 2, 8, 5), "float32"):
                with R.dataflow():
                    t0 = op(i0)
                    R.output(t0)
                return t0

        return Module

    mod = create_model()
    verify(
        remote_obj,
        tracker,
        mod,
        inputs=[np.random.uniform(size=(1, 2, 8, 5)).astype("float32")],
    )


@pytest.mark.parametrize(
    "op",
    [
        R.power,
        R.greater,
        R.add,
        R.multiply,
        R.subtract,
        R.equal,
        R.less,
        R.less_equal,
        R.not_equal,
        R.maximum,
        R.minimum,
        R.greater_equal,
    ],
)
def test_elementwise_binary(op, input_shape=(1, 2, 8, 5)):
    remote_obj, tracker = remote()

    def create_model() -> tvm.IRModule:
        @tvm.script.ir_module
        class Module:
            @R.function
            def main(
                i0: R.Tensor((1, 2, 8, 5), "float32"),
                i1: R.Tensor((1, 2, 8, 5), "float32"),
            ) -> R.Tensor((1, 2, 8, 5), "float32"):
                with R.dataflow():
                    t0 = op(i0, i1)
                    R.output(t0)
                return t0

        return Module

    mod = create_model()
    verify(
        remote_obj,
        tracker,
        mod,
        inputs=[
            np.random.uniform(size=input_shape).astype("float32"),
            np.random.uniform(size=input_shape).astype("float32"),
        ],
    )


def test_divide(input_shape=(1, 2, 8, 5)):
    remote_obj, tracker = remote()

    def create_model(input_shape) -> tvm.IRModule:
        @tvm.script.ir_module
        class Module:
            @R.function
            def main(
                i0: R.Tensor((1, 2, 8, 5), "float32"),
                i1: R.Tensor((1, 2, 8, 5), "float32"),
            ) -> R.Tensor((1, 2, 8, 5), "float32"):
                with R.dataflow():
                    t0 = R.divide(i0, i1)
                    R.output(t0)
                return t0

        return Module

    mod = create_model(input_shape)
    verify(
        remote_obj,
        tracker,
        mod,
        inputs=[
            np.random.uniform(size=input_shape).astype("float32"),
            np.random.uniform(size=input_shape).astype("float32") + np.ones(input_shape, "float32"),
        ],
    )


def test_matmul():
    remote_obj, tracker = remote()

    def create_model() -> tvm.IRModule:
        @tvm.script.ir_module
        class Module:
            @R.function
            def main(
                i0: R.Tensor((5, 3, 4), "float32"),
                i1: R.Tensor((5, 4, 8), "float32"),
            ) -> R.Tensor((5, 3, 8), "float32"):
                with R.dataflow():
                    t0 = R.matmul(i0, i1)
                    R.output(t0)
                return t0

        return Module

    mod = create_model()
    verify(
        remote_obj,
        tracker,
        mod,
        inputs=[
            np.random.random(size=(5, 3, 4)).astype("float32"),
            np.random.random(size=(5, 4, 8)).astype("float32"),
        ],
    )


def test_permute_dims():
    remote_obj, tracker = remote()

    def create_model() -> tvm.IRModule:
        @tvm.script.ir_module
        class Module:
            @R.function
            def main(
                i0: R.Tensor((5, 4, 8), "float32"),
            ) -> R.Tensor((8, 5, 4), "float32"):
                with R.dataflow():
                    t0 = R.permute_dims(i0, axes=[2, 0, 1])
                    R.output(t0)
                return t0

        return Module

    mod = create_model()
    verify(
        remote_obj,
        tracker,
        mod,
        inputs=[
            np.random.random(size=(5, 4, 8)).astype("float32"),
        ],
    )


def test_astype():
    remote_obj, tracker = remote()

    def create_model() -> tvm.IRModule:
        @tvm.script.ir_module
        class Module:
            @R.function
            def main(
                i0: R.Tensor((8, 10, 15), "float32"),
            ) -> R.Tensor((8, 10, 15), "float16"):
                with R.dataflow():
                    t0: R.Tensor((8, 10, 15), "float16") = R.astype(i0, dtype="float16")
                    R.output(t0)
                return t0

        return Module

    mod = create_model()
    verify(
        remote_obj,
        tracker,
        mod,
        inputs=[
            tvm.runtime.tensor(np.random.uniform(size=(8, 10, 15)).astype("float32")),
        ],
    )


def test_mean():
    remote_obj, tracker = remote()

    def create_model() -> tvm.IRModule:
        @tvm.script.ir_module
        class Module:
            @R.function
            def main(
                i0: R.Tensor((1, 10, 15), "float32"),
            ) -> R.Tensor((1, 10, 1), "float32"):
                n = T.int64()
                with R.dataflow():
                    t0: R.Tensor((1, 10, 1), "float32") = R.mean(i0, axis=[-1], keepdims=True)
                    R.output(t0)
                return t0

        return Module

    mod = create_model()
    verify(
        remote_obj,
        tracker,
        mod,
        inputs=[
            tvm.runtime.tensor(np.random.uniform(size=(1, 10, 15)).astype("float32")),
        ],
    )


def test_conv2d():
    remote_obj, tracker = remote()

    def create_model() -> tvm.IRModule:
        @tvm.script.ir_module
        class Module:
            @R.function
            def main(
                i0: R.Tensor((1, 3, 224, 224), "float32"),
                i1: R.Tensor((64, 3, 3, 3), "float32"),
                i2: R.Tensor((1, 64, 1, 1), "float32"),
            ):
                with R.dataflow():
                    t0 = R.nn.conv2d(i0, i1, strides=(1, 1), padding=(1, 1))
                    t0 = R.add(i2, t0)
                    R.output(t0)
                return t0

        return Module

    mod = create_model()
    verify(
        remote_obj,
        tracker,
        mod,
        inputs=[
            np.random.random(size=(1, 3, 224, 224)).astype("float32"),
            np.random.random(size=(64, 3, 3, 3)).astype("float32"),
            np.random.random(size=(1, 64, 1, 1)).astype("float32"),
        ],
    )


def test_max_pool2d():
    remote_obj, tracker = remote()

    def create_model() -> tvm.IRModule:
        @tvm.script.ir_module
        class Module:
            @R.function
            def main(
                i0: R.Tensor((1, 1, 28, 28), "float32"),
            ):
                with R.dataflow():
                    t0 = R.nn.max_pool2d(i0, pool_size=(1, 1), strides=(1, 1), padding=(0, 0))
                    R.output(t0)
                return t0

        return Module

    mod = create_model()
    verify(
        remote_obj,
        tracker,
        mod,
        inputs=[
            np.random.random(size=(1, 1, 28, 28)).astype("float32"),
        ],
    )


def verify(remote_obj, tracker, mod, inputs):
    inputs_tvm: list[tvm.runtime.Tensor] = [tvm.runtime.tensor(v) for v in inputs]
    outputs = _build_and_run_network(remote_obj, tracker, mod, inputs_tvm)
    nnapi_out = outputs[0]
    expected_out = outputs[1]
    tvm.testing.assert_allclose(nnapi_out, expected_out, rtol=1e-4, atol=1e-5)


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