//go:build windows && unit
// +build windows,unit

// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"). You may
// not use this file except in compliance with the License. A copy of the
// License is located at
//
//	http://aws.amazon.com/apache2.0/
//
// or in the "license" file accompanying this file. This file 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.

package ecscni

import (
	"context"
	"encoding/json"
	"fmt"
	"strings"
	"testing"
	"time"

	ni "github.com/aws/amazon-ecs-agent/ecs-agent/netlib/model/networkinterface"

	"github.com/golang/mock/gomock"
	"github.com/pkg/errors"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/containernetworking/cni/libcni"
	cniTypesCurrent "github.com/containernetworking/cni/pkg/types/100"

	mock_libcni "github.com/aws/amazon-ecs-agent/agent/ecscni/mocks_libcni"
)

const (
	eniID                       = "eni-12345678"
	ipv4Address                 = "172.31.21.40"
	eniMACAddress               = "02:7b:64:49:b1:40"
	eniSubnetGatewayIPV4Address = "172.31.1.1/20"
)

// getNetworkConfig creates and returns a sample network config for task namespace setup.
func getNetworkConfig() *Config {
	config := &Config{
		NetworkConfigs: []*NetworkConfig{},
	}

	eniNetworkConfig, _ := NewVPCENIPluginConfigForTaskNSSetup(getTestENI(), config)
	ecsBridgeConfig, _ := NewVPCENIPluginConfigForECSBridgeSetup(config)

	config.NetworkConfigs = append(config.NetworkConfigs,
		&NetworkConfig{
			IfName:           "eth0",
			CNINetworkConfig: eniNetworkConfig,
		},
		&NetworkConfig{
			IfName:           "eth1",
			CNINetworkConfig: ecsBridgeConfig,
		},
	)
	return config
}

// getTestENI returns a sample ENI for the task.
func getTestENI() *ni.NetworkInterface {
	return &ni.NetworkInterface{
		ID:       eniID,
		LinkName: eniID,
		IPV4Addresses: []*ni.IPV4Address{
			{Address: ipv4Address, Primary: true},
		},
		MacAddress:               eniMACAddress,
		SubnetGatewayIPV4Address: eniSubnetGatewayIPV4Address,
	}
}

// TestSetupNS is used to test if the namespace is setup properly as per the provided configuration
func TestSetupNS(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	ecscniClient := NewClient("")
	libcniClient := mock_libcni.NewMockCNI(ctrl)
	ecscniClient.(*cniClient).libcni = libcniClient

	gomock.InOrder(
		// vpc-eni plugin called to setup task namespace.
		libcniClient.EXPECT().AddNetwork(gomock.Any(), gomock.Any(), gomock.Any()).Return(&cniTypesCurrent.Result{}, nil).Do(
			func(ctx context.Context, net *libcni.NetworkConfig, rt *libcni.RuntimeConf) {
				assert.Equal(t, ECSVPCENIPluginExecutable, net.Network.Type, "plugin should be vpc-eni")
			}).Times(2),
	)

	config := getNetworkConfig()
	_, err := ecscniClient.SetupNS(context.TODO(), config, time.Second)

	assert.NoError(t, err)
}

// TestSetupNSTimeout tests the behavior when CNI plugin invocation returns an error.
func TestSetupNSTimeout(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	// Override the maximum retry timeout for the tests
	setupNSBackoffMax = setupNSBackoffMin

	ecscniClient := NewClient("")
	libcniClient := mock_libcni.NewMockCNI(ctrl)
	ecscniClient.(*cniClient).libcni = libcniClient

	gomock.InOrder(
		// vpc-eni plugin will be called.
		libcniClient.EXPECT().AddNetwork(gomock.Any(), gomock.Any(), gomock.Any()).Return(&cniTypesCurrent.Result{}, errors.New("timeout")).Do(
			func(ctx context.Context, net *libcni.NetworkConfig, rt *libcni.RuntimeConf) {
			}).MaxTimes(setupNSMaxRetryCount),
	)

	config := getNetworkConfig()
	_, err := ecscniClient.SetupNS(context.TODO(), config, time.Millisecond)

	assert.Error(t, err)
}

// TestSetupNSWithRetry tests the behavior when CNI plugin invocation returns an error and then succeeds in retry.
func TestSetupNSWithRetry(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	ecscniClient := NewClient("")
	libcniClient := mock_libcni.NewMockCNI(ctrl)
	ecscniClient.(*cniClient).libcni = libcniClient

	gomock.InOrder(
		// First invocation of the plugin for setupNS will fail and the same will succeed in retry.
		libcniClient.EXPECT().AddNetwork(gomock.Any(), gomock.Any(), gomock.Any()).Return(&cniTypesCurrent.Result{}, errors.New("timeout")).Do(
			func(ctx context.Context, net *libcni.NetworkConfig, rt *libcni.RuntimeConf) {
			}),
		libcniClient.EXPECT().AddNetwork(gomock.Any(), gomock.Any(), gomock.Any()).Return(&cniTypesCurrent.Result{}, nil).Do(
			func(ctx context.Context, net *libcni.NetworkConfig, rt *libcni.RuntimeConf) {
				assert.Equal(t, ECSVPCENIPluginExecutable, net.Network.Type, "first plugin should be vpc-eni")
			}).Times(2),
	)

	config := getNetworkConfig()
	_, err := ecscniClient.SetupNS(context.TODO(), config, setupNSBackoffMax)

	assert.NoError(t, err)
}

// TestCleanupNS tests the cleanup of the task namespace when CleanupNS is called.
func TestCleanupNS(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	ecscniClient := NewClient("")
	libcniClient := mock_libcni.NewMockCNI(ctrl)
	ecscniClient.(*cniClient).libcni = libcniClient

	libcniClient.EXPECT().DelNetwork(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).Times(2)

	config := getNetworkConfig()
	err := ecscniClient.CleanupNS(context.TODO(), config, time.Second)

	assert.NoError(t, err)
}

// TestCleanupNSTimeout tests the behavior of CleanupNS when we get an error from CNI invocation
func TestCleanupNSTimeout(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	ecscniClient := NewClient("")
	libcniClient := mock_libcni.NewMockCNI(ctrl)
	ecscniClient.(*cniClient).libcni = libcniClient

	// This will be called for both the endpoints.
	libcniClient.EXPECT().DelNetwork(gomock.Any(), gomock.Any(), gomock.Any()).Do(
		func(x interface{}, y interface{}, z interface{}) {
		}).Return(errors.New("timeout")).MaxTimes(2)

	ctx, cancel := context.WithTimeout(context.TODO(), 1*time.Millisecond)
	defer cancel()

	config := getNetworkConfig()
	err := ecscniClient.CleanupNS(ctx, config, time.Millisecond)

	assert.Error(t, err)
}

// TestConstructNetworkConfig tests if we create an appropriate config for setting up task namespace.
func TestConstructNetworkConfig(t *testing.T) {
	config := getNetworkConfig()
	taskENI := getTestENI()

	// Config for task ns setup to use task ENI.
	taskENIConfig := &VPCENIPluginConfig{}
	err := json.Unmarshal(config.NetworkConfigs[0].CNINetworkConfig.Bytes, taskENIConfig)
	require.NoError(t, err, "unmarshal task eni config from bytes failed")

	subnet := strings.Split(eniSubnetGatewayIPV4Address, "/")
	ipv4Addr := fmt.Sprintf("%s/%s", taskENI.GetPrimaryIPv4Address(), subnet[1])
	assert.Equal(t, VPCENIPluginName, taskENIConfig.Type)
	assert.Equal(t, TaskHNSNetworkNamePrefix, config.NetworkConfigs[0].CNINetworkConfig.Network.Name)
	assert.Equal(t, eniID, taskENIConfig.ENIName)
	assert.EqualValues(t, []string{ipv4Addr}, taskENIConfig.ENIIPAddresses)
	assert.EqualValues(t, taskENI.MacAddress, taskENIConfig.ENIMACAddress)
	assert.EqualValues(t, []string{subnet[0]}, taskENIConfig.GatewayIPAddresses)
	assert.False(t, taskENIConfig.UseExistingNetwork)

	// Config for ecs-bridge endpoint setup for the task.
	ecsBridgeConfig := &VPCENIPluginConfig{}
	err = json.Unmarshal(config.NetworkConfigs[1].CNINetworkConfig.Bytes, ecsBridgeConfig)
	require.NoError(t, err, "unmarshal ecs-bridge config from bytes failed")
	assert.Equal(t, VPCENIPluginName, ecsBridgeConfig.Type)
	assert.Equal(t, ECSBridgeNetworkName, config.NetworkConfigs[1].CNINetworkConfig.Network.Name)
	assert.True(t, ecsBridgeConfig.UseExistingNetwork)
}

// TestCNIPluginVersion tests if the string generated by version is correct
func TestCNIPluginVersion(t *testing.T) {
	testCases := []struct {
		version *cniPluginVersion
		str     string
	}{
		{
			version: &cniPluginVersion{
				Version:      "1",
				GitShortHash: "abcd",
				Built:        "July",
			},
			str: "abcd-1",
		},
		{
			version: &cniPluginVersion{
				Version:      "1",
				GitShortHash: "abcdef",
				Built:        "June",
			},
			str: "abcdef-1",
		},
	}

	for _, tc := range testCases {
		t.Run(fmt.Sprintf("version string %s", tc.str), func(t *testing.T) {
			assert.Equal(t, tc.str, tc.version.str())
		})
	}
}
