//go:build integration && linux && cgo

/*
Copyright (c) Edgeless Systems GmbH

SPDX-License-Identifier: AGPL-3.0-only
*/

package integration

import (
	"context"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"sync"
	"syscall"
	"testing"

	"github.com/bazelbuild/rules_go/go/runfiles"
	"github.com/edgelesssys/constellation/v2/csi/cryptmapper"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"go.uber.org/goleak"
)

const (
	devicePath string = "testDevice"
	deviceName string = "testDeviceName"
)

var toolsEnvs []string = []string{"CP", "DD", "RM", "FSCK_EXT4", "MKFS_EXT4", "BLKID", "FSCK", "MOUNT", "UMOUNT"}

// addToolsToPATH is used to update the PATH to contain necessary tool binaries for
// coreutils, util-linux and ext4.
func addToolsToPATH() error {
	path := ":" + os.Getenv("PATH") + ":"
	for _, tool := range toolsEnvs {
		toolPath := os.Getenv(tool)
		if toolPath == "" {
			continue
		}
		toolPath, err := runfiles.Rlocation(toolPath)
		if err != nil {
			return err
		}
		pathComponent := filepath.Dir(toolPath)
		if strings.Contains(path, ":"+pathComponent+":") {
			continue
		}
		path = ":" + pathComponent + path
	}
	path = strings.Trim(path, ":")
	os.Setenv("PATH", path)
	return nil
}

func setup(devicePath string) {
	if err := exec.Command("dd", "if=/dev/zero", fmt.Sprintf("of=%s", devicePath), "bs=64M", "count=1").Run(); err != nil {
		panic(err)
	}
}

func teardown(devicePath string) {
	if err := exec.Command("rm", "-f", devicePath).Run(); err != nil {
		panic(err)
	}
}

func cp(source, target string) error {
	return exec.Command("cp", source, target).Run()
}

func resize(devicePath string) {
	if err := exec.Command("dd", "if=/dev/zero", fmt.Sprintf("of=%s", devicePath), "bs=32M", "count=1", "oflag=append", "conv=notrunc").Run(); err != nil {
		panic(err)
	}
}

func TestMain(m *testing.M) {
	// try to become root (best effort)
	_ = syscall.Setuid(0)
	if os.Getuid() != 0 {
		fmt.Printf("This test suite requires root privileges, as libcryptsetup uses the kernel's device mapper.\n")
		os.Exit(1)
	}
	if err := addToolsToPATH(); err != nil {
		fmt.Printf("Failed to add tools to PATH: %v\n", err)
		os.Exit(1)
	}

	goleak.VerifyTestMain(m, goleak.IgnoreAnyFunction("github.com/bazelbuild/rules_go/go/tools/bzltestutil.RegisterTimeoutHandler.func1"))

	result := m.Run()
	os.Exit(result)
}

func TestOpenAndClose(t *testing.T) {
	assert := assert.New(t)
	require := require.New(t)
	setup(devicePath)
	defer teardown(devicePath)

	mapper := cryptmapper.New(&fakeKMS{})

	newPath, err := mapper.OpenCryptDevice(context.Background(), devicePath, deviceName, false)
	require.NoError(err)
	defer func() {
		_ = mapper.CloseCryptDevice(deviceName)
	}()

	// assert crypt device got created
	_, err = os.Stat(newPath)
	require.NoError(err)
	// assert no integrity device got created
	_, err = os.Stat(newPath + "_dif")
	assert.True(os.IsNotExist(err))

	// Opening the same device should return the same path and not error
	newPath2, err := mapper.OpenCryptDevice(context.Background(), devicePath, deviceName, false)
	require.NoError(err)
	assert.Equal(newPath, newPath2)

	// Resize the device
	resize(devicePath)

	resizedPath, err := mapper.ResizeCryptDevice(context.Background(), deviceName)
	require.NoError(err)
	assert.Equal("/dev/mapper/"+deviceName, resizedPath)

	assert.NoError(mapper.CloseCryptDevice(deviceName))

	// assert crypt device got removed
	_, err = os.Stat(newPath)
	assert.True(os.IsNotExist(err))

	// check if we can reopen the device
	_, err = mapper.OpenCryptDevice(context.Background(), devicePath, deviceName, true)
	assert.NoError(err)
	assert.NoError(mapper.CloseCryptDevice(deviceName))
}

func TestOpenAndCloseIntegrity(t *testing.T) {
	assert := assert.New(t)
	require := require.New(t)
	setup(devicePath)
	defer teardown(devicePath)

	mapper := cryptmapper.New(&fakeKMS{})

	newPath, err := mapper.OpenCryptDevice(context.Background(), devicePath, deviceName, true)
	require.NoError(err)
	assert.Equal("/dev/mapper/"+deviceName, newPath)

	// assert crypt device got created
	_, err = os.Stat(newPath)
	assert.NoError(err)
	// assert integrity device got created
	_, err = os.Stat(newPath + "_dif")
	assert.NoError(err)

	// Opening the same device should return the same path and not error
	newPath2, err := mapper.OpenCryptDevice(context.Background(), devicePath, deviceName, true)
	require.NoError(err)
	assert.Equal(newPath, newPath2)

	// integrity devices do not support resizing
	resize(devicePath)
	_, err = mapper.ResizeCryptDevice(context.Background(), deviceName)
	assert.Error(err)

	assert.NoError(mapper.CloseCryptDevice(deviceName))

	// assert crypt device got removed
	_, err = os.Stat(newPath)
	assert.True(os.IsNotExist(err))
	// assert integrity device got removed
	_, err = os.Stat(newPath + "_dif")
	assert.True(os.IsNotExist(err))

	// check if we can reopen the device
	_, err = mapper.OpenCryptDevice(context.Background(), devicePath, deviceName, true)
	assert.NoError(err)
	assert.NoError(mapper.CloseCryptDevice(deviceName))
}

func TestDeviceCloning(t *testing.T) {
	assert := assert.New(t)
	require := require.New(t)
	setup(devicePath)
	defer teardown(devicePath)

	mapper := cryptmapper.New(&dynamicKMS{})

	_, err := mapper.OpenCryptDevice(context.Background(), devicePath, deviceName, false)
	assert.NoError(err)

	require.NoError(cp(devicePath, devicePath+"-copy"))
	defer teardown(devicePath + "-copy")

	_, err = mapper.OpenCryptDevice(context.Background(), devicePath+"-copy", deviceName+"-copy", false)
	assert.NoError(err)

	assert.NoError(mapper.CloseCryptDevice(deviceName))
	assert.NoError(mapper.CloseCryptDevice(deviceName + "-copy"))
}

func TestConcurrency(t *testing.T) {
	assert := assert.New(t)
	setup(devicePath)
	defer teardown(devicePath)

	device2 := devicePath + "-2"
	setup(device2)
	defer teardown(device2)

	mapper := cryptmapper.New(&fakeKMS{})

	wg := sync.WaitGroup{}
	runTest := func(path, name string) {
		newPath, err := mapper.OpenCryptDevice(context.Background(), path, name, false)
		assert.NoError(err)
		defer func() {
			_ = mapper.CloseCryptDevice(name)
		}()

		// assert crypt device got created
		_, err = os.Stat(newPath)
		assert.NoError(err)
		// assert no integrity device got created
		_, err = os.Stat(newPath + "_dif")
		assert.True(os.IsNotExist(err))
		assert.NoError(mapper.CloseCryptDevice(name))
		wg.Done()
	}

	wg.Add(2)
	go runTest(devicePath, deviceName)
	go runTest(device2, deviceName+"-2")
	wg.Wait()
}

type fakeKMS struct{}

func (k *fakeKMS) GetDEK(_ context.Context, _ string, dekSize int) ([]byte, error) {
	key := make([]byte, dekSize)
	for i := range key {
		key[i] = 0x41
	}
	return key, nil
}

type dynamicKMS struct{}

func (k *dynamicKMS) GetDEK(_ context.Context, dekID string, dekSize int) ([]byte, error) {
	key := make([]byte, dekSize)
	for i := range key {
		key[i] = 0x41 ^ dekID[i%len(dekID)]
	}
	return key, nil
}
