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

import (
	"errors"
	"testing"

	mock_ec2iface "github.com/aws/amazon-ecs-cli/ecs-cli/modules/clients/aws/ec2/mock/sdk"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/service/ec2"
	"github.com/golang/mock/gomock"
	"github.com/stretchr/testify/assert"
)

func TestDescribeInstances(t *testing.T) {
	mockEC2, client := setupTest(t)

	// 2 ids in the input list
	expectedIds := []*string{aws.String("id1"), aws.String("id2")}

	instance1 := &ec2.Instance{InstanceId: expectedIds[0]}
	instance2 := &ec2.Instance{InstanceId: expectedIds[1]}
	instance3 := &ec2.Instance{InstanceId: aws.String("id3")}
	reservation := &ec2.Reservation{
		Instances: []*ec2.Instance{instance1, instance2, instance3},
	}
	result := &ec2.DescribeInstancesOutput{
		Reservations: []*ec2.Reservation{reservation},
	}

	mockEC2.EXPECT().DescribeInstances(gomock.Any()).Do(func(input interface{}) {
		observedIds := input.(*ec2.DescribeInstancesInput)
		assert.Equal(t, len(expectedIds), len(observedIds.InstanceIds), "Expected request to have ids set")

		for idx := range expectedIds {
			assert.Equal(t, aws.StringValue(expectedIds[idx]), aws.StringValue(observedIds.InstanceIds[idx]), "Expected request instance ids to match")
		}
	}).Return(result, nil)

	output, err := client.DescribeInstances(expectedIds)
	assert.NoError(t, err, "Expected no error while Describing EC2 Instances")
	assert.NotEmpty(t, output, "Expected output to be of length")

	for _, id := range expectedIds {
		assert.NotNil(t, output[aws.StringValue(id)], "Expected output to have an instance")
	}
}

func TestDescribeInstancesWithEmptyList(t *testing.T) {
	_, client := setupTest(t)

	// empty list of input ids
	output, err := client.DescribeInstances([]*string{})
	assert.NoError(t, err, "Expected no error for empty input")
	assert.Empty(t, output, "Expected empty output map for empty input list")

}

func TestDescribeInstancesErrorCase(t *testing.T) {
	mockEC2, client := setupTest(t)

	expectedIds := []*string{aws.String("id1"), aws.String("id2")}

	mockEC2.EXPECT().DescribeInstances(gomock.Any()).Return(nil, errors.New("something failed"))

	_, err := client.DescribeInstances(expectedIds)

	assert.Error(t, err, "Expected error while Describing EC2 Instances")
}

func TestDescribeInstancesErrorCaseWithEmptyOutput(t *testing.T) {
	mockEC2, client := setupTest(t)

	expectedIds := []*string{aws.String("id1"), aws.String("id2")}

	// Describe returned nil reservations in the response
	mockEC2.EXPECT().DescribeInstances(gomock.Any()).Return(&ec2.DescribeInstancesOutput{}, nil)

	_, err := client.DescribeInstances(expectedIds)

	assert.Error(t, err, "Expected error for nil reservations")
}

func TestDescribeInstancesErrorCaseWithEmptyReservation(t *testing.T) {
	mockEC2, client := setupTest(t)

	expectedIds := []*string{aws.String("id1"), aws.String("id2")}

	// Describe returned empty reservations in the response
	mockEC2.EXPECT().DescribeInstances(gomock.Any()).Return(
		&ec2.DescribeInstancesOutput{Reservations: []*ec2.Reservation{}}, nil)

	_, err := client.DescribeInstances(expectedIds)

	assert.Error(t, err, "Expected error for empty reservations")
}

func TestDescribeInstanceTypeOfferings(t *testing.T) {
	mockEC2, client := setupTest(t)

	region := "us-west-2"
	result := &ec2.DescribeInstanceTypeOfferingsOutput{
		InstanceTypeOfferings: []*ec2.InstanceTypeOffering{
			&ec2.InstanceTypeOffering{
				InstanceType: aws.String("t1.micro"),
			},
		},
	}

	mockEC2.EXPECT().DescribeInstanceTypeOfferings(gomock.Any()).Do(func(input interface{}) {
		instanceTypesInput := input.(*ec2.DescribeInstanceTypeOfferingsInput)
		assert.Equal(t, "region", aws.StringValue(instanceTypesInput.LocationType), "Expected request to have LocationType set")
		assert.NotEmpty(t, instanceTypesInput.Filters)
		assert.Equal(t, aws.StringValue(instanceTypesInput.Filters[0].Name), "location")
		assert.Equal(t, aws.StringValue(instanceTypesInput.Filters[0].Values[0]), "us-west-2")
	}).Return(result, nil)

	outputs, err := client.DescribeInstanceTypeOfferings(region)
	assert.NoError(t, err, "Expected no error while Describing EC2 Instance types")
	assert.NotEmpty(t, outputs, "Expected output to be of length")
}

func TestDescribeInstanceTypeOfferingsWithError(t *testing.T) {
	mockEC2, client := setupTest(t)

	region := "us-west-2"

	mockEC2.EXPECT().DescribeInstanceTypeOfferings(gomock.Any()).Do(func(input interface{}) {
		instanceTypesInput := input.(*ec2.DescribeInstanceTypeOfferingsInput)
		assert.Equal(t, "region", aws.StringValue(instanceTypesInput.LocationType), "Expected request to have LocationType set")
		assert.NotEmpty(t, instanceTypesInput.Filters)
		assert.Equal(t, aws.StringValue(instanceTypesInput.Filters[0].Name), "location")
		assert.Equal(t, aws.StringValue(instanceTypesInput.Filters[0].Values[0]), "us-west-2")
	}).Return(nil, errors.New("some error"))

	_, err := client.DescribeInstanceTypeOfferings(region)
	assert.Error(t, err, "Expected error while no region found")
}

func TestDescribeInstanceTypeOfferingsWithEmptyResult(t *testing.T) {
	mockEC2, client := setupTest(t)

	region := "us-west-2"
	result := &ec2.DescribeInstanceTypeOfferingsOutput{
		InstanceTypeOfferings: []*ec2.InstanceTypeOffering{},
	}

	mockEC2.EXPECT().DescribeInstanceTypeOfferings(gomock.Any()).Do(func(input interface{}) {
		instanceTypesInput := input.(*ec2.DescribeInstanceTypeOfferingsInput)
		assert.Equal(t, "region", aws.StringValue(instanceTypesInput.LocationType), "Expected request to have LocationType set")
		assert.NotEmpty(t, instanceTypesInput.Filters)
		assert.Equal(t, aws.StringValue(instanceTypesInput.Filters[0].Name), "location")
		assert.Equal(t, aws.StringValue(instanceTypesInput.Filters[0].Values[0]), "us-west-2")
	}).Return(result, nil)

	_, err := client.DescribeInstanceTypeOfferings(region)
	assert.Error(t, err, "Expected error while no region found")
}

func setupTest(t *testing.T) (*mock_ec2iface.MockEC2API, EC2Client) {
	ctrl := gomock.NewController(t)
	// TODO will having defer within scope of this function call the
	// expectations on the mocks correctly? Might need to move out of this
	// function (see setup function in other client tests)
	defer ctrl.Finish()
	mockEC2 := mock_ec2iface.NewMockEC2API(ctrl)

	client := newClient(mockEC2)

	return mockEC2, client
}
