// Copyright (c) 2017 Intel Corporation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import (
	"fmt"
	"io/ioutil"
	"math/rand"
	"net"
	"os"
	"path/filepath"
	"reflect"
	"syscall"
	"testing"
	"time"

	vc "github.com/kata-containers/runtime/virtcontainers"
	vcAnnotations "github.com/kata-containers/runtime/virtcontainers/pkg/annotations"
	"github.com/kata-containers/runtime/virtcontainers/pkg/oci"
	"github.com/kata-containers/runtime/virtcontainers/pkg/vcmock"
	"github.com/opencontainers/runc/libcontainer/utils"
	specs "github.com/opencontainers/runtime-spec/specs-go"
	"github.com/stretchr/testify/assert"
)

var (
	consolePathTest       = "console-test"
	consoleSocketPathTest = "console-socket-test"
)

type cgroupTestDataType struct {
	resource  string
	linuxSpec *specs.LinuxResources
}

var cgroupTestData = []cgroupTestDataType{
	{
		"memory",
		&specs.LinuxResources{
			Memory: &specs.LinuxMemory{},
		},
	},
	{
		"cpu",
		&specs.LinuxResources{
			CPU: &specs.LinuxCPU{},
		},
	},
	{
		"pids",
		&specs.LinuxResources{
			Pids: &specs.LinuxPids{},
		},
	},
	{
		"blkio",
		&specs.LinuxResources{
			BlockIO: &specs.LinuxBlockIO{},
		},
	},
}

func TestGetContainerInfoContainerIDEmptyFailure(t *testing.T) {
	assert := assert.New(t)
	status, _, err := getContainerInfo("")

	assert.Error(err, "This test should fail because containerID is empty")
	assert.Empty(status.ID, "Expected blank fullID, but got %v", status.ID)
}

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

	pod := &vcmock.Pod{
		MockID: testPodID,
	}

	containerID := testContainerID

	containerStatus := vc.ContainerStatus{
		ID: containerID,
		Annotations: map[string]string{
			vcAnnotations.ContainerTypeKey: string(vc.PodSandbox),
		},
	}

	testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) {
		return []vc.PodStatus{
			{
				ID:               pod.ID(),
				ContainersStatus: []vc.ContainerStatus{containerStatus},
			},
		}, nil
	}

	defer func() {
		testingImpl.ListPodFunc = nil
	}()

	status, podID, err := getContainerInfo(testContainerID)
	assert.NoError(err)
	assert.Equal(podID, pod.ID())
	assert.Equal(status, containerStatus)
}

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

	pod := &vcmock.Pod{
		MockID: testPodID,
	}

	containerID := testContainerID + testContainerID

	containerStatus := vc.ContainerStatus{
		ID: containerID,
		Annotations: map[string]string{
			vcAnnotations.ContainerTypeKey: string(vc.PodSandbox),
		},
	}

	testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) {
		return []vc.PodStatus{
			{
				ID:               pod.ID(),
				ContainersStatus: []vc.ContainerStatus{containerStatus},
			},
		}, nil
	}

	defer func() {
		testingImpl.ListPodFunc = nil
	}()

	_, podID, err := getContainerInfo(testContainerID)
	assert.NoError(err)
	assert.Equal(podID, "")
}

func TestValidCreateParamsContainerIDEmptyFailure(t *testing.T) {
	assert := assert.New(t)
	_, err := validCreateParams("", "")

	assert.Error(err, "This test should fail because containerID is empty")
	assert.False(vcmock.IsMockError(err))
}

func TestGetExistingContainerInfoContainerIDEmptyFailure(t *testing.T) {
	assert := assert.New(t)
	status, _, err := getExistingContainerInfo("")

	assert.Error(err, "This test should fail because containerID is empty")
	assert.Empty(status.ID, "Expected blank fullID, but got %v", status.ID)
}

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

	containerID := testContainerID + testContainerID

	pod := &vcmock.Pod{
		MockID: testPodID,
	}

	testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) {
		return []vc.PodStatus{
			{
				ID: pod.ID(),
				ContainersStatus: []vc.ContainerStatus{
					// 2 containers with same ID
					{
						ID: containerID,
						Annotations: map[string]string{
							vcAnnotations.ContainerTypeKey: string(vc.PodSandbox),
						},
					},
					{
						ID: containerID,
						Annotations: map[string]string{
							vcAnnotations.ContainerTypeKey: string(vc.PodSandbox),
						},
					},
				},
			},
		}, nil
	}

	defer func() {
		testingImpl.ListPodFunc = nil
	}()

	_, err := validCreateParams(testContainerID, "")

	assert.Error(err)
	assert.False(vcmock.IsMockError(err))
}

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

	containerID := testContainerID + testContainerID

	pod := &vcmock.Pod{
		MockID: testPodID,
	}

	testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) {
		return []vc.PodStatus{
			{
				ID: pod.ID(),
				ContainersStatus: []vc.ContainerStatus{
					{
						ID: containerID,
						Annotations: map[string]string{
							vcAnnotations.ContainerTypeKey: string(vc.PodSandbox),
						},
					},
				},
			},
		}, nil
	}

	defer func() {
		testingImpl.ListPodFunc = nil
	}()

	_, err := validCreateParams(testContainerID, "")
	assert.Error(err)
	assert.False(vcmock.IsMockError(err))
}

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

	tmpdir, err := ioutil.TempDir("", "")
	assert.NoError(err)
	defer os.RemoveAll(tmpdir)

	bundlePath := filepath.Join(tmpdir, "bundle")

	testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) {
		return []vc.PodStatus{}, nil
	}

	defer func() {
		testingImpl.ListPodFunc = nil
	}()

	_, err = validCreateParams(testContainerID, bundlePath)
	// bundle is ENOENT
	assert.Error(err)
	assert.False(vcmock.IsMockError(err))
}

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

	tmpdir, err := ioutil.TempDir("", "")
	assert.NoError(err)
	defer os.RemoveAll(tmpdir)

	bundlePath := filepath.Join(tmpdir, "bundle")
	err = createEmptyFile(bundlePath)
	assert.NoError(err)

	testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) {
		return []vc.PodStatus{}, nil
	}

	defer func() {
		testingImpl.ListPodFunc = nil
	}()

	_, err = validCreateParams(testContainerID, bundlePath)
	// bundle exists as a file, not a directory
	assert.Error(err)
	assert.False(vcmock.IsMockError(err))
}

func testProcessCgroupsPath(t *testing.T, ociSpec oci.CompatOCISpec, expected []string) {
	assert := assert.New(t)
	result, err := processCgroupsPath(ociSpec, true)

	assert.NoError(err)

	if reflect.DeepEqual(result, expected) == false {
		assert.FailNow("DeepEqual failed", "Result path %q should match the expected one %q", result, expected)
	}
}

func TestProcessCgroupsPathEmptyPathSuccessful(t *testing.T) {
	ociSpec := oci.CompatOCISpec{}

	ociSpec.Linux = &specs.Linux{
		CgroupsPath: "",
	}

	testProcessCgroupsPath(t, ociSpec, []string{})
}

func TestProcessCgroupsPathEmptyResources(t *testing.T) {
	ociSpec := oci.CompatOCISpec{}

	ociSpec.Linux = &specs.Linux{
		CgroupsPath: "foo",
	}

	testProcessCgroupsPath(t, ociSpec, []string{})
}

func TestProcessCgroupsPathRelativePathSuccessful(t *testing.T) {
	relativeCgroupsPath := "relative/cgroups/path"
	cgroupsDirPath = "/foo/runtime/base"

	ociSpec := oci.CompatOCISpec{}

	ociSpec.Linux = &specs.Linux{
		CgroupsPath: relativeCgroupsPath,
	}

	for _, d := range cgroupTestData {
		ociSpec.Linux.Resources = d.linuxSpec

		p := filepath.Join(cgroupsDirPath, d.resource, relativeCgroupsPath)

		testProcessCgroupsPath(t, ociSpec, []string{p})
	}
}

func TestProcessCgroupsPathAbsoluteNoCgroupMountSuccessful(t *testing.T) {
	absoluteCgroupsPath := "/absolute/cgroups/path"
	cgroupsDirPath = "/foo/runtime/base"

	ociSpec := oci.CompatOCISpec{}

	ociSpec.Linux = &specs.Linux{
		CgroupsPath: absoluteCgroupsPath,
	}

	for _, d := range cgroupTestData {
		ociSpec.Linux.Resources = d.linuxSpec

		p := filepath.Join(cgroupsDirPath, d.resource, absoluteCgroupsPath)

		testProcessCgroupsPath(t, ociSpec, []string{p})
	}
}

func TestProcessCgroupsPathAbsoluteNoCgroupMountDestinationFailure(t *testing.T) {
	assert := assert.New(t)
	absoluteCgroupsPath := "/absolute/cgroups/path"

	ociSpec := oci.CompatOCISpec{}

	ociSpec.Mounts = []specs.Mount{
		{
			Type: "cgroup",
		},
	}

	ociSpec.Linux = &specs.Linux{
		CgroupsPath: absoluteCgroupsPath,
	}

	for _, d := range cgroupTestData {
		ociSpec.Linux.Resources = d.linuxSpec
		for _, isPod := range []bool{true, false} {
			_, err := processCgroupsPath(ociSpec, isPod)
			assert.Error(err, "This test should fail because no cgroup mount destination provided")
		}
	}
}

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

	if os.Geteuid() != 0 {
		t.Skip(testDisabledNeedRoot)
	}

	memoryResource := "memory"
	absoluteCgroupsPath := "/cgroup/mount/destination"

	cgroupMountDest, err := ioutil.TempDir("", "cgroup-memory-")
	assert.NoError(err)
	defer os.RemoveAll(cgroupMountDest)

	resourceMountPath := filepath.Join(cgroupMountDest, memoryResource)
	err = os.MkdirAll(resourceMountPath, cgroupsDirMode)
	assert.NoError(err)

	err = syscall.Mount("go-test", resourceMountPath, "cgroup", 0, memoryResource)
	assert.NoError(err)
	defer syscall.Unmount(resourceMountPath, 0)

	ociSpec := oci.CompatOCISpec{}

	ociSpec.Linux = &specs.Linux{
		Resources: &specs.LinuxResources{
			Memory: &specs.LinuxMemory{},
		},
		CgroupsPath: absoluteCgroupsPath,
	}

	ociSpec.Mounts = []specs.Mount{
		{
			Type:        "cgroup",
			Destination: cgroupMountDest,
		},
	}

	testProcessCgroupsPath(t, ociSpec, []string{filepath.Join(resourceMountPath, absoluteCgroupsPath)})
}

func TestSetupConsoleExistingConsolePathSuccessful(t *testing.T) {
	assert := assert.New(t)
	console, err := setupConsole(consolePathTest, "")

	assert.NoError(err)
	assert.Equal(console, consolePathTest, "Got %q, Expecting %q", console, consolePathTest)
}

func TestSetupConsoleExistingConsolePathAndConsoleSocketPathSuccessful(t *testing.T) {
	assert := assert.New(t)
	console, err := setupConsole(consolePathTest, consoleSocketPathTest)

	assert.NoError(err)
	assert.Equal(console, consolePathTest, "Got %q, Expecting %q", console, consolePathTest)
}

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

	console, err := setupConsole("", "")
	assert.NoError(err)
	assert.Empty(console, "Console path should be empty, got %q instead", console)
}

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

	dir, err := ioutil.TempDir("", "test-socket")
	assert.NoError(err)
	defer os.RemoveAll(dir)

	sockName := filepath.Join(dir, "console.sock")

	l, err := net.Listen("unix", sockName)
	assert.NoError(err)

	console, err := setupConsole("", sockName)
	assert.NoError(err)

	waitCh := make(chan error)
	go func() {
		conn, err1 := l.Accept()
		if err != nil {
			waitCh <- err1
		}

		uConn, ok := conn.(*net.UnixConn)
		if !ok {
			waitCh <- fmt.Errorf("casting to *net.UnixConn failed")
		}

		f, err1 := uConn.File()
		if err != nil {
			waitCh <- err1
		}

		_, err1 = utils.RecvFd(f)
		waitCh <- err1
	}()

	assert.NotEmpty(console, "Console socket path should not be empty")

	err = <-waitCh
	assert.NoError(err)
}

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

	console, err := setupConsole("", "unknown-sock-path")
	assert.Error(err, "This test should fail because the console socket path does not exist")
	assert.Empty(console, "This test should fail because the console socket path does not exist")
}

func testNoNeedForOutput(t *testing.T, detach bool, tty bool, expected bool) {
	assert := assert.New(t)
	result := noNeedForOutput(detach, tty)

	assert.Equal(result, expected)
}

func TestNoNeedForOutputDetachTrueTtyTrue(t *testing.T) {
	testNoNeedForOutput(t, true, true, true)
}

func TestNoNeedForOutputDetachFalseTtyTrue(t *testing.T) {
	testNoNeedForOutput(t, false, true, false)
}

func TestNoNeedForOutputDetachFalseTtyFalse(t *testing.T) {
	testNoNeedForOutput(t, false, false, false)
}

func TestNoNeedForOutputDetachTrueTtyFalse(t *testing.T) {
	testNoNeedForOutput(t, true, false, false)
}

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

	r := rand.New(rand.NewSource(time.Now().Unix()))
	randPath := fmt.Sprintf("/path/to/random/%d", r.Int63())

	assert.False(isCgroupMounted(randPath), "%s does not exist", randPath)

	assert.False(isCgroupMounted(os.TempDir()), "%s is not a cgroup", os.TempDir())

	cgroupsDirPath = ""
	cgroupRootPath, err := getCgroupsDirPath(procMountInfo)
	if err != nil {
		assert.NoError(err)
	}
	memoryCgroupPath := filepath.Join(cgroupRootPath, "memory")
	if _, err := os.Stat(memoryCgroupPath); os.IsNotExist(err) {
		t.Skipf("memory cgroup does not exist: %s", memoryCgroupPath)
	}

	assert.True(isCgroupMounted(memoryCgroupPath), "%s is a cgroup", memoryCgroupPath)
}

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

	tmpdir, err := ioutil.TempDir("", "")
	assert.NoError(err)
	defer os.RemoveAll(tmpdir)

	bundlePath := filepath.Join(tmpdir, "bundle")

	err = makeOCIBundle(bundlePath)
	assert.NoError(err)

	ociConfigFile := filepath.Join(bundlePath, specConfig)
	assert.True(fileExists(ociConfigFile))

	spec, err := readOCIConfigFile(ociConfigFile)
	assert.NoError(err)

	for _, isPod := range []bool{true, false} {
		_, err := processCgroupsPathForResource(spec, "", isPod)
		assert.Error(err)
		assert.False(vcmock.IsMockError(err))
	}
}

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

	type testData struct {
		contents       string
		expectedResult string
		expectError    bool
	}

	dir, err := ioutil.TempDir("", "")
	if err != nil {
		assert.NoError(err)
	}
	defer os.RemoveAll(dir)

	// make sure tested cgroupsDirPath is existed
	testedCgroupDir := filepath.Join(dir, "weirdCgroup")
	err = os.Mkdir(testedCgroupDir, testDirMode)
	assert.NoError(err)

	weirdCgroupPath := filepath.Join(testedCgroupDir, "memory")

	data := []testData{
		{fmt.Sprintf("num1 num2 num3 / %s num6 num7 - cgroup cgroup rw,memory", weirdCgroupPath), testedCgroupDir, false},
		// cgroup mount is not properly formated, if fields post - less than 3
		{fmt.Sprintf("num1 num2 num3 / %s num6 num7 - cgroup cgroup ", weirdCgroupPath), "", true},
		{"a a a a a a a - b c d", "", true},
		{"a \na b \na b c\na b c d", "", true},
		{"", "", true},
	}

	file := filepath.Join(dir, "mountinfo")

	//file does not exist, should error here
	_, err = getCgroupsDirPath(file)
	assert.Error(err)

	for _, d := range data {
		err := ioutil.WriteFile(file, []byte(d.contents), testFileMode)
		assert.NoError(err)

		cgroupsDirPath = ""
		path, err := getCgroupsDirPath(file)
		if d.expectError {
			assert.Error(err, fmt.Sprintf("got %q, test data: %+v", path, d))
		} else {
			assert.NoError(err, fmt.Sprintf("got %q, test data: %+v", path, d))
		}

		assert.Equal(d.expectedResult, path)
	}
}
