//go:build linux

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

package virtcontainers

import (
	"context"
	"fmt"
	"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,
		RootfsType:                    string(EXT4),
		HypervisorPath:                testClhPath,
		NumVCPUsF:                     defaultVCPUs,
		BlockDeviceDriver:             config.VirtioBlock,
		MemorySize:                    defaultMemSzMiB,
		DefaultBridges:                defaultBridges,
		DefaultMaxVCPUs:               uint32(64),
		SharedFS:                      config.VirtioFS,
		VirtioFSCache:                 typeVirtioFSCacheModeAlways,
		VirtioFSDaemon:                testVirtiofsdPath,
		NetRateLimiterBwMaxRate:       int64(0),
		NetRateLimiterBwOneTimeBurst:  int64(0),
		NetRateLimiterOpsMaxRate:      int64(0),
		NetRateLimiterOpsOneTimeBurst: int64(0),
		HotPlugVFIO:                   config.NoPort,
		DisableImageNvdimm:            true,
	}, nil
}

type clhClientMock struct {
	vmInfo          chclient.VmInfo
	restoreRequest  *chclient.RestoreConfig
	snapshotRequest *chclient.VmSnapshotConfig
}

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) VmPausePut(ctx context.Context) (*http.Response, error) {
	c.vmInfo.State = clhStatePaused
	return nil, nil
}

//nolint:golint
func (c *clhClientMock) VmSnapshotPut(ctx context.Context, vmSnapshotConfig chclient.VmSnapshotConfig) (*http.Response, error) {
	c.snapshotRequest = &vmSnapshotConfig
	return nil, nil
}

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

func (c *clhClientMock) VmRestorePut(ctx context.Context, restoreConfig chclient.RestoreConfig) (*http.Response, error) {
	c.restoreRequest = &restoreConfig
	// restoreVM() verifies Paused after restore.
	c.vmInfo.State = clhStatePaused
	return nil, nil
}

func (c *clhClientMock) ResumeVM(ctx context.Context) (*http.Response, error) {
	c.vmInfo.State = clhStateRunning
	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.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.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.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) ([]chclient.PciDeviceInfo, error) { return nil, 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"
	clh.config.VMid = "cleanVMID"

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

	dir := filepath.Join(store.RunVMStoragePath(), clh.id)
	err = os.MkdirAll(dir, os.ModePerm)
	assert.NoError(err, "failed to create dir %s", dir)

	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 TestClhCreateVM(t *testing.T) {
	assert := assert.New(t)

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

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

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

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

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

	config2, err := newClhConfig()
	assert.NoError(err)
	config2.Debug = true

	config3, err := newClhConfig()
	assert.NoError(err)
	config3.Debug = true
	config3.ConfidentialGuest = true

	config4, err := newClhConfig()
	assert.NoError(err)
	config4.SGXEPCSize = 1

	config5, err := newClhConfig()
	assert.NoError(err)
	config5.SharedFS = config.VirtioFSNydus

	type testData struct {
		config      HypervisorConfig
		expectError bool
		configMatch bool
	}

	data := []testData{
		{config0, false, true},
		{config1, false, true},
		{config2, false, true},
		{config3, true, false},
		{config4, false, true},
		{config5, false, true},
	}

	for i, d := range data {
		msg := fmt.Sprintf("test[%d]", i)

		err = clh.CreateVM(context.Background(), "testSandbox", network, &d.config)

		if d.expectError {
			assert.Error(err, msg)
			continue
		}

		assert.NoError(err, msg)

		if d.configMatch {
			assert.Exactly(d.config, clh.config, msg)
		}
	}
}

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

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

	clhConfig, err := newClhConfig()
	assert.NoError(err)
	clhConfig.VMStorePath = store.RunVMStoragePath()
	clhConfig.RunStorePath = store.RunStoragePath()

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

	// First call restoreVM without the VM snapshot files (state.json, config.json) present.
	err = clh.restoreVM(context.Background())
	// An error is expected because restoreVM expects the VM snapshot files to be present.
	assert.Error(err)
	assert.Contains(err.Error(), filepath.Join(clhConfig.VMStorePath, "state.json"))

	// Now create the VM snapshot files and call restoreVM again.
	err = os.MkdirAll(clhConfig.VMStorePath, os.ModePerm)
	assert.NoError(err, "failed to create dir %s", clhConfig.VMStorePath)
	stateFile := filepath.Join(clhConfig.VMStorePath, "state.json")
	configFile := filepath.Join(clhConfig.VMStorePath, "config.json")
	err = os.WriteFile(stateFile, []byte("{}"), 0o600)
	assert.NoError(err)
	err = os.WriteFile(configFile, []byte("{}"), 0o600)
	assert.NoError(err)

	// Call restoreVM again, this time it should succeed.
	err = clh.restoreVM(context.Background())
	assert.NoError(err)

	if assert.NotNil(mockClient.restoreRequest) {
		expectedSourceURL := "file://" + clhConfig.VMStorePath
		assert.Equal(expectedSourceURL, mockClient.restoreRequest.GetSourceUrl())
	}

	info, err := clh.vmInfo()
	assert.NoError(err)
	assert.Equal(clhStatePaused, info.State)
}

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

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

	clhConfig, err := newClhConfig()
	assert.NoError(err)
	// For testing, assume the memory path is located within the VM store path.
	clhConfig.MemoryPath = filepath.Join(store.RunVMStoragePath(), "memory")
	clhConfig.VMStorePath = store.RunVMStoragePath()
	clhConfig.RunStorePath = store.RunStoragePath()

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

	err = clh.SaveVM()
	assert.NoError(err)

	if assert.NotNil(mockClient.snapshotRequest) {
		expectedDestinationURL := "file://" + filepath.Dir(clhConfig.MemoryPath)
		assert.Equal(expectedDestinationURL, mockClient.snapshotRequest.GetDestinationUrl())
	}
}

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

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

	savedVmAddNetPutRequestFunc := vmAddNetPutRequest
	vmAddNetPutRequest = func(clh *cloudHypervisor) ([]chclient.PciDeviceInfo, error) { return nil, 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)

	_, err = clh.loadVirtiofsDaemon("/tmp/xyzabc")
	assert.NoError(err)

	err = clh.stopVirtiofsDaemon(context.Background())
	assert.NoError(err)

	_, _, err = clh.GetVMConsole(context.Background(), "test")
	assert.NoError(err)

	_, err = clh.GetThreadIDs(context.Background())
	assert.NoError(err)

	assert.True(clh.getClhStopSandboxTimeout().Nanoseconds() != 0)

	pid := clh.GetPids()
	assert.True(pid[0] != 0)

	pid2 := *clh.GetVirtioFsPid()
	assert.True(pid2 == 0)

	mem := clh.GetTotalMemoryMB(context.Background())
	assert.True(mem == 0)

	err = clh.PauseVM(context.Background())
	assert.NoError(err)

	err = clh.SaveVM()
	assert.NoError(err)

	err = clh.ResumeVM(context.Background())
	assert.NoError(err)

	err = clh.Check()
	assert.NoError(err)

	err = clh.Cleanup(context.Background())
	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 TestCloudHypervisorColdPlugVFIODevice(t *testing.T) {
	assert := assert.New(t)

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

	clh := &cloudHypervisor{}
	clh.config = clhConfig
	clh.devicesIds = make(map[string]string)
	clh.vmconfig = *chclient.NewVmConfig(*chclient.NewPayloadConfig())

	// Cold-plug a PCI VFIO device
	dev := &config.VFIODev{
		ID:       "gpu0",
		SysfsDev: "/sys/bus/pci/devices/0000:41:00.0",
		BDF:      "0000:41:00.0",
		Type:     config.VFIOPCIDeviceNormalType,
	}
	err = clh.coldPlugVFIODevice(dev)
	assert.NoError(err, "Cold-plug PCI VFIO device expected no error")

	// Verify the device was added to vmconfig.Devices
	assert.NotNil(clh.vmconfig.Devices)
	assert.Len(*clh.vmconfig.Devices, 1)
	assert.Equal("/sys/bus/pci/devices/0000:41:00.0", (*clh.vmconfig.Devices)[0].Path)
	assert.Equal("gpu0", clh.devicesIds["gpu0"])

	// Cold-plug a second device
	dev2 := &config.VFIODev{
		ID:       "gpu1",
		SysfsDev: "/sys/bus/pci/devices/0000:42:00.0",
		BDF:      "0000:42:00.0",
		Type:     config.VFIOPCIDeviceNormalType,
	}
	err = clh.coldPlugVFIODevice(dev2)
	assert.NoError(err, "Cold-plug second VFIO device expected no error")
	assert.Len(*clh.vmconfig.Devices, 2)

	// AP mediated device should fail
	apDev := &config.VFIODev{
		ID:   "ap0",
		Type: config.VFIOAPDeviceMediatedType,
	}
	err = clh.coldPlugVFIODevice(apDev)
	assert.Error(err, "Cold-plug AP mediated device expected error")

	// Error type (0) should fail
	errDev := &config.VFIODev{
		ID:       "bad0",
		SysfsDev: "/sys/bus/pci/devices/0000:43:00.0",
		Type:     config.VFIODeviceErrorType,
	}
	err = clh.coldPlugVFIODevice(errDev)
	assert.Error(err, "Cold-plug error-type device expected error")

	// Empty SysfsDev should fail
	emptySysfsDev := &config.VFIODev{
		ID:   "bad1",
		Type: config.VFIOPCIDeviceNormalType,
	}
	err = clh.coldPlugVFIODevice(emptySysfsDev)
	assert.Error(err, "Cold-plug with empty SysfsDev expected error")
}

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

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

	clh := &cloudHypervisor{}
	clh.config = clhConfig
	clh.devicesIds = make(map[string]string)
	clh.vmconfig = *chclient.NewVmConfig(*chclient.NewPayloadConfig())

	// AddDevice with VFIODev type should cold-plug
	dev := config.VFIODev{
		ID:       "nic0",
		SysfsDev: "/sys/bus/pci/devices/0000:05:00.0",
		BDF:      "0000:05:00.0",
		Type:     config.VFIOPCIDeviceNormalType,
	}
	err = clh.AddDevice(context.Background(), dev, VfioDev)
	assert.NoError(err, "AddDevice VFIO expected no error")
	assert.NotNil(clh.vmconfig.Devices)
	assert.Len(*clh.vmconfig.Devices, 1)
}

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)
}

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

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

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

	hConfig.SharedFS = config.VirtioFS

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

	var ctx context.Context
	c := clh.Capabilities(ctx)
	assert.True(c.IsFsSharingSupported())

	hConfig.SharedFS = config.NoSharedFS

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

	c = clh.Capabilities(ctx)
	assert.False(c.IsFsSharingSupported())

	assert.True(c.IsNetworkDeviceHotplugSupported())
	assert.True(c.IsBlockDeviceHotplugSupported())
}
