//go:build linux

// Copyright (c) 2019 Ericsson Eurolab Deutschland G.m.b.H.
//
// SPDX-License-Identifier: Apache-2.0
//

package virtcontainers

import (
	"context"
	"net/http"
	"os"
	"path/filepath"
	"reflect"
	"strings"
	"testing"

	"github.com/kata-containers/kata-containers/src/runtime/pkg/device/config"
	"github.com/kata-containers/kata-containers/src/runtime/virtcontainers/persist"
	chclient "github.com/kata-containers/kata-containers/src/runtime/virtcontainers/pkg/cloud-hypervisor/client"
	"github.com/kata-containers/kata-containers/src/runtime/virtcontainers/types"
	"github.com/kata-containers/kata-containers/src/runtime/virtcontainers/utils"
	"github.com/pkg/errors"
	"github.com/stretchr/testify/assert"
)

const (
	FAIL = true
	PASS = !FAIL
)

func newClhConfig() (HypervisorConfig, error) {

	setupClh()

	if testClhPath == "" {
		return HypervisorConfig{}, errors.New("hypervisor fake path is empty")
	}

	if testVirtiofsdPath == "" {
		return HypervisorConfig{}, errors.New("virtiofsd fake path is empty")
	}

	if _, err := os.Stat(testClhPath); os.IsNotExist(err) {
		return HypervisorConfig{}, err
	}

	if _, err := os.Stat(testVirtiofsdPath); os.IsNotExist(err) {
		return HypervisorConfig{}, err
	}

	return HypervisorConfig{
		KernelPath:                    testClhKernelPath,
		ImagePath:                     testClhImagePath,
		HypervisorPath:                testClhPath,
		NumVCPUs:                      defaultVCPUs,
		BlockDeviceDriver:             config.VirtioBlock,
		MemorySize:                    defaultMemSzMiB,
		DefaultBridges:                defaultBridges,
		DefaultMaxVCPUs:               uint32(64),
		SharedFS:                      config.VirtioFS,
		VirtioFSCache:                 virtioFsCacheAlways,
		VirtioFSDaemon:                testVirtiofsdPath,
		NetRateLimiterBwMaxRate:       int64(0),
		NetRateLimiterBwOneTimeBurst:  int64(0),
		NetRateLimiterOpsMaxRate:      int64(0),
		NetRateLimiterOpsOneTimeBurst: int64(0),
	}, nil
}

type clhClientMock struct {
	vmInfo chclient.VmInfo
}

func (c *clhClientMock) VmmPingGet(ctx context.Context) (chclient.VmmPingResponse, *http.Response, error) {
	return chclient.VmmPingResponse{}, nil, nil
}

func (c *clhClientMock) ShutdownVMM(ctx context.Context) (*http.Response, error) {
	return nil, nil
}

func (c *clhClientMock) CreateVM(ctx context.Context, vmConfig chclient.VmConfig) (*http.Response, error) {
	c.vmInfo.State = clhStateCreated
	return nil, nil
}

//nolint:golint
func (c *clhClientMock) VmInfoGet(ctx context.Context) (chclient.VmInfo, *http.Response, error) {
	return c.vmInfo, nil, nil
}

func (c *clhClientMock) BootVM(ctx context.Context) (*http.Response, error) {
	c.vmInfo.State = clhStateRunning
	return nil, nil
}

//nolint:golint
func (c *clhClientMock) VmResizePut(ctx context.Context, vmResize chclient.VmResize) (*http.Response, error) {
	return nil, nil
}

//nolint:golint
func (c *clhClientMock) VmAddDevicePut(ctx context.Context, deviceConfig chclient.DeviceConfig) (chclient.PciDeviceInfo, *http.Response, error) {
	return chclient.PciDeviceInfo{}, nil, nil
}

//nolint:golint
func (c *clhClientMock) VmAddDiskPut(ctx context.Context, diskConfig chclient.DiskConfig) (chclient.PciDeviceInfo, *http.Response, error) {
	return chclient.PciDeviceInfo{Bdf: "0000:00:0a.0"}, nil, nil
}

//nolint:golint
func (c *clhClientMock) VmRemoveDevicePut(ctx context.Context, vmRemoveDevice chclient.VmRemoveDevice) (*http.Response, error) {
	return nil, nil
}

func TestCloudHypervisorAddVSock(t *testing.T) {
	assert := assert.New(t)
	clh := cloudHypervisor{}

	clh.addVSock(1, "path")
	assert.Equal(clh.vmconfig.Vsock.Cid, int64(1))
	assert.Equal(clh.vmconfig.Vsock.Socket, "path")
}

// Check addNet appends to the network config list new configurations.
// Check that the elements in the list has the correct values
func TestCloudHypervisorAddNetCheckNetConfigListValues(t *testing.T) {
	assert := assert.New(t)

	macTest := "00:00:00:00:00"

	file, err := os.CreateTemp("", "netFd")
	assert.Nil(err)
	defer os.Remove(file.Name())

	vmFds := make([]*os.File, 1)
	vmFds = append(vmFds, file)

	clh := cloudHypervisor{}
	clh.netDevicesFiles = make(map[string][]*os.File)

	e := &VethEndpoint{}
	e.NetPair.TAPIface.HardAddr = macTest
	e.NetPair.TapInterface.VMFds = vmFds

	err = clh.addNet(e)
	assert.Nil(err)

	assert.Equal(len(*clh.netDevices), 1)
	if err == nil {
		assert.Equal(*(*clh.netDevices)[0].Mac, macTest)
	}

	err = clh.addNet(e)
	assert.Nil(err)

	assert.Equal(len(*clh.netDevices), 2)
	if err == nil {
		assert.Equal(*(*clh.netDevices)[1].Mac, macTest)
	}
}

// Check addNet with valid values, and fail with invalid values
// For Cloud Hypervisor only tap is be required
func TestCloudHypervisorAddNetCheckEnpointTypes(t *testing.T) {
	assert := assert.New(t)

	macTest := "00:00:00:00:00"

	file, err := os.CreateTemp("", "netFd")
	assert.Nil(err)
	defer os.Remove(file.Name())

	vmFds := make([]*os.File, 1)
	vmFds = append(vmFds, file)

	validVeth := &VethEndpoint{}
	validVeth.NetPair.TAPIface.HardAddr = macTest
	validVeth.NetPair.TapInterface.VMFds = vmFds

	type args struct {
		e Endpoint
	}
	// nolint: govet
	tests := []struct {
		name    string
		args    args
		wantErr bool
	}{
		{"TapEndpoint", args{e: &TapEndpoint{}}, true},
		{"Empty VethEndpoint", args{e: &VethEndpoint{}}, true},
		{"Valid VethEndpoint", args{e: validVeth}, false},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			clh := &cloudHypervisor{}
			clh.netDevicesFiles = make(map[string][]*os.File)
			if err := clh.addNet(tt.args.e); (err != nil) != tt.wantErr {
				t.Errorf("cloudHypervisor.addNet() error = %v, wantErr %v", err, tt.wantErr)
			} else if err == nil {
				files := clh.netDevicesFiles[macTest]
				assert.Equal(files, vmFds)
			}
		})
	}
}

// Check AddNet properly sets up the network rate limiter
func TestCloudHypervisorNetRateLimiter(t *testing.T) {
	assert := assert.New(t)

	file, err := os.CreateTemp("", "netFd")
	assert.Nil(err)
	defer os.Remove(file.Name())

	vmFds := make([]*os.File, 1)
	vmFds = append(vmFds, file)

	validVeth := &VethEndpoint{}
	validVeth.NetPair.TapInterface.VMFds = vmFds

	type args struct {
		bwMaxRate       int64
		bwOneTimeBurst  int64
		opsMaxRate      int64
		opsOneTimeBurst int64
	}

	//nolint: govet
	tests := []struct {
		name                  string
		args                  args
		expectsRateLimiter    bool
		expectsBwBucketToken  bool
		expectsOpsBucketToken bool
	}{
		// Bandwidth
		{
			"Bandwidth | max rate with one time burst",
			args{
				bwMaxRate:      int64(1000),
				bwOneTimeBurst: int64(10000),
			},
			true,  // expectsRateLimiter
			true,  // expectsBwBucketToken
			false, // expectsOpsBucketToken
		},
		{
			"Bandwidth | max rate without one time burst",
			args{
				bwMaxRate: int64(1000),
			},
			true,  // expectsRateLimiter
			true,  // expectsBwBucketToken
			false, // expectsOpsBucketToken
		},
		{
			"Bandwidth | no max rate with one time burst",
			args{
				bwOneTimeBurst: int64(10000),
			},
			false, // expectsRateLimiter
			false, // expectsBwBucketToken
			false, // expectsOpsBucketToken
		},
		{
			"Bandwidth | no max rate and no one time burst",
			args{},
			false, // expectsRateLimiter
			false, // expectsBwBucketToken
			false, // expectsOpsBucketToken
		},

		// Operations
		{
			"Operations | max rate with one time burst",
			args{
				opsMaxRate:      int64(1000),
				opsOneTimeBurst: int64(10000),
			},
			true,  // expectsRateLimiter
			false, // expectsBwBucketToken
			true,  // expectsOpsBucketToken
		},
		{
			"Operations | max rate without one time burst",
			args{
				opsMaxRate: int64(1000),
			},
			true,  // expectsRateLimiter
			false, // expectsBwBucketToken
			true,  // expectsOpsBucketToken
		},
		{
			"Operations | no max rate with one time burst",
			args{
				opsOneTimeBurst: int64(10000),
			},
			false, // expectsRateLimiter
			false, // expectsBwBucketToken
			false, // expectsOpsBucketToken
		},
		{
			"Operations | no max rate and no one time burst",
			args{},
			false, // expectsRateLimiter
			false, // expectsBwBucketToken
			false, // expectsOpsBucketToken
		},

		// Bandwidth and Operations
		{
			"Bandwidth and Operations | max rate with one time burst",
			args{
				bwMaxRate:       int64(1000),
				bwOneTimeBurst:  int64(10000),
				opsMaxRate:      int64(1000),
				opsOneTimeBurst: int64(10000),
			},
			true, // expectsRateLimiter
			true, // expectsBwBucketToken
			true, // expectsOpsBucketToken
		},
		{
			"Bandwidth and Operations | max rate without one time burst",
			args{
				bwMaxRate:  int64(1000),
				opsMaxRate: int64(1000),
			},
			true, // expectsRateLimiter
			true, // expectsBwBucketToken
			true, // expectsOpsBucketToken
		},
		{
			"Bandwidth and Operations | no max rate with one time burst",
			args{
				bwOneTimeBurst:  int64(10000),
				opsOneTimeBurst: int64(10000),
			},
			false, // expectsRateLimiter
			false, // expectsBwBucketToken
			false, // expectsOpsBucketToken
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			clhConfig, err := newClhConfig()
			assert.NoError(err)

			clhConfig.NetRateLimiterBwMaxRate = tt.args.bwMaxRate
			clhConfig.NetRateLimiterBwOneTimeBurst = tt.args.bwOneTimeBurst
			clhConfig.NetRateLimiterOpsMaxRate = tt.args.opsMaxRate
			clhConfig.NetRateLimiterOpsOneTimeBurst = tt.args.opsOneTimeBurst

			clh := &cloudHypervisor{}
			clh.netDevicesFiles = make(map[string][]*os.File)
			clh.config = clhConfig
			clh.APIClient = &clhClientMock{}

			if err := clh.addNet(validVeth); err != nil {
				t.Errorf("cloudHypervisor.addNet() error = %v", err)
			} else {
				netConfig := (*clh.netDevices)[0]

				assert.Equal(netConfig.HasRateLimiterConfig(), tt.expectsRateLimiter)
				if tt.expectsRateLimiter {
					rateLimiterConfig := netConfig.GetRateLimiterConfig()
					assert.Equal(rateLimiterConfig.HasBandwidth(), tt.expectsBwBucketToken)
					assert.Equal(rateLimiterConfig.HasOps(), tt.expectsOpsBucketToken)

					if tt.expectsBwBucketToken {
						bwBucketToken := rateLimiterConfig.GetBandwidth()
						assert.Equal(bwBucketToken.GetSize(), int64(utils.RevertBytes(uint64(tt.args.bwMaxRate/8))))
						assert.Equal(bwBucketToken.GetOneTimeBurst(), int64(utils.RevertBytes(uint64(tt.args.bwOneTimeBurst/8))))
					}

					if tt.expectsOpsBucketToken {
						opsBucketToken := rateLimiterConfig.GetOps()
						assert.Equal(opsBucketToken.GetSize(), int64(tt.args.opsMaxRate))
						assert.Equal(opsBucketToken.GetOneTimeBurst(), int64(tt.args.opsOneTimeBurst))
					}
				}
			}
		})
	}
}

func TestCloudHypervisorBootVM(t *testing.T) {
	clh := &cloudHypervisor{}
	clh.APIClient = &clhClientMock{}

	savedVmAddNetPutRequestFunc := vmAddNetPutRequest
	vmAddNetPutRequest = func(clh *cloudHypervisor) error { return nil }
	defer func() {
		vmAddNetPutRequest = savedVmAddNetPutRequestFunc
	}()

	var ctx context.Context
	if err := clh.bootVM(ctx); err != nil {
		t.Errorf("cloudHypervisor.bootVM() error = %v", err)
	}
}

func TestCloudHypervisorCleanupVM(t *testing.T) {
	assert := assert.New(t)
	store, err := persist.GetDriver()
	assert.NoError(err, "persist.GetDriver() unexpected error")

	clh := &cloudHypervisor{
		config: HypervisorConfig{
			VMStorePath:  store.RunVMStoragePath(),
			RunStorePath: store.RunStoragePath(),
		},
	}

	err = clh.cleanupVM(true)
	assert.Error(err, "persist.GetDriver() expected error")

	clh.id = "cleanVMID"

	err = clh.cleanupVM(true)
	assert.NoError(err, "persist.GetDriver() unexpected error")

	dir := filepath.Join(store.RunVMStoragePath(), clh.id)
	os.MkdirAll(dir, os.ModePerm)

	err = clh.cleanupVM(false)
	assert.NoError(err, "persist.GetDriver() unexpected error")

	_, err = os.Stat(dir)
	assert.Error(err, "dir should not exist %s", dir)

	assert.True(os.IsNotExist(err), "persist.GetDriver() unexpected error")
}

func TestClhCreateVMWithInitrd(t *testing.T) {
	assert := assert.New(t)

	clhConfig, err := newClhConfig()
	assert.NoError(err)
	clhConfig.ImagePath = ""
	clhConfig.InitrdPath = testClhInitrdPath

	store, err := persist.GetDriver()
	assert.NoError(err)

	clhConfig.VMStorePath = store.RunVMStoragePath()
	clhConfig.RunStorePath = store.RunStoragePath()

	network, err := NewNetwork()
	assert.NoError(err)

	clh := &cloudHypervisor{
		config: clhConfig,
	}

	sandbox := &Sandbox{
		ctx: context.Background(),
		id:  "testSandbox",
		config: &SandboxConfig{
			HypervisorConfig: clhConfig,
		},
	}

	err = clh.CreateVM(context.Background(), sandbox.id, network, &sandbox.config.HypervisorConfig)
	assert.NoError(err)
	assert.Exactly(clhConfig, clh.config)
}

func TestClhCreateVM(t *testing.T) {
	assert := assert.New(t)

	clhConfig, err := newClhConfig()
	assert.NoError(err)
	assert.NotEmpty(clhConfig.ImagePath)

	store, err := persist.GetDriver()
	assert.NoError(err)

	clhConfig.VMStorePath = store.RunVMStoragePath()
	clhConfig.RunStorePath = store.RunStoragePath()

	network, err := NewNetwork()
	assert.NoError(err)

	clh := &cloudHypervisor{
		config: clhConfig,
	}

	sandbox := &Sandbox{
		ctx: context.Background(),
		id:  "testSandbox",
		config: &SandboxConfig{
			HypervisorConfig: clhConfig,
		},
	}

	err = clh.CreateVM(context.Background(), sandbox.id, network, &sandbox.config.HypervisorConfig)
	assert.NoError(err)
	assert.Exactly(clhConfig, clh.config)
}

func TestCloudHypervisorStartSandbox(t *testing.T) {
	assert := assert.New(t)
	clhConfig, err := newClhConfig()
	assert.NoError(err)

	store, err := persist.GetDriver()
	assert.NoError(err)

	savedVmAddNetPutRequestFunc := vmAddNetPutRequest
	vmAddNetPutRequest = func(clh *cloudHypervisor) error { return nil }
	defer func() {
		vmAddNetPutRequest = savedVmAddNetPutRequestFunc
	}()

	clhConfig.VMStorePath = store.RunVMStoragePath()
	clhConfig.RunStorePath = store.RunStoragePath()

	clh := &cloudHypervisor{
		config:         clhConfig,
		APIClient:      &clhClientMock{},
		virtiofsDaemon: &virtiofsdMock{},
	}

	err = clh.StartVM(context.Background(), 10)
	assert.NoError(err)
}

func TestCloudHypervisorResizeMemory(t *testing.T) {
	assert := assert.New(t)
	clhConfig, err := newClhConfig()
	type args struct {
		reqMemMB          uint32
		memoryBlockSizeMB uint32
	}
	tests := []struct {
		name           string
		args           args
		expectedMemDev MemoryDevice
		wantErr        bool
	}{
		{"Resize to zero", args{0, 128}, MemoryDevice{Probe: false, SizeMB: 0}, FAIL},
		{"Resize to aligned size", args{clhConfig.MemorySize + 128, 128}, MemoryDevice{Probe: false, SizeMB: 128}, PASS},
		{"Resize to aligned size", args{clhConfig.MemorySize + 129, 128}, MemoryDevice{Probe: false, SizeMB: 256}, PASS},
		{"Resize to NOT aligned size", args{clhConfig.MemorySize + 125, 128}, MemoryDevice{Probe: false, SizeMB: 128}, PASS},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert.NoError(err)
			clh := cloudHypervisor{}

			mockClient := &clhClientMock{}
			mockClient.vmInfo.Config = *chclient.NewVmConfig(*chclient.NewPayloadConfig())
			mockClient.vmInfo.Config.Memory = chclient.NewMemoryConfig(int64(utils.MemUnit(clhConfig.MemorySize) * utils.MiB))
			mockClient.vmInfo.Config.Memory.HotplugSize = func(i int64) *int64 { return &i }(int64(40 * utils.GiB.ToBytes()))

			clh.APIClient = mockClient
			clh.config = clhConfig

			newMem, memDev, err := clh.ResizeMemory(context.Background(), tt.args.reqMemMB, tt.args.memoryBlockSizeMB, false)

			if (err != nil) != tt.wantErr {
				t.Errorf("cloudHypervisor.ResizeMemory() error = %v, expected to fail = %v", err, tt.wantErr)
				return
			}

			if err != nil {
				return
			}

			expectedMem := clhConfig.MemorySize + uint32(tt.expectedMemDev.SizeMB)

			if newMem != expectedMem {
				t.Errorf("cloudHypervisor.ResizeMemory() got = %+v, want %+v", newMem, expectedMem)
			}

			if !reflect.DeepEqual(memDev, tt.expectedMemDev) {
				t.Errorf("cloudHypervisor.ResizeMemory() got = %+v, want %+v", memDev, tt.expectedMemDev)
			}
		})
	}
}

func TestCloudHypervisorHotplugAddBlockDevice(t *testing.T) {
	assert := assert.New(t)

	clhConfig, err := newClhConfig()
	assert.NoError(err)

	clh := &cloudHypervisor{}
	clh.config = clhConfig
	clh.APIClient = &clhClientMock{}
	clh.devicesIds = make(map[string]string)

	clh.config.BlockDeviceDriver = config.VirtioBlock
	err = clh.hotplugAddBlockDevice(&config.BlockDrive{Pmem: false})
	assert.NoError(err, "Hotplug disk block device expected no error")

	err = clh.hotplugAddBlockDevice(&config.BlockDrive{Pmem: true})
	assert.Error(err, "Hotplug pmem block device expected error")

	clh.config.BlockDeviceDriver = config.VirtioSCSI
	err = clh.hotplugAddBlockDevice(&config.BlockDrive{Pmem: false})
	assert.Error(err, "Hotplug block device not using 'virtio-blk' expected error")
}

func TestCloudHypervisorHotplugRemoveDevice(t *testing.T) {
	assert := assert.New(t)

	clhConfig, err := newClhConfig()
	assert.NoError(err)

	clh := &cloudHypervisor{}
	clh.config = clhConfig
	clh.APIClient = &clhClientMock{}
	clh.devicesIds = make(map[string]string)

	_, err = clh.HotplugRemoveDevice(context.Background(), &config.BlockDrive{}, BlockDev)
	assert.NoError(err, "Hotplug remove block device expected no error")

	_, err = clh.HotplugRemoveDevice(context.Background(), &config.VFIODev{}, VfioDev)
	assert.NoError(err, "Hotplug remove vfio block device expected no error")

	_, err = clh.HotplugRemoveDevice(context.Background(), nil, NetDev)
	assert.Error(err, "Hotplug remove pmem block device expected error")
}

func TestClhGenerateSocket(t *testing.T) {
	assert := assert.New(t)

	// Ensure the type is fully constructed
	hypervisor, err := NewHypervisor("clh")
	assert.NoError(err)

	clh, ok := hypervisor.(*cloudHypervisor)
	assert.True(ok)

	clh.config = HypervisorConfig{
		VMStorePath:  "/foo",
		RunStorePath: "/bar",
	}

	clh.addVSock(1, "path")

	s, err := clh.GenerateSocket("c")

	assert.NoError(err)
	assert.NotNil(s)

	hvsock, ok := s.(types.HybridVSock)
	assert.True(ok)
	assert.NotEmpty(hvsock.UdsPath)

	// Path must be absolute
	assert.True(strings.HasPrefix(hvsock.UdsPath, "/"), "failed: socket path: %s", hvsock.UdsPath)

	assert.NotZero(hvsock.Port)
}

func TestClhSetConfig(t *testing.T) {
	assert := assert.New(t)

	config, err := newClhConfig()
	assert.NoError(err)

	clh := &cloudHypervisor{}
	assert.Equal(clh.config, HypervisorConfig{})

	err = clh.setConfig(&config)
	assert.NoError(err)

	assert.Equal(clh.config, config)
}
