package util

import (
	"fmt"
	"math"
	"sort"
	"testing"
	"time"

	"github.com/containers/storage/pkg/idtools"
	stypes "github.com/containers/storage/types"
	ruser "github.com/moby/sys/user"
	"github.com/opencontainers/runtime-spec/specs-go"
	"github.com/stretchr/testify/assert"
)

func BreakInsert(mapping []idtools.IDMap, extension idtools.IDMap) (result []idtools.IDMap) {
	result = breakInsert(mapping, extension)
	result = sortAndMergeConsecutiveMappings(result)
	return result
}

//#####################

func TestBreakInsert1(t *testing.T) {
	// extension below mapping
	mapping := []idtools.IDMap{
		{
			ContainerID: 1000,
			HostID:      1000,
			Size:        1,
		},
	}
	extension := idtools.IDMap{
		ContainerID: 2,
		HostID:      2,
		Size:        1,
	}
	expectedResult := []idtools.IDMap{
		{
			ContainerID: 2,
			HostID:      2,
			Size:        1,
		},
		{
			ContainerID: 1000,
			HostID:      1000,
			Size:        1,
		},
	}
	result := BreakInsert(mapping, extension)
	assert.Equal(t, result, expectedResult)
}

func TestBreakInsert2(t *testing.T) {
	// extension below mapping
	mapping := []idtools.IDMap{
		{
			ContainerID: 1000,
			HostID:      1001,
			Size:        2,
		},
	}
	extension := idtools.IDMap{
		ContainerID: 2,
		HostID:      3,
		Size:        2,
	}
	expectedResult := []idtools.IDMap{
		{
			ContainerID: 2,
			HostID:      3,
			Size:        2,
		},
		{
			ContainerID: 1000,
			HostID:      1001,
			Size:        2,
		},
	}
	result := BreakInsert(mapping, extension)
	assert.Equal(t, expectedResult, result)
}

func TestBreakInsert3(t *testing.T) {
	// extension above mapping
	mapping := []idtools.IDMap{
		{
			ContainerID: 2,
			HostID:      3,
			Size:        2,
		},
	}
	extension := idtools.IDMap{
		ContainerID: 1000,
		HostID:      1001,
		Size:        2,
	}
	expectedResult := []idtools.IDMap{
		{
			ContainerID: 2,
			HostID:      3,
			Size:        2,
		},
		{
			ContainerID: 1000,
			HostID:      1001,
			Size:        2,
		},
	}
	result := BreakInsert(mapping, extension)
	assert.Equal(t, expectedResult, result)
}

func TestBreakInsert4(t *testing.T) {
	// extension right below mapping
	mapping := []idtools.IDMap{
		{
			ContainerID: 4,
			HostID:      5,
			Size:        4,
		},
	}
	extension := idtools.IDMap{
		ContainerID: 1,
		HostID:      1,
		Size:        3,
	}
	expectedResult := []idtools.IDMap{
		{
			ContainerID: 1,
			HostID:      1,
			Size:        3,
		},
		{
			ContainerID: 4,
			HostID:      5,
			Size:        4,
		},
	}
	result := BreakInsert(mapping, extension)
	assert.Equal(t, expectedResult, result)
}

func TestSortAndMergeConsecutiveMappings(t *testing.T) {
	// Extension and mapping are consecutive
	mapping := []idtools.IDMap{
		{
			ContainerID: 1,
			HostID:      1,
			Size:        3,
		},
		{
			ContainerID: 4,
			HostID:      4,
			Size:        4,
		},
	}
	expectedResult := []idtools.IDMap{
		{
			ContainerID: 1,
			HostID:      1,
			Size:        7,
		},
	}
	result := sortAndMergeConsecutiveMappings(mapping)
	assert.Equal(t, expectedResult, result)
}

//#####################

func TestParseIDMap(t *testing.T) {
	mapSpec := []string{"+100000:@1002:1"}

	parentMapping := []ruser.IDMap{
		{
			ID:       int64(20),
			ParentID: int64(1002),
			Count:    1,
		},
	}
	expectedResult := []idtools.IDMap{
		{
			ContainerID: 100000,
			HostID:      20,
			Size:        1,
		},
	}
	result, err := ParseIDMap(
		mapSpec,
		"UID",
		parentMapping,
	)
	assert.Equal(t, nil, err)
	assert.Equal(t, expectedResult, result)
}

func TestParseIDMapSizeMissing(t *testing.T) {
	// Size is 1 if not provided
	mapSpec := []string{"+100000:@1002"}

	parentMapping := []ruser.IDMap{
		{
			ID:       int64(20),
			ParentID: int64(1002),
			Count:    1,
		},
	}
	expectedResult := []idtools.IDMap{
		{
			ContainerID: 100000,
			HostID:      20,
			Size:        1,
		},
	}
	result, err := ParseIDMap(
		mapSpec,
		"UID",
		parentMapping,
	)
	assert.Equal(t, nil, err)
	assert.Equal(t, expectedResult, result)
}

func TestParseIDMap2(t *testing.T) {
	mapSpec := []string{"0:2000:100000", "+1:100:1"}
	parentMapping := []ruser.IDMap(nil)
	expectedResult := []idtools.IDMap{
		{
			ContainerID: 0,
			HostID:      2000,
			Size:        1,
		},
		{
			ContainerID: 1,
			HostID:      100,
			Size:        1,
		},
		{
			ContainerID: 2,
			HostID:      2002,
			Size:        99998,
		},
	}
	result, err := ParseIDMap(
		mapSpec,
		"UID",
		parentMapping,
	)
	assert.Equal(t, nil, err)
	assert.Equal(t, expectedResult, result)
}

func TestParseIDMap3(t *testing.T) {
	mapSpec := []string{"0:0:20", "24:24:6", "+7:1000:2", "+12:2000:3", "+18:3000:7"}
	parentMapping := []ruser.IDMap(nil)
	expectedResult := []idtools.IDMap{
		{
			ContainerID: 0,
			HostID:      0,
			Size:        7,
		},
		{
			ContainerID: 7,
			HostID:      1000,
			Size:        2,
		},
		{
			ContainerID: 9,
			HostID:      9,
			Size:        3,
		},
		{
			ContainerID: 12,
			HostID:      2000,
			Size:        3,
		},
		{
			ContainerID: 15,
			HostID:      15,
			Size:        3,
		},
		{
			ContainerID: 18,
			HostID:      3000,
			Size:        7,
		},
		{
			ContainerID: 25,
			HostID:      25,
			Size:        5,
		},
	}
	result, err := ParseIDMap(
		mapSpec,
		"UID",
		parentMapping,
	)
	assert.Equal(t, nil, err)
	assert.Equal(t, expectedResult, result)
}

func TestParseIDMap4(t *testing.T) {
	mapSpec := []string{"0:0:20", "+10:1:1"}
	parentMapping := []ruser.IDMap(nil)
	expectedResult := []idtools.IDMap{
		{
			ContainerID: 0,
			HostID:      0,
			Size:        1,
		},
		{
			ContainerID: 2,
			HostID:      2,
			Size:        8,
		},
		{
			ContainerID: 10,
			HostID:      1,
			Size:        1,
		},
		{
			ContainerID: 11,
			HostID:      11,
			Size:        9,
		},
	}
	result, err := ParseIDMap(
		mapSpec,
		"UID",
		parentMapping,
	)
	assert.Equal(t, nil, err)
	assert.Equal(t, expectedResult, result)
}

func TestParseIDMap5(t *testing.T) {
	mapSpec := []string{"0:20:10", "15:35:10", "+8:23:16"}
	parentMapping := []ruser.IDMap(nil)
	expectedResult := []idtools.IDMap{
		{
			ContainerID: 0,
			HostID:      20,
			Size:        3,
		},
		{
			ContainerID: 8,
			HostID:      23,
			Size:        16,
		},
		{
			ContainerID: 24,
			HostID:      44,
			Size:        1,
		},
	}
	result, err := ParseIDMap(
		mapSpec,
		"UID",
		parentMapping,
	)
	assert.Equal(t, nil, err)
	assert.Equal(t, expectedResult, result)
}

func TestParseIDMapUserGroupFlags(t *testing.T) {
	mapSpec := []string{"u1:3:1", "g2:4:2"}
	parentMapping := []ruser.IDMap(nil)
	expectedResultUser := []idtools.IDMap{
		{
			ContainerID: 1,
			HostID:      3,
			Size:        1,
		},
	}
	expectedResultGroup := []idtools.IDMap{
		{
			ContainerID: 2,
			HostID:      4,
			Size:        2,
		},
	}
	result, err := ParseIDMap(mapSpec, "UID", parentMapping)
	assert.Equal(t, nil, err)
	assert.Equal(t, expectedResultUser, result)
	result, err = ParseIDMap(mapSpec, "GID", parentMapping)
	assert.Equal(t, err, nil)
	assert.Equal(t, expectedResultGroup, result)
}

func TestParseAutoIDMap(t *testing.T) {
	result, err := parseAutoIDMap("3:4:5", "UID", []ruser.IDMap{})
	assert.Equal(t, err, nil)
	assert.Equal(t, result, []idtools.IDMap{
		{
			ContainerID: 3,
			HostID:      4,
			Size:        5,
		},
	})
}

func TestParseAutoIDMapRelative(t *testing.T) {
	parentMapping := []ruser.IDMap{
		{
			ID:       0,
			ParentID: 1000,
			Count:    1,
		},
		{
			ID:       1,
			ParentID: 100000,
			Count:    65536,
		},
	}
	result, err := parseAutoIDMap("100:@100000:1", "UID", parentMapping)
	assert.Equal(t, err, nil)
	assert.Equal(t, result, []idtools.IDMap{
		{
			ContainerID: 100,
			HostID:      1,
			Size:        1,
		},
	})
}

func TestFillIDMap(t *testing.T) {
	availableRanges := [][2]int{{0, 10}, {10000, 20000}}
	idmap := []idtools.IDMap{
		{
			ContainerID: 1,
			HostID:      1000,
			Size:        10,
		},
		{
			ContainerID: 30,
			HostID:      2000,
			Size:        20,
		},
	}
	expectedResult := []idtools.IDMap{
		{
			ContainerID: 0,
			HostID:      0,
			Size:        1,
		},
		{
			ContainerID: 1,
			HostID:      1000,
			Size:        10,
		},
		{
			ContainerID: 11,
			HostID:      1,
			Size:        9,
		},
		{
			ContainerID: 20,
			HostID:      10000,
			Size:        10,
		},
		{
			ContainerID: 30,
			HostID:      2000,
			Size:        20,
		},
		{
			ContainerID: 50,
			HostID:      10010,
			Size:        9990,
		},
	}
	result := fillIDMap(idmap, availableRanges)
	assert.Equal(t, expectedResult, result)
}

func TestGetAvailableIDRanges(t *testing.T) {
	all := [][2]int{{0, 30}, {50, 70}}
	used := [][2]int{{2, 4}, {25, 55}}
	expectedResult := [][2]int{{0, 2}, {4, 25}, {55, 70}}
	result := getAvailableIDRanges(all, used)
	assert.Equal(t, expectedResult, result)
}

func TestValidateSysctls(t *testing.T) {
	strSlice := []string{"net.core.test1=4", "kernel.msgmax=2"}
	result, _ := ValidateSysctls(strSlice)
	assert.Equal(t, result["net.core.test1"], "4")
}

func TestValidateSysctlBadSysctl(t *testing.T) {
	strSlice := []string{"BLAU=BLUE", "GELB^YELLOW"}
	_, err := ValidateSysctls(strSlice)
	assert.Error(t, err)
}

func TestValidateSysctlBadSysctlWithExtraSpaces(t *testing.T) {
	expectedError := "'%s' is invalid, extra spaces found"

	// should fail fast on first sysctl
	strSlice1 := []string{
		"net.ipv4.ping_group_range = 0 0",
		"net.ipv4.ping_group_range=0 0 ",
	}
	_, err := ValidateSysctls(strSlice1)
	assert.Error(t, err)
	assert.Equal(t, err.Error(), fmt.Sprintf(expectedError, strSlice1[0]))

	// should fail on second sysctl
	strSlice2 := []string{
		"net.ipv4.ping_group_range=0 0",
		"net.ipv4.ping_group_range=0 0 ",
	}
	_, err = ValidateSysctls(strSlice2)
	assert.Error(t, err)
	assert.Equal(t, err.Error(), fmt.Sprintf(expectedError, strSlice2[1]))
}

func TestCoresToPeriodAndQuota(t *testing.T) {
	cores := 1.0
	expectedPeriod := DefaultCPUPeriod
	expectedQuota := int64(DefaultCPUPeriod)

	actualPeriod, actualQuota := CoresToPeriodAndQuota(cores)
	assert.Equal(t, actualPeriod, expectedPeriod, "Period does not match")
	assert.Equal(t, actualQuota, expectedQuota, "Quota does not match")
}

func TestPeriodAndQuotaToCores(t *testing.T) {
	var (
		period        uint64 = 100000
		quota         int64  = 50000
		expectedCores        = 0.5
	)

	assert.Equal(t, PeriodAndQuotaToCores(period, quota), expectedCores)
}

func TestParseInputTime(t *testing.T) {
	tm, err := ParseInputTime("1.5", true)
	if err != nil {
		t.Errorf("expected error to be nil but was: %v", err)
	}

	expected, err := time.ParseInLocation(time.RFC3339Nano, "1970-01-01T00:00:01.500000000Z", time.UTC)
	if err != nil {
		t.Fatal(err)
	}

	assert.Equal(t, expected, tm)
}

func TestConvertMappings(t *testing.T) {
	start := []specs.LinuxIDMapping{
		{
			ContainerID: 1,
			HostID:      2,
			Size:        3,
		},
		{
			ContainerID: 4,
			HostID:      5,
			Size:        6,
		},
	}

	converted := RuntimeSpecToIDtools(start)

	convertedBack := IDtoolsToRuntimeSpec(converted)

	assert.Equal(t, len(start), len(convertedBack))

	for i := range start {
		assert.Equal(t, start[i].ContainerID, convertedBack[i].ContainerID)
		assert.Equal(t, start[i].HostID, convertedBack[i].HostID)
		assert.Equal(t, start[i].Size, convertedBack[i].Size)
	}
}

func TestConvertTimeout(t *testing.T) {
	timeout := ConvertTimeout(0)
	assert.Equal(t, uint(0), timeout)

	timeout = ConvertTimeout(100)
	assert.Equal(t, uint(100), timeout)

	timeout = ConvertTimeout(-1)
	assert.Equal(t, uint(math.MaxUint32), timeout)

	timeout = ConvertTimeout(-100)
	assert.Equal(t, uint(math.MaxUint32), timeout)
}

func TestGetRootlessKeepIDMapping(t *testing.T) {
	tests := []struct {
		uid, gid                 int
		uids, gids               []idtools.IDMap
		expectedOptions          *stypes.IDMappingOptions
		expectedUID, expectedGID int
		expectedError            error
	}{
		{
			uid:  1000,
			gid:  1000,
			uids: []idtools.IDMap{},
			gids: []idtools.IDMap{},
			expectedOptions: &stypes.IDMappingOptions{
				HostUIDMapping: false,
				HostGIDMapping: false,
				UIDMap:         []idtools.IDMap{{ContainerID: 1000, HostID: 0, Size: 1}},
				GIDMap:         []idtools.IDMap{{ContainerID: 1000, HostID: 0, Size: 1}},
			},
			expectedUID: 1000,
			expectedGID: 1000,
		},
		{
			uid:  0,
			gid:  0,
			uids: []idtools.IDMap{{ContainerID: 0, HostID: 100000, Size: 65536}},
			gids: []idtools.IDMap{{ContainerID: 0, HostID: 100000, Size: 65536}},
			expectedOptions: &stypes.IDMappingOptions{
				HostUIDMapping: false,
				HostGIDMapping: false,
				UIDMap:         []idtools.IDMap{{ContainerID: 0, HostID: 0, Size: 1}, {ContainerID: 1, HostID: 1, Size: 65536}},
				GIDMap:         []idtools.IDMap{{ContainerID: 0, HostID: 0, Size: 1}, {ContainerID: 1, HostID: 1, Size: 65536}},
			},
			expectedUID: 0,
			expectedGID: 0,
		},
	}

	for _, test := range tests {
		options, uid, gid, err := getRootlessKeepIDMapping(test.uid, test.gid, test.uids, test.gids)
		assert.Nil(t, err)
		assert.Equal(t, test.expectedOptions, options)
		assert.Equal(t, test.expectedUID, uid)
		assert.Equal(t, test.expectedGID, gid)
	}
}

func getDefaultMountOptionsNoStat(path string) (defaultMountOptions, error) {
	return defaultMountOptions{false, true, true}, nil
}

func TestProcessOptions(t *testing.T) {
	tests := []struct {
		name       string
		options    []string
		isTmpfs    bool
		sourcePath string
		expected   []string
		expectErr  bool
	}{
		{
			name:       "tmpfs",
			options:    []string{"rw", "size=512m"},
			isTmpfs:    true,
			sourcePath: "",
			expected:   []string{"nodev", "nosuid", "rprivate", "rw", "size=512m", "tmpcopyup"},
		},
		{
			name:       "duplicate idmap option",
			sourcePath: "/path/to/source",
			options:    []string{"idmap", "idmap"},
			expectErr:  true,
		},
		{
			name:       "mode allowed only with tmpfs",
			sourcePath: "/path/to/source",
			options:    []string{"rw", "rbind", "mode=0123"},
			expectErr:  true,
		},
		{
			name:       "noswap allowed only with tmpfs",
			sourcePath: "/path/to/source",
			options:    []string{"noswap"},
			expectErr:  true,
		},
		{
			name:       "tmpcopyup allowed only with tmpfs",
			sourcePath: "/path/to/source",
			options:    []string{"tmpcopyup"},
			expectErr:  true,
		},
		{
			name:       "notmpcopyup allowed only with tmpfs",
			sourcePath: "/path/to/source",
			options:    []string{"notmpcopyup"},
			expectErr:  true,
		},
		{
			name:       "z not allowed with tmpfs",
			isTmpfs:    true,
			sourcePath: "/path/to/source",
			options:    []string{"z"},
			expectErr:  true,
		},
		{
			name:       "size allowed only with tmpfs",
			sourcePath: "/path/to/source",
			options:    []string{"size=123456"},
			expectErr:  true,
		},
		{
			name:       "conflicting option dev/nodev",
			sourcePath: "/path/to/source",
			options:    []string{"dev", "nodev"},
			expectErr:  true,
		},
		{
			name:       "conflicting option suid/nosuid",
			sourcePath: "/path/to/source",
			options:    []string{"suid", "nosuid"},
			expectErr:  true,
		},
		{
			name:       "conflicting option exec/noexec",
			sourcePath: "/path/to/source",
			options:    []string{"noexec", "exec"},
			expectErr:  true,
		},
		{
			name:       "conflicting option ro/rw",
			sourcePath: "/path/to/source",
			options:    []string{"ro", "rw"},
			expectErr:  true,
		},
		{
			name:       "conflicting option bind/rbind",
			sourcePath: "/path/to/source",
			options:    []string{"bind", "rbind"},
			expectErr:  true,
		},
		{
			name:       "conflicting option bind/rbind",
			sourcePath: "/path/to/source",
			options:    []string{"bind", "rbind"},
			expectErr:  true,
		},
		{
			name:       "default bind mount",
			sourcePath: "/path/to/source",
			expected:   []string{"nodev", "nosuid", "rbind", "rprivate", "rw"},
		},
		{
			name:       "default bind mount with bind",
			sourcePath: "/path/to/source",
			options:    []string{"bind"},
			expected:   []string{"nodev", "nosuid", "bind", "private", "rw"},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			opts, err := processOptionsInternal(tt.options, tt.isTmpfs, tt.sourcePath, getDefaultMountOptionsNoStat)
			if tt.expectErr {
				assert.NotNil(t, err)
			} else {
				assert.Nil(t, err)
				sort.Strings(opts)
				sort.Strings(tt.expected)
				assert.Equal(t, opts, tt.expected)
			}
		})
	}
}

func TestGetRootlessPauseProcessPidPath(t *testing.T) {
	dir, err := GetRootlessPauseProcessPidPath()
	assert.NoError(t, err)
	assert.NotEqual(t, dir, "libpod/tmp/pause.pid")
}
