# 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
import pytest

import tvm
import tvm.testing
from tvm.script import ir as I
from tvm.script import tirx as T


def test_annotate_entry_func_single_primfunc():
    @tvm.script.ir_module
    class MockModule:
        @T.prim_func(private=True, s_tir=True)
        def func1(A: T.Buffer((16,), "float32")):
            for i in T.serial(16):
                if i == 5:
                    if i == 5:
                        A[i] = 0.0

    mod = MockModule
    assert mod
    assert "tirx.is_entry_func" not in (mod["func1"].attrs or {})
    after = tvm.tirx.transform.AnnotateEntryFunc()(mod)
    assert (
        after["func1"].attrs
        and "tirx.is_entry_func" in after["func1"].attrs
        and after["func1"].attrs["tirx.is_entry_func"]
    )


# Test module
@tvm.script.ir_module
class MockModule:
    @T.prim_func(private=True, s_tir=True)
    def func1(A: T.Buffer((16,), "float32")):
        for i in T.serial(16):
            if i == 5:
                if i == 5:
                    A[i] = 0.0

    @T.prim_func(private=True, s_tir=True)
    def func2(A: T.Buffer((32,), "float32")):
        for i in T.serial(32):
            if i == 15:
                if i == 15:
                    A[i] = 0.0


@pytest.mark.xfail
def test_annotate_entry_func_multiple_primfunc():
    mod = MockModule
    assert mod
    assert "target" not in (mod["func1"].attrs or {})
    assert "target" not in (mod["func2"].attrs or {})
    # This should fail
    after = tvm.tirx.transform.AnnotateEntryFunc()(mod)


def test_bind_target():
    mod = MockModule
    assert mod

    target = tvm.target.Target("cuda")
    assert "target" not in (mod["func1"].attrs or {})
    assert "target" not in (mod["func2"].attrs or {})
    after = tvm.tirx.transform.BindTarget(target)(mod)

    assert "target" in after["func1"].attrs
    assert after["func1"].attrs["target"] == target
    assert "target" in after["func2"].attrs
    assert after["func2"].attrs["target"] == target


def test_bind_target_adds_attribute():
    """BindTarget adds the "target" attribute"""

    @I.ir_module
    class Before:
        @T.prim_func(s_tir=True)
        def main():
            T.evaluate(0)

    @I.ir_module
    class Expected:
        @T.prim_func(s_tir=True)
        def main():
            T.func_attr({"target": T.target("cuda")})
            T.evaluate(0)

    After = tvm.tirx.transform.BindTarget(tvm.target.Target("cuda"))(Before)
    tvm.ir.assert_structural_equal(After, Expected)


def test_bind_target_with_host_to_exposed_function():
    """BindTarget adds the host target to externally-exposed functions"""

    @I.ir_module
    class Before:
        @T.prim_func(s_tir=True)
        def main():
            T.func_attr({"global_symbol": "main"})
            T.evaluate(0)

    @I.ir_module
    class Expected:
        @T.prim_func(s_tir=True)
        def main():
            T.func_attr({"global_symbol": "main", "target": T.target("cuda", host="llvm")})
            T.evaluate(0)

    After = tvm.tirx.transform.BindTarget(tvm.target.Target("cuda", host="llvm"))(Before)
    tvm.ir.assert_structural_equal(After, Expected)


def test_bind_target_with_host_to_internal_function():
    """Internal functions have a target annotation, but without the host

    The host portion of the target annotation provides host
    parameters, and is used to expose a function externally as part of
    `MakePackedAPI` and `MakeUnpackedAPI`.  For internal functions, no
    external exposure is required, so the host attribute should not be
    used.
    """

    @I.ir_module
    class Before:
        @T.prim_func(private=True, s_tir=True)
        def main():
            T.evaluate(0)

    @I.ir_module
    class Expected:
        @T.prim_func(private=True, s_tir=True)
        def main():
            T.func_attr({"target": T.target("cuda")})
            T.evaluate(0)

    After = tvm.tirx.transform.BindTarget(tvm.target.Target("cuda", host="llvm"))(Before)
    tvm.ir.assert_structural_equal(After, Expected)


def test_bind_target_ignores_existing():
    """BindTarget should not replace existing annotations"""

    @I.ir_module
    class Before:
        @T.prim_func(s_tir=True)
        def main():
            T.func_attr({"target": T.target("nvptx")})
            T.evaluate(0)

    Expected = Before

    After = tvm.tirx.transform.BindTarget(tvm.target.Target("cuda"))(Before)
    tvm.ir.assert_structural_equal(After, Expected)


def test_bind_target_updates_host():
    """BindTarget should update host for existing annotations"""

    @I.ir_module
    class Before:
        @T.prim_func(s_tir=True)
        def main():
            T.func_attr({"global_symbol": "func", "target": T.target("nvptx")})
            T.evaluate(0)

    @I.ir_module
    class Expected:
        @T.prim_func(s_tir=True)
        def main():
            T.func_attr(
                {
                    "global_symbol": "func",
                    "target": T.target("nvptx", host={"kind": "llvm", "opt-level": 0}),
                }
            )
            T.evaluate(0)

    After = tvm.tirx.transform.BindTarget(
        tvm.target.Target("cuda", host={"kind": "llvm", "opt-level": 0})
    )(Before)
    tvm.ir.assert_structural_equal(After, Expected)


def test_bind_target_multiple_functions():
    """BindTarget may apply to multiple functions in a module"""

    @I.ir_module
    class Before:
        @T.prim_func(s_tir=True)
        def func1():
            T.evaluate(0)

        @T.prim_func(s_tir=True)
        def func2():
            T.evaluate(0)

    @I.ir_module
    class Expected:
        @T.prim_func(s_tir=True)
        def func1():
            T.func_attr({"target": T.target("cuda")})
            T.evaluate(0)

        @T.prim_func(s_tir=True)
        def func2():
            T.func_attr({"target": T.target("cuda")})
            T.evaluate(0)

    After = tvm.tirx.transform.BindTarget(tvm.target.Target("cuda"))(Before)
    tvm.ir.assert_structural_equal(After, Expected)


def test_bind_target_with_device_host_call_same_func():
    """BindTarget should bind the device target to the function if it is called from device"""

    @I.ir_module
    class Before:
        @T.prim_func(private=True, s_tir=True)
        def add(a: T.int32, b: T.int32) -> T.int32:
            return a + b

        @T.prim_func(s_tir=True)
        def main(
            A: T.Buffer((128, 128), "int32"),
            B: T.Buffer((128, 128), "int32"),
            C: T.Buffer((128, 128), "int32"),
        ):
            T.func_attr({"global_symbol": "main"})
            length: T.let[T.int32] = Before.add(64, 64)  # Call from host
            for bx in T.thread_binding(length, "blockIdx.x"):
                for tx in T.thread_binding(length, "threadIdx.x"):
                    C[bx, tx] = Before.add(A[bx, tx], B[bx, tx])  # Call from device

    @I.ir_module
    class Expected:
        @T.prim_func(private=True, s_tir=True)
        def add(a: T.int32, b: T.int32) -> T.int32:
            T.func_attr({"target": T.target("cuda")})
            return a + b

        @T.prim_func(private=True, s_tir=True)
        def add_host(a: T.int32, b: T.int32) -> T.int32:
            T.func_attr({"target": T.target({"kind": "llvm", "opt-level": 0})})
            return a + b

        @T.prim_func(s_tir=True)
        def main(
            A: T.Buffer((128, 128), "int32"),
            B: T.Buffer((128, 128), "int32"),
            C: T.Buffer((128, 128), "int32"),
        ):
            T.func_attr(
                {
                    "global_symbol": "main",
                    "target": T.target("cuda", host={"kind": "llvm", "opt-level": 0}),
                }
            )
            length: T.let[T.int32] = Expected.add_host(64, 64)  # Call from host
            for bx in T.thread_binding(length, "blockIdx.x"):
                for tx in T.thread_binding(length, "threadIdx.x"):
                    C[bx, tx] = Expected.add(A[bx, tx], B[bx, tx])  # Call from device

    After = tvm.tirx.transform.BindTarget(
        tvm.target.Target("cuda", host={"kind": "llvm", "opt-level": 0})
    )(Before)
    tvm.ir.assert_structural_equal(After, Expected)


def test_filter_primfunc():
    mod = MockModule
    assert mod
    # Annotate each function for testing
    mod["func1"] = mod["func1"].with_attr("temp", "test1")
    mod["func2"] = mod["func2"].with_attr("temp", "test2")

    # Test condition that does not filter out anything
    def checker_filter_out_none(func: tvm.tirx.PrimFunc):
        return "temp" in func.attrs

    after = tvm.tirx.transform.Filter(checker_filter_out_none)(mod)
    assert len(after.functions) == 2
    # Filtered functions should satisfy the given condition.
    assert checker_filter_out_none(after["func1"])
    assert checker_filter_out_none(after["func2"])

    # Test condition that selectively filters out primfuncs
    def checker_filter_out_one(func: tvm.tirx.PrimFunc):
        return ("temp" in func.attrs) and func.attrs["temp"] == "test1"

    after = tvm.tirx.transform.Filter(checker_filter_out_one)(mod)
    assert len(after.functions) == 1
    # Filtered functions should satisfy the given condition.
    assert checker_filter_out_one(after["func1"])

    # Test condition that filters out everything
    def checker_filter_out_both(func: tvm.tirx.PrimFunc):
        return "invalid_attr" in func.attrs

    after = tvm.tirx.transform.Filter(checker_filter_out_both)(mod)
    assert len(after.functions) == 0


def test_filter_removes_global_var_map():
    """Filtering out a function should be identical to never adding it

    This test is to guard against hidden state in the IRModule that
    remains after filtering.  Previously, this was observed in the
    `IRModuleNode::global_var_map_`, which retained entries of
    filtered-out functions.
    """

    @I.ir_module
    class Before:
        @T.prim_func(s_tir=True)
        def func():
            T.evaluate(0)

    @I.ir_module
    class Expected:
        pass

    After = tvm.tirx.transform.Filter(lambda prim_func: False)(Before)
    tvm.ir.assert_structural_equal(After, Expected)


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