// 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 converter implements the logic to translate an ecs.TaskDefinition
// structure to a docker compose schema, which will be written to a
// docker-compose.ecs-local.yml file.

package converter

import (
	"testing"
	"time"

	"github.com/aws/amazon-ecs-cli/ecs-cli/modules/cli/local/network"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/service/ecs"
	composeV3 "github.com/docker/cli/cli/compose/types"

	"github.com/stretchr/testify/assert"
)

func TestConvertToComposeService(t *testing.T) {
	// GIVEN
	expectedImage := "nginx"
	expectedName := "web"
	expectedCommand := []string{"CMD-SHELL", "curl -f http://localhost"}
	expectedEntrypoint := []string{"sh", "-c"}
	expectedWorkingDir := "./app"
	expectedHostname := "myHost"
	expectedLinks := []string{"container1"}
	expectedDNS := []string{"1.2.3.4"}
	expectedDNSSearch := []string{"search.example.com"}
	expectedUser := "admin"
	expectedSecurityOpt := []string{"label:type:test_virt"}
	expectedTty := true
	expectedPrivileged := true
	expectedReadOnly := true
	expectedStdinOpen := true
	expectedUlimits := map[string]*composeV3.UlimitsConfig{
		"nofile": &composeV3.UlimitsConfig{
			Soft: 2000,
			Hard: 4000,
		},
	}
	expectedInit := true
	expectedDevices := []string{"/dev/sda:/dev/xvdc:r"}
	expectedTmpfs := []string{"/run:size=64MiB,rw,noexec,nosuid"}
	expectedShmSize := "128MiB"
	expectedCapAdd := []string{"NET_ADMIN", "MKNOD"}
	expectedCapDrop := []string{"KILL"}
	expectedEnvironment := map[string]*string{
		"rails_env":             aws.String("development"),
		"DB_PASSWORD":           aws.String("${web_DB_PASSWORD}"),
		"API_KEY":               aws.String("${web_API_KEY}"),
		ecsCredsProviderEnvName: aws.String(endpointsTempCredsPath),
		ecsMetadataURIEnvName:   aws.String(endpointsMetadataV3URI),
	}
	expectedExtraHosts := []string{"somehost:162.242.195.82", "otherhost:50.31.209.229"}
	expectedHealthCheck := &composeV3.HealthCheckConfig{
		Test: []string{"CMD-SHELL", "echo hello"},
	}
	expectedLabels := composeV3.Labels{
		"foo":                          "bar",
		"ecs-local.secret.DB_PASSWORD": "arn:aws:secretsmanager:us-west-2:01234567:secret:mySecretSecret",
		"ecs-local.secret.API_KEY":     "arn:aws:ssm:us-west-2:01234567:parameter/mySecretParameter",
	}
	expectedLogging := &composeV3.LoggingConfig{
		Driver: "awslogs",
		Options: map[string]string{
			"awslogs-group":         "/ecs/fargate-task-definition",
			"awslogs-region":        "us-east-1",
			"awslogs-stream-prefix": "ecs",
		},
	}
	expectedVolumes := []composeV3.ServiceVolumeConfig{
		{
			Target:   "/tmp/cache",
			Source:   "",
			ReadOnly: true,
			Type:     "bind",
		},
	}
	expectedIpc := ecs.IpcModeNone
	expectedPid := ecs.PidModeHost
	expectedNetworks := map[string]*composeV3.ServiceNetworkConfig{
		network.EcsLocalNetworkName: nil,
	}
	expectedPorts := []composeV3.ServicePortConfig{
		{
			Target:    uint32(3000),
			Published: uint32(80),
			Protocol:  "tcp",
		},
	}
	expectedSysctls := []string{
		"net.core.somaxconn=1024",
		"net.ipv4.tcp_syncookies=0",
	}

	taskDefinition := &ecs.TaskDefinition{
		ContainerDefinitions: []*ecs.ContainerDefinition{
			{
				Image:                  aws.String(expectedImage),
				Name:                   aws.String(expectedName),
				Command:                aws.StringSlice(expectedCommand),
				EntryPoint:             aws.StringSlice(expectedEntrypoint),
				WorkingDirectory:       aws.String(expectedWorkingDir),
				Hostname:               aws.String(expectedHostname),
				Links:                  aws.StringSlice(expectedLinks),
				DnsServers:             aws.StringSlice(expectedDNS),
				DnsSearchDomains:       aws.StringSlice(expectedDNSSearch),
				User:                   aws.String(expectedUser),
				DockerSecurityOptions:  aws.StringSlice(expectedSecurityOpt),
				PseudoTerminal:         aws.Bool(expectedTty),
				Privileged:             aws.Bool(expectedPrivileged),
				Interactive:            aws.Bool(expectedStdinOpen),
				ReadonlyRootFilesystem: aws.Bool(expectedReadOnly),
				Ulimits: []*ecs.Ulimit{
					{
						Name:      aws.String("nofile"),
						SoftLimit: aws.Int64(2000),
						HardLimit: aws.Int64(4000),
					},
				},
				Environment: []*ecs.KeyValuePair{
					{
						Name:  aws.String("rails_env"),
						Value: aws.String("development"),
					},
				},
				Secrets: []*ecs.Secret{
					{
						Name:      aws.String("DB_PASSWORD"),
						ValueFrom: aws.String("arn:aws:secretsmanager:us-west-2:01234567:secret:mySecretSecret"),
					},
					{
						Name:      aws.String("API_KEY"),
						ValueFrom: aws.String("arn:aws:ssm:us-west-2:01234567:parameter/mySecretParameter"),
					},
				},
				ExtraHosts: []*ecs.HostEntry{
					{
						Hostname:  aws.String("somehost"),
						IpAddress: aws.String("162.242.195.82"),
					},
					{
						Hostname:  aws.String("otherhost"),
						IpAddress: aws.String("50.31.209.229"),
					},
				},
				HealthCheck: &ecs.HealthCheck{
					Command: aws.StringSlice([]string{"CMD-SHELL", "echo hello"}),
				},
				DockerLabels: map[string]*string{"foo": aws.String("bar")},
				LogConfiguration: &ecs.LogConfiguration{
					LogDriver: aws.String("awslogs"),
					Options: map[string]*string{
						"awslogs-group":         aws.String("/ecs/fargate-task-definition"),
						"awslogs-region":        aws.String("us-east-1"),
						"awslogs-stream-prefix": aws.String("ecs"),
					},
				},
				MountPoints: []*ecs.MountPoint{
					{
						ContainerPath: aws.String("/tmp/cache"),
						ReadOnly:      aws.Bool(true),
						SourceVolume:  aws.String("volume-1"),
					},
				},
				PortMappings: []*ecs.PortMapping{
					{
						ContainerPort: aws.Int64(3000),
						HostPort:      aws.Int64(80),
						Protocol:      aws.String("tcp"),
					},
				},
				SystemControls: []*ecs.SystemControl{
					{
						Namespace: aws.String("net.core.somaxconn"),
						Value:     aws.String("1024"),
					},
					{
						Namespace: aws.String("net.ipv4.tcp_syncookies"),
						Value:     aws.String("0"),
					},
				},
				LinuxParameters: &ecs.LinuxParameters{
					InitProcessEnabled: aws.Bool(true),
					SharedMemorySize:   aws.Int64(128),
					Capabilities: &ecs.KernelCapabilities{
						Add:  aws.StringSlice(expectedCapAdd),
						Drop: aws.StringSlice(expectedCapDrop),
					},
					Devices: []*ecs.Device{
						{
							HostPath:      aws.String("/dev/sda"),
							ContainerPath: aws.String("/dev/xvdc"),
							Permissions:   aws.StringSlice([]string{"read"}),
						},
					},
					Tmpfs: []*ecs.Tmpfs{
						{
							ContainerPath: aws.String("/run"),
							MountOptions:  aws.StringSlice([]string{"rw", "noexec", "nosuid"}),
							Size:          aws.Int64(64),
						},
					},
				},
			},
		},
	}

	containerDef := taskDefinition.ContainerDefinitions[0]

	commonValues := &CommonContainerValues{
		Ipc:     expectedIpc,
		Pid:     expectedPid,
		Creds:   endpointsTempCredsPath,
	}

	// WHEN
	service, err := convertToComposeService(containerDef, commonValues)

	// THEN
	assert.NoError(t, err, "Unexpected error converting Container Definition")
	assert.Equal(t, expectedName, service.Name, "Expected Name to match")
	assert.Equal(t, expectedImage, service.Image, "Expected Image to match")
	assert.Equal(t, composeV3.ShellCommand(expectedCommand), service.Command, "Expected Command to match")
	assert.Equal(t, composeV3.ShellCommand(expectedEntrypoint), service.Entrypoint, "Expected Entry point to match")
	assert.Equal(t, expectedWorkingDir, service.WorkingDir, "Expected WorkingDir to match")
	assert.Equal(t, expectedHostname, service.Hostname, "Expected Hostname to match")
	assert.Equal(t, expectedLinks, service.Links, "Expected Links to match")
	assert.Equal(t, composeV3.StringList(expectedDNS), service.DNS, "Expected DNS to match")
	assert.Equal(t, composeV3.StringList(expectedDNSSearch), service.DNSSearch, "Expected DNSSearch to match")
	assert.Equal(t, expectedUser, service.User, "Expected User to match")
	assert.Equal(t, expectedSecurityOpt, service.SecurityOpt, "Expected SecurityOpt to match")
	assert.Equal(t, expectedTty, service.Tty, "Expected Tty to match")
	assert.Equal(t, expectedPrivileged, service.Privileged, "Expected Privileged to match")
	assert.Equal(t, expectedStdinOpen, service.StdinOpen, "Expected StdinOpen to match")
	assert.Equal(t, expectedReadOnly, service.ReadOnly, "Expected ReadOnly to match")
	assert.Equal(t, expectedUlimits, service.Ulimits, "Expected Ulimits to match")
	assert.Equal(t, composeV3.MappingWithEquals(expectedEnvironment), service.Environment, "Expected Environment to match")
	assert.Equal(t, composeV3.HostsList(expectedExtraHosts), service.ExtraHosts, "Expected ExtraHosts to match")
	assert.Equal(t, expectedHealthCheck, service.HealthCheck, "Expected HealthCheck to match")
	assert.Equal(t, expectedLabels, service.Labels, "Expected Labels to match")
	assert.Equal(t, expectedLogging, service.Logging, "Expected Logging to match")
	assert.Equal(t, expectedVolumes, service.Volumes, "Expected Volumes to match")
	assert.Equal(t, expectedNetworks, service.Networks, "Expected Networks to match")
	assert.Equal(t, expectedPid, service.Pid, "Expected Pid to match")
	assert.Equal(t, expectedIpc, service.Ipc, "Expected Ipc to match")
	assert.Equal(t, expectedPorts, service.Ports, "Expected Ports to match")
	assert.Equal(t, composeV3.StringList(expectedSysctls), service.Sysctls, "Expected Sysctls to match")
	assert.Empty(t, service.NetworkMode, "Expected NetworkMode to be empty")

	// Fields from LinuxParameters
	assert.Equal(t, composeV3.StringList(expectedTmpfs), service.Tmpfs, "Expected Tmpfs to match")
	assert.Equal(t, aws.Bool(expectedInit), service.Init, "Expected Init to match")
	assert.Equal(t, expectedDevices, service.Devices, "Expected Devices to match")
	assert.Equal(t, expectedShmSize, service.ShmSize, "Expected ShmSize to match")
	assert.Equal(t, expectedCapAdd, service.CapAdd, "Expected CapAdd to match")
	assert.Equal(t, expectedCapDrop, service.CapDrop, "Expected CapDrop to match")
}

func TestCreateComposeService_SetsLabels(t *testing.T) {
	// GIVEN
	taskDefinition := &ecs.TaskDefinition{
		ContainerDefinitions: []*ecs.ContainerDefinition{
			{
				Image: aws.String("myApp"),
			},
		},
	}

	expectedInputType := "remote"
	expectedValue := "arn:aws:ecs:us-west-2:123412341234:task-definition/myTaskDef:1"
	expectedMetadata := &LocalCreateMetadata{
		InputType: expectedInputType,
		Value:     expectedValue,
	}

	// WHEN
	services, err := createComposeServices(taskDefinition, expectedMetadata)
	service := services[0]

	// THEN
	assert.NoError(t, err, "Unexpected error creating Compose services")
	assert.Equal(t, expectedInputType, service.Labels[TaskDefinitionLabelType], "Expected Metadata Type label to match")
	assert.Equal(t, expectedValue, service.Labels[TaskDefinitionLabelValue], "Expected Metadata Value label to match")
}

func TestConvertToTmpfs(t *testing.T) {
	expectedTmpfs := []string{
		"/run:size=64MiB,rw,noexec,nosuid",
		"/foo:size=1GiB",
	}

	input := []*ecs.Tmpfs{
		{
			ContainerPath: aws.String("/run"),
			MountOptions:  aws.StringSlice([]string{"rw", "noexec", "nosuid"}),
			Size:          aws.Int64(64),
		},
		{
			ContainerPath: aws.String("/foo"),
			Size:          aws.Int64(1024),
		},
	}

	actual, err := convertToTmpfs(input)
	assert.NoError(t, err, "Unexpected error converting Tmpfs")
	assert.ElementsMatch(t, expectedTmpfs, actual)
}

func TestConvertToTmpfs_ErrorsIfNoSize(t *testing.T) {
	input := []*ecs.Tmpfs{
		{
			ContainerPath: aws.String("/run"),
			MountOptions:  aws.StringSlice([]string{"rw", "noexec", "nosuid"}),
		},
	}

	_, err := convertToTmpfs(input)
	assert.Error(t, err)
}

func TestConvertToTmpfs_ErrorsIfNoPath(t *testing.T) {
	input := []*ecs.Tmpfs{
		{
			MountOptions: aws.StringSlice([]string{"rw", "noexec", "nosuid"}),
			Size:         aws.Int64(1024),
		},
	}

	_, err := convertToTmpfs(input)
	assert.Error(t, err)
}

func TestConvertUlimits(t *testing.T) {
	expected := map[string]*composeV3.UlimitsConfig{
		"nofile": &composeV3.UlimitsConfig{
			Soft: 2000,
			Hard: 4000,
		},
		// Ignoring "Single" field - hack
		"rss": &composeV3.UlimitsConfig{
			Soft: 65535,
			Hard: 65535,
		},
	}

	input := []*ecs.Ulimit{
		{
			Name:      aws.String("nofile"),
			HardLimit: aws.Int64(4000),
			SoftLimit: aws.Int64(2000),
		},
		{
			Name:      aws.String("rss"),
			HardLimit: aws.Int64(65535),
			SoftLimit: aws.Int64(65535),
		},
	}

	actual, err := convertUlimits(input)

	assert.NoError(t, err, "Unexpected error converting Ulimits")
	assert.Equal(t, expected["rss"], actual["rss"])
	assert.Equal(t, expected["nofile"], actual["nofile"])
}

func TestConvertDevices(t *testing.T) {
	expected := []string{
		"/dev/sda",
		"/dev/sda:/dev/xvdc",
		"/dev/sda:/dev/xvdc:r",
		"/dev/nvid:/dev/xvdc:rw",
	}

	input := []*ecs.Device{
		{
			HostPath: aws.String("/dev/sda"),
		},
		{
			HostPath:      aws.String("/dev/sda"),
			ContainerPath: aws.String("/dev/xvdc"),
		},
		{
			HostPath:      aws.String("/dev/sda"),
			ContainerPath: aws.String("/dev/xvdc"),
			Permissions:   aws.StringSlice([]string{"read"}),
		},
		{
			HostPath:      aws.String("/dev/nvid"),
			ContainerPath: aws.String("/dev/xvdc"),
			Permissions:   aws.StringSlice([]string{"read", "write"}),
		},
	}

	actual, err := convertDevices(input)

	assert.NoError(t, err, "Unexpected error converting Devices")
	assert.ElementsMatch(t, expected, actual)
}

func TestConvertShmSize(t *testing.T) {
	input := aws.Int64(1024)
	expected := "1GiB"
	actual := convertShmSize(input)

	assert.Equal(t, expected, actual)
}

func TestConvertShmSize_Nil(t *testing.T) {
	expected := ""
	actual := convertShmSize(nil)

	assert.Equal(t, expected, actual)
}

func TestConvertCapAddCapDrop(t *testing.T) {
	addCapabilities := []string{"NET_ADMIN", "MKNOD"}
	dropCapabilities := []string{"KILL"}

	input := &ecs.KernelCapabilities{
		Add:  aws.StringSlice(addCapabilities),
		Drop: aws.StringSlice(dropCapabilities),
	}
	actualCapAdd := convertCapAdd(input)
	actualCapDrop := convertCapDrop(input)

	assert.ElementsMatch(t, addCapabilities, actualCapAdd)
	assert.ElementsMatch(t, dropCapabilities, actualCapDrop)
}

func TestConvertExtraHosts(t *testing.T) {
	input := []*ecs.HostEntry{
		{
			Hostname:  aws.String("somehost"),
			IpAddress: aws.String("162.242.195.82"),
		},
		{
			Hostname:  aws.String("otherhost"),
			IpAddress: aws.String("50.31.209.229"),
		},
	}

	expected := []string{"somehost:162.242.195.82", "otherhost:50.31.209.229"}
	actual := convertExtraHosts(input)

	assert.Equal(t, expected, actual)
}

func TestConvertHealthCheck(t *testing.T) {
	command := []string{"CMD", "curl", "-f", "http://localhost"}
	input := &ecs.HealthCheck{
		Command:     aws.StringSlice(command),
		Retries:     aws.Int64(3),
		Interval:    aws.Int64(90),
		Timeout:     aws.Int64(10),
		StartPeriod: aws.Int64(40),
	}

	interval := time.Duration(90) * time.Second
	timeout := time.Duration(10) * time.Second
	startPeriod := time.Duration(40) * time.Second
	retries := uint64(3)

	expected := &composeV3.HealthCheckConfig{
		Test:        command,
		Retries:     &retries,
		Interval:    &interval,
		Timeout:     &timeout,
		StartPeriod: &startPeriod,
	}
	actual := convertHealthCheck(input)

	assert.Equal(t, expected, actual)
}

func TestConvertLogging(t *testing.T) {
	input := &ecs.LogConfiguration{
		LogDriver: aws.String("awslogs"),
		Options: map[string]*string{
			"awslogs-group":         aws.String("/ecs/fargate-task-definition"),
			"awslogs-region":        aws.String("us-east-1"),
			"awslogs-stream-prefix": aws.String("ecs"),
		},
	}

	expected := &composeV3.LoggingConfig{
		Driver: "awslogs",
		Options: map[string]string{
			"awslogs-group":         "/ecs/fargate-task-definition",
			"awslogs-region":        "us-east-1",
			"awslogs-stream-prefix": "ecs",
		},
	}

	actual := convertLogging(input)

	assert.Equal(t, expected, actual)
}

func TestConvertToVolumes(t *testing.T) {
	inputMountPoints := []*ecs.MountPoint{
		{
			ContainerPath: aws.String("/tmp/cache"),
			ReadOnly:      aws.Bool(true),
			SourceVolume:  aws.String("volume-1"),
		},
		{
			ContainerPath: aws.String("/tmp/cache2"),
			ReadOnly:      aws.Bool(false),
			SourceVolume:  aws.String("volume-2"),
		},
		{
			ContainerPath: aws.String("/tmp/cache3"),
			ReadOnly:      aws.Bool(false),
			SourceVolume:  aws.String("covfefe"),
		},
	}
	inputVolumes := []*ecs.Volume{
		{
			Host: &ecs.HostVolumeProperties{},
			Name: aws.String("volume-1"),
		},
		{
			Host: &ecs.HostVolumeProperties{
				SourcePath: aws.String("/tmp/foo"),
			},
			Name: aws.String("volume-2"),
		},
		{
			Host: &ecs.HostVolumeProperties{
				SourcePath: aws.String("/tmp/bar"),
			},
			Name: aws.String("covfefe"),
		},
	}

	expected := []composeV3.ServiceVolumeConfig{
		{
			Target:   "/tmp/cache",
			ReadOnly: true,
			Source:   "",
			Type:     "bind",
		},
		{
			Target:   "/tmp/cache2",
			ReadOnly: false,
			Source:   "/tmp/foo",
			Type:     "bind",
		},
		{
			Target:   "/tmp/cache3",
			ReadOnly: false,
			Source:   "/tmp/bar",
			Type:     "bind",
		},
	}

	actual := convertToVolumes(inputMountPoints, inputVolumes)

	assert.ElementsMatch(t, expected, actual)
}

func TestConvertToPorts(t *testing.T) {
	input := []*ecs.PortMapping{
		{
			ContainerPort: aws.Int64(3000),
			Protocol:      aws.String("tcp"),
			HostPort:      aws.Int64(80),
		},
	}

	expected := []composeV3.ServicePortConfig{
		{
			Target:    uint32(3000),
			Published: uint32(80),
			Protocol:  "tcp",
		},
	}

	actual := convertToPorts(input)

	assert.Equal(t, expected, actual)
}

func TestConvertToSysctls(t *testing.T) {
	input := []*ecs.SystemControl{
		{
			Namespace: aws.String("net.core.somaxconn"),
			Value:     aws.String("1024"),
		},
		{
			Namespace: aws.String("net.ipv4.tcp_syncookies"),
			Value:     aws.String("0"),
		},
	}

	expected := []string{
		"net.core.somaxconn=1024",
		"net.ipv4.tcp_syncookies=0",
	}

	actual := convertToSysctls(input)

	assert.Equal(t, expected, actual)
}

func TestNamedVolumesMap(t *testing.T) {
	input := []*ecs.Volume{
		{
			Host: &ecs.HostVolumeProperties{
				SourcePath: aws.String("/tmp/bar"),
			},
			Name: aws.String("volume-0"),
		},
		{
			Host: &ecs.HostVolumeProperties{
				SourcePath: aws.String("/tmp/foo"),
			},
			Name: aws.String("volume-1"),
		},
		{
			Host: &ecs.HostVolumeProperties{},
			Name: aws.String("volume-2"),
		},
	}

	expected := map[string]string{
		"volume-0": "/tmp/bar",
		"volume-1": "/tmp/foo",
		"volume-2": "",
	}

	actual := namedVolumesMap(input)

	assert.Equal(t, expected, actual)
}

func TestResolveCredentials(t *testing.T) {
    taskRoleARN := "arn:aws:iam:us-west-2:01234567:role/myTaskRole"

    expected := "/role/myTaskRole"
    actual := resolveCredentials(taskRoleARN, true)

    assert.Equal(t, expected, actual)

    expected = endpointsTempCredsPath
    actual = resolveCredentials(taskRoleARN, false)

    assert.Equal(t, expected, actual)
}
