// 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 project

import (
	"flag"
	"io/ioutil"
	"os"
	"path/filepath"
	"testing"

	"github.com/aws/amazon-ecs-cli/ecs-cli/modules/cli/compose/context"
	"github.com/aws/amazon-ecs-cli/ecs-cli/modules/cli/compose/entity/service"
	"github.com/aws/amazon-ecs-cli/ecs-cli/modules/commands/flags"
	cliconfig "github.com/aws/amazon-ecs-cli/ecs-cli/modules/config"
	composeutils "github.com/aws/amazon-ecs-cli/ecs-cli/modules/utils/compose"
	"github.com/aws/amazon-ecs-cli/ecs-cli/modules/utils/regcredio"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/docker/libcompose/config"
	"github.com/docker/libcompose/lookup"
	"github.com/stretchr/testify/assert"
	"github.com/urfave/cli"
)

const testProjectName = "test-project"

func TestParseCompose_V2(t *testing.T) {
	// Setup docker-compose file
	composeFileString := `version: '2'
services:
  wordpress:
    image: wordpress
    ports: ["80:80"]
  mysql:
    image: mysql`

	tmpfile, err := ioutil.TempFile("", "test")
	assert.NoError(t, err, "Unexpected error in creating test file")

	defer os.Remove(tmpfile.Name())

	_, err = tmpfile.Write([]byte(composeFileString))
	assert.NoError(t, err, "Unexpected error in writing to test file")

	err = tmpfile.Close()
	assert.NoError(t, err, "Unexpected error closing file")

	// Set up project
	project := setupTestProject(t)
	project.ecsContext.ComposeFiles = append(project.ecsContext.ComposeFiles, tmpfile.Name())

	// verify container configs are populated
	err = project.parseCompose()
	assert.NoError(t, err, "Unexpected error parsing file")
	assert.NotEmpty(t, project.ContainerConfigs(), "Expected container configs to not be empty")

	// verify project name is set
	assert.Equal(t, testProjectName, project.ecsContext.ProjectName, "Expected ProjectName to be overridden.")

	// verify top-level volumes are empty
	assert.Empty(t, project.VolumeConfigs().VolumeWithHost, "Expected volume configs to be empty")
	assert.Empty(t, project.VolumeConfigs().VolumeEmptyHost, "Expected volume configs to be empty")
}

func TestParseCompose_V2_WithVolumeConfigs(t *testing.T) {
	// Setup docker-compose file
	composeFileString := `version: '2'
services:
  wordpress:
    image: wordpress
  mysql:
    image: mysql
    volumes:
      - banana:/tmp/cache
      - :/tmp/cache
      - ./cache:/tmp/cache:ro
volumes:
  banana:`

	tmpfile, err := ioutil.TempFile("", "test")
	assert.NoError(t, err, "Unexpected error in creating test file")

	defer os.Remove(tmpfile.Name())

	_, err = tmpfile.Write([]byte(composeFileString))
	assert.NoError(t, err, "Unexpected error in writing to test file")

	err = tmpfile.Close()
	assert.NoError(t, err, "Unexpected error closing file")

	// Set up project
	project := setupTestProject(t)
	project.ecsContext.ComposeFiles = append(project.ecsContext.ComposeFiles, tmpfile.Name())

	err = project.parseCompose()
	expectedNamedVolumes := []string{"banana", "volume-1"}
	expectedHosts := map[string]string{"./cache": "volume-2"}

	// verify VolumeConfigs are populated
	assert.Equal(t, expectedHosts, project.VolumeConfigs().VolumeWithHost, "Expected volume configs to match")
	assert.Equal(t, expectedNamedVolumes, project.VolumeConfigs().VolumeEmptyHost, "Expected volume configs to match")
}

func TestParseECSParams(t *testing.T) {
	ecsParamsString := `version: 1
task_definition:
  ecs_network_mode: host
  task_role_arn: arn:aws:iam::123456789012:role/my_role
  services:
    mysql:
      essential: false

run_params:
  network_configuration:
    awsvpc_configuration:
      subnets: [subnet-feedface, subnet-deadbeef]
      security_groups:
        - sg-bafff1ed
        - sg-c0ffeefe`

	content := []byte(ecsParamsString)

	tmpfile, err := ioutil.TempFile("", "ecs-params")
	assert.NoError(t, err, "Could not create ecs fields tempfile")

	ecsParamsFileName := tmpfile.Name()
	defer os.Remove(ecsParamsFileName)

	project := setupTestProjectWithEcsParams(t, ecsParamsFileName)

	_, err = tmpfile.Write(content)
	assert.NoError(t, err, "Could not write data to ecs fields tempfile")

	if err := project.parseECSParams(); err != nil {
		t.Fatalf("Unexpected error parsing the ecs-params data [%s]: %v", ecsParamsString, err)
	}

	ecsParams := project.ecsContext.ECSParams
	assert.NotNil(t, ecsParams, "Expected ecsParams to be set on project")
	assert.Equal(t, "1", ecsParams.Version, "Expected Version to match")

	td := ecsParams.TaskDefinition

	assert.Equal(t, "host", td.NetworkMode, "Expected NetworkMode to match")
	assert.Equal(t, "arn:aws:iam::123456789012:role/my_role", td.TaskRoleArn, "Expected TaskRoleArn to match")

	networkConfigs := ecsParams.RunParams.NetworkConfiguration.AwsVpcConfiguration
	assert.Equal(t, []string{"subnet-feedface", "subnet-deadbeef"}, networkConfigs.Subnets, "Expected Subnets to match")
	assert.Equal(t, []string{"sg-bafff1ed", "sg-c0ffeefe"}, networkConfigs.SecurityGroups, "Expected SecurityGroups to match")

	err = tmpfile.Close()
	assert.NoError(t, err, "Could not close tempfile")
}
func TestParseECSParamsWithEnvironment(t *testing.T) {
	ecsParamsString := `version: 1
task_definition:
  task_size:
    mem_limit: ${MEM_LIMIT}
    cpu_limit: $CPU_LIMIT`

	os.Setenv("MEM_LIMIT", "1000")
	os.Setenv("CPU_LIMIT", "200")

	content := []byte(ecsParamsString)

	tmpfile, err := ioutil.TempFile("", "ecs-params")
	assert.NoError(t, err, "Could not create ecs fields tempfile")

	ecsParamsFileName := tmpfile.Name()
	defer os.Remove(ecsParamsFileName)

	project := setupTestProjectWithEcsParams(t, ecsParamsFileName)

	_, err = tmpfile.Write(content)
	assert.NoError(t, err, "Could not write data to ecs fields tempfile")

	err = project.parseECSParams()
	if assert.NoError(t, err) {
		ecsParams := project.ecsContext.ECSParams
		ts := ecsParams.TaskDefinition.TaskSize
		assert.Equal(t, "200", ts.Cpu, "Expected CPU to match")
		assert.Equal(t, "1000", ts.Memory, "Expected CPU to match")
	}

	err = tmpfile.Close()
	assert.NoError(t, err, "Could not close tempfile")
}

func TestParseECSParams_NoFile(t *testing.T) {
	project := setupTestProject(t)
	err := project.parseECSParams()
	if assert.NoError(t, err) {
		assert.Nil(t, project.ecsContext.ECSParams)
	}
}

func TestParseECSParams_WithFargateParams(t *testing.T) {
	ecsParamsString := `version: 1
task_definition:
  ecs_network_mode: awsvpc
  task_execution_role: arn:aws:iam::123456789012:role/fargate_role
  task_size:
    mem_limit: 1000
    cpu_limit: 200

run_params:
  network_configuration:
    awsvpc_configuration:
      subnets: [subnet-feedface, subnet-deadbeef]
      security_groups:
        - sg-bafff1ed
        - sg-c0ffeefe
      assign_public_ip: ENABLED`

	content := []byte(ecsParamsString)

	tmpfile, err := ioutil.TempFile("", "ecs-params")
	assert.NoError(t, err, "Could not create ecs fields tempfile")

	ecsParamsFileName := tmpfile.Name()
	defer os.Remove(ecsParamsFileName)

	project := setupTestProjectWithEcsParams(t, ecsParamsFileName)

	_, err = tmpfile.Write(content)
	assert.NoError(t, err, "Could not write data to ecs fields tempfile")

	err = project.parseECSParams()
	if assert.NoError(t, err) {
		ecsParams := project.ecsContext.ECSParams
		assert.NotNil(t, ecsParams, "Expected ecsParams to be set on project")
		assert.Equal(t, "1", ecsParams.Version, "Expected Version to match")

		td := ecsParams.TaskDefinition
		assert.Equal(t, "awsvpc", td.NetworkMode, "Expected NetworkMode to match")
		assert.Equal(t, "arn:aws:iam::123456789012:role/fargate_role", td.ExecutionRole, "Expected ExecutionRole to match")

		ts := td.TaskSize
		assert.Equal(t, "200", ts.Cpu, "Expected CPU to match")
		assert.Equal(t, "1000", ts.Memory, "Expected CPU to match")

		networkConfig := ecsParams.RunParams.NetworkConfiguration.AwsVpcConfiguration
		assert.Equal(t, []string{"subnet-feedface", "subnet-deadbeef"}, networkConfig.Subnets, "Expected Subnets to match")
		assert.Equal(t, []string{"sg-bafff1ed", "sg-c0ffeefe"}, networkConfig.SecurityGroups, "Expected SecurityGroups to match")
		assert.Equal(t, composeutils.Enabled, networkConfig.AssignPublicIp, "Expected AssignPublicIp to match")

	}

	err = tmpfile.Close()
	assert.NoError(t, err, "Could not close tempfile")
}

func TestThrowErrorForUnsupportedComposeVersion(t *testing.T) {
	unsupportedVersion := "4"
	composeFileString := `version: '` + unsupportedVersion + `'
services:
  wordpress:
    image: wordpress
    ports: ["80:80"]
    mem_reservation: 500000000
  mysql:
    image: mysql`

	// set up compose file
	tmpfile, err := ioutil.TempFile("", "test")
	if err != nil {
		t.Fatal("Unexpected error in creating test file", err)
	}
	defer os.Remove(tmpfile.Name())
	if _, err := tmpfile.Write([]byte(composeFileString)); err != nil {
		t.Fatal("Unexpected error writing to test file: ", err)
	}
	if err := tmpfile.Close(); err != nil {
		t.Fatal("Unexpected error closing test file: ", err)
	}

	// set up project and parse
	project := setupTestProject(t)
	project.ecsContext.ComposeFiles = append(project.ecsContext.ComposeFiles, tmpfile.Name())
	observedError := project.parseCompose()

	expectedError := "Unsupported Docker Compose version found: " + unsupportedVersion

	if assert.Error(t, observedError) {
		assert.Equal(t, expectedError, observedError.Error())
	}
}

func setupTestProject(t *testing.T) *ecsProject {
	return setupTestProjectWithEcsParams(t, "")
}

func setupTestProjectWithEcsParams(t *testing.T, ecsParamsFileName string) *ecsProject {
	return setupTestProjectWithECSRegistryCreds(t, ecsParamsFileName, "")
}

// TODO: refactor into all-purpose 'setupTestProject' func
func setupTestProjectWithECSRegistryCreds(t *testing.T, ecsParamsFileName, credFileName string) *ecsProject {
	envLookup, err := mockGetDefaultEnvironment()
	assert.NoError(t, err, "Unexpected error setting up environment lookup")

	resourceLookup, err := composeutils.GetDefaultResourceLookup()
	assert.NoError(t, err, "Unexpected error setting up resource lookup")

	flagSet := flag.NewFlagSet("ecs-cli", 0)
	flagSet.String(flags.ProjectNameFlag, testProjectName, "")
	flagSet.String(flags.ECSParamsFileNameFlag, ecsParamsFileName, "")
	flagSet.String(flags.RegistryCredsFileNameFlag, credFileName, "")

	parentContext := cli.NewContext(nil, flagSet, nil)
	cliContext := cli.NewContext(nil, nil, parentContext)

	ecsContext := &context.ECSContext{
		CLIContext: cliContext,
	}
	ecsContext.EnvironmentLookup = envLookup
	ecsContext.ResourceLookup = resourceLookup

	return &ecsProject{
		ecsContext: ecsContext,
	}
}

// This function mimics the implementation inside `project.NewProject()` for .env file lookup.
// The following function is only used for unit tests.
func mockGetDefaultEnvironment() (*lookup.ComposableEnvLookup, error) {
	cwd, err := os.Getwd()
	if err != nil {
		return nil, err
	}
	return &lookup.ComposableEnvLookup{
		Lookups: []config.EnvironmentLookup{
			&lookup.EnvfileLookup{
				Path: filepath.Join(cwd, ".env"),
			},
			&lookup.OsEnvLookup{},
		},
	}, nil
}

func TestParseECSRegistryCreds(t *testing.T) {
	credsInputString := `version: "1"
registry_credential_outputs:
  task_execution_role: someTestRole
  container_credentials:
    my.example.registry.net:
      credentials_parameter: arn:aws:secretsmanager::secret:amazon-ecs-cli-setup-my.example.registry.net
      container_names:
      - web
    another.example.io:
      credentials_parameter: arn:aws:secretsmanager::secret:amazon-ecs-cli-setup-another.example.io
      kms_key_id: arn:aws:kms::key/some-key-57yrt
      container_names:
      - test`

	content := []byte(credsInputString)

	tmpfile, err := ioutil.TempFile("", regcredio.ECSCredFileBaseName)
	assert.NoError(t, err, "Could not create ecs registry creds tempfile")

	credFileName := tmpfile.Name()
	defer os.Remove(credFileName)

	project := setupTestProjectWithECSRegistryCreds(t, "", credFileName)

	_, err = tmpfile.Write(content)
	assert.NoError(t, err, "Could not write data to ecs registry creds tempfile")

	if err := project.parseECSRegistryCreds(); err != nil {
		t.Fatalf("Unexpected error parsing the "+regcredio.ECSCredFileBaseName+" data [%s]: %v", credsInputString, err)
	}

	ecsRegCreds := project.ecsRegistryCreds
	assert.NotNil(t, ecsRegCreds, "Expected "+regcredio.ECSCredFileBaseName+" to be set on project")
	assert.Equal(t, "1", ecsRegCreds.Version, "Expected Version to match")

	credResources := ecsRegCreds.CredentialResources
	assert.NotNil(t, credResources, "Expected credential resources to be non-nil")
	assert.Equal(t, "someTestRole", credResources.TaskExecutionRole)
	assert.NotNil(t, credResources.ContainerCredentials, "Expected ContainerCredentials to be non-nil")

	firstOutputEntry := credResources.ContainerCredentials["my.example.registry.net"]
	assert.NotEmpty(t, firstOutputEntry)
	assert.Equal(t, "arn:aws:secretsmanager::secret:amazon-ecs-cli-setup-my.example.registry.net", firstOutputEntry.CredentialARN)
	assert.Equal(t, "", firstOutputEntry.KMSKeyID)
	assert.ElementsMatch(t, []string{"web"}, firstOutputEntry.ContainerNames)

	secondOutputEntry := credResources.ContainerCredentials["another.example.io"]
	assert.NotEmpty(t, secondOutputEntry)
	assert.Equal(t, "arn:aws:secretsmanager::secret:amazon-ecs-cli-setup-another.example.io", secondOutputEntry.CredentialARN)
	assert.Equal(t, "arn:aws:kms::key/some-key-57yrt", secondOutputEntry.KMSKeyID)
	assert.ElementsMatch(t, []string{"test"}, secondOutputEntry.ContainerNames)
}

func TestParseECSRegistryCreds_NoFile(t *testing.T) {
	project := setupTestProject(t)
	err := project.parseECSRegistryCreds()
	if assert.NoError(t, err) {
		assert.Nil(t, project.ecsRegistryCreds)
	}
}

func TestParseCompose_V2_WithTaskDefinitionPlacementConstraint(t *testing.T) {
	// Setup docker-compose file
	composeFileString := `version: '2'
services:
  mysql:
    image: mysql`

	tmpfile, err := ioutil.TempFile("", "test")
	assert.NoError(t, err, "Unexpected error in creating test file")

	defer os.Remove(tmpfile.Name())

	_, err = tmpfile.Write([]byte(composeFileString))
	assert.NoError(t, err, "Unexpected error in writing to test file")

	err = tmpfile.Close()
	assert.NoError(t, err, "Unexpected error closing file")

	ecsParamsString := `version: 1
task_definition:
  placement_constraints:
    - type: memberOf
      expression: "attribute:name == value"`

	content := []byte(ecsParamsString)

	ecsParamsTmpfile, err := ioutil.TempFile("", "ecs-params")
	assert.NoError(t, err, "Could not create ecs fields tempfile")

	ecsParamsFileName := ecsParamsTmpfile.Name()
	defer os.Remove(ecsParamsFileName)

	_, err = ecsParamsTmpfile.Write(content)
	assert.NoError(t, err, "Could not write data to ecs fields tempfile")

	project := setupTestProjectWithEcsParams(t, ecsParamsFileName)
	project.ecsContext.CommandConfig = &cliconfig.CommandConfig{}
	project.ecsContext.ComposeFiles = append(project.ecsContext.ComposeFiles, tmpfile.Name())

	err = project.parseCompose()
	assert.NoError(t, err, "Could not parse compose")

	err = project.parseECSParams()
	assert.NoError(t, err, "Could not parse ecs params")

	project.entity = service.NewService(project.ecsContext)
	err = project.transformTaskDefinition()
	assert.NoError(t, err, "Could not transform task definition")

	placementConstraints := project.entity.TaskDefinition().PlacementConstraints
	assert.NotEmpty(t, placementConstraints, "Expected PlacementConstraints to not be empty")
	assert.Equal(t, "memberOf", aws.StringValue(placementConstraints[0].Type), "Expected Type to match")
	assert.Equal(t, "attribute:name == value", aws.StringValue(placementConstraints[0].Expression), "Expected Expression to match")
}
