//go:build linux

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

package virtcontainers

import (
	"context"
	"fmt"
	"os"
	"testing"

	"github.com/intel-go/cpuid"
	govmmQemu "github.com/kata-containers/kata-containers/src/runtime/pkg/govmm/qemu"
	"github.com/kata-containers/kata-containers/src/runtime/virtcontainers/types"
	"github.com/stretchr/testify/assert"
)

func qemuConfig(machineType string) HypervisorConfig {
	return HypervisorConfig{
		HypervisorMachineType: machineType,
	}
}

func newTestQemu(assert *assert.Assertions, machineType string) qemuArch {
	config := qemuConfig(machineType)
	arch, err := newQemuArch(config)
	assert.NoError(err)
	return arch
}

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

	config := qemuConfig("no-such-machine-type")
	_, err := newQemuArch(config)
	assert.Error(err)
}

func TestQemuAmd64Capabilities(t *testing.T) {
	assert := assert.New(t)
	config := HypervisorConfig{}

	amd64 := newTestQemu(assert, QemuQ35)
	caps := amd64.capabilities(config)
	assert.True(caps.IsBlockDeviceHotplugSupported())
	assert.True(caps.IsNetworkDeviceHotplugSupported())

	amd64 = newTestQemu(assert, QemuMicrovm)
	caps = amd64.capabilities(config)
	assert.False(caps.IsBlockDeviceHotplugSupported())
	assert.False(caps.IsNetworkDeviceHotplugSupported())
}

func TestQemuAmd64Bridges(t *testing.T) {
	assert := assert.New(t)
	len := 5

	amd64 := newTestQemu(assert, QemuMicrovm)
	amd64.bridges(uint32(len))
	bridges := amd64.getBridges()
	assert.Nil(bridges)

	amd64 = newTestQemu(assert, QemuQ35)
	amd64.bridges(uint32(len))
	bridges = amd64.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 TestQemuAmd64CPUModel(t *testing.T) {
	assert := assert.New(t)
	amd64 := newTestQemu(assert, QemuQ35)

	expectedOut := defaultCPUModel
	model := amd64.cpuModel()
	assert.Equal(expectedOut, model)

	amd64.disableNestingChecks()
	base, ok := amd64.(*qemuAmd64)
	assert.True(ok)
	base.vmFactory = true
	expectedOut = defaultCPUModel
	model = amd64.cpuModel()
	assert.Equal(expectedOut, model)
}

func TestQemuAmd64MemoryTopology(t *testing.T) {
	assert := assert.New(t)
	amd64 := newTestQemu(assert, QemuQ35)
	memoryOffset := uint64(1024)

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

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

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

	f, err := os.CreateTemp("", "img")
	assert.NoError(err)
	defer func() { _ = f.Close() }()
	defer func() { _ = os.Remove(f.Name()) }()

	imageStat, err := f.Stat()
	assert.NoError(err)

	// Save default supportedQemuMachines options
	machinesCopy := make([]govmmQemu.Machine, len(supportedQemuMachines))
	assert.Equal(len(supportedQemuMachines), copy(machinesCopy, supportedQemuMachines))

	cfg := qemuConfig(QemuQ35)
	cfg.ImagePath = f.Name()
	cfg.DisableImageNvdimm = false
	amd64, err := newQemuArch(cfg)
	assert.NoError(err)
	assert.Contains(amd64.machine().Options, qemuNvdimmOption)

	expectedOut := []govmmQemu.Device{
		govmmQemu.Object{
			Driver:   govmmQemu.NVDIMM,
			Type:     govmmQemu.MemoryBackendFile,
			DeviceID: "nv0",
			ID:       "mem0",
			MemPath:  f.Name(),
			Size:     (uint64)(imageStat.Size()),
			ReadOnly: true,
		},
	}

	devices, err := amd64.appendImage(context.Background(), nil, f.Name())
	assert.NoError(err)
	assert.Equal(expectedOut, devices)

	// restore default supportedQemuMachines options
	assert.Equal(len(supportedQemuMachines), copy(supportedQemuMachines, machinesCopy))

	cfg.DisableImageNvdimm = true
	amd64, err = newQemuArch(cfg)
	assert.NoError(err)
	assert.NotContains(amd64.machine().Options, qemuNvdimmOption)

	found := false
	devices, err = amd64.appendImage(context.Background(), nil, f.Name())
	assert.NoError(err)
	for _, d := range devices {
		if b, ok := d.(govmmQemu.BlockDevice); ok {
			assert.Equal(b.Driver, govmmQemu.VirtioBlock)
			assert.True(b.ShareRW)
			found = true
		}
	}
	assert.True(found)

	// restore default supportedQemuMachines options
	assert.Equal(len(supportedQemuMachines), copy(supportedQemuMachines, machinesCopy))
}

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

	// Check Q35
	amd64 := newTestQemu(assert, QemuQ35)

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

	devices = []govmmQemu.Device{}
	devices = amd64.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)
}

// TestQemuAmd64BridgesWithFirmware verifies that on Q35 + OVMF the runtime
// switches from the legacy conventional pci-bridge to a pcie-pci-bridge
// cold-plugged onto a dedicated pcie-root-port (which carries the PCI Firmware
// Spec resource-reservation hints OVMF actually honours).
func TestQemuAmd64BridgesWithFirmware(t *testing.T) {
	assert := assert.New(t)

	cfg := qemuConfig(QemuQ35)
	cfg.FirmwarePath = "/path/to/OVMF.fd"
	arch, err := newQemuArch(cfg)
	assert.NoError(err)

	arch.bridges(2)
	bridges := arch.getBridges()
	assert.Len(bridges, 2)

	for i, b := range bridges {
		// The bridge's logical Type stays PCI because the secondary
		// bus exposed by a pcie-pci-bridge is conventional PCI, which
		// is what genericAddDeviceToBridge matches against. The fact
		// that it is emitted as a pcie-pci-bridge is encoded by it
		// having a parent (its pcie-root-port).
		assert.Equal(types.PCI, b.Type, "bridge %d should be PCI", i)
		assert.True(b.HasParent(), "bridge %d should be nested behind a parent", i)
		assert.Equal(fmt.Sprintf("rp-%s", b.ID), b.ParentID)
	}

	devices := arch.appendBridges(nil)
	// 2 bridges -> 2 root ports + 2 pcie-pci-bridges = 4 devices
	assert.Len(devices, 4)

	for i, b := range bridges {
		rp, ok := devices[i*2].(govmmQemu.PCIeRootPortDevice)
		assert.True(ok, "device %d should be a pcie-root-port", i*2)
		assert.Equal(b.ParentID, rp.ID)
		assert.Equal(defaultBridgeBus, rp.Bus)
		assert.Equal(fmt.Sprintf("%d", b.ParentAddr), rp.Addr)
		// Resource reservations OVMF needs to honour hot-plug.
		assert.NotEmpty(rp.BusReserve)
		assert.NotEmpty(rp.IOReserve)
		assert.NotEmpty(rp.MemReserve)
		assert.NotEmpty(rp.Pref64Reserve)

		br, ok := devices[i*2+1].(govmmQemu.BridgeDevice)
		assert.True(ok, "device %d should be a pcie-pci-bridge", i*2+1)
		assert.Equal(govmmQemu.PCIEBridge, br.Type)
		assert.Equal(b.ID, br.ID)
		assert.Equal(rp.ID, br.Bus, "pcie-pci-bridge must sit on its root-port's bus")
	}
}

// TestBridgePciPath verifies the 2-level (legacy) and 3-level (nested behind a
// pcie-root-port) guest PCI path construction.
func TestBridgePciPath(t *testing.T) {
	assert := assert.New(t)

	// 2-level: directly on pcie.0
	flat := types.NewBridge(types.PCI, "pci-bridge-0", make(map[uint32]string), 2)
	p, err := bridgePciPath(flat, "01")
	assert.NoError(err)
	assert.Equal("02/01", p.String())

	// 3-level: pcie-pci-bridge on slot 0 of a pcie-root-port at slot 2.
	// Type stays PCI because the secondary bus is conventional PCI; what
	// makes it a pcie-pci-bridge at QEMU emission time is HasParent().
	nested := types.NewNestedBridge(types.PCI, "pci-bridge-0", make(map[uint32]string), 0, "rp-pci-bridge-0", 2)
	p, err = bridgePciPath(nested, "01")
	assert.NoError(err)
	assert.Equal("02/00/01", p.String())
}

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

	cfg := qemuConfig(QemuQ35)
	cfg.InitrdPath = "dummy-initrd"
	amd64, err := newQemuArch(cfg)
	assert.NoError(err)

	assert.NotContains(amd64.machine().Options, qemuNvdimmOption)
}

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

	config := qemuConfig(QemuQ35)
	config.IOMMU = true
	qemu, err := newQemuArch(config)
	assert.NoError(err)

	p := qemu.kernelParameters(false)
	assert.Contains(p, Param{"intel_iommu", "on"})

	m := qemu.machine()
	assert.Contains(m.Options, "kernel_irqchip=split")
}

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

	cfg := qemuConfig(QemuMicrovm)
	amd64, err := newQemuArch(cfg)
	assert.NoError(err)
	assert.False(cfg.DisableImageNvdimm)

	for _, m := range supportedQemuMachines {
		assert.NotContains(m.Options, qemuNvdimmOption)
	}

	assert.False(amd64.supportGuestMemoryHotplug())
}

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

	amd64 := newTestQemu(assert, QemuQ35)

	id := amd64.(*qemuAmd64).devLoadersCount
	firmware := "tdvf.fd"
	var bios string
	var err error
	devices, bios, err = amd64.appendProtectionDevice(devices, firmware, "", []byte(""))
	assert.NoError(err)

	// non-protection
	assert.NotEmpty(bios)

	// pef protection
	amd64.(*qemuAmd64).protection = pefProtection
	devices, bios, err = amd64.appendProtectionDevice(devices, firmware, "", []byte(""))
	assert.Error(err)
	assert.Empty(bios)

	// Secure Execution protection
	amd64.(*qemuAmd64).protection = seProtection
	devices, bios, err = amd64.appendProtectionDevice(devices, firmware, "", []byte(""))
	assert.Error(err)
	assert.Empty(bios)

	// CCA protection
	amd64.(*qemuAmd64).protection = ccaProtection
	devices, bios, err = amd64.appendProtectionDevice(devices, firmware, "", []byte(""))
	assert.Error(err)
	assert.Empty(bios)

	// sev protection
	amd64.(*qemuAmd64).protection = sevProtection

	devices, bios, err = amd64.appendProtectionDevice(devices, firmware, "", []byte(""))
	assert.NoError(err)
	assert.Empty(bios)

	expectedOut := []govmmQemu.Device{
		govmmQemu.Object{
			Type:            govmmQemu.SEVGuest,
			ID:              "sev",
			Debug:           false,
			File:            firmware,
			CBitPos:         cpuid.AMDMemEncrypt.CBitPosition,
			ReducedPhysBits: 1,
		},
	}

	assert.Equal(expectedOut, devices)

	// snp protection
	amd64.(*qemuAmd64).protection = snpProtection

	devices, bios, err = amd64.appendProtectionDevice(devices, firmware, "", []uint8(nil))
	assert.NoError(err)
	assert.Empty(bios)

	expectedOut = append(expectedOut,
		govmmQemu.Object{
			Type:            govmmQemu.SNPGuest,
			ID:              "snp",
			Debug:           false,
			File:            firmware,
			CBitPos:         cpuid.AMDMemEncrypt.CBitPosition,
			ReducedPhysBits: 1,
		},
	)

	assert.Equal(expectedOut, devices)

	// tdxProtection
	amd64.(*qemuAmd64).protection = tdxProtection

	devices, bios, err = amd64.appendProtectionDevice(devices, firmware, "", []byte(""))
	assert.NoError(err)
	assert.Empty(bios)

	expectedOut = append(expectedOut,
		govmmQemu.Object{
			Driver:         govmmQemu.Loader,
			Type:           govmmQemu.TDXGuest,
			ID:             "tdx",
			DeviceID:       fmt.Sprintf("fd%d", id),
			Debug:          false,
			File:           firmware,
			InitdataDigest: []byte(""),
		},
	)

	assert.Equal(expectedOut, devices)
}
