package legacyperm

import (
	"context"
	"errors"
	"testing"

	"code.justin.tv/beefcake/server/internal/awsmocks"
	"code.justin.tv/beefcake/server/internal/config"
	"code.justin.tv/beefcake/server/internal/optional"
	"code.justin.tv/beefcake/server/internal/testconfig"
	"code.justin.tv/beefcake/server/rpc/beefcake"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/awserr"
	"github.com/aws/aws-sdk-go/service/dynamodb"
	"github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"
)

type legacyPermissionsTest struct {
	LegacyPermissions *LegacyPermissions
	Config            *config.Config
	DynamoDB          *awsmocks.DynamoDBAPI
}

func newLegacyPermissionsTest(t *testing.T) *legacyPermissionsTest {
	dynamoDB := new(awsmocks.DynamoDBAPI)
	config := testconfig.New(t)
	return &legacyPermissionsTest{
		LegacyPermissions: &LegacyPermissions{
			Config:   config,
			DynamoDB: dynamoDB,
		},
		Config:   config,
		DynamoDB: dynamoDB,
	}
}

func (ct legacyPermissionsTest) Teardown(t *testing.T) {
	ct.DynamoDB.AssertExpectations(t)
}

func TestLegacyPermissions(t *testing.T) {
	const testID = "test-id"
	const testName = "test-name"
	const testDescription = "test-description"
	const testRoleID = "test-role-id"
	const testRoleName = "test-role-name"

	marshalled := func(t *testing.T, in interface{}) map[string]*dynamodb.AttributeValue {
		out, err := dynamodbattribute.MarshalMap(in)
		require.NoError(t, err)
		return out
	}

	marshalledItem := func(t *testing.T, in interface{}) *dynamodb.AttributeValue {
		out, err := dynamodbattribute.Marshal(in)
		require.NoError(t, err)
		return out
	}

	t.Run("All", func(t *testing.T) {
		tcs := []struct {
			Name         string
			Last         bool
			ExpectedCont bool
		}{
			{Name: "has more", Last: false, ExpectedCont: true},
			{Name: "last page", Last: true, ExpectedCont: false},
		}

		for _, tc := range tcs {
			t.Run("Success "+tc.Name, func(t *testing.T) {
				ct := newLegacyPermissionsTest(t)
				defer ct.Teardown(t)

				ct.DynamoDB.
					On("ScanPagesWithContext", mock.Anything, &dynamodb.ScanInput{
						ExpressionAttributeNames: map[string]*string{
							"#0": aws.String("ID"),
							"#1": aws.String("Name"),
							"#2": aws.String("Description"),
						},
						Limit:                aws.Int64(100),
						ProjectionExpression: aws.String("#0, #1, #2"),
						TableName:            aws.String(ct.Config.LegacyPermissionsTableName.Get()),
					}, mock.Anything).
					Run(func(args mock.Arguments) {
						fn := args.Get(2).(func(*dynamodb.ScanOutput, bool) bool)
						assert.Equal(t, tc.ExpectedCont, fn(&dynamodb.ScanOutput{
							Items: []map[string]*dynamodb.AttributeValue{
								marshalled(t, &LegacyPermission{ID: "3", Name: testName, Description: testDescription}),
								marshalled(t, &LegacyPermission{ID: "1", Name: testName, Description: testDescription}),
								marshalled(t, &LegacyPermission{ID: "2", Name: testName, Description: testDescription}),
							},
						}, tc.Last))
					}).
					Return(nil)

				res, err := ct.LegacyPermissions.All(context.Background())
				require.NoError(t, err)
				assert.Equal(t, []*beefcake.GetLegacyPermissionsResponse_LegacyPermission{
					{Id: "1", Name: testName, Description: testDescription},
					{Id: "2", Name: testName, Description: testDescription},
					{Id: "3", Name: testName, Description: testDescription},
				}, res)
			})
		}
	})

	t.Run("Create", func(t *testing.T) {
		t.Run("Success", func(t *testing.T) {
			ct := newLegacyPermissionsTest(t)
			defer ct.Teardown(t)

			ct.DynamoDB.
				On("PutItemWithContext", mock.Anything, &dynamodb.PutItemInput{
					ConditionExpression: aws.String("attribute_not_exists (#0)"),
					ExpressionAttributeNames: map[string]*string{
						"#0": aws.String(ct.Config.LegacyPermissionsHashKey.Get()),
					},
					Item: map[string]*dynamodb.AttributeValue{
						"ID":           {S: aws.String(testID)},
						"Name":         {S: aws.String(testName)},
						"Description":  {S: aws.String(testDescription)},
						RolesAttribute: {M: map[string]*dynamodb.AttributeValue{}},
					},
					TableName: aws.String(ct.Config.LegacyPermissionsTableName.Get()),
				}).
				Return(&dynamodb.PutItemOutput{}, nil)

			require.NoError(t, ct.LegacyPermissions.Create(context.Background(), testID, testName, testDescription))
		})
	})

	t.Run("Get", func(t *testing.T) {
		t.Run("Success", func(t *testing.T) {
			ct := newLegacyPermissionsTest(t)
			defer ct.Teardown(t)

			lp := &LegacyPermission{
				ID:          testID,
				Name:        testName,
				Description: testDescription,
				Roles:       make(Roles, 0),
			}

			ct.DynamoDB.
				On("GetItemWithContext", mock.Anything, &dynamodb.GetItemInput{
					Key: map[string]*dynamodb.AttributeValue{
						ct.Config.LegacyPermissionsHashKey.Get(): {S: aws.String(testID)},
					},
					TableName: aws.String(ct.Config.LegacyPermissionsTableName.Get()),
				}).
				Return(&dynamodb.GetItemOutput{Item: marshalled(t, lp)}, nil)

			res, err := ct.LegacyPermissions.Get(context.Background(), testID)
			require.NoError(t, err)

			assert.Equal(t, lp, res)
		})

		t.Run("Missing", func(t *testing.T) {
			ct := newLegacyPermissionsTest(t)
			defer ct.Teardown(t)

			ct.DynamoDB.
				On("GetItemWithContext", mock.Anything, &dynamodb.GetItemInput{
					Key: map[string]*dynamodb.AttributeValue{
						ct.Config.LegacyPermissionsHashKey.Get(): {S: aws.String(testID)},
					},
					TableName: aws.String(ct.Config.LegacyPermissionsTableName.Get()),
				}).
				Return(nil, awserr.New(dynamodb.ErrCodeConditionalCheckFailedException, "", errors.New("")))

			_, err := ct.LegacyPermissions.Get(context.Background(), testID)
			assert.Equal(t, ErrLegacyPermissionDoesNotExist{ID: testID}, err)
		})
	})

	t.Run("Update", func(t *testing.T) {
		t.Run("Success", func(t *testing.T) {
			ct := newLegacyPermissionsTest(t)
			defer ct.Teardown(t)

			ct.DynamoDB.
				On("UpdateItemWithContext", mock.Anything, &dynamodb.UpdateItemInput{
					ConditionExpression: aws.String("attribute_exists (#0)"),
					ExpressionAttributeNames: map[string]*string{
						"#0": aws.String(ct.Config.LegacyPermissionsHashKey.Get()),
						"#1": aws.String(NameAttribute),
						"#2": aws.String(DescriptionAttribute),
					},
					ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{
						":0": {S: aws.String(testName)},
						":1": {S: aws.String(testDescription)},
					},
					Key: map[string]*dynamodb.AttributeValue{
						ct.Config.LegacyPermissionsHashKey.Get(): {S: aws.String(testID)},
					},
					TableName:        aws.String(ct.Config.LegacyPermissionsTableName.Get()),
					UpdateExpression: aws.String("SET #1 = :0, #2 = :1\n"),
				}).
				Return(nil, nil)

			require.NoError(t, ct.LegacyPermissions.Update(context.Background(), testID, UpdateOptions{
				optional.String(testName),
				optional.String(testDescription),
			}))
		})

		t.Run("Success - only name", func(t *testing.T) {
			ct := newLegacyPermissionsTest(t)
			defer ct.Teardown(t)

			ct.DynamoDB.
				On("UpdateItemWithContext", mock.Anything, &dynamodb.UpdateItemInput{
					ConditionExpression: aws.String("attribute_exists (#0)"),
					ExpressionAttributeNames: map[string]*string{
						"#0": aws.String(ct.Config.LegacyPermissionsHashKey.Get()),
						"#1": aws.String(NameAttribute),
					},
					ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{
						":0": {S: aws.String(testName)},
					},
					Key: map[string]*dynamodb.AttributeValue{
						ct.Config.LegacyPermissionsHashKey.Get(): {S: aws.String(testID)},
					},
					TableName:        aws.String(ct.Config.LegacyPermissionsTableName.Get()),
					UpdateExpression: aws.String("SET #1 = :0\n"),
				}).
				Return(nil, nil)

			require.NoError(t, ct.LegacyPermissions.Update(context.Background(), testID, UpdateOptions{
				Name: optional.String(testName),
			}))
		})

		t.Run("Success - only description", func(t *testing.T) {
			ct := newLegacyPermissionsTest(t)
			defer ct.Teardown(t)

			ct.DynamoDB.
				On("UpdateItemWithContext", mock.Anything, &dynamodb.UpdateItemInput{
					ConditionExpression: aws.String("attribute_exists (#0)"),
					ExpressionAttributeNames: map[string]*string{
						"#0": aws.String(ct.Config.LegacyPermissionsHashKey.Get()),
						"#1": aws.String(DescriptionAttribute),
					},
					ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{
						":0": {S: aws.String(testDescription)},
					},
					Key: map[string]*dynamodb.AttributeValue{
						ct.Config.LegacyPermissionsHashKey.Get(): {S: aws.String(testID)},
					},
					TableName:        aws.String(ct.Config.LegacyPermissionsTableName.Get()),
					UpdateExpression: aws.String("SET #1 = :0\n"),
				}).
				Return(nil, nil)

			require.NoError(t, ct.LegacyPermissions.Update(context.Background(), testID, UpdateOptions{
				Description: optional.String(testDescription),
			}))
		})

		t.Run("Success - no updates", func(t *testing.T) {
			ct := newLegacyPermissionsTest(t)
			defer ct.Teardown(t)

			require.NoError(t, ct.LegacyPermissions.Update(context.Background(), testID, UpdateOptions{}))
		})
	})

	t.Run("Delete", func(t *testing.T) {
		t.Run("Success", func(t *testing.T) {
			ct := newLegacyPermissionsTest(t)
			defer ct.Teardown(t)

			ct.DynamoDB.
				On("DeleteItemWithContext", mock.Anything, &dynamodb.DeleteItemInput{
					Key: map[string]*dynamodb.AttributeValue{
						ct.Config.LegacyPermissionsHashKey.Get(): {S: aws.String(testID)},
					},
					TableName: aws.String(ct.Config.LegacyPermissionsTableName.Get()),
				}).
				Return(nil, nil)

			require.NoError(t, ct.LegacyPermissions.Delete(context.Background(), testID))
		})
	})

	t.Run("AddRole", func(t *testing.T) {
		mockUpdateCall := func(ct *legacyPermissionsTest) *mock.Call {
			return ct.DynamoDB.
				On("UpdateItemWithContext", mock.Anything, &dynamodb.UpdateItemInput{
					ConditionExpression: aws.String("attribute_exists (#0)"),
					ExpressionAttributeNames: map[string]*string{
						"#0": aws.String(ct.Config.LegacyPermissionsHashKey.Get()),
						"#1": aws.String(RolesAttribute),
						"#2": aws.String(testRoleID),
					},
					ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{
						":0": marshalledItem(t, Role(beefcake.LegacyPermission_Role{
							Id:   testRoleID,
							Name: testRoleName,
						})),
					},
					Key: map[string]*dynamodb.AttributeValue{
						ct.Config.LegacyPermissionsHashKey.Get(): {S: aws.String(testID)},
					},
					TableName:        aws.String(ct.Config.LegacyPermissionsTableName.Get()),
					UpdateExpression: aws.String("SET #1.#2 = :0\n"),
				})
		}

		t.Run("Success", func(t *testing.T) {
			ct := newLegacyPermissionsTest(t)
			defer ct.Teardown(t)

			mockUpdateCall(ct).
				Return(nil, nil)

			require.NoError(t, ct.LegacyPermissions.AddRole(context.Background(), testID, testRoleID, testRoleName))
		})

		t.Run("Condition failed should not error", func(t *testing.T) {
			ct := newLegacyPermissionsTest(t)
			defer ct.Teardown(t)

			mockUpdateCall(ct).
				Return(nil, awserr.New(dynamodb.ErrCodeConditionalCheckFailedException, "", errors.New("")))

			require.NoError(t, ct.LegacyPermissions.AddRole(context.Background(), testID, testRoleID, testRoleName))
		})
	})

	t.Run("RemoveRole", func(t *testing.T) {
		t.Run("Success", func(t *testing.T) {
			ct := newLegacyPermissionsTest(t)
			defer ct.Teardown(t)

			ct.DynamoDB.
				On("UpdateItemWithContext", mock.Anything, &dynamodb.UpdateItemInput{
					ExpressionAttributeNames: map[string]*string{
						"#0": aws.String(RolesAttribute),
						"#1": aws.String(testRoleID),
					},
					Key: map[string]*dynamodb.AttributeValue{
						ct.Config.LegacyPermissionsHashKey.Get(): {S: aws.String(testID)},
					},
					TableName:        aws.String(ct.Config.LegacyPermissionsTableName.Get()),
					UpdateExpression: aws.String("REMOVE #0.#1\n"),
				}).
				Return(nil, nil)

			require.NoError(t, ct.LegacyPermissions.RemoveRole(context.Background(), testID, testRoleID))
		})
	})
}
