//go:build linux && unit
// +build linux,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 networkutils

import (
	"context"
	"errors"
	"net"
	"testing"
	"time"

	"github.com/golang/mock/gomock"
	"github.com/stretchr/testify/assert"
	"github.com/vishvananda/netlink"

	mock_netlinkwrapper "github.com/aws/amazon-ecs-agent/ecs-agent/utils/netlinkwrapper/mocks"
)

const (
	randomDevice     = "eth1"
	validMAC         = "00:0a:95:9d:68:16"
	pciDevPath       = " ../../devices/pci0000:00/0000:00:03.0/net/eth1"
	virtualDevPath   = "../../devices/virtual/net/lo"
	invalidDevPath   = "../../virtual/net/lo"
	incorrectDevPath = "../../devices/totally/wrong/net/path"
)

// TestGetMACAddress checks obtaining MACAddress by using netlinkClient
func TestGetMACAddress(t *testing.T) {
	mockCtrl := gomock.NewController(t)
	defer mockCtrl.Finish()
	mockNetlink := mock_netlinkwrapper.NewMockNetLink(mockCtrl)
	pm, _ := net.ParseMAC(validMAC)
	mockNetlink.EXPECT().LinkByName(randomDevice).Return(
		&netlink.Device{
			LinkAttrs: netlink.LinkAttrs{
				HardwareAddr: pm,
				Name:         randomDevice,
			},
		}, nil)
	ctx := context.TODO()
	mac, err := GetMACAddress(ctx, time.Millisecond, randomDevice, mockNetlink)
	assert.Nil(t, err)
	assert.Equal(t, validMAC, mac)
}

// TestGetMACAddressWithNetlinkError attempts to test the netlinkClient
// error code path
func TestGetMACAddressWithNetlinkError(t *testing.T) {
	mockCtrl := gomock.NewController(t)
	defer mockCtrl.Finish()
	mockNetlink := mock_netlinkwrapper.NewMockNetLink(mockCtrl)
	// LinkByName returns an error. This will ensure that a non-retriable
	// error is generated and halts the backoff-rety loop
	mockNetlink.EXPECT().LinkByName(randomDevice).Return(
		&netlink.Device{},
		errors.New("Dummy Netlink Error"))
	ctx := context.TODO()
	mac, err := GetMACAddress(ctx, 2*macAddressBackoffMax, randomDevice, mockNetlink)
	assert.Error(t, err)
	assert.Empty(t, mac)
}

// TestGetMACAddressNotFoundRetry tests if netlink.LinkByName gets invoked
// multiple times if the mac address for a device is returned as empty string
func TestGetMACAddressNotFoundRetry(t *testing.T) {
	mockCtrl := gomock.NewController(t)
	defer mockCtrl.Finish()
	mockNetlink := mock_netlinkwrapper.NewMockNetLink(mockCtrl)
	pm, _ := net.ParseMAC(validMAC)
	gomock.InOrder(
		// Return empty mac address on first invocation
		mockNetlink.EXPECT().LinkByName(randomDevice).Return(&netlink.Device{}, nil),
		// Return a valid mac address on first invocation. Even though the first
		// invocation did not result in an error, it did return an empty mac address.
		// Hence, we expect it to be retried
		mockNetlink.EXPECT().LinkByName(randomDevice).Return(&netlink.Device{
			LinkAttrs: netlink.LinkAttrs{
				HardwareAddr: pm,
				Name:         randomDevice,
			},
		}, nil),
	)
	ctx := context.TODO()
	// Set max retry duration to twice that of the max backoff to ensure that there's
	// enough time to retry
	mac, err := GetMACAddress(ctx, 2*macAddressBackoffMax, randomDevice, mockNetlink)
	assert.NoError(t, err)
	assert.Equal(t, validMAC, mac)
}

// TestGetMACAddressNotFoundRetryExpires tests if the backoff-retry logic for
// retrieving mac address returns error on timeout
func TestGetMACAddressNotFoundRetryExpires(t *testing.T) {
	mockCtrl := gomock.NewController(t)
	defer mockCtrl.Finish()
	mockNetlink := mock_netlinkwrapper.NewMockNetLink(mockCtrl)
	// Return empty mac address everytime
	mockNetlink.EXPECT().LinkByName(randomDevice).Return(&netlink.Device{}, nil).MinTimes(1)
	ctx := context.TODO()
	// Set max retry duration to twice that of the min backoff to ensure that there's
	// enough time to retry
	mac, err := GetMACAddress(ctx, 2*macAddressBackoffMin, randomDevice, mockNetlink)
	assert.Error(t, err)
	assert.Empty(t, mac)
}

// TestIsValidDevicePathTableTest does a table test for device path validity
func TestIsValidDevicePathTableTest(t *testing.T) {
	var table = []struct {
		input  string
		output bool
	}{
		{pciDevPath, true},
		{virtualDevPath, false},
		{invalidDevPath, false},
		{incorrectDevPath, false},
	}

	for _, entry := range table {
		status := IsValidNetworkDevice(entry.input)
		assert.Equal(t, entry.output, status)
	}
}
