// Copyright (c) 2017 Intel Corporation
//
// SPDX-License-Identifier: Apache-2.0
//

package oci

import (
	"fmt"
	"os"
	"path"
	"path/filepath"
	"runtime"
	"strconv"
	"strings"
	"testing"

	ctrAnnotations "github.com/containerd/containerd/pkg/cri/annotations"
	podmanAnnotations "github.com/containers/podman/v4/pkg/annotations"
	specs "github.com/opencontainers/runtime-spec/specs-go"
	"github.com/stretchr/testify/assert"
	"golang.org/x/sys/unix"

	"github.com/kata-containers/kata-containers/src/runtime/pkg/device/config"
	vc "github.com/kata-containers/kata-containers/src/runtime/virtcontainers"
	vcAnnotations "github.com/kata-containers/kata-containers/src/runtime/virtcontainers/pkg/annotations"
	dockerAnnotations "github.com/kata-containers/kata-containers/src/runtime/virtcontainers/pkg/annotations/dockershim"
	"github.com/kata-containers/kata-containers/src/runtime/virtcontainers/pkg/compatoci"
	"github.com/kata-containers/kata-containers/src/runtime/virtcontainers/types"
)

const (
	containerID = "virtc-oci-test"
	fileMode    = os.FileMode(0640)
	dirMode     = os.FileMode(0750)
)

var (
	tempRoot       = ""
	tempBundlePath = ""
)

func createConfig(fileName string, fileData string) (string, error) {
	configPath := path.Join(tempBundlePath, fileName)

	err := os.WriteFile(configPath, []byte(fileData), fileMode)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Unable to create config file %s %v\n", configPath, err)
		return "", err
	}

	return configPath, nil
}

func TestMinimalSandboxConfig(t *testing.T) {
	assert := assert.New(t)
	configPath, err := createConfig("config.json", minimalConfig)
	assert.NoError(err)

	savedFunc := config.GetHostPathFunc

	// Simply assign container path to host path for device.
	config.GetHostPathFunc = func(devInfo config.DeviceInfo, vhostUserStoreEnabled bool,
		vhostUserStorePath string) (string, error) {
		return devInfo.ContainerPath, nil
	}

	defer func() {
		config.GetHostPathFunc = savedFunc
	}()

	runtimeConfig := RuntimeConfig{
		HypervisorType: vc.QemuHypervisor,
	}

	capList := []string{"CAP_AUDIT_WRITE", "CAP_KILL", "CAP_NET_BIND_SERVICE"}

	expectedCmd := types.Cmd{
		Args: []string{"sh"},
		Envs: []types.EnvVar{
			{
				Var:   "PATH",
				Value: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
			},
			{
				Var:   "TERM",
				Value: "xterm",
			},
		},
		WorkDir:             "/",
		User:                "0",
		PrimaryGroup:        "0",
		SupplementaryGroups: []string{"10", "29"},
		Interactive:         true,
		NoNewPrivileges:     true,
		Capabilities: &specs.LinuxCapabilities{
			Bounding:    capList,
			Effective:   capList,
			Inheritable: capList,
			Permitted:   capList,
			Ambient:     capList,
		},
	}

	expectedMounts := []vc.Mount{
		{
			Source:      "proc",
			Destination: "/proc",
			Type:        "proc",
			Options:     nil,
			HostPath:    "",
		},
		{
			Source:      "tmpfs",
			Destination: "/dev",
			Type:        "tmpfs",
			Options:     []string{"nosuid", "strictatime", "mode=755", "size=65536k"},
			HostPath:    "",
		},
		{
			Source:      "devpts",
			Destination: "/dev/pts",
			Type:        "devpts",
			Options:     []string{"nosuid", "noexec", "newinstance", "ptmxmode=0666", "mode=0620", "gid=5"},
			HostPath:    "",
		},
	}

	spec, err := compatoci.ParseConfigJSON(tempBundlePath)
	assert.NoError(err)

	devInfo := config.DeviceInfo{
		ContainerPath: "/dev/vfio/17",
		Major:         242,
		Minor:         0,
		DevType:       "c",
		UID:           0,
		GID:           0,
	}

	expectedDeviceInfo := []config.DeviceInfo{
		devInfo,
	}

	expectedContainerConfig := vc.ContainerConfig{
		ID:             containerID,
		RootFs:         vc.RootFs{Target: path.Join(tempBundlePath, "rootfs"), Mounted: true},
		ReadonlyRootfs: true,
		Cmd:            expectedCmd,
		Annotations: map[string]string{
			vcAnnotations.BundlePathKey:    tempBundlePath,
			vcAnnotations.ContainerTypeKey: string(vc.SingleContainer),
		},
		Mounts:      expectedMounts,
		DeviceInfos: expectedDeviceInfo,
		Resources: specs.LinuxResources{Devices: []specs.LinuxDeviceCgroup{
			{Allow: false, Type: "", Major: (*int64)(nil), Minor: (*int64)(nil), Access: "rwm"},
		}},
		CustomSpec: &spec,
	}

	expectedNetworkConfig := vc.NetworkConfig{}

	expectedSandboxConfig := vc.SandboxConfig{
		ID:       containerID,
		Hostname: "testHostname",

		HypervisorType: vc.QemuHypervisor,

		NetworkConfig: expectedNetworkConfig,

		Containers: []vc.ContainerConfig{expectedContainerConfig},

		Annotations: map[string]string{
			vcAnnotations.BundlePathKey: tempBundlePath,
		},

		SystemdCgroup: true,
	}

	sandboxConfig, err := SandboxConfig(spec, runtimeConfig, tempBundlePath, containerID, false, true)
	assert.NoError(err)

	assert.Exactly(sandboxConfig, expectedSandboxConfig)
	assert.NoError(os.Remove(configPath))
}

func TestContainerType(t *testing.T) {
	assert := assert.New(t)
	tests := []struct {
		description     string
		annotationKey   string
		annotationValue string
		expectedType    vc.ContainerType
		expectedErr     bool
	}{
		{
			description:     "no annotation, expect single container",
			annotationKey:   "",
			annotationValue: "",
			expectedType:    vc.SingleContainer,
			expectedErr:     false,
		},
		{
			description:     "unexpected annotation, expect error",
			annotationKey:   ctrAnnotations.ContainerType,
			annotationValue: "foo",
			expectedType:    vc.UnknownContainerType,
			expectedErr:     true,
		},
		{
			description:     "containerd sandbox",
			annotationKey:   ctrAnnotations.ContainerType,
			annotationValue: string(ctrAnnotations.ContainerTypeSandbox),
			expectedType:    vc.PodSandbox,
			expectedErr:     false,
		},
		{
			description:     "containerd container",
			annotationKey:   ctrAnnotations.ContainerType,
			annotationValue: string(ctrAnnotations.ContainerTypeContainer),
			expectedType:    vc.PodContainer,
			expectedErr:     false,
		},
		{
			description:     "crio unexpected annotation, expect error",
			annotationKey:   podmanAnnotations.ContainerType,
			annotationValue: "foo",
			expectedType:    vc.UnknownContainerType,
			expectedErr:     true,
		},
		{
			description:     "crio sandbox",
			annotationKey:   podmanAnnotations.ContainerType,
			annotationValue: string(podmanAnnotations.ContainerTypeSandbox),
			expectedType:    vc.PodSandbox,
			expectedErr:     false,
		},
		{
			description:     "crio container",
			annotationKey:   podmanAnnotations.ContainerType,
			annotationValue: string(podmanAnnotations.ContainerTypeContainer),
			expectedType:    vc.PodContainer,
			expectedErr:     false,
		},
		{
			description:     "dockershim unexpected annotation, expect error",
			annotationKey:   dockerAnnotations.ContainerTypeLabelKey,
			annotationValue: "foo",
			expectedType:    vc.UnknownContainerType,
			expectedErr:     true,
		},
		{
			description:     "dockershim sandbox",
			annotationKey:   dockerAnnotations.ContainerTypeLabelKey,
			annotationValue: string(dockerAnnotations.ContainerTypeLabelSandbox),
			expectedType:    vc.PodSandbox,
			expectedErr:     false,
		},
		{
			description:     "dockershim container",
			annotationKey:   dockerAnnotations.ContainerTypeLabelKey,
			annotationValue: string(dockerAnnotations.ContainerTypeLabelContainer),
			expectedType:    vc.PodContainer,
			expectedErr:     false,
		},
	}
	for _, tt := range tests {
		ociSpec := specs.Spec{
			Annotations: map[string]string{
				tt.annotationKey: tt.annotationValue,
			},
		}
		containerType, err := ContainerType(ociSpec)
		if tt.expectedErr {
			assert.Error(err)
		} else {
			assert.NoError(err)
		}
		assert.Equal(tt.expectedType, containerType, "test fail: %v", tt.description)
	}
}

func TestSandboxIDSuccessful(t *testing.T) {
	var ociSpec specs.Spec
	testSandboxID := "testSandboxID"
	assert := assert.New(t)

	ociSpec.Annotations = map[string]string{
		podmanAnnotations.SandboxID: testSandboxID,
	}

	sandboxID, err := SandboxID(ociSpec)
	assert.NoError(err)
	assert.Equal(sandboxID, testSandboxID)
}

func TestSandboxIDFailure(t *testing.T) {
	var ociSpec specs.Spec
	assert := assert.New(t)

	sandboxID, err := SandboxID(ociSpec)
	assert.Error(err)
	assert.Empty(sandboxID)
}

func TestAddKernelParamValid(t *testing.T) {
	var config RuntimeConfig
	assert := assert.New(t)

	expected := []vc.Param{
		{
			Key:   "foo",
			Value: "bar",
		},
	}

	err := config.AddKernelParam(expected[0])
	assert.NoError(err)
	assert.Exactly(config.HypervisorConfig.KernelParams, expected)
}

func TestAddKernelParamInvalid(t *testing.T) {
	var config RuntimeConfig

	invalid := []vc.Param{
		{
			Key:   "",
			Value: "bar",
		},
	}

	err := config.AddKernelParam(invalid[0])
	assert.Error(t, err)
}

func TestDeviceTypeFailure(t *testing.T) {
	var ociSpec specs.Spec

	invalidDeviceType := "f"
	ociSpec.Linux = &specs.Linux{}
	ociSpec.Linux.Devices = []specs.LinuxDevice{
		{
			Path: "/dev/vfio",
			Type: invalidDeviceType,
		},
	}

	_, err := containerDeviceInfos(ociSpec)
	assert.NotNil(t, err, "This test should fail as device type [%s] is invalid ", invalidDeviceType)
}

func TestContains(t *testing.T) {
	s := []string{"char", "block", "pipe"}

	assert.True(t, contains(s, "char"))
	assert.True(t, contains(s, "pipe"))
	assert.False(t, contains(s, "chara"))
	assert.False(t, contains(s, "socket"))
}

func TestDevicePathEmpty(t *testing.T) {
	var ociSpec specs.Spec

	ociSpec.Linux = &specs.Linux{}
	ociSpec.Linux.Devices = []specs.LinuxDevice{
		{
			Type:  "c",
			Major: 252,
			Minor: 1,
		},
	}

	_, err := containerDeviceInfos(ociSpec)
	assert.NotNil(t, err, "This test should fail as path cannot be empty for device")
}

func TestGetShmSize(t *testing.T) {
	containerConfig := vc.ContainerConfig{
		Mounts: []vc.Mount{},
	}

	shmSize, err := getShmSize(containerConfig)
	assert.Nil(t, err)
	assert.Equal(t, shmSize, uint64(0))

	m := vc.Mount{
		Source:      "/dev/shm",
		Destination: "/dev/shm",
		Type:        "tmpfs",
		Options:     nil,
	}

	containerConfig.Mounts = append(containerConfig.Mounts, m)
	shmSize, err = getShmSize(containerConfig)
	assert.Nil(t, err)
	assert.Equal(t, shmSize, uint64(vc.DefaultShmSize))

	containerConfig.Mounts[0].Source = "/var/run/shared/shm"
	containerConfig.Mounts[0].Type = "bind"
	_, err = getShmSize(containerConfig)
	assert.NotNil(t, err)
}

func TestGetShmSizeBindMounted(t *testing.T) {
	if os.Geteuid() != 0 {
		t.Skip("Test disabled as requires root privileges")
	}

	dir := t.TempDir()

	shmPath := filepath.Join(dir, "shm")
	err := os.Mkdir(shmPath, 0700)
	assert.Nil(t, err)

	size := 8192
	if runtime.GOARCH == "ppc64le" {
		// PAGE_SIZE on ppc64le is 65536
		size = 65536
	}

	shmOptions := "mode=1777,size=" + strconv.Itoa(size)
	err = unix.Mount("shm", shmPath, "tmpfs", unix.MS_NOEXEC|unix.MS_NOSUID|unix.MS_NODEV, shmOptions)
	assert.Nil(t, err)

	defer unix.Unmount(shmPath, 0)

	containerConfig := vc.ContainerConfig{
		Mounts: []vc.Mount{
			{
				Source:      shmPath,
				Destination: "/dev/shm",
				Type:        "bind",
				Options:     nil,
			},
		},
	}

	shmSize, err := getShmSize(containerConfig)
	assert.Nil(t, err)
	assert.Equal(t, shmSize, uint64(size))
}

func TestMain(m *testing.M) {
	var err error
	tempRoot, err = os.MkdirTemp("", "virtc-")
	if err != nil {
		panic(err)
	}

	tempBundlePath = filepath.Join(tempRoot, "ocibundle")

	/* Create temp bundle directory if necessary */
	err = os.MkdirAll(tempBundlePath, dirMode)
	if err != nil {
		fmt.Printf("Unable to create %s %v\n", tempBundlePath, err)
		os.Exit(1)
	}

	ret := m.Run()

	os.RemoveAll(tempRoot)

	os.Exit(ret)
}

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

	tmpdir := t.TempDir()

	// Create a pretend asset file
	// (required since the existence of binary asset annotations is verified).
	fakeAssetFile := filepath.Join(tmpdir, "fake-binary")

	err := os.WriteFile(fakeAssetFile, []byte(""), fileMode)
	assert.NoError(err)

	expectedAnnotations := map[string]string{
		vcAnnotations.FirmwarePath: fakeAssetFile,
		vcAnnotations.FirmwareHash: "ffff",

		vcAnnotations.HypervisorPath: fakeAssetFile,
		vcAnnotations.HypervisorHash: "bbbbb",

		vcAnnotations.HypervisorCtlPath: fakeAssetFile,
		vcAnnotations.HypervisorCtlHash: "cc",

		vcAnnotations.ImagePath: fakeAssetFile,
		vcAnnotations.ImageHash: "52ss2550983",

		vcAnnotations.InitrdPath: fakeAssetFile,
		vcAnnotations.InitrdHash: "aaaa",

		vcAnnotations.JailerPath: fakeAssetFile,
		vcAnnotations.JailerHash: "dddd",

		vcAnnotations.KernelPath: fakeAssetFile,
		vcAnnotations.KernelHash: "3l2353we871g",
	}

	config := vc.SandboxConfig{
		Annotations: make(map[string]string),
	}

	ocispec := specs.Spec{
		Annotations: expectedAnnotations,
	}

	runtimeConfig := RuntimeConfig{
		HypervisorType: vc.QemuHypervisor,
	}

	// Try annotations without enabling them first
	err = addAnnotations(ocispec, &config, runtimeConfig)

	assert.Error(err)
	assert.Exactly(map[string]string{}, config.Annotations)

	// Check if annotation not enabled correctly
	runtimeConfig.HypervisorConfig.EnableAnnotations = []string{"nonexistent"}
	err = addAnnotations(ocispec, &config, runtimeConfig)

	assert.Error(err)

	// Ensure it fails if all annotations enabled but path lists are not set
	runtimeConfig.HypervisorConfig.EnableAnnotations = []string{".*"}
	err = addAnnotations(ocispec, &config, runtimeConfig)
	assert.Error(err)

	tmpdirGlob := tmpdir + "/*"

	// Check that it works if all path lists are enabled
	runtimeConfig.HypervisorConfig.HypervisorPathList = []string{tmpdirGlob}
	runtimeConfig.HypervisorConfig.JailerPathList = []string{tmpdirGlob}
	runtimeConfig.HypervisorConfig.HypervisorCtlPathList = []string{tmpdirGlob}

	err = addAnnotations(ocispec, &config, runtimeConfig)
	assert.NoError(err)
	assert.Exactly(expectedAnnotations, config.Annotations)
}

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

	config := vc.SandboxConfig{
		Annotations: make(map[string]string),
		AgentConfig: vc.KataAgentConfig{},
	}

	ocispec := specs.Spec{
		Annotations: make(map[string]string),
	}

	expectedAgentConfig := vc.KataAgentConfig{
		KernelModules: []string{
			"e1000e InterruptThrottleRate=3000,3000,3000 EEE=1",
			"i915 enable_ppgtt=0",
		},
		ContainerPipeSize: 1024,
	}

	runtimeConfig := RuntimeConfig{
		HypervisorType: vc.QemuHypervisor,
	}

	ocispec.Annotations[vcAnnotations.KernelModules] = strings.Join(expectedAgentConfig.KernelModules, KernelModulesSeparator)
	ocispec.Annotations[vcAnnotations.AgentContainerPipeSize] = "1024"
	addAnnotations(ocispec, &config, runtimeConfig)
	assert.Exactly(expectedAgentConfig, config.AgentConfig)
}

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

	config := vc.SandboxConfig{
		Annotations: make(map[string]string),
		AgentConfig: vc.KataAgentConfig{},
	}

	ocispec := specs.Spec{
		Annotations: make(map[string]string),
	}

	expectedAgentConfig := vc.KataAgentConfig{
		ContainerPipeSize: 0,
	}

	runtimeConfig := RuntimeConfig{
		HypervisorType: vc.QemuHypervisor,
	}

	ocispec.Annotations[vcAnnotations.AgentContainerPipeSize] = "foo"
	err := addAnnotations(ocispec, &config, runtimeConfig)
	assert.Error(err)
	assert.Exactly(expectedAgentConfig, config.AgentConfig)
}

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

	config := vc.SandboxConfig{
		Annotations: make(map[string]string),
	}

	ocispec := specs.Spec{
		Annotations: make(map[string]string),
	}

	expectedHyperConfig := vc.HypervisorConfig{
		KernelParams: []vc.Param{
			{
				Key:   "vsyscall",
				Value: "emulate",
			},
			{
				Key:   "iommu",
				Value: "on",
			},
		},
	}

	runtimeConfig := RuntimeConfig{
		HypervisorType: vc.QemuHypervisor,
	}
	runtimeConfig.HypervisorConfig.EnableAnnotations = []string{".*"}
	runtimeConfig.HypervisorConfig.FileBackedMemRootList = []string{"/dev/shm*"}
	runtimeConfig.HypervisorConfig.VirtioFSDaemonList = []string{"/bin/*ls*"}

	ocispec.Annotations[vcAnnotations.KernelParams] = "vsyscall=emulate iommu=on"
	addHypervisorConfigOverrides(ocispec, &config, runtimeConfig)
	assert.Exactly(expectedHyperConfig, config.HypervisorConfig)

	ocispec.Annotations[vcAnnotations.DefaultVCPUs] = "1"
	ocispec.Annotations[vcAnnotations.DefaultMaxVCPUs] = "1"
	ocispec.Annotations[vcAnnotations.DefaultMemory] = "1024"
	ocispec.Annotations[vcAnnotations.MemSlots] = "20"
	ocispec.Annotations[vcAnnotations.MemOffset] = "512"
	ocispec.Annotations[vcAnnotations.VirtioMem] = "true"
	ocispec.Annotations[vcAnnotations.MemPrealloc] = "true"
	ocispec.Annotations[vcAnnotations.FileBackedMemRootDir] = "/dev/shm"
	ocispec.Annotations[vcAnnotations.HugePages] = "true"
	ocispec.Annotations[vcAnnotations.IOMMU] = "true"
	ocispec.Annotations[vcAnnotations.BlockDeviceDriver] = "virtio-scsi"
	ocispec.Annotations[vcAnnotations.BlockDeviceAIO] = "io_uring"
	ocispec.Annotations[vcAnnotations.DisableBlockDeviceUse] = "true"
	ocispec.Annotations[vcAnnotations.EnableIOThreads] = "true"
	ocispec.Annotations[vcAnnotations.BlockDeviceCacheSet] = "true"
	ocispec.Annotations[vcAnnotations.BlockDeviceCacheDirect] = "true"
	ocispec.Annotations[vcAnnotations.BlockDeviceCacheNoflush] = "true"
	ocispec.Annotations[vcAnnotations.SharedFS] = "virtio-fs"
	ocispec.Annotations[vcAnnotations.VirtioFSDaemon] = "/bin/false"
	ocispec.Annotations[vcAnnotations.VirtioFSCache] = "/home/cache"
	ocispec.Annotations[vcAnnotations.VirtioFSExtraArgs] = "[ \"arg0\", \"arg1\" ]"
	ocispec.Annotations[vcAnnotations.Msize9p] = "512"
	ocispec.Annotations[vcAnnotations.MachineType] = "q35"
	ocispec.Annotations[vcAnnotations.MachineAccelerators] = "nofw"
	ocispec.Annotations[vcAnnotations.CPUFeatures] = "pmu=off"
	ocispec.Annotations[vcAnnotations.DisableVhostNet] = "true"
	ocispec.Annotations[vcAnnotations.GuestHookPath] = "/usr/bin/"
	ocispec.Annotations[vcAnnotations.DisableImageNvdimm] = "true"
	ocispec.Annotations[vcAnnotations.HotplugVFIOOnRootBus] = "true"
	ocispec.Annotations[vcAnnotations.PCIeRootPort] = "2"
	ocispec.Annotations[vcAnnotations.IOMMUPlatform] = "true"
	ocispec.Annotations[vcAnnotations.SGXEPC] = "64Mi"
	ocispec.Annotations[vcAnnotations.UseLegacySerial] = "true"
	// 10Mbit
	ocispec.Annotations[vcAnnotations.RxRateLimiterMaxRate] = "10000000"
	ocispec.Annotations[vcAnnotations.TxRateLimiterMaxRate] = "10000000"

	addAnnotations(ocispec, &config, runtimeConfig)
	assert.Equal(config.HypervisorConfig.NumVCPUs, uint32(1))
	assert.Equal(config.HypervisorConfig.DefaultMaxVCPUs, uint32(1))
	assert.Equal(config.HypervisorConfig.MemorySize, uint32(1024))
	assert.Equal(config.HypervisorConfig.MemSlots, uint32(20))
	assert.Equal(config.HypervisorConfig.MemOffset, uint64(512))
	assert.Equal(config.HypervisorConfig.VirtioMem, true)
	assert.Equal(config.HypervisorConfig.MemPrealloc, true)
	assert.Equal(config.HypervisorConfig.FileBackedMemRootDir, "/dev/shm")
	assert.Equal(config.HypervisorConfig.HugePages, true)
	assert.Equal(config.HypervisorConfig.IOMMU, true)
	assert.Equal(config.HypervisorConfig.BlockDeviceDriver, "virtio-scsi")
	assert.Equal(config.HypervisorConfig.BlockDeviceAIO, "io_uring")
	assert.Equal(config.HypervisorConfig.DisableBlockDeviceUse, true)
	assert.Equal(config.HypervisorConfig.EnableIOThreads, true)
	assert.Equal(config.HypervisorConfig.BlockDeviceCacheSet, true)
	assert.Equal(config.HypervisorConfig.BlockDeviceCacheDirect, true)
	assert.Equal(config.HypervisorConfig.BlockDeviceCacheNoflush, true)
	assert.Equal(config.HypervisorConfig.SharedFS, "virtio-fs")
	assert.Equal(config.HypervisorConfig.VirtioFSDaemon, "/bin/false")
	assert.Equal(config.HypervisorConfig.VirtioFSCache, "/home/cache")
	assert.ElementsMatch(config.HypervisorConfig.VirtioFSExtraArgs, [2]string{"arg0", "arg1"})
	assert.Equal(config.HypervisorConfig.Msize9p, uint32(512))
	assert.Equal(config.HypervisorConfig.HypervisorMachineType, "q35")
	assert.Equal(config.HypervisorConfig.MachineAccelerators, "nofw")
	assert.Equal(config.HypervisorConfig.CPUFeatures, "pmu=off")
	assert.Equal(config.HypervisorConfig.DisableVhostNet, true)
	assert.Equal(config.HypervisorConfig.GuestHookPath, "/usr/bin/")
	assert.Equal(config.HypervisorConfig.DisableImageNvdimm, true)
	assert.Equal(config.HypervisorConfig.HotplugVFIOOnRootBus, true)
	assert.Equal(config.HypervisorConfig.PCIeRootPort, uint32(2))
	assert.Equal(config.HypervisorConfig.IOMMUPlatform, true)
	assert.Equal(config.HypervisorConfig.SGXEPCSize, int64(67108864))
	assert.Equal(config.HypervisorConfig.LegacySerial, true)
	assert.Equal(config.HypervisorConfig.RxRateLimiterMaxRate, uint64(10000000))
	assert.Equal(config.HypervisorConfig.TxRateLimiterMaxRate, uint64(10000000))

	// In case an absurd large value is provided, the config value if not over-ridden
	ocispec.Annotations[vcAnnotations.DefaultVCPUs] = "655536"
	err := addAnnotations(ocispec, &config, runtimeConfig)
	assert.Error(err)

	ocispec.Annotations[vcAnnotations.DefaultVCPUs] = "-1"
	err = addAnnotations(ocispec, &config, runtimeConfig)
	assert.Error(err)

	ocispec.Annotations[vcAnnotations.DefaultVCPUs] = "1"
	ocispec.Annotations[vcAnnotations.DefaultMaxVCPUs] = "-1"
	err = addAnnotations(ocispec, &config, runtimeConfig)
	assert.Error(err)

	ocispec.Annotations[vcAnnotations.DefaultMaxVCPUs] = "1"
	ocispec.Annotations[vcAnnotations.DefaultMemory] = fmt.Sprintf("%d", vc.MinHypervisorMemory+1)
	assert.Error(err)
}

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

	config := vc.SandboxConfig{
		Annotations: make(map[string]string),
	}

	ocispec := specs.Spec{
		Annotations: make(map[string]string),
	}

	runtimeConfig := RuntimeConfig{
		HypervisorType: vc.QemuHypervisor,
	}
	ocispec.Annotations[vcAnnotations.KernelParams] = "vsyscall=emulate iommu=on"
	err := addAnnotations(ocispec, &config, runtimeConfig)
	assert.Error(err)
	assert.Exactly(vc.HypervisorConfig{}, config.HypervisorConfig)

	// Enable annotations
	runtimeConfig.HypervisorConfig.EnableAnnotations = []string{".*"}

	ocispec.Annotations[vcAnnotations.FileBackedMemRootDir] = "/dev/shm"
	ocispec.Annotations[vcAnnotations.VirtioFSDaemon] = "/bin/false"
	ocispec.Annotations[vcAnnotations.EntropySource] = "/dev/urandom"

	config.HypervisorConfig.FileBackedMemRootDir = "do-not-touch"
	config.HypervisorConfig.VirtioFSDaemon = "dangerous-daemon"
	config.HypervisorConfig.EntropySource = "truly-random"

	err = addAnnotations(ocispec, &config, runtimeConfig)
	assert.Error(err)
	assert.Equal(config.HypervisorConfig.FileBackedMemRootDir, "do-not-touch")
	assert.Equal(config.HypervisorConfig.VirtioFSDaemon, "dangerous-daemon")
	assert.Equal(config.HypervisorConfig.EntropySource, "truly-random")

	// Now enable them and check again
	runtimeConfig.HypervisorConfig.FileBackedMemRootList = []string{"/dev/*m"}
	runtimeConfig.HypervisorConfig.VirtioFSDaemonList = []string{"/bin/*ls*"}
	runtimeConfig.HypervisorConfig.EntropySourceList = []string{"/dev/*random*"}
	err = addAnnotations(ocispec, &config, runtimeConfig)
	assert.NoError(err)
	assert.Equal(config.HypervisorConfig.FileBackedMemRootDir, "/dev/shm")
	assert.Equal(config.HypervisorConfig.VirtioFSDaemon, "/bin/false")
	assert.Equal(config.HypervisorConfig.EntropySource, "/dev/urandom")

	// In case an absurd large value is provided, the config value if not over-ridden
	ocispec.Annotations[vcAnnotations.DefaultVCPUs] = "655536"
	err = addAnnotations(ocispec, &config, runtimeConfig)
	assert.Error(err)

	ocispec.Annotations[vcAnnotations.DefaultVCPUs] = "-1"
	err = addAnnotations(ocispec, &config, runtimeConfig)
	assert.Error(err)

	ocispec.Annotations[vcAnnotations.DefaultVCPUs] = "1"
	ocispec.Annotations[vcAnnotations.DefaultMaxVCPUs] = "-1"
	err = addAnnotations(ocispec, &config, runtimeConfig)
	assert.Error(err)

	ocispec.Annotations[vcAnnotations.DefaultMaxVCPUs] = "1"
	ocispec.Annotations[vcAnnotations.DefaultMemory] = fmt.Sprintf("%d", vc.MinHypervisorMemory+1)
	assert.Error(err)
}

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

	config := vc.SandboxConfig{
		Annotations: make(map[string]string),
	}

	ocispec := specs.Spec{
		Annotations: make(map[string]string),
	}

	runtimeConfig := RuntimeConfig{
		HypervisorType: vc.QemuHypervisor,
	}

	ocispec.Annotations[vcAnnotations.DisableGuestSeccomp] = "true"
	ocispec.Annotations[vcAnnotations.SandboxCgroupOnly] = "true"
	ocispec.Annotations[vcAnnotations.DisableNewNetNs] = "true"
	ocispec.Annotations[vcAnnotations.InterNetworkModel] = "macvtap"

	addAnnotations(ocispec, &config, runtimeConfig)
	assert.Equal(config.DisableGuestSeccomp, true)
	assert.Equal(config.SandboxCgroupOnly, true)
	assert.Equal(config.NetworkConfig.DisableNewNetwork, true)
	assert.Equal(config.NetworkConfig.InterworkingModel, vc.NetXConnectMacVtapModel)
}

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

	//nolint: govet
	type testData struct {
		regexps  []string
		toMatch  string
		expected bool
	}

	data := []testData{
		{[]string{}, "", false},
		{[]string{}, "nonempty", false},
		{[]string{"simple"}, "simple", true},
		{[]string{"simple"}, "some_simple_text", true},
		{[]string{"simple"}, "simp", false},
		{[]string{"one", "two"}, "one", true},
		{[]string{"one", "two"}, "two", true},
		{[]string{"o*"}, "oooo", true},
		{[]string{"o*"}, "oooa", true},
		{[]string{"^o*$"}, "oooa", false},
	}

	for _, d := range data {
		matched := regexpContains(d.regexps, d.toMatch)
		assert.Equal(d.expected, matched, "%+v", d)
	}
}

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

	//nolint: govet
	type testData struct {
		globs    []string
		toMatch  string
		expected bool
	}

	data := []testData{
		{[]string{}, "", false},
		{[]string{}, "nonempty", false},
		{[]string{"simple"}, "simple", false},
		{[]string{"simple"}, "some_simple_text", false},
		{[]string{"/bin/ls"}, "/bin/ls", true},
		{[]string{"/bin/ls", "/bin/false"}, "/bin/ls", true},
		{[]string{"/bin/ls", "/bin/false"}, "/bin/false", true},
		{[]string{"/bin/ls", "/bin/false"}, "/bin/bar", false},
		{[]string{"/bin/*ls*"}, "/bin/ls", true},
		{[]string{"/bin/*ls*"}, "/bin/false", true},
		{[]string{"bin/ls"}, "/bin/ls", false},
		{[]string{"./bin/ls"}, "/bin/ls", false},
		{[]string{"*/bin/ls"}, "/bin/ls", false},
	}

	for _, d := range data {
		matched := checkPathIsInGlobs(d.globs, d.toMatch)
		assert.Equal(d.expected, matched, "%+v", d)
	}
}

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

	testCases := []struct {
		annotations map[string]string
		result      bool
	}{
		{
			annotations: map[string]string{podmanAnnotations.ContainerType: "abc"},
			result:      false,
		},
		{
			annotations: map[string]string{podmanAnnotations.ContainerType: podmanAnnotations.ContainerTypeSandbox},
			result:      true,
		},
		{
			annotations: map[string]string{podmanAnnotations.ContainerType: podmanAnnotations.ContainerTypeContainer},
			result:      true,
		},
	}

	for i := range testCases {
		tc := testCases[i]
		ocispec := specs.Spec{
			Annotations: tc.annotations,
		}
		result := IsCRIOContainerManager(&ocispec)
		assert.Equal(tc.result, result, "test case %d", (i + 1))
	}
}

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

	const key = "my_key"

	validErr := fmt.Errorf("invalid value range: must between [10-1000]")
	validFunc := func(v uint64) error {
		if v < 10 || v > 1000 {
			return validErr
		}
		return nil
	}

	// nolint: govet
	testCases := []struct {
		annotations map[string]string
		expected    uint64
		err         error
		validFunc   func(uint64) error
	}{
		{
			annotations: map[string]string{key: ""},
			expected:    0,
			err:         fmt.Errorf(errAnnotationPositiveNumericKey, key),
			validFunc:   nil,
		},
		{
			annotations: map[string]string{key: "a"},
			expected:    0,
			err:         fmt.Errorf(errAnnotationPositiveNumericKey, key),
			validFunc:   nil,
		},
		{
			annotations: map[string]string{key: "16"},
			expected:    16,
			err:         nil,
			validFunc:   nil,
		},
		{
			annotations: map[string]string{key: "16"},
			expected:    16,
			err:         nil,
			validFunc:   validFunc,
		},
		{
			annotations: map[string]string{key: "8"},
			expected:    0,
			err:         validErr,
			validFunc:   validFunc,
		},
		{
			annotations: map[string]string{key: "0"},
			expected:    0,
			err:         nil,
			validFunc:   nil,
		},
		{
			annotations: map[string]string{key: "-1"},
			expected:    0,
			err:         fmt.Errorf(errAnnotationPositiveNumericKey, key),
			validFunc:   nil,
		},
	}

	for i := range testCases {
		tc := testCases[i]
		ocispec := specs.Spec{
			Annotations: tc.annotations,
		}
		var val uint64 = 0

		err := newAnnotationConfiguration(ocispec, key).setUintWithCheck(func(v uint64) error {
			if tc.validFunc != nil {
				if err := tc.validFunc(v); err != nil {
					return err
				}
			}
			val = v
			return nil
		})

		assert.Equal(tc.err, err, "test case %d check error", (i + 1))
		if tc.err == nil {
			assert.Equal(tc.expected, val, "test case %d check parsed result", (i + 1))
		}
	}
}

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

	const (
		u32Key  = "u32_key"
		u64Key  = "u64_key"
		boolKey = "bool_key"
	)

	// nolint: govet
	testCases := []struct {
		annotationKey       string
		annotationValueList []string
		expected            bool
		err                 error
	}{
		{
			annotationKey:       boolKey,
			annotationValueList: []string{"1", "t", "T", "true", "TRUE", "True"},
			expected:            true,
			err:                 nil,
		},
		{
			annotationKey:       boolKey,
			annotationValueList: []string{"0", "f", "F", "false", "FALSE", "False"},
			expected:            false,
			err:                 nil,
		},
		{
			annotationKey:       boolKey,
			annotationValueList: []string{"a", "FalSE", "Fal", "TRue", "TRU", "falsE"},
			expected:            false,
			err:                 fmt.Errorf(errAnnotationBoolKey, boolKey),
		},
	}

	for i := range testCases {
		tc := testCases[i]
		for _, annotaionValue := range tc.annotationValueList {
			ocispec := specs.Spec{
				Annotations: map[string]string{tc.annotationKey: annotaionValue},
			}
			var val bool = false

			err := newAnnotationConfiguration(ocispec, tc.annotationKey).setBool(func(v bool) {
				val = v
			})

			assert.Equal(tc.err, err, "test case %d check error", (i + 1))
			if tc.err == nil {
				assert.Equal(tc.expected, val, "test case %d check parsed result", (i + 1))
			}
		}
	}
}

func getCtrResourceSpec(memory, quota int64, period uint64) *specs.Spec {
	return &specs.Spec{
		Linux: &specs.Linux{
			Resources: &specs.LinuxResources{
				CPU: &specs.LinuxCPU{
					Quota:  &quota,
					Period: &period,
				},
				Memory: &specs.LinuxMemory{
					Limit: &memory,
				},
			},
		},
	}

}

func makeSizingAnnotations(memory, quota, period string) *specs.Spec {
	spec := specs.Spec{
		Annotations: make(map[string]string),
	}
	spec.Annotations[ctrAnnotations.SandboxCPUPeriod] = period
	spec.Annotations[ctrAnnotations.SandboxCPUQuota] = quota
	spec.Annotations[ctrAnnotations.SandboxMem] = memory

	return &spec
}

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

	testCases := []struct {
		spec        *specs.Spec
		expectedCPU uint32
		expectedMem uint32
	}{
		{
			spec:        nil,
			expectedCPU: 0,
			expectedMem: 0,
		},
		{
			spec:        &specs.Spec{},
			expectedCPU: 0,
			expectedMem: 0,
		},
		{
			spec: &specs.Spec{
				Linux: &specs.Linux{
					Resources: &specs.LinuxResources{
						CPU:    &specs.LinuxCPU{},
						Memory: &specs.LinuxMemory{},
					},
				},
			},
			expectedCPU: 0,
			expectedMem: 0,
		},
		{
			spec:        getCtrResourceSpec(1024*1024, 200, 100),
			expectedCPU: 2,
			expectedMem: 1,
		},
		{
			spec:        getCtrResourceSpec(1024*1024*1024, 200, 1),
			expectedCPU: 200,
			expectedMem: 1024,
		},
		{
			spec:        getCtrResourceSpec(-1*1024*1024*1024, 200, 1),
			expectedCPU: 200,
			expectedMem: 0,
		},
		{
			spec:        getCtrResourceSpec(0, 10, 0),
			expectedCPU: 0,
			expectedMem: 0,
		},
		{
			spec:        getCtrResourceSpec(-1, 10, 1),
			expectedCPU: 10,
			expectedMem: 0,
		},
	}

	for _, tt := range testCases {

		cpu, mem := CalculateContainerSizing(tt.spec)
		assert.Equal(tt.expectedCPU, cpu, "unexpected CPU")
		assert.Equal(tt.expectedMem, mem, "unexpected memory")
	}
}

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

	testCases := []struct {
		spec        *specs.Spec
		expectedCPU uint32
		expectedMem uint32
	}{
		{
			spec:        nil,
			expectedCPU: 0,
			expectedMem: 0,
		},
		{
			spec:        &specs.Spec{},
			expectedCPU: 0,
			expectedMem: 0,
		},
		{
			spec:        makeSizingAnnotations("1048576", "200", "100"),
			expectedCPU: 2,
			expectedMem: 1,
		},
		{
			spec:        makeSizingAnnotations("1024", "200", "1"),
			expectedCPU: 200,
			expectedMem: 0,
		},
		{
			spec:        makeSizingAnnotations("foobar", "200", "spaghetti"),
			expectedCPU: 0,
			expectedMem: 0,
		},
		{
			spec:        makeSizingAnnotations("-1048576", "-100", "1"),
			expectedCPU: 0,
			expectedMem: 0,
		},
		{
			spec:        makeSizingAnnotations("-1", "100", "1"),
			expectedCPU: 100,
			expectedMem: 0,
		},
		{
			spec:        makeSizingAnnotations("4294967296", "400", "100"),
			expectedCPU: 4,
			expectedMem: 4096,
		},
	}

	for _, tt := range testCases {

		cpu, mem := CalculateSandboxSizing(tt.spec)
		assert.Equal(tt.expectedCPU, cpu, "unexpected CPU")
		assert.Equal(tt.expectedMem, mem, "unexpected memory")
	}
}

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

	testCases := []struct {
		out vc.Mount
		in  specs.Mount
	}{
		{
			in: specs.Mount{
				Source:      "proc",
				Destination: "/proc",
				Type:        "proc",
				Options:     nil,
			},
			out: vc.Mount{
				Source:      "proc",
				Destination: "/proc",
				Type:        "proc",
				Options:     nil,
			},
		},
		{
			in: specs.Mount{
				Source:      "proc",
				Destination: "/proc",
				Type:        "proc",
				Options:     []string{"ro"},
			},
			out: vc.Mount{
				Source:      "proc",
				Destination: "/proc",
				Type:        "proc",
				Options:     []string{"ro"},
				ReadOnly:    true,
			},
		},
		{
			in: specs.Mount{
				Source:      "/abc",
				Destination: "/def",
				Type:        "none",
				Options:     []string{"bind"},
			},
			out: vc.Mount{
				Source:      "/abc",
				Destination: "/def",
				Type:        "bind",
				Options:     []string{"bind"},
			},
		}, {
			in: specs.Mount{
				Source:      "/abc",
				Destination: "/def",
				Type:        "none",
				Options:     []string{"rbind"},
			},
			out: vc.Mount{
				Source:      "/abc",
				Destination: "/def",
				Type:        "bind",
				Options:     []string{"rbind"},
			},
		},
	}

	for _, tt := range testCases {
		actualMount := newMount(tt.in)

		assert.Equal(tt.out.Source, actualMount.Source, "unexpected mount source")
		assert.Equal(tt.out.Destination, actualMount.Destination, "unexpected mount destination")
		assert.Equal(tt.out.Type, actualMount.Type, "unexpected mount type")
		assert.Equal(tt.out.Options, actualMount.Options, "unexpected mount options")
		assert.Equal(tt.out.ReadOnly, actualMount.ReadOnly, "unexpected mount ReadOnly")
	}
}
