# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.

import itertools
import os
import unittest

import numpy as np
import torch
from pytorchvideo.models.head import ResNetBasicHead
from pytorchvideo.models.net import Net
from pytorchvideo.models.resnet import (
    BottleneckBlock,
    create_acoustic_bottleneck_block,
    create_acoustic_resnet,
    create_bottleneck_block,
    create_res_block,
    create_res_stage,
    create_resnet,
    create_resnet_with_roi_head,
    ResBlock,
    ResStage,
    SeparableBottleneckBlock,
)
from pytorchvideo.models.stem import ResNetBasicStem
from torch import nn


class TestBottleneckBlock(unittest.TestCase):
    def setUp(self):
        super().setUp()
        torch.set_rng_state(torch.manual_seed(42).get_state())

    def test_create_simple_bottleneck_block(self):
        """
        Test simple BottleneckBlock with different dimensions.
        """
        for dim_in, dim_inner, dim_out in itertools.product(
            (4, 8, 16), (2, 4), (4, 8, 16)
        ):
            model = BottleneckBlock(
                conv_a=nn.Conv3d(
                    dim_in, dim_inner, kernel_size=1, stride=1, padding=0, bias=False
                ),
                norm_a=nn.BatchNorm3d(dim_inner),
                act_a=nn.ReLU(),
                conv_b=nn.Conv3d(
                    dim_inner, dim_inner, kernel_size=3, stride=1, padding=1, bias=False
                ),
                norm_b=nn.BatchNorm3d(dim_inner),
                act_b=nn.ReLU(),
                conv_c=nn.Conv3d(
                    dim_inner, dim_out, kernel_size=1, stride=1, padding=0, bias=False
                ),
                norm_c=nn.BatchNorm3d(dim_out),
            )

            # Test forwarding.
            for input_tensor in TestBottleneckBlock._get_inputs(dim_in):
                if input_tensor.shape[1] != dim_in:
                    with self.assertRaises(RuntimeError):
                        output_tensor = model(input_tensor)
                    continue

                output_tensor = model(input_tensor)
                input_shape = input_tensor.shape
                output_shape = output_tensor.shape

                output_shape_gt = (
                    input_shape[0],
                    dim_out,
                    input_shape[2],
                    input_shape[3],
                    input_shape[4],
                )

                self.assertEqual(
                    output_shape,
                    output_shape_gt,
                    "Output shape {} is different from expected shape {}".format(
                        output_shape, output_shape_gt
                    ),
                )

    def test_create_complex_bottleneck_block(self):
        """
        Test complex BottleneckBlock with different dimensions.
        """
        for dim_in, dim_inner, dim_out in itertools.product(
            (4, 8, 16), (2, 4), (4, 8, 16)
        ):
            model = BottleneckBlock(
                conv_a=nn.Conv3d(
                    dim_in,
                    dim_inner,
                    kernel_size=[3, 1, 1],
                    stride=[2, 1, 1],
                    padding=[1, 0, 0],
                    bias=False,
                ),
                norm_a=nn.BatchNorm3d(dim_inner),
                act_a=nn.ReLU(),
                conv_b=nn.Conv3d(
                    dim_inner,
                    dim_inner,
                    kernel_size=[1, 3, 3],
                    stride=[1, 2, 2],
                    padding=[0, 1, 1],
                    groups=1,
                    dilation=[1, 1, 1],
                    bias=False,
                ),
                norm_b=nn.BatchNorm3d(dim_inner),
                act_b=nn.ReLU(),
                conv_c=nn.Conv3d(
                    dim_inner,
                    dim_out,
                    kernel_size=[1, 1, 1],
                    stride=[1, 1, 1],
                    padding=[0, 0, 0],
                    bias=False,
                ),
                norm_c=nn.BatchNorm3d(dim_out),
            )

            # Test forwarding.
            for input_tensor in TestBottleneckBlock._get_inputs(dim_in):
                if input_tensor.shape[1] != dim_in:
                    with self.assertRaises(Exception):
                        output_tensor = model(input_tensor)
                    continue

                output_tensor = model(input_tensor)
                input_shape = input_tensor.shape
                output_shape = output_tensor.shape

                output_shape_gt = (
                    input_shape[0],
                    dim_out,
                    (input_shape[2] - 1) // 2 + 1,
                    (input_shape[3] - 1) // 2 + 1,
                    (input_shape[4] - 1) // 2 + 1,
                )

                self.assertEqual(
                    output_shape,
                    output_shape_gt,
                    "Output shape {} is different from expected shape {}".format(
                        output_shape, output_shape_gt
                    ),
                )

    def test_create_separable_bottleneck_block_sum(self):
        """
        Test SeparableBottleneckBlock with different dimensions.
        """
        for dim_in, dim_inner, dim_out in itertools.product(
            (4, 8, 16), (2, 4), (4, 8, 16)
        ):
            model = SeparableBottleneckBlock(
                conv_a=nn.Conv3d(
                    dim_in,
                    dim_inner,
                    kernel_size=[3, 1, 1],
                    stride=[2, 1, 1],
                    padding=[1, 0, 0],
                    bias=False,
                ),
                norm_a=nn.BatchNorm3d(dim_inner),
                act_a=nn.ReLU(),
                conv_b=nn.ModuleList(
                    [
                        nn.Conv3d(
                            dim_inner,
                            dim_inner,
                            kernel_size=[1, 3, 3],
                            stride=[1, 2, 2],
                            padding=[0, 1, 1],
                            groups=1,
                            dilation=[1, 1, 1],
                            bias=False,
                        ),
                        nn.Conv3d(
                            dim_inner,
                            dim_inner,
                            kernel_size=[1, 3, 3],
                            stride=[1, 2, 2],
                            padding=[0, 1, 1],
                            groups=1,
                            dilation=[1, 1, 1],
                            bias=False,
                        ),
                    ]
                ),
                norm_b=nn.ModuleList(
                    [nn.BatchNorm3d(dim_inner), nn.BatchNorm3d(dim_inner)]
                ),
                act_b=nn.ModuleList([nn.ReLU(), nn.ReLU()]),
                conv_c=nn.Conv3d(
                    dim_inner,
                    dim_out,
                    kernel_size=[1, 1, 1],
                    stride=[1, 1, 1],
                    padding=[0, 0, 0],
                    bias=False,
                ),
                norm_c=nn.BatchNorm3d(dim_out),
                reduce_method="sum",
            )

            # Test forwarding.
            for input_tensor in TestBottleneckBlock._get_inputs(dim_in):
                if input_tensor.shape[1] != dim_in:
                    with self.assertRaises(Exception):
                        output_tensor = model(input_tensor)
                    continue

                output_tensor = model(input_tensor)
                input_shape = input_tensor.shape
                output_shape = output_tensor.shape

                output_shape_gt = (
                    input_shape[0],
                    dim_out,
                    (input_shape[2] - 1) // 2 + 1,
                    (input_shape[3] - 1) // 2 + 1,
                    (input_shape[4] - 1) // 2 + 1,
                )

                self.assertEqual(
                    output_shape,
                    output_shape_gt,
                    "Output shape {} is different from expected shape {}".format(
                        output_shape, output_shape_gt
                    ),
                )

    def test_separable_complex_bottleneck_block_cat(self):
        """
        Test SeparableBottleneckBlock with different dimensions.
        """
        for dim_in, dim_inner, dim_out in itertools.product(
            (4, 8, 16), (2, 4), (4, 8, 16)
        ):
            model = SeparableBottleneckBlock(
                conv_a=nn.Conv3d(
                    dim_in,
                    dim_inner,
                    kernel_size=[3, 1, 1],
                    stride=[2, 1, 1],
                    padding=[1, 0, 0],
                    bias=False,
                ),
                norm_a=nn.BatchNorm3d(dim_inner),
                act_a=nn.ReLU(),
                conv_b=nn.ModuleList(
                    [
                        nn.Conv3d(
                            dim_inner,
                            dim_inner,
                            kernel_size=[1, 3, 3],
                            stride=[1, 2, 2],
                            padding=[0, 1, 1],
                            groups=1,
                            dilation=[1, 1, 1],
                            bias=False,
                        ),
                        nn.Conv3d(
                            dim_inner,
                            dim_inner,
                            kernel_size=[1, 3, 3],
                            stride=[1, 2, 2],
                            padding=[0, 1, 1],
                            groups=1,
                            dilation=[1, 1, 1],
                            bias=False,
                        ),
                    ]
                ),
                norm_b=nn.ModuleList(
                    [nn.BatchNorm3d(dim_inner), nn.BatchNorm3d(dim_inner)]
                ),
                act_b=nn.ModuleList([nn.ReLU(), nn.ReLU()]),
                conv_c=nn.Conv3d(
                    dim_inner * 2,
                    dim_out,
                    kernel_size=[1, 1, 1],
                    stride=[1, 1, 1],
                    padding=[0, 0, 0],
                    bias=False,
                ),
                norm_c=nn.BatchNorm3d(dim_out),
                reduce_method="cat",
            )

            # Test forwarding.
            for input_tensor in TestBottleneckBlock._get_inputs(dim_in):
                if input_tensor.shape[1] != dim_in:
                    with self.assertRaises(Exception):
                        output_tensor = model(input_tensor)
                    continue

                output_tensor = model(input_tensor)
                input_shape = input_tensor.shape
                output_shape = output_tensor.shape

                output_shape_gt = (
                    input_shape[0],
                    dim_out,
                    (input_shape[2] - 1) // 2 + 1,
                    (input_shape[3] - 1) // 2 + 1,
                    (input_shape[4] - 1) // 2 + 1,
                )

                self.assertEqual(
                    output_shape,
                    output_shape_gt,
                    "Output shape {} is different from expected shape {}".format(
                        output_shape, output_shape_gt
                    ),
                )

    def test_create_acoustic_bottleneck_block_with_callable(self):
        """
        Test builder `create_acoustic_bottleneck_block` with callable inputs.
        """
        for (norm_model, act_model) in itertools.product(
            (nn.BatchNorm3d,), (nn.ReLU, nn.Softmax, nn.Sigmoid)
        ):
            model = create_acoustic_bottleneck_block(
                dim_in=32,
                dim_inner=16,
                dim_out=64,
                conv_a_kernel_size=(3, 1, 1),
                conv_a_stride=(1, 1, 1),
                conv_a_padding=(1, 0, 0),
                conv_b_kernel_size=(3, 3, 3),
                conv_b_stride=(1, 1, 1),
                conv_b_padding=(1, 1, 1),
                conv_b_num_groups=1,
                conv_b_dilation=(1, 1, 1),
                norm=norm_model,
                activation=act_model,
            )
            model_gt = SeparableBottleneckBlock(
                conv_a=nn.Conv3d(
                    32,
                    16,
                    kernel_size=[3, 1, 1],
                    stride=[1, 1, 1],
                    padding=[1, 0, 0],
                    bias=False,
                ),
                norm_a=norm_model(16),
                act_a=act_model(),
                conv_b=nn.ModuleList(
                    [
                        nn.Conv3d(
                            16,
                            16,
                            kernel_size=[1, 3, 3],
                            stride=[1, 1, 1],
                            padding=[0, 1, 1],
                            dilation=1,
                            bias=False,
                        ),
                        nn.Conv3d(
                            16,
                            16,
                            kernel_size=[3, 1, 1],
                            stride=[1, 1, 1],
                            padding=[1, 0, 0],
                            dilation=1,
                            bias=False,
                        ),
                    ]
                ),
                norm_b=nn.ModuleList([norm_model(16), norm_model(16)]),
                act_b=nn.ModuleList([act_model(), act_model()]),
                conv_c=nn.Conv3d(
                    16,
                    64,
                    kernel_size=[1, 1, 1],
                    stride=[1, 1, 1],
                    padding=[0, 0, 0],
                    bias=False,
                ),
                norm_c=norm_model(64),
            )

            model.load_state_dict(
                model_gt.state_dict(), strict=True
            )  # explicitly use strict mode.

            # Test forwarding.
            for input_tensor in TestBottleneckBlock._get_inputs(dim_in=32):
                with torch.no_grad():
                    if input_tensor.shape[1] != 32:
                        with self.assertRaises(RuntimeError):
                            output_tensor = model(input_tensor)
                        continue

                    output_tensor = model(input_tensor)
                    output_tensor_gt = model_gt(input_tensor)
                self.assertEqual(
                    output_tensor.shape,
                    output_tensor_gt.shape,
                    "Output shape {} is different from expected shape {}".format(
                        output_tensor.shape, output_tensor_gt.shape
                    ),
                )
                self.assertTrue(
                    np.allclose(output_tensor.numpy(), output_tensor_gt.numpy())
                )

    def test_create_bottleneck_block_with_callable(self):
        """
        Test builder `create_bottleneck_block` with callable inputs.
        """
        for (norm_model, act_model) in itertools.product(
            (nn.BatchNorm3d,), (nn.ReLU, nn.Softmax, nn.Sigmoid)
        ):
            model = create_bottleneck_block(
                dim_in=32,
                dim_inner=16,
                dim_out=64,
                conv_a_kernel_size=(3, 1, 1),
                conv_a_stride=(1, 1, 1),
                conv_a_padding=(1, 0, 0),
                conv_b_kernel_size=(1, 3, 3),
                conv_b_stride=(1, 1, 1),
                conv_b_padding=(0, 1, 1),
                conv_b_num_groups=1,
                conv_b_dilation=(1, 1, 1),
                norm=norm_model,
                activation=act_model,
            )
            model_gt = BottleneckBlock(
                conv_a=nn.Conv3d(
                    32,
                    16,
                    kernel_size=[3, 1, 1],
                    stride=[1, 1, 1],
                    padding=[1, 0, 0],
                    bias=False,
                ),
                norm_a=norm_model(16),
                act_a=act_model(),
                conv_b=nn.Conv3d(
                    16,
                    16,
                    kernel_size=[1, 3, 3],
                    stride=[1, 1, 1],
                    padding=[0, 1, 1],
                    bias=False,
                ),
                norm_b=norm_model(16),
                act_b=act_model(),
                conv_c=nn.Conv3d(
                    16,
                    64,
                    kernel_size=[1, 1, 1],
                    stride=[1, 1, 1],
                    padding=[0, 0, 0],
                    bias=False,
                ),
                norm_c=norm_model(64),
            )

            model.load_state_dict(
                model_gt.state_dict(), strict=True
            )  # explicitly use strict mode.

            # Test forwarding.
            for input_tensor in TestBottleneckBlock._get_inputs(dim_in=32):
                with torch.no_grad():
                    if input_tensor.shape[1] != 32:
                        with self.assertRaises(RuntimeError):
                            output_tensor = model(input_tensor)
                        continue

                    output_tensor = model(input_tensor)
                    output_tensor_gt = model_gt(input_tensor)
                self.assertEqual(
                    output_tensor.shape,
                    output_tensor_gt.shape,
                    "Output shape {} is different from expected shape {}".format(
                        output_tensor.shape, output_tensor_gt.shape
                    ),
                )
                self.assertTrue(
                    np.allclose(output_tensor.numpy(), output_tensor_gt.numpy())
                )

    @staticmethod
    def _get_inputs(dim_in: int = 3) -> torch.tensor:
        """
        Provide different tensors as test cases.

        Yield:
            (torch.tensor): tensor as test case input.
        """
        # Prepare random segmentation as test cases.
        shapes = (
            # Forward succeeded.
            (1, dim_in, 3, 7, 7),
            (1, dim_in, 5, 7, 7),
            (1, dim_in, 7, 7, 7),
            (2, dim_in, 3, 7, 7),
            (4, dim_in, 3, 7, 7),
            (8, dim_in, 3, 7, 7),
            (2, dim_in, 3, 7, 14),
            (2, dim_in, 3, 14, 7),
            (2, dim_in, 3, 14, 14),
            # Forward failed.
            (8, dim_in * 2, 3, 7, 7),
            (8, dim_in * 4, 5, 7, 7),
        )
        for shape in shapes:
            yield torch.rand(shape)


class TestResBottleneckBlock(unittest.TestCase):
    def setUp(self):
        super().setUp()
        torch.set_rng_state(torch.manual_seed(42).get_state())

    def test_create_res_block(self):
        """
        Test simple ResBlock with different inputs.
        """
        for dim_in, dim_inner, dim_out in itertools.product(
            (4, 8, 16), (2, 4), (4, 8, 16)
        ):
            model = ResBlock(
                branch1_conv=nn.Conv3d(
                    dim_in, dim_out, kernel_size=(1, 1, 1), stride=(1, 1, 1)
                )
                if dim_in != dim_out
                else None,
                branch1_norm=nn.BatchNorm3d(num_features=dim_out)
                if dim_in != dim_out
                else None,
                branch2=BottleneckBlock(
                    conv_a=nn.Conv3d(
                        dim_in,
                        dim_inner,
                        kernel_size=[3, 1, 1],
                        stride=[1, 1, 1],
                        padding=[1, 0, 0],
                        bias=False,
                    ),
                    norm_a=nn.BatchNorm3d(dim_inner),
                    act_a=nn.ReLU(),
                    conv_b=nn.Conv3d(
                        dim_inner,
                        dim_inner,
                        kernel_size=[1, 3, 3],
                        stride=[1, 1, 1],
                        padding=[0, 1, 1],
                        bias=False,
                    ),
                    norm_b=nn.BatchNorm3d(dim_inner),
                    act_b=nn.ReLU(),
                    conv_c=nn.Conv3d(
                        dim_inner,
                        dim_out,
                        kernel_size=[1, 1, 1],
                        stride=[1, 1, 1],
                        padding=[0, 0, 0],
                        bias=False,
                    ),
                    norm_c=nn.BatchNorm3d(dim_out),
                ),
                activation=nn.ReLU(),
                branch_fusion=lambda x, y: x + y,
            )

            # Test forwarding.
            for input_tensor in TestBottleneckBlock._get_inputs(dim_in):
                if input_tensor.shape[1] != dim_in:
                    with self.assertRaises(RuntimeError):
                        output_tensor = model(input_tensor)
                    continue

                output_tensor = model(input_tensor)

                input_shape = input_tensor.shape
                output_shape = output_tensor.shape
                output_shape_gt = (
                    input_shape[0],
                    dim_out,
                    input_shape[2],
                    input_shape[3],
                    input_shape[4],
                )

                self.assertEqual(
                    output_shape,
                    output_shape_gt,
                    "Output shape {} is different from expected shape {}".format(
                        output_shape, output_shape_gt
                    ),
                )

    def test_create_res_block_with_callable(self):
        """
        Test builder `create_res_block` with callable inputs.
        """
        for (norm, activation) in itertools.product(
            (nn.BatchNorm3d, None), (nn.ReLU, nn.Softmax, nn.Sigmoid, None)
        ):
            model = create_res_block(
                dim_in=32,
                dim_inner=16,
                dim_out=64,
                bottleneck=create_bottleneck_block,
                conv_a_kernel_size=(3, 1, 1),
                conv_a_stride=(1, 1, 1),
                conv_a_padding=(1, 0, 0),
                conv_b_kernel_size=(1, 3, 3),
                conv_b_stride=(1, 2, 2),
                conv_b_padding=(0, 1, 1),
                conv_b_num_groups=1,
                conv_b_dilation=(1, 1, 1),
                norm=norm,
                norm_eps=1e-5,
                norm_momentum=0.1,
                activation_bottleneck=activation,
                activation_block=activation,
            )
            model_gt = ResBlock(
                branch1_conv=nn.Conv3d(
                    32, 64, kernel_size=(1, 1, 1), stride=(1, 2, 2), bias=False
                ),
                branch1_norm=None if norm is None else norm(num_features=64),
                branch2=BottleneckBlock(
                    conv_a=nn.Conv3d(
                        32,
                        16,
                        kernel_size=[3, 1, 1],
                        stride=[1, 1, 1],
                        padding=[1, 0, 0],
                        bias=False,
                    ),
                    norm_a=None if norm is None else norm(16),
                    act_a=None if activation is None else activation(),
                    conv_b=nn.Conv3d(
                        16,
                        16,
                        kernel_size=[1, 3, 3],
                        stride=[1, 2, 2],
                        padding=[0, 1, 1],
                        bias=False,
                    ),
                    norm_b=None if norm is None else norm(16),
                    act_b=None if activation is None else activation(),
                    conv_c=nn.Conv3d(
                        16,
                        64,
                        kernel_size=[1, 1, 1],
                        stride=[1, 1, 1],
                        padding=[0, 0, 0],
                        bias=False,
                    ),
                    norm_c=None if norm is None else norm(64),
                ),
                activation=None if activation is None else activation(),
                branch_fusion=lambda x, y: x + y,
            )

            model.load_state_dict(
                model_gt.state_dict(), strict=True
            )  # explicitly use strict mode.

            # Test forwarding.
            for input_tensor in TestBottleneckBlock._get_inputs(dim_in=32):
                with torch.no_grad():
                    if input_tensor.shape[1] != 32:
                        with self.assertRaises(RuntimeError):
                            output_tensor = model(input_tensor)
                        continue

                    output_tensor = model(input_tensor)
                    output_tensor_gt = model_gt(input_tensor)

                self.assertEqual(
                    output_tensor.shape,
                    output_tensor_gt.shape,
                    "Output shape {} is different from expected shape {}".format(
                        output_tensor.shape, output_tensor_gt.shape
                    ),
                )
                self.assertTrue(
                    np.allclose(output_tensor.numpy(), output_tensor_gt.numpy())
                )

    @staticmethod
    def _get_inputs(dim_in: int = 3) -> torch.tensor:
        """
        Provide different tensors as test cases.

        Yield:
            (torch.tensor): tensor as test case input.
        """
        # Prepare random inputs as test cases.
        shapes = (
            # Forward succeeded.
            (1, dim_in, 3, 7, 7),
            (1, dim_in, 5, 7, 7),
            (1, dim_in, 7, 7, 7),
            (2, dim_in, 3, 7, 7),
            (4, dim_in, 3, 7, 7),
            (8, dim_in, 3, 7, 7),
            (2, dim_in, 3, 7, 14),
            (2, dim_in, 3, 14, 7),
            (2, dim_in, 3, 14, 14),
            # Forward failed.
            (8, dim_in * 2, 3, 7, 7),
            (8, dim_in * 4, 5, 7, 7),
        )
        for shape in shapes:
            yield torch.rand(shape)


class TestResStageTransform(unittest.TestCase):
    def setUp(self):
        super().setUp()
        torch.set_rng_state(torch.manual_seed(42).get_state())

    def test_create_res_stage(self):
        """
        Test simple ResStage with different inputs.
        """
        for dim_in, dim_inner, dim_out in itertools.product(
            (4, 8, 16), (2, 4), (4, 8, 16)
        ):
            model = ResStage(
                res_blocks=nn.ModuleList(
                    [
                        ResBlock(
                            branch1_conv=nn.Conv3d(
                                dim_in, dim_out, kernel_size=(1, 1, 1)
                            )
                            if dim_in != dim_out
                            else None,
                            branch1_norm=nn.BatchNorm3d(num_features=dim_out)
                            if dim_in != dim_out
                            else None,
                            branch2=BottleneckBlock(
                                conv_a=nn.Conv3d(
                                    dim_in,
                                    dim_inner,
                                    kernel_size=[3, 1, 1],
                                    stride=[1, 1, 1],
                                    padding=[1, 0, 0],
                                    bias=False,
                                ),
                                norm_a=nn.BatchNorm3d(dim_inner),
                                act_a=nn.ReLU(),
                                conv_b=nn.Conv3d(
                                    dim_inner,
                                    dim_inner,
                                    kernel_size=[1, 3, 3],
                                    stride=[1, 1, 1],
                                    padding=[0, 1, 1],
                                    bias=False,
                                ),
                                norm_b=nn.BatchNorm3d(dim_inner),
                                act_b=nn.ReLU(),
                                conv_c=nn.Conv3d(
                                    dim_inner,
                                    dim_out,
                                    kernel_size=[1, 1, 1],
                                    stride=[1, 1, 1],
                                    padding=[0, 0, 0],
                                    bias=False,
                                ),
                                norm_c=nn.BatchNorm3d(dim_out),
                            ),
                            activation=nn.ReLU(),
                            branch_fusion=lambda x, y: x + y,
                        ),
                        ResBlock(
                            branch1_conv=None,
                            branch1_norm=None,
                            branch2=BottleneckBlock(
                                conv_a=nn.Conv3d(
                                    dim_out,
                                    dim_inner,
                                    kernel_size=[3, 1, 1],
                                    stride=[1, 1, 1],
                                    padding=[1, 0, 0],
                                    bias=False,
                                ),
                                norm_a=nn.BatchNorm3d(dim_inner),
                                act_a=nn.ReLU(),
                                conv_b=nn.Conv3d(
                                    dim_inner,
                                    dim_inner,
                                    kernel_size=[1, 3, 3],
                                    stride=[1, 1, 1],
                                    padding=[0, 1, 1],
                                    bias=False,
                                ),
                                norm_b=nn.BatchNorm3d(dim_inner),
                                act_b=nn.ReLU(),
                                conv_c=nn.Conv3d(
                                    dim_inner,
                                    dim_out,
                                    kernel_size=[1, 1, 1],
                                    stride=[1, 1, 1],
                                    padding=[0, 0, 0],
                                    bias=False,
                                ),
                                norm_c=nn.BatchNorm3d(dim_out),
                            ),
                            activation=nn.ReLU(),
                            branch_fusion=lambda x, y: x + y,
                        ),
                    ]
                )
            )

            # Test forwarding.
            for tensor in TestResStageTransform._get_inputs(dim_in):
                if tensor.shape[1] != dim_in:
                    with self.assertRaises(RuntimeError):
                        out = model(tensor)
                    continue

                out = model(tensor)

                input_shape = tensor.shape
                output_shape = out.shape
                output_shape_gt = (
                    input_shape[0],
                    dim_out,
                    input_shape[2],
                    input_shape[3],
                    input_shape[4],
                )

                self.assertEqual(
                    output_shape,
                    output_shape_gt,
                    "Output shape {} is different from expected shape {}".format(
                        output_shape, output_shape_gt
                    ),
                )

    def test_create_res_stage_with_callable(self):
        """
        Test builder `create_res_stage` with callable inputs.
        """
        dim_in, dim_inner, dim_out = 32, 16, 64
        for (norm, activation) in itertools.product(
            (nn.BatchNorm3d, None), (nn.ReLU, nn.Sigmoid, None)
        ):
            model = create_res_stage(
                depth=2,
                dim_in=dim_in,
                dim_inner=dim_inner,
                dim_out=dim_out,
                bottleneck=create_bottleneck_block,
                conv_a_kernel_size=(3, 1, 1),
                conv_a_stride=(1, 1, 1),
                conv_a_padding=(1, 0, 0),
                conv_b_kernel_size=(1, 3, 3),
                conv_b_stride=(1, 1, 1),
                conv_b_padding=(0, 1, 1),
                conv_b_num_groups=1,
                conv_b_dilation=(1, 1, 1),
                norm=norm,
                norm_eps=1e-5,
                norm_momentum=0.1,
                activation=activation,
            )
            model_gt = ResStage(
                res_blocks=nn.ModuleList(
                    [
                        ResBlock(
                            branch1_conv=nn.Conv3d(
                                dim_in, dim_out, kernel_size=(1, 1, 1), bias=False
                            )
                            if dim_in != dim_out
                            else None,
                            branch1_norm=None
                            if norm is None
                            else norm(num_features=dim_out)
                            if dim_in != dim_out
                            else None,
                            branch2=BottleneckBlock(
                                conv_a=nn.Conv3d(
                                    dim_in,
                                    dim_inner,
                                    kernel_size=[3, 1, 1],
                                    stride=[1, 1, 1],
                                    padding=[1, 0, 0],
                                    bias=False,
                                ),
                                norm_a=None if norm is None else norm(dim_inner),
                                act_a=None if activation is None else activation(),
                                conv_b=nn.Conv3d(
                                    dim_inner,
                                    dim_inner,
                                    kernel_size=[1, 3, 3],
                                    stride=[1, 1, 1],
                                    padding=[0, 1, 1],
                                    bias=False,
                                ),
                                norm_b=None if norm is None else norm(dim_inner),
                                act_b=None if activation is None else activation(),
                                conv_c=nn.Conv3d(
                                    dim_inner,
                                    dim_out,
                                    kernel_size=[1, 1, 1],
                                    stride=[1, 1, 1],
                                    padding=[0, 0, 0],
                                    bias=False,
                                ),
                                norm_c=None if norm is None else norm(dim_out),
                            ),
                            activation=None if activation is None else activation(),
                            branch_fusion=lambda x, y: x + y,
                        ),
                        ResBlock(
                            branch1_conv=None,
                            branch1_norm=None,
                            branch2=BottleneckBlock(
                                conv_a=nn.Conv3d(
                                    dim_out,
                                    dim_inner,
                                    kernel_size=[3, 1, 1],
                                    stride=[1, 1, 1],
                                    padding=[1, 0, 0],
                                    bias=False,
                                ),
                                norm_a=None if norm is None else norm(dim_inner),
                                act_a=None if activation is None else activation(),
                                conv_b=nn.Conv3d(
                                    dim_inner,
                                    dim_inner,
                                    kernel_size=[1, 3, 3],
                                    stride=[1, 1, 1],
                                    padding=[0, 1, 1],
                                    bias=False,
                                ),
                                norm_b=None if norm is None else norm(dim_inner),
                                act_b=None if activation is None else activation(),
                                conv_c=nn.Conv3d(
                                    dim_inner,
                                    dim_out,
                                    kernel_size=[1, 1, 1],
                                    stride=[1, 1, 1],
                                    padding=[0, 0, 0],
                                    bias=False,
                                ),
                                norm_c=None if norm is None else norm(dim_out),
                            ),
                            activation=None if activation is None else activation(),
                            branch_fusion=lambda x, y: x + y,
                        ),
                    ]
                )
            )
            model.load_state_dict(
                model_gt.state_dict(), strict=True
            )  # explicitly use strict mode.

            # Test forwarding.
            for tensor in TestResStageTransform._get_inputs(dim_in=dim_in):
                with torch.no_grad():
                    if tensor.shape[1] != 32:
                        with self.assertRaises(RuntimeError):
                            out = model(tensor)
                        continue

                    out = model(tensor)
                    out_gt = model_gt(tensor)

                self.assertEqual(
                    out.shape,
                    out_gt.shape,
                    "Output shape {} is different from expected shape {}".format(
                        out.shape, out_gt.shape
                    ),
                )
                self.assertTrue(np.allclose(out.numpy(), out_gt.numpy()))

    @staticmethod
    def _get_inputs(dim_in: int = 3) -> torch.tensor:
        """
        Provide different tensors as test cases.

        Yield:
            (torch.tensor): tensor as test case input.
        """
        # Prepare random inputs as test cases.
        shapes = (
            # Forward succeeded.
            (1, dim_in, 3, 7, 7),
            (1, dim_in, 5, 7, 7),
            (1, dim_in, 7, 7, 7),
            (2, dim_in, 3, 7, 7),
            (4, dim_in, 3, 7, 7),
            (8, dim_in, 3, 7, 7),
            (2, dim_in, 3, 7, 14),
            (2, dim_in, 3, 14, 7),
            (2, dim_in, 3, 14, 14),
            # Forward failed.
            (8, dim_in * 2, 3, 7, 7),
            (8, dim_in * 4, 5, 7, 7),
        )
        for shape in shapes:
            yield torch.rand(shape)


class TestResNet(unittest.TestCase):
    def setUp(self):
        super().setUp()
        torch.set_rng_state(torch.manual_seed(42).get_state())

    def _build_resnet(
        self,
        input_channel,
        input_clip_length,
        input_crop_size,
        model_depth,
        norm,
        activation,
    ):
        _MODEL_STAGE_DEPTH = {50: (3, 4, 6, 3), 101: (3, 4, 23, 3), 152: (3, 8, 36, 3)}
        stem_dim_out = 8
        model_num_class = 10
        stages = []
        # create the Stem for ResNet
        stem = ResNetBasicStem(
            conv=nn.Conv3d(
                input_channel,
                stem_dim_out,
                kernel_size=[3, 7, 7],
                stride=[1, 2, 2],
                padding=[1, 3, 3],
                bias=False,
            ),
            norm=None if norm is None else norm(stem_dim_out),
            activation=None if activation is None else activation(),
            pool=nn.MaxPool3d(
                kernel_size=[1, 3, 3], stride=[1, 2, 2], padding=[0, 1, 1]
            ),
        )
        stages.append(stem)

        # get the number of Blocks for each Stage
        stage_depths = _MODEL_STAGE_DEPTH[model_depth]

        stage_dim_in = stem_dim_out
        stage_dim_out = stage_dim_in * 4
        stage_spatial_stride = (2, 1, 1, 1)
        stage_temporal_stride = (2, 1, 1, 1)

        # create each Stage for ResNet
        for i in range(len(stage_depths)):
            stage_dim_inner = stage_dim_out // 4
            depth = stage_depths[i]

            block_dim_in = stage_dim_in
            block_dim_inner = stage_dim_inner
            block_dim_out = stage_dim_out

            blocks = []
            for j in range(depth):
                spatial_stride = stage_spatial_stride[i] if j == 0 else 1
                temporal_stride = stage_temporal_stride[i] if j == 0 else 1
                # create each Block for the Stage
                block = ResBlock(
                    branch1_conv=nn.Conv3d(
                        block_dim_in,
                        block_dim_out,
                        kernel_size=(1, 1, 1),
                        stride=(temporal_stride, spatial_stride, spatial_stride),
                        bias=False,
                    )
                    if block_dim_in != block_dim_out
                    else None,
                    branch1_norm=None
                    if norm is None
                    else norm(block_dim_out)
                    if block_dim_in != block_dim_out
                    else None,
                    branch2=BottleneckBlock(
                        conv_a=nn.Conv3d(
                            block_dim_in,
                            block_dim_inner,
                            kernel_size=[3, 1, 1],
                            stride=[temporal_stride, 1, 1],
                            padding=[1, 0, 0],
                            bias=False,
                        ),
                        norm_a=None if norm is None else norm(block_dim_inner),
                        act_a=None if activation is None else activation(),
                        conv_b=nn.Conv3d(
                            block_dim_inner,
                            block_dim_inner,
                            kernel_size=[1, 3, 3],
                            stride=[1, spatial_stride, spatial_stride],
                            padding=[0, 1, 1],
                            bias=False,
                        ),
                        norm_b=None if norm is None else norm(block_dim_inner),
                        act_b=None if activation is None else activation(),
                        conv_c=nn.Conv3d(
                            block_dim_inner,
                            block_dim_out,
                            kernel_size=[1, 1, 1],
                            stride=[1, 1, 1],
                            padding=[0, 0, 0],
                            bias=False,
                        ),
                        norm_c=None if norm is None else norm(block_dim_out),
                    ),
                    activation=None if activation is None else activation(),
                    branch_fusion=lambda x, y: x + y,
                )

                block_dim_in = block_dim_out
                blocks.append(block)

            stage = ResStage(nn.ModuleList(blocks))
            stages.append(stage)

            stage_dim_in = stage_dim_out
            stage_dim_out = stage_dim_out * 2

        # Create Head for ResNet
        total_spatial_stride = 4 * np.prod(stage_spatial_stride)
        total_temporal_stride = np.prod(stage_temporal_stride)
        head_pool_kernel_size = (
            input_clip_length // total_temporal_stride,
            input_crop_size // total_spatial_stride,
            input_crop_size // total_spatial_stride,
        )

        head = ResNetBasicHead(
            proj=nn.Linear(stage_dim_in, model_num_class),
            activation=nn.Softmax(),
            pool=nn.AvgPool3d(kernel_size=head_pool_kernel_size, stride=[1, 1, 1]),
            dropout=None,
            output_pool=nn.AdaptiveAvgPool3d(1),
        )
        stages.append(head)

        return (Net(blocks=nn.ModuleList(stages)), model_num_class)

    def test_create_resnet(self):
        """
        Test simple ResNet with different inputs.
        """
        for input_channel, input_clip_length, input_crop_size in itertools.product(
            (3, 2), (2, 4), (56, 64)
        ):
            model_depth = 50
            model, num_class = self._build_resnet(
                input_channel,
                input_clip_length,
                input_crop_size,
                model_depth,
                nn.BatchNorm3d,
                nn.ReLU,
            )

            # Test forwarding.
            for tensor in TestResNet._get_inputs(
                input_channel, input_clip_length, input_crop_size
            ):
                if tensor.shape[1] != input_channel:
                    with self.assertRaises(RuntimeError):
                        out = model(tensor)
                    continue

                out = model(tensor)

                output_shape = out.shape
                output_shape_gt = (tensor.shape[0], num_class)

                self.assertEqual(
                    output_shape,
                    output_shape_gt,
                    "Output shape {} is different from expected shape {}".format(
                        output_shape, output_shape_gt
                    ),
                )

    def test_create_resnet_with_callable(self):
        """
        Test builder `create_resnet` with callable inputs.
        """
        for (norm, activation) in itertools.product(
            (nn.BatchNorm3d, None), (nn.ReLU, nn.Sigmoid, None)
        ):
            input_channel = 3
            input_clip_length = 4
            input_crop_size = 56
            model_depth = 50
            stage_spatial_stride = (2, 1, 1, 1)
            stage_temporal_stride = (2, 1, 1, 1)
            model_gt, num_class = self._build_resnet(
                input_channel,
                input_clip_length,
                input_crop_size,
                model_depth,
                norm,
                activation,
            )

            total_spatial_stride = 4 * np.prod(stage_spatial_stride)
            total_temporal_stride = np.prod(stage_temporal_stride)
            head_pool_kernel_size = (
                input_clip_length // total_temporal_stride,
                input_crop_size // total_spatial_stride,
                input_crop_size // total_spatial_stride,
            )

            model = create_resnet(
                input_channel=input_channel,
                model_depth=50,
                model_num_class=num_class,
                dropout_rate=0,
                norm=norm,
                activation=activation,
                stem_dim_out=8,
                stem_conv_kernel_size=(3, 7, 7),
                stem_conv_stride=(1, 2, 2),
                stem_pool=nn.MaxPool3d,
                stem_pool_kernel_size=(1, 3, 3),
                stem_pool_stride=(1, 2, 2),
                stage_conv_a_kernel_size=((3, 1, 1),) * 4,
                stage_conv_b_kernel_size=((1, 3, 3),) * 4,
                stage_spatial_h_stride=stage_spatial_stride,
                stage_spatial_w_stride=stage_spatial_stride,
                stage_temporal_stride=stage_temporal_stride,
                bottleneck=create_bottleneck_block,
                head_pool=nn.AvgPool3d,
                head_pool_kernel_size=head_pool_kernel_size,
                head_output_size=(1, 1, 1),
                head_activation=nn.Softmax,
            )

            model.load_state_dict(
                model_gt.state_dict(), strict=True
            )  # explicitly use strict mode.

            # Test forwarding.
            for tensor in TestResNet._get_inputs(
                input_channel, input_clip_length, input_crop_size
            ):
                with torch.no_grad():
                    if tensor.shape[1] != input_channel:
                        with self.assertRaises(RuntimeError):
                            out = model(tensor)
                        continue

                    out = model(tensor)
                    out_gt = model_gt(tensor)

                self.assertEqual(
                    out.shape,
                    out_gt.shape,
                    "Output shape {} is different from expected shape {}".format(
                        out.shape, out_gt.shape
                    ),
                )
                self.assertTrue(
                    np.allclose(out.numpy(), out_gt.numpy(), rtol=1e-1, atol=1e-1)
                )

    def test_create_acoustic_resnet_with_callable(self):
        """
        Test builder `create_acoustic_resnet` with callable inputs.
        """
        _input_channel = 1
        for (norm, activation) in itertools.product(
            (nn.BatchNorm3d, None), (nn.ReLU, nn.Sigmoid, None)
        ):
            model = create_acoustic_resnet(
                input_channel=_input_channel,
                stem_conv_kernel_size=(3, 3, 3),
                model_depth=50,
                model_num_class=400,
                dropout_rate=0,
                norm=norm,
                activation=activation,
                stem_dim_out=8,
                stem_pool=None,
                stem_pool_kernel_size=(1, 3, 3),
                stem_pool_stride=(1, 2, 2),
                stage_conv_a_kernel_size=(3, 1, 1),
                stage_conv_b_kernel_size=(1, 3, 3),
                stage_spatial_h_stride=(2, 1, 1, 1),
                stage_spatial_w_stride=(2, 1, 1, 1),
                stage_temporal_stride=(2, 1, 1, 1),
                head_pool=nn.AvgPool3d,
                head_output_size=(1, 1, 1),
                head_activation=nn.Softmax,
            )

            # Test forwarding.
            for tensor in TestResNet._get_acoustic_inputs(_input_channel, 8, 56):
                with torch.no_grad():
                    if tensor.shape[1] != _input_channel:
                        with self.assertRaises(RuntimeError):
                            model(tensor)
                        continue
                    model(tensor)

    def test_load_hubconf(self):
        path = os.path.join(
            os.path.dirname(os.path.realpath(__file__)),
            "..",
        )
        input_channel = 3
        input_clip_length = 2
        input_crop_size = 56
        model = torch.hub.load(
            repo_or_dir=path, source="local", model="slow_r50", pretrained=False
        )
        self.assertIsNotNone(model)

        # Test forwarding.
        for tensor in TestResNet._get_inputs(
            input_channel, input_clip_length, input_crop_size
        ):
            with torch.no_grad():
                if tensor.shape[1] != input_channel:
                    with self.assertRaises(RuntimeError):
                        model(tensor)
                    continue

    def test_load_hubconf_detection(self):
        path = os.path.join(
            os.path.dirname(os.path.realpath(__file__)),
            "..",
        )
        input_channel = 3
        input_clip_length = 4
        input_crop_size = 56
        model = torch.hub.load(
            repo_or_dir=path,
            source="local",
            model="slow_r50_detection",
            pretrained=False,
        )
        self.assertIsNotNone(model)

        # Test forwarding.
        bbox_test_imputs = torch.tensor([[0.0, 10, 15, 20, 25], [0.0, 11, 16, 21, 26]])
        for tensor in TestResNet._get_inputs(
            input_channel, input_clip_length, input_crop_size
        ):
            with torch.no_grad():
                if tensor.shape[1] != input_channel:
                    with self.assertRaises(RuntimeError):
                        model(tensor, bbox_test_imputs)
                    continue
                model(tensor, bbox_test_imputs)

    def test_create_resnet_with_roi_head_with_callable(self):
        input_channel = 3
        input_clip_length = 4
        input_crop_size = 56
        model = create_resnet_with_roi_head()
        self.assertIsNotNone(model)

        # Test forwarding.
        bbox_test_imputs = torch.tensor([[0.0, 10, 15, 20, 25], [0.0, 11, 16, 21, 26]])
        for tensor in TestResNet._get_inputs(
            input_channel, input_clip_length, input_crop_size
        ):
            with torch.no_grad():
                if tensor.shape[1] != input_channel:
                    with self.assertRaises(RuntimeError):
                        model(tensor, bbox_test_imputs)
                    continue
                model(tensor, bbox_test_imputs)

    @staticmethod
    def _get_inputs(
        channel: int = 3, clip_length: int = 8, crop_size: int = 224
    ) -> torch.tensor:
        """
        Provide different tensors as test cases.

        Yield:
            (torch.tensor): tensor as test case input.
        """
        # Prepare random inputs as test cases.
        shapes = (
            (1, channel, clip_length, crop_size, crop_size),
            (2, channel, clip_length, crop_size, crop_size),
        )
        for shape in shapes:
            yield torch.rand(shape)

    @staticmethod
    def _get_acoustic_inputs(
        channel: int = 1, clip_length: int = 130, freq_size: int = 80
    ) -> torch.tensor:
        """
        Provide different tensors as test cases.

        Yield:
            (torch.tensor): tensor as test case input.
        """
        # Prepare random inputs as test cases.
        shapes = (
            (1, channel, clip_length, 1, freq_size),
            (2, channel, clip_length, 1, freq_size),
        )
        for shape in shapes:
            yield torch.rand(shape)
