package main

import (
	"context"
	"testing"
	"time"

	"code.justin.tv/beefcake/server/internal/config"
	"code.justin.tv/beefcake/server/internal/role"
	"code.justin.tv/beefcake/server/internal/testconfig"
	"code.justin.tv/beefcake/server/internal/user"
	"code.justin.tv/beefcake/server/internal/user/usermocks"
	"code.justin.tv/beefcake/server/rpc/beefcake"
	"github.com/aws/aws-lambda-go/events"
	"github.com/golang/protobuf/ptypes"
	"github.com/golang/protobuf/ptypes/timestamp"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"
)

type handlerTest struct {
	Handler *handler
	Config  *config.Config
	Users   *usermocks.UsersAPI
}

func newHandlerTest(t *testing.T) *handlerTest {
	config := testconfig.New(t)
	users := new(usermocks.UsersAPI)
	return &handlerTest{
		Handler: &handler{
			Config: config,
			Users:  users,
		},
		Config: config,
		Users:  users,
	}
}

func (ht *handlerTest) Teardown(t *testing.T) {
	ht.Users.AssertExpectations(t)
}

func TestHandler(t *testing.T) {
	const testRoleID = "testRoleID"
	const testRoleName = "testRoleName"

	testCases := []struct {
		CaseName string

		OldMembers     []string
		OldPermissions user.Permissions

		NewPermissions user.Permissions
		NewMembers     []string

		RemovedMembers           []string
		RemovedMemberPermissions user.Permissions

		Members                  []string
		MemberAddedPermissions   user.Permissions
		MemberRemovedPermissions user.Permissions
	}{
		{
			CaseName: "Member added",

			OldMembers: []string{
				"oldMember1",
			},
			OldPermissions: user.Permissions(map[string][]byte{
				"rolePerm1": []byte("perm1"),
			}),

			NewMembers: []string{
				"oldMember1",
				"newMember1",
			},
			NewPermissions: user.Permissions(map[string][]byte{
				"rolePerm1": []byte("perm1"),
			}),

			RemovedMembers:           []string{},
			RemovedMemberPermissions: user.Permissions(map[string][]byte{}),

			Members: []string{
				"oldMember1",
				"newMember1",
			},
			MemberAddedPermissions: user.Permissions(map[string][]byte{
				"rolePerm1": []byte("perm1"),
			}),
			MemberRemovedPermissions: user.Permissions(map[string][]byte{}),
		},
		{
			CaseName: "Member removed",

			OldMembers: []string{
				"oldMember1",
				"oldMember2",
			},
			OldPermissions: user.Permissions(map[string][]byte{
				"rolePerm1": []byte("perm1"),
			}),

			NewMembers: []string{
				"oldMember1",
			},
			NewPermissions: user.Permissions(map[string][]byte{
				"rolePerm1": []byte("perm1"),
			}),

			RemovedMembers: []string{
				"oldMember2",
			},
			RemovedMemberPermissions: user.Permissions(map[string][]byte{
				"rolePerm1": []byte("perm1"),
			}),

			Members: []string{
				"oldMember1",
			},
			MemberAddedPermissions: user.Permissions(map[string][]byte{
				"rolePerm1": []byte("perm1"),
			}),
			MemberRemovedPermissions: user.Permissions(map[string][]byte{}),
		},
		{
			CaseName: "First member added",

			OldMembers: []string{},
			OldPermissions: user.Permissions(map[string][]byte{
				"rolePerm1": []byte("perm1"),
			}),

			NewMembers: []string{
				"newMember1",
			},
			NewPermissions: user.Permissions(map[string][]byte{
				"rolePerm1": []byte("perm1"),
			}),

			RemovedMembers:           []string{},
			RemovedMemberPermissions: user.Permissions(map[string][]byte{}),

			Members: []string{
				"newMember1",
			},
			MemberAddedPermissions: user.Permissions(map[string][]byte{
				"rolePerm1": []byte("perm1"),
			}),
			MemberRemovedPermissions: user.Permissions(map[string][]byte{}),
		},
		{
			CaseName: "Last member removed",

			OldMembers: []string{
				"oldMember1",
			},
			OldPermissions: user.Permissions(map[string][]byte{
				"rolePerm1": []byte("perm1"),
			}),

			NewMembers: []string{},
			NewPermissions: user.Permissions(map[string][]byte{
				"rolePerm1": []byte("perm1"),
			}),

			RemovedMembers: []string{
				"oldMember1",
			},
			RemovedMemberPermissions: user.Permissions(map[string][]byte{
				"rolePerm1": []byte("perm1"),
			}),

			Members:                  []string{},
			MemberAddedPermissions:   user.Permissions(map[string][]byte{}),
			MemberRemovedPermissions: user.Permissions(map[string][]byte{}),
		},
		{
			CaseName: "Permission added",

			OldMembers: []string{
				"oldMember1",
			},
			OldPermissions: user.Permissions(map[string][]byte{
				"rolePerm1": []byte("perm1"),
			}),

			NewMembers: []string{
				"oldMember1",
			},
			NewPermissions: user.Permissions(map[string][]byte{
				"rolePerm1": []byte("perm1"),
				"rolePerm2": []byte("perm2"),
			}),

			RemovedMembers:           []string{},
			RemovedMemberPermissions: user.Permissions(map[string][]byte{}),

			Members: []string{
				"oldMember1",
			},
			MemberAddedPermissions: user.Permissions(map[string][]byte{
				"rolePerm1": []byte("perm1"),
				"rolePerm2": []byte("perm2"),
			}),
			MemberRemovedPermissions: user.Permissions(map[string][]byte{}),
		},
		{
			CaseName: "Permission removed",

			OldMembers: []string{
				"oldMember1",
			},
			OldPermissions: user.Permissions(map[string][]byte{
				"rolePerm1": []byte("perm1"),
				"rolePerm2": []byte("perm2"),
			}),

			NewMembers: []string{
				"oldMember1",
			},
			NewPermissions: user.Permissions(map[string][]byte{
				"rolePerm1": []byte("perm1"),
			}),

			RemovedMembers:           []string{},
			RemovedMemberPermissions: user.Permissions(map[string][]byte{}),

			Members: []string{
				"oldMember1",
			},
			MemberAddedPermissions: user.Permissions(map[string][]byte{
				"rolePerm1": []byte("perm1"),
			}),
			MemberRemovedPermissions: user.Permissions(map[string][]byte{
				"rolePerm2": []byte("perm2"),
			}),
		},
		{
			CaseName: "First permission added",

			OldMembers: []string{
				"oldMember1",
			},
			OldPermissions: user.Permissions(map[string][]byte{}),

			NewMembers: []string{
				"oldMember1",
			},
			NewPermissions: user.Permissions(map[string][]byte{
				"rolePerm1": []byte("perm1"),
			}),

			RemovedMembers:           []string{},
			RemovedMemberPermissions: user.Permissions(map[string][]byte{}),

			Members: []string{
				"oldMember1",
			},
			MemberAddedPermissions: user.Permissions(map[string][]byte{
				"rolePerm1": []byte("perm1"),
			}),
			MemberRemovedPermissions: user.Permissions(map[string][]byte{}),
		},
		{
			CaseName: "Last permission removed",

			OldMembers: []string{
				"oldMember1",
			},
			OldPermissions: user.Permissions(map[string][]byte{
				"rolePerm1": []byte("perm1"),
			}),

			NewMembers: []string{
				"oldMember1",
			},
			NewPermissions: user.Permissions(map[string][]byte{}),

			RemovedMembers:           []string{},
			RemovedMemberPermissions: user.Permissions(map[string][]byte{}),

			Members: []string{
				"oldMember1",
			},
			MemberAddedPermissions: user.Permissions(map[string][]byte{}),
			MemberRemovedPermissions: user.Permissions(map[string][]byte{
				"rolePerm1": []byte("perm1"),
			}),
		},
	}

	for _, tc := range testCases {
		t.Run(tc.CaseName, func(t *testing.T) {
			ht := newHandlerTest(t)
			defer ht.Teardown(t)

			testMembershipExpirationT := time.Now().Add(time.Hour)
			testMembershipExpiration := func(t *testing.T) *timestamp.Timestamp {
				out, err := ptypes.TimestampProto(testMembershipExpirationT)
				require.NoError(t, err)
				return out
			}

			for _, m := range tc.RemovedMembers {
				ht.Users.
					On("RemoveRoleMembership", mock.Anything, m, testRoleID, tc.RemovedMemberPermissions).
					Return(nil).
					Once()
			}

			for _, m := range tc.Members {
				ht.Users.
					On("AddRoleMembership", mock.Anything, m, &beefcake.User_RoleMembership{
						Id:                   testRoleID,
						Name:                 testRoleName,
						MembershipExpiration: testMembershipExpiration(t),
					}, tc.MemberAddedPermissions).
					Return(nil).
					Once()

				ht.Users.
					On("RemovePermissions", mock.Anything, m, tc.MemberRemovedPermissions).
					Return(nil).
					Once()
			}

			membershipsMap := func(ms []string) events.DynamoDBAttributeValue {
				out := map[string]events.DynamoDBAttributeValue{}
				for _, key := range ms {
					membership, err := role.UserMembership(beefcake.Role_UserMembership{
						UserId:     key,
						Expiration: testMembershipExpiration(t),
					}).MarshalBytes()
					require.NoError(t, err)

					out[key] = events.NewBinaryAttribute(membership)
				}
				return events.NewMapAttribute(out)
			}

			permissionsToMap := func(perms user.Permissions) events.DynamoDBAttributeValue {
				out := map[string]events.DynamoDBAttributeValue{}
				for key, val := range perms {
					out[key] = events.NewBinaryAttribute(val)
				}
				return events.NewMapAttribute(out)
			}

			ht.Handler.Handle(context.Background(), events.DynamoDBEvent{
				Records: []events.DynamoDBEventRecord{
					{
						Change: events.DynamoDBStreamRecord{
							OldImage: map[string]events.DynamoDBAttributeValue{
								role.IDAttribute:              events.NewStringAttribute(testRoleID),
								role.NameAttribute:            events.NewStringAttribute(testRoleName),
								role.UserMembershipsAttribute: membershipsMap(tc.OldMembers),
								role.PermissionsAttribute:     permissionsToMap(tc.OldPermissions),
							},
							NewImage: map[string]events.DynamoDBAttributeValue{
								role.IDAttribute:              events.NewStringAttribute(testRoleID),
								role.NameAttribute:            events.NewStringAttribute(testRoleName),
								role.UserMembershipsAttribute: membershipsMap(tc.NewMembers),
								role.PermissionsAttribute:     permissionsToMap(tc.NewPermissions),
							},
						},
					},
				},
			})
		})
	}
}
