//go:build linux

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

package virtcontainers

import (
	"context"
	"fmt"
	"net"
	"os"
	"path/filepath"
	"testing"

	govmmQemu "github.com/kata-containers/kata-containers/src/runtime/pkg/govmm/qemu"
	"github.com/stretchr/testify/assert"

	"github.com/kata-containers/kata-containers/src/runtime/pkg/device/config"
	"github.com/kata-containers/kata-containers/src/runtime/virtcontainers/persist/fs"
	"github.com/kata-containers/kata-containers/src/runtime/virtcontainers/types"
	"github.com/pkg/errors"
)

const (
	qemuArchBaseQemuPath = "/usr/bin/qemu-system-x86_64"
)

var qemuArchBaseMachine = govmmQemu.Machine{
	Type: "q35",
}

var qemuArchBaseQemuPaths = map[string]string{
	qemuArchBaseMachine.Type: qemuArchBaseQemuPath,
}

var qemuArchBaseKernelParamsNonDebug = []Param{
	{"quiet", ""},
	{"systemd.show_status", "false"},
}

var qemuArchBaseKernelParamsDebug = []Param{
	{"debug", ""},
	{"systemd.show_status", "true"},
	{"systemd.log_level", "debug"},
}

var qemuArchBaseKernelParams = []Param{
	{"root", "/dev/vda"},
	{"rootfstype", "ext4"},
}

func newQemuArchBase() *qemuArchBase {
	return &qemuArchBase{
		qemuMachine:          qemuArchBaseMachine,
		qemuExePath:          qemuArchBaseQemuPaths[qemuArchBaseMachine.Type],
		nestedRun:            false,
		kernelParamsNonDebug: qemuArchBaseKernelParamsNonDebug,
		kernelParamsDebug:    qemuArchBaseKernelParamsDebug,
		kernelParams:         qemuArchBaseKernelParams,
	}
}

func TestQemuArchBaseEnableNestingChecks(t *testing.T) {
	assert := assert.New(t)
	qemuArchBase := newQemuArchBase()

	qemuArchBase.enableNestingChecks()
	assert.True(qemuArchBase.nestedRun)
}

func TestQemuArchBaseDisableNestingChecks(t *testing.T) {
	assert := assert.New(t)
	qemuArchBase := newQemuArchBase()

	qemuArchBase.disableNestingChecks()
	assert.False(qemuArchBase.nestedRun)
}

func TestQemuArchBaseMachine(t *testing.T) {
	assert := assert.New(t)
	qemuArchBase := newQemuArchBase()

	m := qemuArchBase.machine()
	assert.Equal(m.Type, qemuArchBaseMachine.Type)
}

func TestQemuArchBaseQemuPath(t *testing.T) {
	assert := assert.New(t)
	qemuArchBase := newQemuArchBase()

	p := qemuArchBase.qemuPath()
	assert.Equal(p, qemuArchBaseQemuPath)
}

func TestQemuArchBaseKernelParameters(t *testing.T) {
	assert := assert.New(t)
	qemuArchBase := newQemuArchBase()

	// with debug params
	expectedParams := qemuArchBaseKernelParams
	debugParams := qemuArchBaseKernelParamsDebug
	expectedParams = append(expectedParams, debugParams...)
	p := qemuArchBase.kernelParameters(true)
	assert.Equal(expectedParams, p)

	// with non-debug params
	expectedParams = qemuArchBaseKernelParams
	nonDebugParams := qemuArchBaseKernelParamsNonDebug
	expectedParams = append(expectedParams, nonDebugParams...)
	p = qemuArchBase.kernelParameters(false)
	assert.Equal(expectedParams, p)
}

func TestQemuArchBaseCapabilities(t *testing.T) {
	assert := assert.New(t)
	qemuArchBase := newQemuArchBase()

	c := qemuArchBase.capabilities()
	assert.True(c.IsBlockDeviceHotplugSupported())
}

func TestQemuArchBaseBridges(t *testing.T) {
	assert := assert.New(t)
	qemuArchBase := newQemuArchBase()
	len := 5

	qemuArchBase.bridges(uint32(len))
	bridges := qemuArchBase.getBridges()
	assert.Len(bridges, len)

	for i, b := range bridges {
		id := fmt.Sprintf("%s-bridge-%d", types.PCI, i)
		assert.Equal(types.PCI, b.Type)
		assert.Equal(id, b.ID)
		assert.NotNil(b.Devices)
	}
}

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

	// addDeviceToBridge successfully
	q := newQemuArchBase()
	q.qemuMachine.Type = QemuQ35

	q.bridges(1)
	for i := uint32(1); i <= types.PCIBridgeMaxCapacity; i++ {
		_, _, err := q.addDeviceToBridge(context.Background(), fmt.Sprintf("qemu-bridge-%d", i), types.PCI)
		assert.Nil(err)
	}

	// fail to add device to bridge cause no more available bridge slot
	_, _, err := q.addDeviceToBridge(context.Background(), "qemu-bridge-31", types.PCI)
	exceptErr := errors.New("no more bridge slots available")
	assert.Equal(exceptErr.Error(), err.Error())

	// addDeviceToBridge fails cause q.Bridges == 0
	q = newQemuArchBase()
	q.qemuMachine.Type = QemuQ35
	q.bridges(0)
	_, _, err = q.addDeviceToBridge(context.Background(), "qemu-bridge", types.PCI)
	if assert.Error(err) {
		exceptErr = errors.New("failed to get available address from bridges")
		assert.Equal(exceptErr.Error(), err.Error())
	}
}

func TestQemuArchBaseCPUTopology(t *testing.T) {
	assert := assert.New(t)
	qemuArchBase := newQemuArchBase()
	vcpus := uint32(2)

	expectedSMP := govmmQemu.SMP{
		CPUs:    vcpus,
		Sockets: defaultMaxVCPUs,
		Cores:   defaultCores,
		Threads: defaultThreads,
		MaxCPUs: defaultMaxVCPUs,
	}

	smp := qemuArchBase.cpuTopology(vcpus, defaultMaxVCPUs)
	assert.Equal(expectedSMP, smp)
}

func TestQemuArchBaseCPUModel(t *testing.T) {
	assert := assert.New(t)
	qemuArchBase := newQemuArchBase()

	assert.Equal(defaultCPUModel, qemuArchBase.cpuModel())
}

func TestQemuArchBaseMemoryTopology(t *testing.T) {
	assert := assert.New(t)
	qemuArchBase := newQemuArchBase()

	hostMem := uint64(100)
	mem := uint64(120)
	slots := uint8(12)
	expectedMemory := govmmQemu.Memory{
		Size:   fmt.Sprintf("%dM", mem),
		Slots:  slots,
		MaxMem: fmt.Sprintf("%dM", hostMem),
	}

	m := qemuArchBase.memoryTopology(mem, hostMem, slots)
	assert.Equal(expectedMemory, m)
}

func testQemuArchBaseAppend(t *testing.T, structure interface{}, expected []govmmQemu.Device) {
	var devices []govmmQemu.Device
	var err error
	assert := assert.New(t)
	qemuArchBase := newQemuArchBase()

	switch s := structure.(type) {
	case types.Volume:
		devices, err = qemuArchBase.append9PVolume(context.Background(), devices, s)
	case types.Socket:
		devices = qemuArchBase.appendSocket(devices, s)
	case config.BlockDrive:
		devices, err = qemuArchBase.appendBlockDevice(context.Background(), devices, s)
	case config.VFIODev:
		devices = qemuArchBase.appendVFIODevice(devices, s)
	case config.VhostUserDeviceAttrs:
		devices, err = qemuArchBase.appendVhostUserDevice(context.Background(), devices, s)
	}

	assert.NoError(err)
	assert.Equal(devices, expected)
}

func TestQemuArchBaseAppendConsoles(t *testing.T) {
	var devices []govmmQemu.Device
	var err error
	assert := assert.New(t)
	qemuArchBase := newQemuArchBase()

	path := filepath.Join(filepath.Join(fs.MockRunStoragePath(), "test"), consoleSocket)

	expectedOut := []govmmQemu.Device{
		govmmQemu.SerialDevice{
			Driver:   govmmQemu.VirtioSerial,
			ID:       "serial0",
			MaxPorts: uint(2),
		},
		govmmQemu.CharDevice{
			Driver:   govmmQemu.Console,
			Backend:  govmmQemu.Socket,
			DeviceID: "console0",
			ID:       "charconsole0",
			Path:     path,
		},
	}

	devices, err = qemuArchBase.appendConsole(context.Background(), devices, path)
	assert.NoError(err)
	assert.Equal(expectedOut, devices)
	assert.Contains(qemuArchBase.kernelParams, Param{"console", "hvc0"})
	assert.Contains(qemuArchBase.kernelParams, Param{"console", "hvc1"})
}

func TestQemuArchBaseAppendConsolesLegacy(t *testing.T) {
	var devices []govmmQemu.Device
	var err error
	assert := assert.New(t)
	qemuArchBase := newQemuArchBase()
	qemuArchBase.legacySerial = true

	path := filepath.Join(filepath.Join(fs.MockRunStoragePath(), "test"), consoleSocket)

	expectedOut := []govmmQemu.Device{
		govmmQemu.LegacySerialDevice{
			Chardev: "charconsole0",
		},
		govmmQemu.CharDevice{
			Driver:   govmmQemu.LegacySerial,
			Backend:  govmmQemu.Socket,
			DeviceID: "console0",
			ID:       "charconsole0",
			Path:     path,
		},
	}

	devices, err = qemuArchBase.appendConsole(context.Background(), devices, path)
	assert.NoError(err)
	assert.Equal(expectedOut, devices)
	assert.Contains(qemuArchBase.kernelParams, Param{"console", "ttyS0"})
}

func TestQemuArchBaseAppendImage(t *testing.T) {
	var devices []govmmQemu.Device
	assert := assert.New(t)
	qemuArchBase := newQemuArchBase()

	image, err := os.CreateTemp("", "img")
	assert.NoError(err)
	defer os.Remove(image.Name())
	err = image.Close()
	assert.NoError(err)

	devices, err = qemuArchBase.appendImage(context.Background(), devices, image.Name())
	assert.NoError(err)
	assert.Len(devices, 1)

	drive, ok := devices[0].(govmmQemu.BlockDevice)
	assert.True(ok)

	expectedOut := []govmmQemu.Device{
		govmmQemu.BlockDevice{
			Driver:    govmmQemu.VirtioBlock,
			ID:        drive.ID,
			File:      image.Name(),
			AIO:       govmmQemu.Threads,
			Format:    "raw",
			Interface: "none",
			ShareRW:   true,
			ReadOnly:  true,
		},
	}

	assert.Equal(expectedOut, devices)
}

func TestQemuArchBaseAppendBridges(t *testing.T) {
	var devices []govmmQemu.Device
	assert := assert.New(t)
	qemuArchBase := newQemuArchBase()

	qemuArchBase.bridges(1)
	bridges := qemuArchBase.getBridges()
	assert.Len(bridges, 1)

	devices = qemuArchBase.appendBridges(devices)
	assert.Len(devices, 1)

	expectedOut := []govmmQemu.Device{
		govmmQemu.BridgeDevice{
			Type:          govmmQemu.PCIBridge,
			Bus:           defaultBridgeBus,
			ID:            bridges[0].ID,
			Chassis:       1,
			SHPC:          false,
			Addr:          "2",
			IOReserve:     "4k",
			MemReserve:    "1m",
			Pref64Reserve: "1m",
		},
	}

	assert.Equal(expectedOut, devices)
}

func TestQemuArchBaseAppend9PVolume(t *testing.T) {
	mountTag := "testMountTag"
	hostPath := "testHostPath"

	expectedOut := []govmmQemu.Device{
		govmmQemu.FSDevice{
			Driver:        govmmQemu.Virtio9P,
			FSDriver:      govmmQemu.Local,
			ID:            fmt.Sprintf("extra-9p-%s", mountTag),
			Path:          hostPath,
			MountTag:      mountTag,
			SecurityModel: govmmQemu.None,
			Multidev:      govmmQemu.Remap,
		},
	}

	volume := types.Volume{
		MountTag: mountTag,
		HostPath: hostPath,
	}

	testQemuArchBaseAppend(t, volume, expectedOut)
}

func TestQemuArchBaseAppendSocket(t *testing.T) {
	deviceID := "channelTest"
	id := "charchTest"
	hostPath := "/tmp/hyper_test.sock"
	name := "sh.hyper.channel.test"

	expectedOut := []govmmQemu.Device{
		govmmQemu.CharDevice{
			Driver:   govmmQemu.VirtioSerialPort,
			Backend:  govmmQemu.Socket,
			DeviceID: deviceID,
			ID:       id,
			Path:     hostPath,
			Name:     name,
		},
	}

	socket := types.Socket{
		DeviceID: deviceID,
		ID:       id,
		HostPath: hostPath,
		Name:     name,
	}

	testQemuArchBaseAppend(t, socket, expectedOut)
}

func TestQemuArchBaseAppendBlockDevice(t *testing.T) {
	id := "blockDevTest"
	file := "/root"
	format := "raw"

	expectedOut := []govmmQemu.Device{
		govmmQemu.BlockDevice{
			Driver:    govmmQemu.VirtioBlock,
			ID:        id,
			File:      "/root",
			AIO:       govmmQemu.Threads,
			Format:    govmmQemu.BlockDeviceFormat(format),
			Interface: "none",
		},
	}

	drive := config.BlockDrive{
		File:   file,
		Format: format,
		ID:     id,
	}

	testQemuArchBaseAppend(t, drive, expectedOut)
}

func TestQemuArchBaseAppendVhostUserDevice(t *testing.T) {
	socketPath := "nonexistentpath.sock"
	macAddress := "00:11:22:33:44:55:66"
	id := "deadbeef"

	expectedOut := []govmmQemu.Device{
		govmmQemu.VhostUserDevice{
			SocketPath:    socketPath,
			CharDevID:     fmt.Sprintf("char-%s", id),
			TypeDevID:     fmt.Sprintf("net-%s", id),
			Address:       macAddress,
			VhostUserType: govmmQemu.VhostUserNet,
		},
	}

	vhostUserDevice := config.VhostUserDeviceAttrs{
		Type:       config.VhostUserNet,
		MacAddress: macAddress,
	}
	vhostUserDevice.DevID = id
	vhostUserDevice.SocketPath = socketPath

	testQemuArchBaseAppend(t, vhostUserDevice, expectedOut)
}

func TestQemuArchBaseAppendVFIODevice(t *testing.T) {
	bdf := "02:10.1"

	expectedOut := []govmmQemu.Device{
		govmmQemu.VFIODevice{
			BDF: bdf,
		},
	}

	vfDevice := config.VFIODev{
		BDF: bdf,
	}

	testQemuArchBaseAppend(t, vfDevice, expectedOut)
}

func TestQemuArchBaseAppendVFIODeviceWithVendorDeviceID(t *testing.T) {
	bdf := "02:10.1"
	vendorID := "0x1234"
	deviceID := "0x5678"

	expectedOut := []govmmQemu.Device{
		govmmQemu.VFIODevice{
			BDF:      bdf,
			VendorID: vendorID,
			DeviceID: deviceID,
		},
	}

	vfDevice := config.VFIODev{
		BDF:      bdf,
		VendorID: vendorID,
		DeviceID: deviceID,
	}

	testQemuArchBaseAppend(t, vfDevice, expectedOut)
}

func TestQemuArchBaseAppendSCSIController(t *testing.T) {
	var devices []govmmQemu.Device
	assert := assert.New(t)
	qemuArchBase := newQemuArchBase()

	expectedOut := []govmmQemu.Device{
		govmmQemu.SCSIController{
			ID: scsiControllerID,
		},
	}

	devices, ioThread, err := qemuArchBase.appendSCSIController(context.Background(), devices, false)
	assert.Equal(expectedOut, devices)
	assert.Nil(ioThread)
	assert.NoError(err)

	_, ioThread, err = qemuArchBase.appendSCSIController(context.Background(), devices, true)
	assert.NotNil(ioThread)
	assert.NoError(err)
}

func TestQemuArchBaseAppendNetwork(t *testing.T) {
	var devices []govmmQemu.Device
	var err error
	assert := assert.New(t)
	qemuArchBase := newQemuArchBase()

	macAddr := net.HardwareAddr{0x02, 0x00, 0xCA, 0xFE, 0x00, 0x04}

	macvlanEp := &MacvlanEndpoint{
		NetPair: NetworkInterfacePair{
			TapInterface: TapInterface{
				ID:   "uniqueTestID-4",
				Name: "br4_kata",
				TAPIface: NetworkInterface{
					Name: "tap4_kata",
				},
			},
			VirtIface: NetworkInterface{
				Name:     "eth4",
				HardAddr: macAddr.String(),
			},
			NetInterworkingModel: DefaultNetInterworkingModel,
		},
		EndpointType: MacvlanEndpointType,
	}

	macvtapEp := &MacvtapEndpoint{
		EndpointType: MacvtapEndpointType,
		EndpointProperties: NetworkInfo{
			Iface: NetlinkIface{
				Type: "macvtap",
			},
		},
	}

	expectedOut := []govmmQemu.Device{
		govmmQemu.NetDevice{
			Type:       networkModelToQemuType(macvlanEp.NetPair.NetInterworkingModel),
			Driver:     govmmQemu.VirtioNet,
			ID:         fmt.Sprintf("network-%d", 0),
			IFName:     macvlanEp.NetPair.TAPIface.Name,
			MACAddress: macvlanEp.NetPair.TAPIface.HardAddr,
			DownScript: "no",
			Script:     "no",
			FDs:        macvlanEp.NetPair.VMFds,
			VhostFDs:   macvlanEp.NetPair.VhostFds,
		},
		govmmQemu.NetDevice{
			Type:       govmmQemu.MACVTAP,
			Driver:     govmmQemu.VirtioNet,
			ID:         fmt.Sprintf("network-%d", 1),
			IFName:     macvtapEp.Name(),
			MACAddress: macvtapEp.HardwareAddr(),
			DownScript: "no",
			Script:     "no",
			FDs:        macvtapEp.VMFds,
			VhostFDs:   macvtapEp.VhostFds,
		},
	}

	devices, err = qemuArchBase.appendNetwork(context.Background(), devices, macvlanEp)
	assert.NoError(err)
	devices, err = qemuArchBase.appendNetwork(context.Background(), devices, macvtapEp)
	assert.NoError(err)
	assert.Equal(expectedOut, devices)
}

func TestQemuArchBaseAppendIOMMU(t *testing.T) {
	var devices []govmmQemu.Device
	var err error
	assert := assert.New(t)
	qemuArchBase := newQemuArchBase()

	expectedOut := []govmmQemu.Device{
		govmmQemu.IommuDev{
			Intremap:    true,
			DeviceIotlb: true,
			CachingMode: true,
		},
	}

	qemuArchBase.qemuMachine.Type = QemuQ35
	devices, err = qemuArchBase.appendIOMMU(devices)
	assert.NoError(err)
	assert.Equal(expectedOut, devices)
}
