package awsutil

import (
	"testing"

	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/service/rds"
	"github.com/aws/aws-sdk-go/service/rds/rdsiface"
)

type mockRDS struct {
	mock.Mock
	rdsiface.RDSAPI
}

func (c *mockRDS) DescribeDBInstances(input *rds.DescribeDBInstancesInput) (*rds.DescribeDBInstancesOutput, error) {
	retVal := c.Called(input)
	return retVal.Get(0).(*rds.DescribeDBInstancesOutput), retVal.Error(1)
}

func (c *mockRDS) DescribeDBInstancesPages(input *rds.DescribeDBInstancesInput, fn func(p *rds.DescribeDBInstancesOutput, lastPage bool) (shouldContinue bool)) error {
	input.Marker = nil
	input.MaxRecords = aws.Int64(100)

	for {
		resp, err := c.DescribeDBInstances(input)
		if err != nil {
			return err
		}

		if !fn(resp, resp.Marker != nil) || resp.Marker == nil {
			return nil
		}

		input.Marker = resp.Marker
	}
}

func (c *mockRDS) ListTagsForResource(input *rds.ListTagsForResourceInput) (*rds.ListTagsForResourceOutput, error) {
	args := c.Called(input)
	return args.Get(0).(*rds.ListTagsForResourceOutput), args.Error(1)
}

func checkTags(t *testing.T, tags ResourceTags, arn, identifier, engine, address, port, superuser string, dbname *string, tagStrings ...string) {
	instance, ok := tags[identifier]
	require.True(t, ok)
	require.NotNil(t, instance)
	require.Equal(t, arn, instance.Arn)
	require.Equal(t, identifier, instance.Name)

	actualEngine, ok := instance.Data["driver"]
	require.True(t, ok)
	require.Equal(t, engine, actualEngine)

	actualAddress, ok := instance.Data["host"]
	require.True(t, ok)
	require.Equal(t, address, actualAddress)

	actualPort, ok := instance.Data["port"]
	require.True(t, ok)
	require.Equal(t, port, actualPort)

	actualSuperuser, ok := instance.Data["superuser"]
	require.True(t, ok)
	require.Equal(t, superuser, actualSuperuser)

	actualDbname, ok := instance.Data["dbname"]
	if dbname == nil {
		require.False(t, ok)
	} else {
		require.True(t, ok)
		require.Equal(t, *dbname, actualDbname)
	}

	require.Len(t, instance.Tags, len(tagStrings)/2)
	i := 0
	for _, tag := range instance.Tags {
		require.NotNil(t, tag)
		require.NotNil(t, tag.Key)
		require.NotNil(t, tag.Value)
		require.Equal(t, tagStrings[i], *tag.Key)
		i++
		require.Equal(t, tagStrings[i], *tag.Value)
		i++
	}
}

func TestListTags(t *testing.T) {
	rdsClient := &mockRDS{}

	rdsClient.On("DescribeDBInstances", &rds.DescribeDBInstancesInput{
		MaxRecords: aws.Int64(100),
	}).Return(&rds.DescribeDBInstancesOutput{
		DBInstances: []*rds.DBInstance{
			&rds.DBInstance{
				Engine: aws.String("postgres"),
				Endpoint: &rds.Endpoint{
					Address: aws.String("twitch.tv"),
					Port:    aws.Int64(1000),
				},
				DBInstanceArn:        aws.String("arn:instance-1"),
				DBInstanceIdentifier: aws.String("instance-1"),
				MasterUsername:       aws.String("user"),
				DBName:               aws.String("database"),
			},
		},
		Marker: aws.String("marker"),
	}, nil)

	rdsClient.On("DescribeDBInstances", &rds.DescribeDBInstancesInput{
		MaxRecords: aws.Int64(100),
		Marker:     aws.String("marker"),
	}).Return(&rds.DescribeDBInstancesOutput{
		DBInstances: []*rds.DBInstance{
			&rds.DBInstance{
				Engine: aws.String("mysql"),
				Endpoint: &rds.Endpoint{
					Address: aws.String("justin.tv"),
					Port:    aws.Int64(5432),
				},
				DBInstanceArn:        aws.String("arn:instance-2"),
				DBInstanceIdentifier: aws.String("instance-2"),
				MasterUsername:       aws.String("bob"),
				DBName:               aws.String("justintv_prod"),
			},
		},
	}, nil)

	rdsClient.On("ListTagsForResource", &rds.ListTagsForResourceInput{
		ResourceName: aws.String("arn:instance-1"),
	}).Return(&rds.ListTagsForResourceOutput{
		TagList: []*rds.Tag{
			&rds.Tag{
				Key:   aws.String("key1"),
				Value: aws.String("value1"),
			},
		},
	}, nil)

	rdsClient.On("ListTagsForResource", &rds.ListTagsForResourceInput{
		ResourceName: aws.String("arn:instance-2"),
	}).Return(&rds.ListTagsForResourceOutput{
		TagList: []*rds.Tag{
			&rds.Tag{
				Key:   aws.String("key2"),
				Value: aws.String("value2"),
			},
		},
	}, nil)

	tags, err := InstanceTags(rdsClient)

	require.Nil(t, err)

	require.Len(t, tags, 2)
	checkTags(t, tags, "arn:instance-1", "instance-1", "postgres", "twitch.tv", "1000", "user", aws.String("database"), "key1", "value1")
	checkTags(t, tags, "arn:instance-2", "instance-2", "mysql", "justin.tv", "5432", "bob", aws.String("justintv_prod"), "key2", "value2")
}

func TestListTagsMissingUsername(t *testing.T) {
	rdsClient := &mockRDS{}

	rdsClient.On("DescribeDBInstances", &rds.DescribeDBInstancesInput{
		MaxRecords: aws.Int64(100),
	}).Return(&rds.DescribeDBInstancesOutput{
		DBInstances: []*rds.DBInstance{
			&rds.DBInstance{
				Engine: aws.String("postgres"),
				Endpoint: &rds.Endpoint{
					Address: aws.String("twitch.tv"),
					Port:    aws.Int64(1000),
				},
				DBInstanceArn:        aws.String("arn:instance-1"),
				DBInstanceIdentifier: aws.String("instance-1"),
				MasterUsername:       aws.String("user"),
				DBName:               aws.String("database"),
			},
		},
		Marker: aws.String("marker"),
	}, nil)

	rdsClient.On("DescribeDBInstances", &rds.DescribeDBInstancesInput{
		MaxRecords: aws.Int64(100),
		Marker:     aws.String("marker"),
	}).Return(&rds.DescribeDBInstancesOutput{
		DBInstances: []*rds.DBInstance{
			&rds.DBInstance{
				Engine: aws.String("mysql"),
				Endpoint: &rds.Endpoint{
					Address: aws.String("justin.tv"),
					Port:    aws.Int64(5432),
				},
				DBInstanceArn:        aws.String("arn:instance-2"),
				DBInstanceIdentifier: aws.String("instance-2"),
				DBName:               aws.String("justintv_prod"),
			},
		},
	}, nil)

	rdsClient.On("ListTagsForResource", &rds.ListTagsForResourceInput{
		ResourceName: aws.String("arn:instance-1"),
	}).Return(&rds.ListTagsForResourceOutput{
		TagList: []*rds.Tag{
			&rds.Tag{
				Key:   aws.String("key1"),
				Value: aws.String("value1"),
			},
		},
	}, nil)

	rdsClient.On("ListTagsForResource", &rds.ListTagsForResourceInput{
		ResourceName: aws.String("arn:instance-2"),
	}).Return(&rds.ListTagsForResourceOutput{
		TagList: []*rds.Tag{
			&rds.Tag{
				Key:   aws.String("key2"),
				Value: aws.String("value2"),
			},
		},
	}, nil)

	tags, err := InstanceTags(rdsClient)

	require.Nil(t, err)
	require.Len(t, tags, 1)
	checkTags(t, tags, "arn:instance-1", "instance-1", "postgres", "twitch.tv", "1000", "user", aws.String("database"), "key1", "value1")
}

func TestListTagsMissingDBName(t *testing.T) {
	rdsClient := &mockRDS{}

	rdsClient.On("DescribeDBInstances", &rds.DescribeDBInstancesInput{
		MaxRecords: aws.Int64(100),
	}).Return(&rds.DescribeDBInstancesOutput{
		DBInstances: []*rds.DBInstance{
			&rds.DBInstance{
				Engine: aws.String("postgres"),
				Endpoint: &rds.Endpoint{
					Address: aws.String("twitch.tv"),
					Port:    aws.Int64(1000),
				},
				DBInstanceArn:        aws.String("arn:instance-1"),
				DBInstanceIdentifier: aws.String("instance-1"),
				MasterUsername:       aws.String("user"),
				DBName:               aws.String("database"),
			},
		},
		Marker: aws.String("marker"),
	}, nil)

	rdsClient.On("DescribeDBInstances", &rds.DescribeDBInstancesInput{
		MaxRecords: aws.Int64(100),
		Marker:     aws.String("marker"),
	}).Return(&rds.DescribeDBInstancesOutput{
		DBInstances: []*rds.DBInstance{
			&rds.DBInstance{
				Engine: aws.String("mysql"),
				Endpoint: &rds.Endpoint{
					Address: aws.String("justin.tv"),
					Port:    aws.Int64(5432),
				},
				DBInstanceArn:        aws.String("arn:instance-2"),
				DBInstanceIdentifier: aws.String("instance-2"),
				MasterUsername:       aws.String("bob"),
			},
		},
	}, nil)

	rdsClient.On("ListTagsForResource", &rds.ListTagsForResourceInput{
		ResourceName: aws.String("arn:instance-1"),
	}).Return(&rds.ListTagsForResourceOutput{
		TagList: []*rds.Tag{
			&rds.Tag{
				Key:   aws.String("key1"),
				Value: aws.String("value1"),
			},
		},
	}, nil)

	rdsClient.On("ListTagsForResource", &rds.ListTagsForResourceInput{
		ResourceName: aws.String("arn:instance-2"),
	}).Return(&rds.ListTagsForResourceOutput{
		TagList: []*rds.Tag{
			&rds.Tag{
				Key:   aws.String("key2"),
				Value: aws.String("value2"),
			},
		},
	}, nil)

	tags, err := InstanceTags(rdsClient)

	require.Nil(t, err)

	require.Len(t, tags, 2)
	checkTags(t, tags, "arn:instance-1", "instance-1", "postgres", "twitch.tv", "1000", "user", aws.String("database"), "key1", "value1")
	checkTags(t, tags, "arn:instance-2", "instance-2", "mysql", "justin.tv", "5432", "bob", nil, "key2", "value2")
}
