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

package main

import (
	"context"
	"encoding/json"
	"flag"
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"regexp"
	"strings"
	"testing"
	"time"

	"github.com/kata-containers/runtime/pkg/katautils"
	vc "github.com/kata-containers/runtime/virtcontainers"
	vcAnnotations "github.com/kata-containers/runtime/virtcontainers/pkg/annotations"
	"github.com/kata-containers/runtime/virtcontainers/pkg/vcmock"
	"github.com/stretchr/testify/assert"
	"github.com/urfave/cli"
)

type TestFileWriter struct {
	Name string
	File *os.File
}

var hypervisorDetails1 = hypervisorDetails{
	HypervisorAsset: asset{
		Path: "/hypervisor/path",
	},
	ImageAsset: asset{
		Path: "/image/path",
	},
	KernelAsset: asset{
		Path: "/kernel/path",
	},
}

var hypervisorDetails2 = hypervisorDetails{
	HypervisorAsset: asset{
		Path: "/hypervisor/path2",
	},
	ImageAsset: asset{
		Path: "/image/path2",
	},
	KernelAsset: asset{
		Path: "/kernel/path2",
	},
}

var hypervisorDetails3 = hypervisorDetails{
	HypervisorAsset: asset{
		Path: "/hypervisor/path3",
	},
	ImageAsset: asset{
		Path: "/image/path3",
	},
	KernelAsset: asset{
		Path: "/kernel/path3",
	},
}

var testStatuses = []fullContainerState{
	{
		containerState: containerState{
			Version:        "",
			ID:             "1",
			InitProcessPid: 1234,
			Status:         "running",
			Bundle:         "/somewhere/over/the/rainbow",
			Created:        time.Now().UTC(),
			Annotations:    map[string]string(nil),
			Owner:          "#0",
		},

		CurrentHypervisorDetails: hypervisorDetails1,
		LatestHypervisorDetails:  hypervisorDetails1,
		StaleAssets:              []string{},
	},
	{
		containerState: containerState{
			Version:        "",
			ID:             "2",
			InitProcessPid: 2345,
			Status:         "stopped",
			Bundle:         "/this/path/is/invalid",
			Created:        time.Now().UTC(),
			Annotations:    map[string]string(nil),
			Owner:          "#0",
		},

		CurrentHypervisorDetails: hypervisorDetails2,
		LatestHypervisorDetails:  hypervisorDetails2,
		StaleAssets:              []string{},
	},
	{
		containerState: containerState{
			Version:        "",
			ID:             "3",
			InitProcessPid: 9999,
			Status:         "ready",
			Bundle:         "/foo/bar/baz",
			Created:        time.Now().UTC(),
			Annotations:    map[string]string(nil),
			Owner:          "#0",
		},

		CurrentHypervisorDetails: hypervisorDetails3,
		LatestHypervisorDetails:  hypervisorDetails3,
		StaleAssets:              []string{},
	},
}

// Implement the io.Writer interface
func (w *TestFileWriter) Write(bytes []byte) (n int, err error) {
	return w.File.Write(bytes)
}

func formatListDataAsBytes(formatter formatState, state []fullContainerState, showAll bool) (bytes []byte, err error) {
	tmpfile, err := ioutil.TempFile("", "formatListData-")
	if err != nil {
		return nil, err
	}

	defer os.Remove(tmpfile.Name())

	err = formatter.Write(state, showAll, tmpfile)
	if err != nil {
		return nil, err
	}

	tmpfile.Close()

	return ioutil.ReadFile(tmpfile.Name())
}

func formatListDataAsString(formatter formatState, state []fullContainerState, showAll bool) (lines []string, err error) {
	bytes, err := formatListDataAsBytes(formatter, state, showAll)
	if err != nil {
		return nil, err
	}

	lines = strings.Split(string(bytes), "\n")

	// Remove last line if empty
	length := len(lines)
	last := lines[length-1]
	if last == "" {
		lines = lines[:length-1]
	}

	return lines, nil
}

func TestStateToIDList(t *testing.T) {

	// no header
	expectedLength := len(testStatuses)

	// showAll should not affect the output
	for _, showAll := range []bool{true, false} {
		lines, err := formatListDataAsString(&formatIDList{}, testStatuses, showAll)
		if err != nil {
			t.Fatal(err)
		}

		var expected []string
		for _, s := range testStatuses {
			expected = append(expected, s.ID)
		}

		length := len(lines)

		if length != expectedLength {
			t.Fatalf("Expected %d lines, got %d: %v", expectedLength, length, lines)
		}

		assert.Equal(t, lines, expected, "lines + expected")
	}
}

func TestStateToTabular(t *testing.T) {
	// +1 for header line
	expectedLength := len(testStatuses) + 1

	expectedDefaultHeaderPattern := `\AID\s+PID\s+STATUS\s+BUNDLE\s+CREATED\s+OWNER`
	expectedExtendedHeaderPattern := `HYPERVISOR\s+KERNEL\s+IMAGE\s+LATEST-KERNEL\s+LATEST-IMAGE\s+STALE`
	endingPattern := `\s*\z`

	lines, err := formatListDataAsString(&formatTabular{}, testStatuses, false)
	if err != nil {
		t.Fatal(err)
	}

	length := len(lines)

	expectedHeaderPattern := expectedDefaultHeaderPattern + endingPattern
	expectedHeaderRE := regexp.MustCompile(expectedHeaderPattern)

	if length != expectedLength {
		t.Fatalf("Expected %d lines, got %d", expectedLength, length)
	}

	header := lines[0]

	matches := expectedHeaderRE.FindAllStringSubmatch(header, -1)
	if matches == nil {
		t.Fatalf("Header line failed to match:\n"+
			"pattern : %v\n"+
			"line    : %v\n",
			expectedDefaultHeaderPattern,
			header)
	}

	for i, status := range testStatuses {
		lineIndex := i + 1
		line := lines[lineIndex]

		expectedLinePattern := fmt.Sprintf(`\A%s\s+%d\s+%s\s+%s\s+%s\s+%s\s*\z`,
			regexp.QuoteMeta(status.ID),
			status.InitProcessPid,
			regexp.QuoteMeta(status.Status),
			regexp.QuoteMeta(status.Bundle),
			regexp.QuoteMeta(status.Created.Format(time.RFC3339Nano)),
			regexp.QuoteMeta(status.Owner))

		expectedLineRE := regexp.MustCompile(expectedLinePattern)

		matches := expectedLineRE.FindAllStringSubmatch(line, -1)
		if matches == nil {
			t.Fatalf("Data line failed to match:\n"+
				"pattern : %v\n"+
				"line    : %v\n",
				expectedLinePattern,
				line)
		}
	}

	// Try again with full details this time
	lines, err = formatListDataAsString(&formatTabular{}, testStatuses, true)
	if err != nil {
		t.Fatal(err)
	}

	length = len(lines)

	expectedHeaderPattern = expectedDefaultHeaderPattern + `\s+` + expectedExtendedHeaderPattern + endingPattern
	expectedHeaderRE = regexp.MustCompile(expectedHeaderPattern)

	if length != expectedLength {
		t.Fatalf("Expected %d lines, got %d", expectedLength, length)
	}

	header = lines[0]

	matches = expectedHeaderRE.FindAllStringSubmatch(header, -1)
	if matches == nil {
		t.Fatalf("Header line failed to match:\n"+
			"pattern : %v\n"+
			"line    : %v\n",
			expectedDefaultHeaderPattern,
			header)
	}

	for i, status := range testStatuses {
		lineIndex := i + 1
		line := lines[lineIndex]

		expectedLinePattern := fmt.Sprintf(`\A%s\s+%d\s+%s\s+%s\s+%s\s+%s\s+%s\s+%s\s+%s\s+%s\s+%s\s+%s\s*\z`,
			regexp.QuoteMeta(status.ID),
			status.InitProcessPid,
			regexp.QuoteMeta(status.Status),
			regexp.QuoteMeta(status.Bundle),
			regexp.QuoteMeta(status.Created.Format(time.RFC3339Nano)),
			regexp.QuoteMeta(status.Owner),
			regexp.QuoteMeta(status.CurrentHypervisorDetails.HypervisorAsset.Path),
			regexp.QuoteMeta(status.CurrentHypervisorDetails.KernelAsset.Path),
			regexp.QuoteMeta(status.CurrentHypervisorDetails.ImageAsset.Path),
			regexp.QuoteMeta(status.LatestHypervisorDetails.KernelAsset.Path),
			regexp.QuoteMeta(status.LatestHypervisorDetails.ImageAsset.Path),
			regexp.QuoteMeta("-"))

		expectedLineRE := regexp.MustCompile(expectedLinePattern)

		matches := expectedLineRE.FindAllStringSubmatch(line, -1)
		if matches == nil {
			t.Fatalf("Data line failed to match:\n"+
				"pattern : %v\n"+
				"line    : %v\n",
				expectedLinePattern,
				line)
		}
	}
}

func TestStateToJSON(t *testing.T) {
	expectedLength := len(testStatuses)

	// showAll should not affect the output
	for _, showAll := range []bool{true, false} {
		bytes, err := formatListDataAsBytes(&formatJSON{}, testStatuses, showAll)
		if err != nil {
			t.Fatal(err)
		}

		// Force capacity to match the original otherwise assert.Equal() complains.
		states := make([]fullContainerState, 0, len(testStatuses))

		err = json.Unmarshal(bytes, &states)
		if err != nil {
			t.Fatal(err)
		}

		length := len(states)

		if length != expectedLength {
			t.Fatalf("Expected %d lines, got %d", expectedLength, length)
		}

		// golang tip (what will presumably become v1.9) now
		// stores a monotonic clock value as part of time.Time's
		// internal representation (this is shown by a suffix in
		// the form "m=±ddd.nnnnnnnnn" when calling String() on
		// the time.Time object). However, this monotonic value
		// is stripped out when marshaling.
		//
		// This behaviour change makes comparing the original
		// object and the marshaled-and-then-unmarshaled copy of
		// the object doomed to failure.
		//
		// The solution? Manually strip the monotonic time out
		// of the original before comparison (yuck!)
		//
		// See:
		//
		// - https://go-review.googlesource.com/c/36255/7/src/time/time.go#54
		//
		for i := 0; i < expectedLength; i++ {
			// remove monotonic time part
			testStatuses[i].Created = testStatuses[i].Created.Truncate(0)
		}

		assert.Equal(t, states, testStatuses, "states + testStatuses")
	}
}

func TestListCLIFunctionNoContainers(t *testing.T) {
	ctx := createCLIContext(nil)
	ctx.App.Name = "foo"
	ctx.App.Metadata["foo"] = "bar"

	fn, ok := listCLICommand.Action.(func(context *cli.Context) error)
	assert.True(t, ok)

	err := fn(ctx)

	// no config in the Metadata
	assert.Error(t, err)
}

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

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

	ctx := createCLIContext(nil)
	ctx.App.Name = "foo"

	runtimeConfig, err := newTestRuntimeConfig(tmpdir, testConsole, true)
	assert.NoError(err)

	ctx.App.Metadata = map[string]interface{}{
		"runtimeConfig": runtimeConfig,
	}

	_, err = getContainers(context.Background(), ctx)
	assert.Error(err)
	assert.True(vcmock.IsMockError(err))
}

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

	testingImpl.ListSandboxFunc = func(ctx context.Context) ([]vc.SandboxStatus, error) {
		// No pre-existing sandboxes
		return []vc.SandboxStatus{}, nil
	}

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

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

	ctx := createCLIContext(nil)
	ctx.App.Name = "foo"

	runtimeConfig, err := newTestRuntimeConfig(tmpdir, testConsole, true)
	assert.NoError(err)

	ctx.App.Metadata = map[string]interface{}{
		"runtimeConfig": runtimeConfig,
	}

	state, err := getContainers(context.Background(), ctx)
	assert.NoError(err)
	assert.Equal(state, []fullContainerState(nil))
}

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

	sandbox := &vcmock.Sandbox{
		MockID: testSandboxID,
	}

	testingImpl.ListSandboxFunc = func(ctx context.Context) ([]vc.SandboxStatus, error) {
		return []vc.SandboxStatus{
			{
				ID:               sandbox.ID(),
				ContainersStatus: []vc.ContainerStatus(nil),
			},
		}, nil
	}

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

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

	ctx := createCLIContext(nil)
	ctx.App.Name = "foo"

	runtimeConfig, err := newTestRuntimeConfig(tmpdir, testConsole, true)
	assert.NoError(err)

	ctx.App.Metadata = map[string]interface{}{
		"runtimeConfig": runtimeConfig,
	}

	state, err := getContainers(context.Background(), ctx)
	assert.NoError(err)
	assert.Equal(state, []fullContainerState(nil))
}

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

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

	sandbox := &vcmock.Sandbox{
		MockID: testSandboxID,
	}

	rootfs := filepath.Join(tmpdir, "rootfs")
	err = os.MkdirAll(rootfs, testDirMode)
	assert.NoError(err)

	testingImpl.ListSandboxFunc = func(ctx context.Context) ([]vc.SandboxStatus, error) {
		return []vc.SandboxStatus{
			{
				ID: sandbox.ID(),
				ContainersStatus: []vc.ContainerStatus{
					{
						ID:          sandbox.ID(),
						Annotations: map[string]string{},
						RootFs:      rootfs,
					},
				},
			},
		}, nil
	}

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

	ctx := createCLIContext(nil)
	ctx.App.Name = "foo"

	runtimeConfig, err := newTestRuntimeConfig(tmpdir, testConsole, true)
	assert.NoError(err)

	ctx.App.Metadata["runtimeConfig"] = runtimeConfig

	_, err = getContainers(context.Background(), ctx)
	assert.NoError(err)
}

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

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

	quietFlags := flag.NewFlagSet("test", 0)
	quietFlags.Bool("quiet", true, "")

	tableFlags := flag.NewFlagSet("test", 0)
	tableFlags.String("format", "table", "")

	jsonFlags := flag.NewFlagSet("test", 0)
	jsonFlags.String("format", "json", "")

	invalidFlags := flag.NewFlagSet("test", 0)
	invalidFlags.String("format", "not-a-valid-format", "")

	type testData struct {
		format string
		flags  *flag.FlagSet
	}

	data := []testData{
		{"quiet", quietFlags},
		{"table", tableFlags},
		{"json", jsonFlags},
		{"invalid", invalidFlags},
	}

	sandbox := &vcmock.Sandbox{
		MockID: testSandboxID,
	}

	rootfs := filepath.Join(tmpdir, "rootfs")

	testingImpl.ListSandboxFunc = func(ctx context.Context) ([]vc.SandboxStatus, error) {
		return []vc.SandboxStatus{
			{
				ID: sandbox.ID(),
				ContainersStatus: []vc.ContainerStatus{
					{
						ID: sandbox.ID(),
						Annotations: map[string]string{
							vcAnnotations.ContainerTypeKey: string(vc.PodSandbox),
						},
						RootFs: rootfs,
					},
				},
			},
		}, nil
	}

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

	savedOutputFile := defaultOutputFile
	defer func() {
		defaultOutputFile = savedOutputFile
	}()

	// purposely invalid
	var invalidFile *os.File

	for _, d := range data {
		// start off with an invalid output file
		defaultOutputFile = invalidFile

		ctx := createCLIContext(d.flags)
		ctx.App.Name = "foo"
		ctx.App.Metadata["foo"] = "bar"

		fn, ok := listCLICommand.Action.(func(context *cli.Context) error)
		assert.True(ok, d)

		err = fn(ctx)

		// no config in the Metadata
		assert.Error(err, d)

		runtimeConfig, err := newTestRuntimeConfig(tmpdir, testConsole, true)
		assert.NoError(err, d)

		ctx.App.Metadata["runtimeConfig"] = runtimeConfig

		_ = os.Remove(rootfs)

		err = fn(ctx)
		assert.Error(err)

		err = os.MkdirAll(rootfs, testDirMode)
		assert.NoError(err)

		err = fn(ctx)

		// invalid output file
		assert.Error(err, d)
		assert.False(vcmock.IsMockError(err), d)

		output := filepath.Join(tmpdir, "output")
		f, err := os.OpenFile(output, os.O_WRONLY|os.O_CREATE, testFileMode)
		assert.NoError(err)
		defer f.Close()

		// output file is now valid
		defaultOutputFile = f

		err = fn(ctx)
		if d.format == "invalid" {
			assert.Error(err)
			assert.False(vcmock.IsMockError(err), d)
		} else {
			assert.NoError(err)
		}
	}
}

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

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

	runtimeConfig, err := newTestRuntimeConfig(tmpdir, testConsole, true)
	assert.NoError(err)

	sandbox := &vcmock.Sandbox{
		MockID: testSandboxID,
	}

	rootfs := filepath.Join(tmpdir, "rootfs")
	err = os.MkdirAll(rootfs, testDirMode)
	assert.NoError(err)

	testingImpl.ListSandboxFunc = func(ctx context.Context) ([]vc.SandboxStatus, error) {
		return []vc.SandboxStatus{
			{
				ID: sandbox.ID(),
				ContainersStatus: []vc.ContainerStatus{
					{
						ID: sandbox.ID(),
						Annotations: map[string]string{
							vcAnnotations.ContainerTypeKey: string(vc.PodSandbox),
						},
						RootFs: rootfs,
					},
				},
			},
		}, nil
	}

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

	set := flag.NewFlagSet("test", 0)
	set.Bool("quiet", true, "")

	ctx := createCLIContext(set)
	ctx.App.Name = "foo"
	ctx.App.Metadata["runtimeConfig"] = runtimeConfig

	savedOutputFile := defaultOutputFile
	defer func() {
		defaultOutputFile = savedOutputFile
	}()

	output := filepath.Join(tmpdir, "output")
	f, err := os.OpenFile(output, os.O_CREATE|os.O_WRONLY|os.O_SYNC, testFileMode)
	assert.NoError(err)
	defer f.Close()

	defaultOutputFile = f

	fn, ok := listCLICommand.Action.(func(context *cli.Context) error)
	assert.True(ok)

	err = fn(ctx)
	assert.NoError(err)
	f.Close()

	text, err := katautils.GetFileContents(output)
	assert.NoError(err)

	trimmed := strings.TrimSpace(text)
	assert.Equal(testSandboxID, trimmed)
}

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

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

	_, err = getDirOwner("")
	// invalid parameter
	assert.Error(err)

	dir := filepath.Join(tmpdir, "dir")

	_, err = getDirOwner(dir)
	// ENOENT
	assert.Error(err)

	err = createEmptyFile(dir)
	assert.NoError(err)

	_, err = getDirOwner(dir)
	// wrong file type
	assert.Error(err)

	err = os.Remove(dir)
	assert.NoError(err)

	err = os.MkdirAll(dir, testDirMode)
	assert.NoError(err)

	uid := uint32(os.Getuid())

	dirUID, err := getDirOwner(dir)
	assert.NoError(err)
	assert.Equal(dirUID, uid)
}
