// 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"
	"testing"

	"github.com/kata-containers/kata-containers/src/runtime/virtcontainers/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/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("hypervisor 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,
	}, 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, vmAddDevice chclient.VmAddDevice) (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{}, 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) {
	macTest := "00:00:00:00:00"
	tapPath := "/path/to/tap"

	assert := assert.New(t)

	clh := cloudHypervisor{}

	e := &VethEndpoint{}
	e.NetPair.TAPIface.HardAddr = macTest
	e.NetPair.TapInterface.TAPIface.Name = tapPath

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

	assert.Equal(len(clh.vmconfig.Net), 1)
	if err == nil {
		assert.Equal(clh.vmconfig.Net[0].Mac, macTest)
		assert.Equal(clh.vmconfig.Net[0].Tap, tapPath)
	}

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

	assert.Equal(len(clh.vmconfig.Net), 2)
	if err == nil {
		assert.Equal(clh.vmconfig.Net[1].Mac, macTest)
		assert.Equal(clh.vmconfig.Net[1].Tap, tapPath)
	}
}

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

	tapPath := "/path/to/tap"

	validVeth := &VethEndpoint{}
	validVeth.NetPair.TapInterface.TAPIface.Name = tapPath

	type args struct {
		e Endpoint
	}
	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{}
			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 {
				assert.Equal(clh.vmconfig.Net[0].Tap, tapPath)
			}
		})
	}
}

func TestCloudHypervisorBootVM(t *testing.T) {
	clh := &cloudHypervisor{}
	clh.APIClient = &clhClientMock{}
	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{
		store: store,
	}

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

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

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

	clh := &cloudHypervisor{
		config: clhConfig,
		store:  store,
	}

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

	err = clh.createSandbox(context.Background(), sandbox.id, NetworkNamespace{}, &sandbox.config.HypervisorConfig)
	assert.NoError(err)
	assert.Exactly(clhConfig, clh.config)
}

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

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

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

	err = clh.startSandbox(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.Memory.Size = int64(utils.MemUnit(clhConfig.MemorySize) * utils.MiB)
			mockClient.vmInfo.Config.Memory.HotplugSize = int64(40 * utils.GiB.ToBytes())

			clh.APIClient = mockClient
			clh.config = clhConfig

			newMem, memDev, err := clh.resizeMemory(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 TestCheckVersion(t *testing.T) {
	clh := &cloudHypervisor{}
	assert := assert.New(t)
	testcases := []struct {
		name  string
		major int
		minor int
		pass  bool
	}{
		{
			name:  "minor lower than supported version",
			major: supportedMajorVersion,
			minor: 2,
			pass:  false,
		},
		{
			name:  "minor equal to supported version",
			major: supportedMajorVersion,
			minor: supportedMinorVersion,
			pass:  true,
		},
		{
			name:  "major exceeding supported version",
			major: 1,
			minor: supportedMinorVersion,
			pass:  true,
		},
	}
	for _, tc := range testcases {
		clh.version = CloudHypervisorVersion{
			Major:    tc.major,
			Minor:    tc.minor,
			Revision: 0,
		}
		err := clh.checkVersion()
		msg := fmt.Sprintf("test: %+v, clh.version: %v, result: %v", tc, clh.version, err)
		if tc.pass {
			assert.NoError(err, msg)
		} else {
			assert.Error(err, msg)
		}
	}
}

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

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

	clh := &cloudHypervisor{}
	clh.config = clhConfig
	clh.APIClient = &clhClientMock{}

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

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

	clh := &cloudHypervisor{}
	clh.config = clhConfig
	clh.APIClient = &clhClientMock{}

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

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