# 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.

import tvm
import tvm.testing
from tvm import relax
from tvm.ir import assert_structural_equal
from tvm.relax.frontend import nn
from tvm.script import ir as I
from tvm.script import relax as R


def test_linear():
    class Activation(nn.Module):
        define_subroutine = True

        def forward(self, state: relax.Expr) -> relax.Var:
            return nn.op.silu(state)

    class Layer(nn.Module):
        define_subroutine = True

        def __init__(self, in_features, out_features):
            self.weights = nn.Parameter((in_features, out_features), dtype="float32")
            self.activation = Activation()

        def forward(self, input: relax.Expr) -> relax.Var:
            state = nn.op.matmul(input, self.weights)
            return self.activation(state)

    @I.ir_module
    class Expected:
        @R.function
        def forward(
            state: R.Tensor(("batch_size", 64), dtype="float32"),
            _io: R.Any,
            weights: R.Tensor((64, 32), dtype="float32"),
        ) -> R.Tuple(R.Tensor(("batch_size", 32), dtype="float32"), R.Tuple(R.Any)):
            R.func_attr({"num_input": 2})
            with R.dataflow():
                state = Expected.layer(state, weights)
                dataflow_output = (state, (_io,))
                R.output(dataflow_output)
            return dataflow_output

        @R.function
        def _initialize_effect() -> R.Tuple(R.Any):
            with R.dataflow():
                _io: R.Any = R.null_value()
                lv: R.Tuple(R.Any) = (_io,)
                gv: R.Tuple(R.Any) = lv
                R.output(gv)

            return gv

        @R.function(private=True)
        def layer(
            state: R.Tensor(("batch_size", 64), dtype="float32"),
            weights: R.Tensor((64, 32), dtype="float32"),
        ) -> R.Tensor(("batch_size", 32), dtype="float32"):
            with R.dataflow():
                state = R.matmul(state, weights)
                state = Expected.activation(state)
                dataflow_output = state
                R.output(dataflow_output)
            return dataflow_output

        @R.function(private=True)
        def activation(
            state: R.Tensor(("batch_size", 32), dtype="float32"),
        ) -> R.Tensor(("batch_size", 32), dtype="float32"):
            with R.dataflow():
                state = R.nn.silu(state)
                dataflow_output = state
                R.output(dataflow_output)
            return dataflow_output

    mod = Layer(64, 32)
    batch_size = tvm.tirx.Var("batch_size", "int64")
    tvm_mod, _ = mod.export_tvm(
        spec={"forward": {"input": nn.spec.Tensor((batch_size, 64), "float32")}}, debug=True
    )
    assert_structural_equal(Expected, tvm_mod, True)


def test_different_shapes_produce_distinct_subroutines():
    """Regression test: same Module class with different input shapes
    must generate distinct subroutines, not reuse a cached one."""

    class Linear(nn.Module):
        define_subroutine = True

        def __init__(self, in_features, out_features):
            self.weights = nn.Parameter((in_features, out_features), dtype="float32")

        def forward(self, input: relax.Expr) -> relax.Var:
            return nn.op.matmul(input, self.weights)

    class Model(nn.Module):
        def __init__(self):
            self.linear_a = Linear(32, 16)
            self.linear_b = Linear(64, 16)

        def forward(self, x: relax.Expr, y: relax.Expr) -> relax.Var:
            a = self.linear_a(x)
            b = self.linear_b(y)
            return nn.op.add(a, b)

    mod = Model()
    batch_size = tvm.tirx.Var("batch_size", "int64")
    tvm_mod, _ = mod.export_tvm(
        spec={
            "forward": {
                "x": nn.spec.Tensor((batch_size, 32), "float32"),
                "y": nn.spec.Tensor((batch_size, 64), "float32"),
            }
        },
        debug=True,
    )

    # Collect all private functions (subroutines) in the module
    subroutine_funcs = [
        func
        for gvar, func in tvm_mod.functions.items()
        if isinstance(func, relax.Function)
        and gvar.name_hint
        not in (
            "forward",
            "_initialize_effect",
        )
    ]

    # There must be two distinct Linear subroutines (one for in_features=32,
    # one for in_features=64), not a single cached one reused for both.
    assert len(subroutine_funcs) == 2, (
        f"Expected 2 distinct subroutines for different input shapes, got {len(subroutine_funcs)}"
    )


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