// Copyright 2015-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"). You may
// not use this file except in compliance with the License. A copy of the
// License is located at
//
//	http://aws.amazon.com/apache2.0/
//
// or in the "license" file accompanying this file. This file 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 adapter

import (
	"fmt"
	"math/rand"
	"strconv"
	"strings"
	"testing"
	"time"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/service/ecs"
	"github.com/docker/cli/cli/compose/types"
	"github.com/docker/libcompose/config"
	"github.com/docker/libcompose/yaml"
	"github.com/stretchr/testify/assert"
)

const (
	portNumber     = 8000
	portMapping    = "8000:8000"
	containerPath  = "/tmp/cache"
	containerPath2 = "/tmp/cache2"
	hostPath       = "./cache"
	namedVolume    = "named_volume"
)

func TestConvertVolumesFrom_V1_NoOptions(t *testing.T) {
	volumesFrom := []*ecs.VolumeFrom{
		{
			ReadOnly:        aws.Bool(false),
			SourceContainer: aws.String("container_name"),
		},
	}
	v1VolumesInput := []string{"container_name"}
	actual, err := ConvertToVolumesFrom(v1VolumesInput)
	assert.NoError(t, err, "Unexpected error converting Volumes From")
	assert.Equal(t, volumesFrom, actual, "Expected VolumesFrom to match")
}

func TestConvertVolumesFrom_V1_Ro(t *testing.T) {
	volumesFrom := []*ecs.VolumeFrom{
		{
			ReadOnly:        aws.Bool(true),
			SourceContainer: aws.String("container_name"),
		},
	}
	v1VolumesInput := []string{"container_name:ro"}
	actual, err := ConvertToVolumesFrom(v1VolumesInput)
	assert.NoError(t, err, "Unexpected error converting Volumes From")
	assert.Equal(t, volumesFrom, actual, "Expected VolumesFrom to match")
}

func TestConvertVolumesFrom_V1_Rw(t *testing.T) {
	volumesFrom := []*ecs.VolumeFrom{
		{
			ReadOnly:        aws.Bool(false),
			SourceContainer: aws.String("container_name"),
		},
	}
	v1VolumesInput := []string{"container_name:rw"}
	actual, err := ConvertToVolumesFrom(v1VolumesInput)
	assert.NoError(t, err, "Unexpected error converting Volumes From")
	assert.Equal(t, volumesFrom, actual, "Expected VolumesFrom to match")
}

func TestConvertVolumesFrom_V2_NoOptions(t *testing.T) {
	volumesFrom := []*ecs.VolumeFrom{
		{
			ReadOnly:        aws.Bool(false),
			SourceContainer: aws.String("container_name"),
		},
	}
	v2VolumesInput := []string{"container:container_name"}
	actual, err := ConvertToVolumesFrom(v2VolumesInput)
	assert.NoError(t, err, "Unexpected error converting Volumes From")
	assert.Equal(t, volumesFrom, actual, "Expected VolumesFrom to match")
}

func TestConvertVolumesFrom_V2_Ro(t *testing.T) {
	volumesFrom := []*ecs.VolumeFrom{
		{
			ReadOnly:        aws.Bool(true),
			SourceContainer: aws.String("container_name"),
		},
	}
	v2VolumesInput := []string{"container:container_name:ro"}
	actual, err := ConvertToVolumesFrom(v2VolumesInput)
	assert.NoError(t, err, "Unexpected error converting Volumes From")
	assert.Equal(t, volumesFrom, actual, "Expected VolumesFrom to match")
}

func TestConvertVolumesFrom_V2_Rw(t *testing.T) {
	volumesFrom := []*ecs.VolumeFrom{
		{
			ReadOnly:        aws.Bool(false),
			SourceContainer: aws.String("container_name"),
		},
	}
	v2VolumesInput := []string{"container:container_name:rw"}
	actual, err := ConvertToVolumesFrom(v2VolumesInput)
	assert.NoError(t, err, "Unexpected error converting Volumes From")
	assert.Equal(t, volumesFrom, actual, "Expected VolumesFrom to match")
}

func TestConvertToTmpfs(t *testing.T) {
	tmpfs := []string{"/run:rw,noexec,nosuid,size=65536k", "/foo:size=1gb", "/bar:size=1gb,rw,runbindable", "/run:size=64MiB" }

	tmpfsMounts, err := ConvertToTmpfs(tmpfs)
	assert.NoError(t, err, "Unexpected error converting tmpfs")
	mount1 := tmpfsMounts[0]
	mount2 := tmpfsMounts[1]
	mount3 := tmpfsMounts[2]
	mount4 := tmpfsMounts[3]

	assert.Equal(t, "/run", aws.StringValue(mount1.ContainerPath))
	assert.Equal(t, []string{"rw", "noexec", "nosuid"}, aws.StringValueSlice(mount1.MountOptions))
	assert.Equal(t, int64(64), aws.Int64Value(mount1.Size))

	assert.Equal(t, "/foo", aws.StringValue(mount2.ContainerPath))
	assert.Equal(t, []string{}, aws.StringValueSlice(mount2.MountOptions))
	assert.Equal(t, int64(1024), aws.Int64Value(mount2.Size))

	assert.Equal(t, "/bar", aws.StringValue(mount3.ContainerPath))
	assert.Equal(t, []string{"rw", "runbindable"}, aws.StringValueSlice(mount3.MountOptions))
	assert.Equal(t, int64(1024), aws.Int64Value(mount3.Size))

	assert.Equal(t, "/run", aws.StringValue(mount4.ContainerPath))
	assert.Equal(t, []string{}, aws.StringValueSlice(mount4.MountOptions))
	assert.Equal(t, int64(64), aws.Int64Value(mount4.Size))
}

func TestConvertToTmpfs_NoPath(t *testing.T) {
	tmpfs := []string{"size=65536k"}
	_, err := ConvertToTmpfs(tmpfs)

	assert.Error(t, err)
}

func TestConvertToTmpfs_BadOptionFormat(t *testing.T) {
	tmpfs := []string{"/run,size=65536k"}
	_, err := ConvertToTmpfs(tmpfs)

	assert.Error(t, err)
}

func TestConvertToTmpfs_NoSize(t *testing.T) {
	tmpfs := []string{"/run"}
	_, err := ConvertToTmpfs(tmpfs)

	assert.Error(t, err)
}

func TestConvertToTmpfs_WithOptionsNoSize(t *testing.T) {
	tmpfs := []string{"/run:rw"}
	_, err := ConvertToTmpfs(tmpfs)

	assert.Error(t, err)
}

func TestConvertToTmpfs_WithMalformedSize(t *testing.T) {
	tmpfs := []string{"/run:1gb"}
	_, err := ConvertToTmpfs(tmpfs)

	assert.Error(t, err)
}

func TestConvertToPortMappings(t *testing.T) {
	implicitTCP := portMapping                      // 8000:8000
	explicitTCP := portMapping + "/tcp"             // "8000:8000/tcp"
	udpPort := portMapping + "/udp"                 // "8000:8000/udp"
	containerPortOnly := strconv.Itoa(portNumber)   // "8000"
	portWithIPAddress := "127.0.0.1:" + portMapping // "127.0.0.1:8000:8000"

	portMappingsIn := []string{implicitTCP, explicitTCP, udpPort, containerPortOnly, portWithIPAddress}
	portMappingsOut, err := ConvertToPortMappings("test", portMappingsIn)

	assert.NoError(t, err, "Unexpected error converting port mapping")

	if len(portMappingsIn) != len(portMappingsOut) {
		t.Errorf("Incorrect conversion. Input [%v] Output [%v]", portMappingsIn, portMappingsOut)
	}
	verifyPortMapping(t, portMappingsOut[0], portNumber, portNumber, ecs.TransportProtocolTcp)
	verifyPortMapping(t, portMappingsOut[1], portNumber, portNumber, ecs.TransportProtocolTcp)
	verifyPortMapping(t, portMappingsOut[2], portNumber, portNumber, ecs.TransportProtocolUdp)
	verifyPortMapping(t, portMappingsOut[3], 0, portNumber, ecs.TransportProtocolTcp)
	verifyPortMapping(t, portMappingsOut[4], portNumber, portNumber, ecs.TransportProtocolTcp)
}

func verifyPortMapping(t *testing.T, output *ecs.PortMapping, hostPort, containerPort int64, protocol string) {
	if protocol != *output.Protocol {
		t.Errorf("Expected protocol [%s] But was [%s]", protocol, *output.Protocol)
	}
	if hostPort != *output.HostPort {
		t.Errorf("Expected hostPort [%d] But was [%d]", hostPort, *output.HostPort)
	}
	if containerPort != *output.ContainerPort {
		t.Errorf("Expected containerPort [%d] But was [%d]", containerPort, *output.ContainerPort)
	}
}

func TestConvertToMountPoints(t *testing.T) {
	// Valid inputs with host and container paths set:
	// /tmp/cache
	// /tmp/cache2
	// ./cache:/tmp/cache
	// /tmp/cache:ro
	// ./cache:/tmp/cache:ro
	// ./cache:/tmp/cache:rw
	// named_volume:/tmp/cache
	onlyContainerPath := yaml.Volume{Destination: containerPath}
	onlyContainerPath2 := yaml.Volume{Destination: containerPath2}
	hostAndContainerPath := yaml.Volume{Source: hostPath, Destination: containerPath}
	onlyContainerPathWithRO := yaml.Volume{Destination: containerPath, AccessMode: "ro"}
	hostAndContainerPathWithRO := yaml.Volume{Source: hostPath, Destination: containerPath, AccessMode: "ro"}
	hostAndContainerPathWithRW := yaml.Volume{Source: hostPath, Destination: containerPath, AccessMode: "rw"}
	namedVolumeAndContainerPath := yaml.Volume{Source: namedVolume, Destination: containerPath}

	mountPointsIn := yaml.Volumes{
		Volumes: []*yaml.Volume{
			&onlyContainerPath,
			&onlyContainerPath2,
			&hostAndContainerPath,
			&onlyContainerPathWithRO,
			&hostAndContainerPathWithRO,
			&hostAndContainerPathWithRW,
			&namedVolumeAndContainerPath,
		},
	}

	expectedVolumeWithHost := map[string]string{hostPath: "volume-3"}
	expectedVolumeEmptyHost := []string{namedVolume, "volume-1", "volume-2", "volume-4"}
	expectedMountPoints := []*ecs.MountPoint{
		{
			ContainerPath: aws.String("/tmp/cache"),
			ReadOnly:      aws.Bool(false),
			SourceVolume:  aws.String("volume-1"),
		},
		{
			ContainerPath: aws.String("/tmp/cache2"),
			ReadOnly:      aws.Bool(false),
			SourceVolume:  aws.String("volume-2"),
		},
		{
			ContainerPath: aws.String("/tmp/cache"),
			ReadOnly:      aws.Bool(false),
			SourceVolume:  aws.String("volume-3"),
		},
		{
			ContainerPath: aws.String("/tmp/cache"),
			ReadOnly:      aws.Bool(true),
			SourceVolume:  aws.String("volume-4"),
		},
		{
			ContainerPath: aws.String("/tmp/cache"),
			ReadOnly:      aws.Bool(true),
			SourceVolume:  aws.String("volume-3"),
		},
		{
			ContainerPath: aws.String("/tmp/cache"),
			ReadOnly:      aws.Bool(false),
			SourceVolume:  aws.String("volume-3"),
		},
		{
			ContainerPath: aws.String("/tmp/cache"),
			ReadOnly:      aws.Bool(false),
			SourceVolume:  aws.String("named_volume"),
		},
	}

	volumes := &Volumes{
		VolumeWithHost:  make(map[string]string), // This field should be empty before ConvertToMountPoints is called
		VolumeEmptyHost: []string{namedVolume},   // We expect ConvertToVolumes to already have been called, so any named volumes should have been added to VolumeEmptyHost
	}

	mountPointsOut, err := ConvertToMountPoints(&mountPointsIn, volumes)
	assert.NoError(t, err, "Unexpected error converting MountPoints")

	// Expect top-level volumes fields to be populated
	assert.Equal(t, expectedVolumeWithHost, volumes.VolumeWithHost, "Expected volumeWithHost to match")
	assert.Equal(t, expectedVolumeEmptyHost, volumes.VolumeEmptyHost, "Expected volumeEmptyHost to match")

	assert.ElementsMatch(t, expectedMountPoints, mountPointsOut, "Expected Mount Points to match")

	if mountPointsOut[0].SourceVolume == mountPointsOut[1].SourceVolume {
		t.Errorf("Expected volume %v (onlyContainerPath) and %v (onlyContainerPath2) to be different", mountPointsOut[0].SourceVolume, mountPointsOut[1].SourceVolume)
	}

	if mountPointsOut[1].SourceVolume == mountPointsOut[3].SourceVolume {
		t.Errorf("Expected volume %v (onlyContainerPath2) and %v (onlyContainerPathWithRO) to be different", mountPointsOut[0].SourceVolume, mountPointsOut[1].SourceVolume)
	}
}

func TestConvertToMountPointsWithInvalidAccessMode(t *testing.T) {
	volumes := &Volumes{
		VolumeWithHost:  make(map[string]string),
		VolumeEmptyHost: []string{namedVolume},
	}

	hostAndContainerPathWithIncorrectAccess := yaml.Volume{
		Source:      hostPath,
		Destination: containerPath,
		AccessMode:  "readonly",
	}

	mountPointsIn := yaml.Volumes{
		Volumes: []*yaml.Volume{&hostAndContainerPathWithIncorrectAccess},
	}

	_, err := ConvertToMountPoints(&mountPointsIn, volumes)
	assert.Error(t, err, "Expected error converting MountPoints")
}

func TestConvertToMountPointsNullContainerVolumes(t *testing.T) {
	volumes := &Volumes{
		VolumeWithHost:  make(map[string]string),
		VolumeEmptyHost: []string{namedVolume},
	}
	mountPointsOut, err := ConvertToMountPoints(nil, volumes)
	assert.NoError(t, err, "Unexpected error converting MountPoints")
	assert.Empty(t, mountPointsOut, "Expected mount points to be empty")
}

func TestConvertToMountPointsWithNoCorrespondingNamedVolume(t *testing.T) {
	volumes := &Volumes{
		VolumeWithHost:  make(map[string]string),
		VolumeEmptyHost: []string{}, // Top-level named volumes is empty
	}

	namedVolume := yaml.Volume{
		Source:      namedVolume,
		Destination: containerPath,
	}

	mountPointsIn := yaml.Volumes{
		Volumes: []*yaml.Volume{&namedVolume},
	}

	_, err := ConvertToMountPoints(&mountPointsIn, volumes)
	assert.Error(t, err, "Expected error converting MountPoints")
}

func TestGetSourcePathAndUpdateVolumesWithEmptySourcePath(t *testing.T) {
	expectedSourcePath := "volume-0"
	volumes := &Volumes{
		VolumeWithHost:  make(map[string]string),
		VolumeEmptyHost: []string{}, // Top-level named volumes is empty
	}

	observedSourcePath, err := GetSourcePathAndUpdateVolumes("", volumes)

	assert.NoError(t, err, "Unexpected error getting Mount Point source path")
	assert.Equal(t, expectedSourcePath, observedSourcePath)
	assert.Equal(t, expectedSourcePath, volumes.VolumeEmptyHost[0])
}

func TestGetSourcePathAndUpdateVolumesWithNamedVol(t *testing.T) {
	namedSourcePath := "logging"
	volumes := &Volumes{
		VolumeWithHost:  make(map[string]string),
		VolumeEmptyHost: []string{namedSourcePath},
	}

	observedSourcePath, err := GetSourcePathAndUpdateVolumes(namedSourcePath, volumes)

	assert.NoError(t, err, "Unexpected error getting Mount Point source path")
	assert.Equal(t, namedSourcePath, observedSourcePath)
	assert.Equal(t, namedSourcePath, volumes.VolumeEmptyHost[0])
}

func TestConvertToDevices(t *testing.T) {
	testCases := []struct {
		input                 string
		expectedHostPath      string
		expectedContainerPath string
		expectedPermissions   []string
	}{
		{"/dev/sda", "/dev/sda", "", []string{}},
		{"/dev/sda:/dev/xvdc", "/dev/sda", "/dev/xvdc", []string{}},
		{"/dev/sda:/dev/xvdc:w", "/dev/sda", "/dev/xvdc", []string{"write"}},
		{"/dev/nvid:/dev/xvdc:rw", "/dev/nvid", "/dev/xvdc", []string{"read", "write"}},
	}
	for _, test := range testCases {
		t.Run(fmt.Sprintf("Convert %s", test.input), func(t *testing.T) {
			inputSlice := []string{test.input}
			outputDevices, err := ConvertToDevices(inputSlice)
			assert.NoError(t, err, "Unexpected error converting Devices")
			assert.Equal(t, 1, len(outputDevices), "Expected Devices length to be 1")

			var expectedContainer *string
			var expectedPerms []*string

			if test.expectedContainerPath != "" {
				expectedContainer = aws.String(test.expectedContainerPath)
			}

			if len(test.expectedPermissions) != 0 {
				expectedPerms = aws.StringSlice(test.expectedPermissions)
			}

			outputDev := *outputDevices[0]
			assert.Equal(t, test.expectedHostPath, *outputDev.HostPath, "Expected HostPath to match")
			assert.Equal(t, expectedContainer, outputDev.ContainerPath, "Expected ContainerPath to match")
			assert.ElementsMatch(t, expectedPerms, outputDev.Permissions, "Expected Permissions to match")
		})
	}
}

func TestConvertToDevices_ErrorOnInvalidOptions(t *testing.T) {
	testCases := []string{
		"/dev/xf:/dex/gru:rw:m", // too many args
		"/dev/xx:/dex/gru:ytr",  // invalid option flags (y, t)
		"/dev/xx:/dex/sda:rrmw", // too many options (max is 3)
	}
	for _, test := range testCases {
		t.Run(fmt.Sprintf("Expect error for %s", test), func(t *testing.T) {
			_, err := ConvertToDevices([]string{test})
			assert.Error(t, err, "Expected error for invalid device option")
		})
	}
}

func TestConvertToExtraHosts(t *testing.T) {
	hostname := "test.local"
	ipAddress := "127.10.10.10"

	extraHost := hostname + ":" + ipAddress

	extraHostsIn := []string{extraHost}
	extraHostsOut, err := ConvertToExtraHosts(extraHostsIn)
	if err != nil {
		t.Errorf("Expected to convert [%v] extra hosts without errors. But got [%v]", extraHostsIn, err)
	}
	if len(extraHostsIn) != len(extraHostsOut) {
		t.Errorf("Incorrect conversion. Input [%v] Output [%v]", extraHostsIn, extraHostsOut)
	}
	verifyExtraHost(t, extraHostsOut[0], hostname, ipAddress)

	incorrectHost := hostname + "=" + ipAddress
	_, err = ConvertToExtraHosts([]string{incorrectHost})
	if err == nil {
		t.Errorf("Expected to get formatting error for extraHost=[%s], but got none", incorrectHost)
	}

	extraHostWithPort := fmt.Sprintf("%s:%s:%d", hostname, ipAddress, portNumber)
	_, err = ConvertToExtraHosts([]string{extraHostWithPort})
	if err == nil {
		t.Errorf("Expected to get formatting error for extraHost=[%s], but got none", extraHostWithPort)
	}

}

func verifyExtraHost(t *testing.T, output *ecs.HostEntry, hostname, ipAddress string) {
	if hostname != aws.StringValue(output.Hostname) {
		t.Errorf("Expected hostname [%s] But was [%s]", hostname, aws.StringValue(output.Hostname))
	}
	if ipAddress != aws.StringValue(output.IpAddress) {
		t.Errorf("Expected ipAddress [%s] But was [%s]", ipAddress, aws.StringValue(output.IpAddress))
	}
}

func TestConvertToUlimits(t *testing.T) {
	softLimit := int64(1024)
	hardLimit := int64(2048)
	typeName := "nofile"
	basicType := yaml.NewUlimit(typeName, softLimit, softLimit)         // "nofile=1024"
	typeWithHardLimit := yaml.NewUlimit(typeName, softLimit, hardLimit) // "nofile=1024:2048"

	ulimitsIn := yaml.Ulimits{
		Elements: []yaml.Ulimit{basicType, typeWithHardLimit},
	}
	ulimitsOut, err := ConvertToULimits(ulimitsIn)
	if err != nil {
		t.Errorf("Expected to convert [%v] ulimits without errors. But got [%v]", ulimitsIn, err)
	}
	if len(ulimitsIn.Elements) != len(ulimitsOut) {
		t.Errorf("Incorrect conversion. Input [%v] Output [%v]", ulimitsIn, ulimitsOut)
	}
	verifyUlimit(t, ulimitsOut[0], typeName, softLimit, softLimit)
	verifyUlimit(t, ulimitsOut[1], typeName, softLimit, hardLimit)
}

func verifyUlimit(t *testing.T, output *ecs.Ulimit, name string, softLimit, hardLimit int64) {
	if name != *output.Name {
		t.Errorf("Expected name [%s] But was [%s]", name, *output.Name)
	}
	if softLimit != *output.SoftLimit {
		t.Errorf("Expected softLimit [%d] But was [%d]", softLimit, *output.SoftLimit)
	}
	if hardLimit != *output.HardLimit {
		t.Errorf("Expected hardLimit [%d] But was [%d]", hardLimit, *output.HardLimit)
	}
}

func TestConvertToVolumes(t *testing.T) {
	libcomposeVolumeConfigs := map[string]*config.VolumeConfig{
		namedVolume: nil,
	}

	expected := &Volumes{
		VolumeWithHost:  make(map[string]string), // map with key:=hostSourcePath value:=VolumeName
		VolumeEmptyHost: []string{namedVolume},   // Declare one volume with an empty host
	}

	actual, err := ConvertToVolumes(libcomposeVolumeConfigs)

	assert.NoError(t, err, "Unexpected error converting libcompose volume configs")
	assert.Equal(t, expected, actual, "Named volumes should match")
}

func TestConvertToVolumes_ErrorsWithDriverSubfield(t *testing.T) {
	libcomposeVolumeConfigs := map[string]*config.VolumeConfig{
		namedVolume: &config.VolumeConfig{
			Driver: "noodles",
		},
	}

	_, err := ConvertToVolumes(libcomposeVolumeConfigs)

	assert.Error(t, err, "Expected error converting libcompose volume configs when driver is specified")
}

func TestConvertToVolumes_ErrorsWithDriverOptsSubfield(t *testing.T) {
	driverOpts := map[string]string{"foo": "bar"}

	libcomposeVolumeConfigs := map[string]*config.VolumeConfig{
		namedVolume: &config.VolumeConfig{
			DriverOpts: driverOpts,
		},
	}

	_, err := ConvertToVolumes(libcomposeVolumeConfigs)

	assert.Error(t, err, "Expected error converting libcompose volume configs when driver options are specified")
}

func TestConvertToVolumes_ErrorsWithExternalSubfield(t *testing.T) {
	external := yaml.External{External: false}

	libcomposeVolumeConfigs := map[string]*config.VolumeConfig{
		namedVolume: &config.VolumeConfig{
			External: external,
		},
	}

	_, err := ConvertToVolumes(libcomposeVolumeConfigs)

	assert.Error(t, err, "Expected error converting libcompose volume configs when external is specified")

	external = yaml.External{External: true}
	libcomposeVolumeConfigs = map[string]*config.VolumeConfig{
		namedVolume: &config.VolumeConfig{
			External: external,
		},
	}

	_, err = ConvertToVolumes(libcomposeVolumeConfigs)

	assert.Error(t, err, "Expected error converting libcompose volume configs when external is specified")
}

func TestRegisterTaskDefinitionInputEquivalence(t *testing.T) {
	family := aws.String("family1")
	dockerLabels := map[string]string{
		"label1":         "",
		"com.foo.label2": "value",
	}
	cdefs := []*ecs.ContainerDefinition{}
	N := 10
	for i := 0; i < N; i++ {
		command := make([]string, i+1)
		for j := 0; j < i+1; j++ {
			command[j] = strings.Repeat(string(rune(65+j)), i+1)
		}
		cdefs = append(cdefs, &ecs.ContainerDefinition{
			Name:         aws.String(strings.Repeat(string(rune(65+i)), i+1)),
			Command:      aws.StringSlice(command),
			DockerLabels: aws.StringMap(dockerLabels),
		})
	}
	inputA := ecs.RegisterTaskDefinitionInput{
		Family:               family,
		ContainerDefinitions: cdefs,
	}

	shuffle_cdefs := make([]*ecs.ContainerDefinition, len(cdefs))
	for i, v := range rand.Perm(len(cdefs)) {
		shuffle_cdefs[v] = cdefs[i]
	}

	inputB := ecs.RegisterTaskDefinitionInput{
		ContainerDefinitions: shuffle_cdefs,
		Family:               family,
	}

	strA, err := SortedGoString(SortedContainerDefinitionsByName(&inputA))
	assert.NoError(t, err, "Unexpected error generating sorted map string")
	strB, err := SortedGoString(SortedContainerDefinitionsByName(&inputB))
	assert.NoError(t, err, "Unexpected error generating sorted map string")

	assert.Equal(t, strA, strB, "Sorted inputs should match")
}

func TestSortedGoString(t *testing.T) {
	family := aws.String("family1")
	name := aws.String("foo")
	command := aws.StringSlice([]string{"dark", "side", "of", "the", "moon"})
	dockerLabels := map[string]string{
		"label1":         "",
		"com.foo.label2": "value",
	}

	inputA := ecs.RegisterTaskDefinitionInput{
		Family: family,
		ContainerDefinitions: []*ecs.ContainerDefinition{
			{
				Name:         name,
				Command:      command,
				DockerLabels: aws.StringMap(dockerLabels),
			},
		},
	}
	inputB := ecs.RegisterTaskDefinitionInput{
		ContainerDefinitions: []*ecs.ContainerDefinition{
			{
				Command:      command,
				Name:         name,
				DockerLabels: aws.StringMap(dockerLabels),
			},
		},
		Family: family,
	}

	strA, err := SortedGoString(inputA)
	assert.NoError(t, err, "Unexpected error generating sorted map string")
	strB, err := SortedGoString(inputB)
	assert.NoError(t, err, "Unexpected error generating sorted map string")

	assert.Equal(t, strA, strB, "Sorted inputs should match")
}

func TestConvertCamelCaseToUnderScore(t *testing.T) {
	testCases := []struct {
		input    string
		expected string
	}{
		{"CamelCaseString", "camel_case_string"},
		{"lowercase", "lowercase"},
		{"UPPERCASE", "uppercase"},
		{"CamelWithACRONYM", "camel_with_acronym"},
		{"Uppercase", "uppercase"},
	}
	for _, test := range testCases {
		t.Run(fmt.Sprintf("%s should be %s", test.input, test.expected), func(t *testing.T) {
			output := ConvertCamelCaseToUnderScore(test.input)
			assert.Equal(t, test.expected, output, "Expected output to match")
		})
	}
}

func TestConvertToHealthCheck(t *testing.T) {
	timeout := 10 * time.Second
	interval := time.Minute
	retries := uint64(3)
	startPeriod := 2 * time.Minute
	input := &types.HealthCheckConfig{
		Test:        []string{"CMD", "echo 'echo is not a good health check test command'"},
		Timeout:     &timeout,
		Interval:    &interval,
		Retries:     &retries,
		StartPeriod: &startPeriod,
	}
	output := ConvertToHealthCheck(input)
	assert.ElementsMatch(t, input.Test, aws.StringValueSlice(output.Command))
	assert.Equal(t, aws.Int64(10), output.Timeout)
	assert.Equal(t, aws.Int64(60), output.Interval)
	assert.Equal(t, aws.Int64(3), output.Retries)
	assert.Equal(t, aws.Int64(120), output.StartPeriod)
}

func TestConvertDurationStrToSecondsEmptyString(t *testing.T) {
	res, err := ConvertDurationStrToSeconds("")
	assert.NoError(t, err, "empty string should not result in conversion error")
	assert.Nil(t, res, "conversion result should be nil to indicate that the input duration string is empty")
}

func TestConvertDurationStrToSeconds(t *testing.T) {
	res, err := ConvertDurationStrToSeconds("1m15s")
	assert.NoError(t, err, "valid duration string should not result in conversion error")
	assert.Equal(t, int64(75), *res)
}

func TestConvertDurationStrToSecondsInvalidDurationStr(t *testing.T) {
	_, err := ConvertDurationStrToSeconds("chickenWings")
	assert.Error(t, err, "invalid duration string should result in conversion error")
}
