# 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.
"""Tests for the Executable class."""

import os
import tempfile

import numpy as np

import tvm
import tvm.testing
from tvm.runtime import Executable
from tvm.script import tirx as T


@tvm.script.ir_module
class MyModule:
    @T.prim_func(s_tir=True)
    def add(
        A: T.Buffer((10,), "float32"),
        B: T.Buffer((10,), "float32"),
        C: T.Buffer((10,), "float32"),
    ):
        for i in range(10):
            C[i] = A[i] + B[i]


def test_executable_init():
    """Test initialization of Executable class."""
    lib = tvm.tirx.build(MyModule, target="llvm")
    executable = Executable(lib)

    assert executable.mod is lib
    assert executable._jitted_mod is None


def test_executable_getitem():
    """Test __getitem__ method of Executable class."""
    lib = tvm.tirx.build(MyModule, target="llvm")
    executable = Executable(lib)

    # Jit the module first
    executable.jit()

    # Test __getitem__
    add_func = executable["add"]

    # Verify the function works
    a = tvm.runtime.tensor(np.array([1.0] * 10, dtype="float32"))
    b = tvm.runtime.tensor(np.array([2.0] * 10, dtype="float32"))
    c = tvm.runtime.tensor(np.array([0.0] * 10, dtype="float32"))

    add_func(a, b, c)

    # Check results
    tvm.testing.assert_allclose(c.numpy(), np.array([3.0] * 10, dtype="float32"))


def test_executable_jit_already_jitted():
    """Test jit method when module is already jitted."""
    lib = tvm.tirx.build(MyModule, target="llvm")
    executable = Executable(lib)

    # First jit call
    jitted_mod1 = executable.jit()

    # Second jit call should return the cached jitted module
    jitted_mod2 = executable.jit()
    assert jitted_mod2 is jitted_mod1

    # Test with force_recompile
    jitted_mod3 = executable.jit(force_recompile=True)
    # The module might be different after force recompilation

    # Verify both modules work correctly
    a = tvm.runtime.tensor(np.array([1.0] * 10, dtype="float32"))
    b = tvm.runtime.tensor(np.array([2.0] * 10, dtype="float32"))
    c1 = tvm.runtime.tensor(np.array([0.0] * 10, dtype="float32"))
    c2 = tvm.runtime.tensor(np.array([0.0] * 10, dtype="float32"))

    jitted_mod1["add"](a, b, c1)
    jitted_mod3["add"](a, b, c2)

    tvm.testing.assert_allclose(c1.numpy(), np.array([3.0] * 10, dtype="float32"))
    tvm.testing.assert_allclose(c2.numpy(), np.array([3.0] * 10, dtype="float32"))


def test_executable_export_library():
    """Test export_library method."""
    lib = tvm.tirx.build(MyModule, target="llvm")
    executable = Executable(lib)

    # Create a temporary directory for the library
    temp_dir = tempfile.mkdtemp()
    try:
        lib_path = os.path.join(temp_dir, "test_lib.so")
        executable.export_library(lib_path)

        # Verify the library was created
        assert os.path.exists(lib_path)

        # Load the library back
        loaded_mod = tvm.runtime.load_module(lib_path)
        assert loaded_mod is not None

        # Test the loaded module
        a = tvm.runtime.tensor(np.array([1.0] * 10, dtype="float32"))
        b = tvm.runtime.tensor(np.array([2.0] * 10, dtype="float32"))
        c = tvm.runtime.tensor(np.array([0.0] * 10, dtype="float32"))

        loaded_mod["add"](a, b, c)

        # Check results
        tvm.testing.assert_allclose(c.numpy(), np.array([3.0] * 10, dtype="float32"))
    finally:
        # Clean up
        if os.path.exists(temp_dir):
            import shutil

            shutil.rmtree(temp_dir)


def test_executable_export_library_with_workspace():
    """Test export_library method with workspace_dir."""
    lib = tvm.tirx.build(MyModule, target="llvm")
    executable = Executable(lib)

    # Create temporary directories
    temp_dir = tempfile.mkdtemp()
    workspace_dir = tempfile.mkdtemp()

    try:
        lib_path = os.path.join(temp_dir, "test_lib.so")
        executable.export_library(lib_path, workspace_dir=workspace_dir)

        # Verify the library was created
        assert os.path.exists(lib_path)

        # Load the library back
        loaded_mod = tvm.runtime.load_module(lib_path)
        assert loaded_mod is not None

        # Test the loaded module
        a = tvm.runtime.tensor(np.array([1.0] * 10, dtype="float32"))
        b = tvm.runtime.tensor(np.array([2.0] * 10, dtype="float32"))
        c = tvm.runtime.tensor(np.array([0.0] * 10, dtype="float32"))

        loaded_mod["add"](a, b, c)

        # Check results
        tvm.testing.assert_allclose(c.numpy(), np.array([3.0] * 10, dtype="float32"))
    finally:
        # Clean up
        for directory in [temp_dir, workspace_dir]:
            if os.path.exists(directory):
                import shutil

                shutil.rmtree(directory)


def test_executable_integration():
    """Integration test for Executable with a simple TVM module."""
    # Create target and build
    target = tvm.target.Target("llvm")
    lib = tvm.tirx.build(MyModule, target=target)

    # Create an executable
    executable = Executable(lib)

    # Test jit
    jitted_mod = executable.jit()
    assert jitted_mod is not None

    # Test __getitem__
    add_func = executable["add"]
    assert add_func is not None

    # Test the function works
    a = tvm.runtime.tensor(np.array([1.0] * 10, dtype="float32"))
    b = tvm.runtime.tensor(np.array([2.0] * 10, dtype="float32"))
    c = tvm.runtime.tensor(np.array([0.0] * 10, dtype="float32"))

    add_func(a, b, c)

    # Check results
    tvm.testing.assert_allclose(c.numpy(), np.array([3.0] * 10, dtype="float32"))

    # Test export_library
    temp_dir = tempfile.mkdtemp()
    try:
        lib_path = os.path.join(temp_dir, "test_lib.so")
        executable.export_library(lib_path)

        # Verify the library was created
        assert os.path.exists(lib_path)

        # Load the library back
        loaded_mod = tvm.runtime.load_module(lib_path)
        assert loaded_mod is not None

        # Test the loaded module
        loaded_add = loaded_mod["add"]
        c_loaded = tvm.runtime.tensor(np.array([0.0] * 10, dtype="float32"))
        loaded_add(a, b, c_loaded)

        # Check results
        tvm.testing.assert_allclose(c_loaded.numpy(), np.array([3.0] * 10, dtype="float32"))

    finally:
        # Clean up
        if os.path.exists(temp_dir):
            import shutil

            shutil.rmtree(temp_dir)


def test_executable_jit_force_recompile():
    """Test jit method with force_recompile=True."""
    # Create target and build
    target = tvm.target.Target("c")
    lib = tvm.tirx.build(MyModule, target=target)

    # Create an executable
    executable = Executable(lib)

    # First jit call
    jitted_mod1 = executable.jit()

    # Second jit call without force_recompile should return the same module
    jitted_mod2 = executable.jit()
    assert jitted_mod1 is jitted_mod2

    # Third jit call with force_recompile should return a new module
    jitted_mod3 = executable.jit(force_recompile=True)
    assert jitted_mod3 is not jitted_mod1

    # Test the function works
    a = tvm.runtime.tensor(np.array([1.0] * 10, dtype="float32"))
    b = tvm.runtime.tensor(np.array([2.0] * 10, dtype="float32"))
    c = tvm.runtime.tensor(np.array([0.0] * 10, dtype="float32"))

    jitted_mod3["add"](a, b, c)

    # Check results
    tvm.testing.assert_allclose(c.numpy(), np.array([3.0] * 10, dtype="float32"))


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