package store

import (
	"testing"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/service/autoscaling"
	"github.com/aws/aws-sdk-go/service/autoscaling/autoscalingiface"
	"github.com/aws/aws-sdk-go/service/ec2"
	"github.com/aws/aws-sdk-go/service/ec2/ec2iface"
	"github.com/aws/aws-sdk-go/service/rds"
	"github.com/aws/aws-sdk-go/service/rds/rdsiface"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"

	"code.justin.tv/d8a/buddy/lib/config"
	"code.justin.tv/d8a/buddy/lib/sandstorm"
)

type MockRDS struct {
	mock.Mock
	rdsiface.RDSAPI
}

func (c *MockRDS) DescribeDBInstances(input *rds.DescribeDBInstancesInput) (*rds.DescribeDBInstancesOutput, error) {
	args := c.Called(input)
	return args.Get(0).(*rds.DescribeDBInstancesOutput), args.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) DescribeDBSubnetGroups(input *rds.DescribeDBSubnetGroupsInput) (*rds.DescribeDBSubnetGroupsOutput, error) {
	args := c.Called(input)
	return args.Get(0).(*rds.DescribeDBSubnetGroupsOutput), args.Error(1)
}

func (c *MockRDS) DescribeDBSubnetGroupsPages(input *rds.DescribeDBSubnetGroupsInput, fn func(p *rds.DescribeDBSubnetGroupsOutput, lastPage bool) (shouldContinue bool)) error {
	input.Marker = nil
	input.MaxRecords = aws.Int64(100)

	for {
		resp, err := c.DescribeDBSubnetGroups(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 (c *MockRDS) CreateDBInstance(input *rds.CreateDBInstanceInput) (*rds.CreateDBInstanceOutput, error) {
	args := c.Called(input)
	return args.Get(0).(*rds.CreateDBInstanceOutput), args.Error(1)
}

func (c *MockRDS) CreateDBSubnetGroup(input *rds.CreateDBSubnetGroupInput) (*rds.CreateDBSubnetGroupOutput, error) {
	args := c.Called(input)
	return args.Get(0).(*rds.CreateDBSubnetGroupOutput), args.Error(1)
}

type MockEC2 struct {
	mock.Mock
	ec2iface.EC2API
}

func (c *MockEC2) CreateTags(input *ec2.CreateTagsInput) (*ec2.CreateTagsOutput, error) {
	args := c.Called(input)
	return args.Get(0).(*ec2.CreateTagsOutput), args.Error(1)
}

type MockSandstorm struct {
	mock.Mock
	sandstorm.SandstormAPI
}

func (c *MockSandstorm) GetStorePassword() (string, error) {
	args := c.Called()
	return args.String(0), args.Error(1)
}

func (c *MockSandstorm) WriteStorePassword(newPassword string) error {
	args := c.Called(newPassword)
	return args.Error(0)
}

type MockAutoscaling struct {
	mock.Mock
	autoscalingiface.AutoScalingAPI
}

func (c *MockAutoscaling) DescribeAutoScalingGroups(input *autoscaling.DescribeAutoScalingGroupsInput) (*autoscaling.DescribeAutoScalingGroupsOutput, error) {
	args := c.Called(input)
	return args.Get(0).(*autoscaling.DescribeAutoScalingGroupsOutput), args.Error(1)
}

func (c *MockAutoscaling) DescribeAutoScalingInstances(input *autoscaling.DescribeAutoScalingInstancesInput) (*autoscaling.DescribeAutoScalingInstancesOutput, error) {
	args := c.Called(input)
	return args.Get(0).(*autoscaling.DescribeAutoScalingInstancesOutput), args.Error(1)
}

func TestLocalTaggedInstance(t *testing.T) {
	rdsClient := &MockRDS{}
	ec2Client := &MockEC2{}
	sandstormClient := &MockSandstorm{}
	autoscalingClient := &MockAutoscaling{}

	localInstance := &ec2.Instance{
		Tags: []*ec2.Tag{
			&ec2.Tag{
				Key:   aws.String("rds-buddy-test-team-store"),
				Value: aws.String("test-instance-store"),
			},
		},
	}
	configFile := &config.ConfigFile{
		SandstormTeam: "test-team",
	}

	rdsClient.On("DescribeDBInstances", &rds.DescribeDBInstancesInput{
		DBInstanceIdentifier: aws.String("test-instance-store"),
	}).Return(&rds.DescribeDBInstancesOutput{
		DBInstances: []*rds.DBInstance{
			&rds.DBInstance{
				DBInstanceIdentifier: aws.String("test-instance-store"),
			},
		},
	}, nil)

	instance, err := GetBuddyStore(rdsClient, ec2Client, autoscalingClient, localInstance, sandstormClient, configFile, true)
	require.Nil(t, err)
	require.NotNil(t, instance)
	require.Equal(t, "test-instance-store", *instance.DBInstanceIdentifier)
	rdsClient.AssertExpectations(t)
	ec2Client.AssertExpectations(t)
	sandstormClient.AssertExpectations(t)
}

func TestLocalTestNoTaggedInstanceNonSetup(t *testing.T) {
	rdsClient := &MockRDS{}
	ec2Client := &MockEC2{}
	sandstormClient := &MockSandstorm{}
	autoscalingClient := &MockAutoscaling{}

	localInstance := &ec2.Instance{
		Tags: []*ec2.Tag{
			&ec2.Tag{
				Key:   aws.String("rds-buddy-test-team-store"),
				Value: aws.String("test-instance-store"),
			},
		},
	}
	configFile := &config.ConfigFile{
		SandstormTeam: "test-team",
	}

	rdsClient.On("DescribeDBInstances", &rds.DescribeDBInstancesInput{
		DBInstanceIdentifier: aws.String("test-instance-store"),
	}).Return(&rds.DescribeDBInstancesOutput{
		DBInstances: []*rds.DBInstance{},
	}, nil)

	instance, err := GetBuddyStore(rdsClient, ec2Client, autoscalingClient, localInstance, sandstormClient, configFile, false)
	require.NotNil(t, err)
	require.Nil(t, instance)
	require.Equal(t, "the buddy store is not cached in the local instance's tags- you may need to run `buddy-cli setup` to properly configure the store", err.Error())
	rdsClient.AssertExpectations(t)
	ec2Client.AssertExpectations(t)
	sandstormClient.AssertExpectations(t)
}

func TestScanTagInstance(t *testing.T) {
	rdsClient := &MockRDS{}
	ec2Client := &MockEC2{}
	sandstormClient := &MockSandstorm{}
	autoscalingClient := &MockAutoscaling{}

	localInstance := &ec2.Instance{
		InstanceId: aws.String("local-instance"),
		Tags: []*ec2.Tag{
			&ec2.Tag{
				Key:   aws.String("rds-buddy-test-team-store"),
				Value: aws.String("test-instance-store"),
			},
		},
	}
	configFile := &config.ConfigFile{
		SandstormTeam: "test-team",
	}

	rdsClient.On("DescribeDBInstances", &rds.DescribeDBInstancesInput{
		DBInstanceIdentifier: aws.String("test-instance-store"),
	}).Return(&rds.DescribeDBInstancesOutput{
		DBInstances: []*rds.DBInstance{},
	}, nil)

	rdsClient.On("DescribeDBInstances", &rds.DescribeDBInstancesInput{
		MaxRecords: aws.Int64(100),
	}).Return(&rds.DescribeDBInstancesOutput{
		DBInstances: []*rds.DBInstance{
			&rds.DBInstance{
				DBInstanceIdentifier: aws.String("test-instance-store-2"),
				DBInstanceArn:        aws.String("test-instance-store-2:an-arn"),
				DBName:               aws.String("test"),
				Engine:               aws.String("postgres"),
				MasterUsername:       aws.String("dbuser"),
				Endpoint: &rds.Endpoint{
					Address: aws.String("twitch.tv"),
					Port:    aws.Int64(8000),
				},
			},
		},
	}, nil)

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

	rdsClient.On("DescribeDBInstances", &rds.DescribeDBInstancesInput{
		MaxRecords: aws.Int64(100),
		Filters: []*rds.Filter{
			&rds.Filter{
				Name: aws.String("db-instance-id"),
				Values: []*string{
					aws.String("test-instance-store-2"),
				},
			},
		},
	}).Return(&rds.DescribeDBInstancesOutput{
		DBInstances: []*rds.DBInstance{
			&rds.DBInstance{
				DBInstanceIdentifier: aws.String("test-instance-store-2"),
				DBInstanceArn:        aws.String("test-instance-store-2:an-arn"),
			},
		},
	}, nil)

	ec2Client.On("CreateTags", &ec2.CreateTagsInput{
		Resources: []*string{
			aws.String("local-instance"),
		},
		Tags: []*ec2.Tag{
			&ec2.Tag{
				Key:   aws.String("rds-buddy-test-team-store"),
				Value: aws.String("test-instance-store-2"),
			},
		},
	}).Return(&ec2.CreateTagsOutput{}, nil)

	instance, err := GetBuddyStore(rdsClient, ec2Client, autoscalingClient, localInstance, sandstormClient, configFile, true)
	require.Nil(t, err)
	require.NotNil(t, instance)
	require.Equal(t, "test-instance-store-2", *instance.DBInstanceIdentifier)
	rdsClient.AssertExpectations(t)
	ec2Client.AssertExpectations(t)
	sandstormClient.AssertExpectations(t)
}

func TestCreateInstance(t *testing.T) {
	rdsClient := &MockRDS{}
	ec2Client := &MockEC2{}
	sandstormClient := &MockSandstorm{}
	autoscalingClient := &MockAutoscaling{}

	localInstance := &ec2.Instance{
		InstanceId: aws.String("local-instance"),
		SubnetId:   aws.String("a-subnet"),
		SecurityGroups: []*ec2.GroupIdentifier{
			&ec2.GroupIdentifier{
				GroupId: aws.String("sg-0"),
			},
			&ec2.GroupIdentifier{
				GroupId: aws.String("sg-1"),
			},
		},
		Tags: []*ec2.Tag{
			&ec2.Tag{
				Key:   aws.String("rds-buddy-test-team-store"),
				Value: aws.String("test-instance-store"),
			},
		},
	}
	configFile := &config.ConfigFile{
		SandstormTeam: "test-team",
		Environment:   "staging",
	}

	rdsClient.On("DescribeDBInstances", &rds.DescribeDBInstancesInput{
		DBInstanceIdentifier: aws.String("test-instance-store"),
	}).Return(&rds.DescribeDBInstancesOutput{
		DBInstances: []*rds.DBInstance{},
	}, nil)

	rdsClient.On("DescribeDBInstances", &rds.DescribeDBInstancesInput{
		MaxRecords: aws.Int64(100),
	}).Return(&rds.DescribeDBInstancesOutput{
		DBInstances: []*rds.DBInstance{
			&rds.DBInstance{
				DBInstanceIdentifier: aws.String("test-instance-store-2"),
				DBInstanceArn:        aws.String("test-instance-store-2:an-arn"),
				MasterUsername:       aws.String("dbuser"),
				DBName:               aws.String("test"),
				Engine:               aws.String("postgres"),
				Endpoint: &rds.Endpoint{
					Address: aws.String("twitch.tv"),
					Port:    aws.Int64(8000),
				},
			},
		},
	}, nil)

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

	autoscalingClient.On("DescribeAutoScalingInstances", &autoscaling.DescribeAutoScalingInstancesInput{
		InstanceIds: []*string{aws.String("local-instance")},
	}).Return(&autoscaling.DescribeAutoScalingInstancesOutput{
		AutoScalingInstances: []*autoscaling.InstanceDetails{
			&autoscaling.InstanceDetails{
				AutoScalingGroupName: aws.String("local-asg"),
			},
		},
	}, nil)

	autoscalingClient.On("DescribeAutoScalingGroups", &autoscaling.DescribeAutoScalingGroupsInput{
		AutoScalingGroupNames: []*string{aws.String("local-asg")},
	}).Return(&autoscaling.DescribeAutoScalingGroupsOutput{
		AutoScalingGroups: []*autoscaling.Group{
			&autoscaling.Group{
				VPCZoneIdentifier: aws.String("subnet-0,subnet-1,subnet-2"),
			},
		},
	}, nil)

	sandstormClient.On("GetStorePassword").Return("", nil)

	password := ""
	sandstormClient.On("WriteStorePassword", mock.AnythingOfTypeArgument("string")).Run(func(args mock.Arguments) {
		password = args.String(0)
	}).Return(nil)

	rdsClient.On("DescribeDBSubnetGroups", &rds.DescribeDBSubnetGroupsInput{
		MaxRecords: aws.Int64(100),
	}).Return(&rds.DescribeDBSubnetGroupsOutput{
		DBSubnetGroups: []*rds.DBSubnetGroup{},
	}, nil)

	rdsClient.On("CreateDBSubnetGroup", &rds.CreateDBSubnetGroupInput{
		DBSubnetGroupDescription: aws.String("DB Subnet Group for test-team RDS Buddy Datastore"),
		DBSubnetGroupName:        aws.String("rds-buddy-test-team-staging-store-subnet"),
		SubnetIds:                []*string{aws.String("subnet-0"), aws.String("subnet-1"), aws.String("subnet-2")},
		Tags: []*rds.Tag{
			&rds.Tag{
				Key:   aws.String("rds-buddy-test-team-store"),
				Value: aws.String("true"),
			},
			&rds.Tag{
				Key:   aws.String("rds-buddy-environment"),
				Value: aws.String("staging"),
			},
			&rds.Tag{
				Key:   aws.String("Owner"),
				Value: aws.String("test-team"),
			},
			&rds.Tag{
				Key:   aws.String("Project"),
				Value: aws.String("rds-buddy-test-team"),
			},
		},
	}).Return(&rds.CreateDBSubnetGroupOutput{
		DBSubnetGroup: &rds.DBSubnetGroup{
			DBSubnetGroupArn:         aws.String("a-subnet-arn"),
			DBSubnetGroupDescription: aws.String("DB Subnet Group for test-team RDS Buddy Datastore"),
			DBSubnetGroupName:        aws.String("rds-buddy-test-team-store-subnet"),
			Subnets: []*rds.Subnet{
				&rds.Subnet{
					SubnetIdentifier: aws.String("subnet-0"),
				},
				&rds.Subnet{
					SubnetIdentifier: aws.String("subnet-1"),
				},
				&rds.Subnet{
					SubnetIdentifier: aws.String("subnet-2"),
				},
			},
		},
	}, nil)

	rdsClient.On("CreateDBInstance", mock.MatchedBy(func(actualInput *rds.CreateDBInstanceInput) bool {
		//The expectedinput depends on the password, which isn't populated when we set this expectation
		//so we make the expectation dynamic by using an argumentmatcher
		expectedInput := &rds.CreateDBInstanceInput{
			AllocatedStorage:           aws.Int64(200),
			DBInstanceClass:            aws.String("db.r3.large"),
			DBInstanceIdentifier:       aws.String("rds-buddy-test-team-staging-store"),
			DBName:                     aws.String("buddy"),
			DBSubnetGroupName:          aws.String("rds-buddy-test-team-store-subnet"),
			Engine:                     aws.String("postgres"),
			EngineVersion:              aws.String("9.5.2"),
			MasterUserPassword:         aws.String(password),
			MasterUsername:             aws.String("buddy"),
			MultiAZ:                    aws.Bool(true),
			PreferredBackupWindow:      aws.String("10:24-10:54"),
			PreferredMaintenanceWindow: aws.String("wed:07:40-wed:08:10"),
			PubliclyAccessible:         aws.Bool(false),
			StorageType:                aws.String("gp2"),
			VpcSecurityGroupIds: []*string{
				aws.String("sg-0"),
				aws.String("sg-1"),
			},
			Tags: []*rds.Tag{
				&rds.Tag{
					Key:   aws.String("rds-buddy-test-team-store"),
					Value: aws.String("true"),
				},
				&rds.Tag{
					Key:   aws.String("rds-buddy-environment"),
					Value: aws.String("staging"),
				},
				&rds.Tag{
					Key:   aws.String("Owner"),
					Value: aws.String("test-team"),
				},
				&rds.Tag{
					Key:   aws.String("Project"),
					Value: aws.String("rds-buddy-test-team"),
				},
			},
		}

		return assert.ObjectsAreEqual(expectedInput, actualInput)
	})).Return(&rds.CreateDBInstanceOutput{
		DBInstance: &rds.DBInstance{
			DBInstanceIdentifier: aws.String("test-instance-store-3"),
			MasterUsername:       aws.String("dbuser"),
			DBName:               aws.String("test"),
			Endpoint: &rds.Endpoint{
				Address: aws.String("twitch.tv"),
				Port:    aws.Int64(8000),
			},
			Engine: aws.String("postgres"),
		},
	}, nil)

	rdsClient.On("DescribeDBInstances", &rds.DescribeDBInstancesInput{
		DBInstanceIdentifier: aws.String("test-instance-store-3"),
	}).Return(&rds.DescribeDBInstancesOutput{
		DBInstances: []*rds.DBInstance{
			&rds.DBInstance{
				DBInstanceStatus:     aws.String("available"),
				DBInstanceIdentifier: aws.String("test-instance-store-3"),
			},
		},
	}, nil)

	ec2Client.On("CreateTags", &ec2.CreateTagsInput{
		Resources: []*string{
			aws.String("local-instance"),
		},
		Tags: []*ec2.Tag{
			&ec2.Tag{
				Key:   aws.String("rds-buddy-test-team-store"),
				Value: aws.String("test-instance-store-3"),
			},
		},
	}).Return(&ec2.CreateTagsOutput{}, nil)

	instance, err := GetBuddyStore(rdsClient, ec2Client, autoscalingClient, localInstance, sandstormClient, configFile, true)
	require.Nil(t, err)
	require.NotNil(t, instance)
	require.Equal(t, "test-instance-store-3", *instance.DBInstanceIdentifier)
	rdsClient.AssertExpectations(t)
	ec2Client.AssertExpectations(t)
	sandstormClient.AssertExpectations(t)
}
