// +build e2e

package e2e

import (
	"context"
	"testing"
	"time"

	"code.justin.tv/beefcake/server/rpc/beefcake"
	"github.com/gofrs/uuid"
	"github.com/golang/protobuf/ptypes/wrappers"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/twitchtv/twirp"
)

func TestTwirp(t *testing.T) {
	const testRoleName = "test-role-name"

	testID := func(t *testing.T) string {
		id, err := uuid.NewV4()
		require.NoError(t, err)
		return id.String()
	}

	testPermission := func() *beefcake.Permission {
		return &beefcake.Permission{Value: &beefcake.Permission_TwitchUser{
			TwitchUser: &beefcake.Permission_TwitchUserPermission{
				Action: beefcake.Permission_TwitchUserPermission_GET_PII,
				Resource: &beefcake.Permission_TwitchUserPermission_TwitchUserId{
					TwitchUserId: "test",
				},
			},
		}}
	}

	waitUntilCondition := func(t *testing.T, cond func(ctx context.Context) bool) {
		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()

		for {
			select {
			case <-ctx.Done():
				require.True(t, false, "timed out waiting for condition")
			case <-time.After(100 * time.Millisecond):
			}

			if cond(ctx) {
				return
			}
		}
	}

	waitUntilUserPermissions := func(t *testing.T, userID string, expected []*beefcake.AttachedPermission) {
		var returned []*beefcake.AttachedPermission

		waitUntilCondition(t, func(ctx context.Context) bool {
			res, err := et.Beefcake.GetUser(ctx, &beefcake.GetUserRequest{
				Id: userID,
			})
			require.NoError(t, err, "Error looking up permission")
			returned = res.GetPermissions()

			return len(returned) == len(expected)
		})

		assert.Equal(t, expected, returned)
	}

	waitUntilUserMemberships := func(t *testing.T, userID string, expected []*beefcake.User_RoleMembership) {
		var returned []*beefcake.User_RoleMembership

		waitUntilCondition(t, func(ctx context.Context) bool {
			res, err := et.Beefcake.GetUser(ctx, &beefcake.GetUserRequest{
				Id: userID,
			})
			require.NoError(t, err, "Error looking up memberships")
			returned = res.GetRoleMemberships()

			t.Logf("waiting for memberships for user<%v>: <%v>. have: <%v>", userID, expected, returned)

			return len(returned) == len(expected)
		})

		assert.Equal(t, expected, returned)
	}

	t.Run("Single role, permission, and membership", func(t *testing.T) {
		t.Parallel()

		rolePermID := func(t *testing.T, roleID string) string {
			role, err := et.Beefcake.GetRole(context.Background(), &beefcake.GetRoleRequest{Id: roleID})
			require.NoError(t, err, "getting our role")

			require.Len(t, role.GetPermissions(), 1)
			rolePermID := role.GetPermissions()[0].GetId()
			assert.NotEmpty(t, rolePermID)
			return rolePermID
		}

		initialState := func(t *testing.T, userID string) (roleID string) {
			waitUntilUserPermissions(t, userID, nil)

			role, err := et.Beefcake.CreateRole(context.Background(), &beefcake.CreateRoleRequest{
				Name: testRoleName,
			})
			require.NoError(t, err, "creating role should succeed")
			require.NotEmpty(t, role.Id)
			return role.Id
		}

		addPermission := func(t *testing.T, roleID string) {
			_, err := et.Beefcake.AddRolePermission(context.Background(), &beefcake.AddRolePermissionRequest{
				RoleId:     roleID,
				Permission: testPermission(),
			})
			require.NoError(t, err, "adding initial role perms should succeed")
		}

		removePermission := func(t *testing.T, roleID string) {
			_, err := et.Beefcake.RemoveRolePermission(context.Background(), &beefcake.RemoveRolePermissionRequest{
				RoleId:           roleID,
				RolePermissionId: rolePermID(t, roleID),
			})
			require.NoError(t, err, "removing permission should succeed")
		}

		addMembership := func(t *testing.T, roleID, userID string) {
			_, err := et.Beefcake.AddUserToRole(context.Background(), &beefcake.AddUserToRoleRequest{
				RoleId: roleID,
				UserId: userID,
			})
			require.NoError(t, err, "adding user to role should succeed")
		}

		removeMembership := func(t *testing.T, roleID, userID string) {
			_, err := et.Beefcake.RemoveUserFromRole(context.Background(), &beefcake.RemoveUserFromRoleRequest{
				RoleId: roleID,
				UserId: userID,
			})
			require.NoError(t, err, "removing user from role should succeed")
		}

		deleteRole := func(t *testing.T, roleID string) {
			_, err := et.Beefcake.DeleteRole(context.Background(), &beefcake.DeleteRoleRequest{Id: roleID})
			require.NoError(t, err)
		}

		assertUserHasPermission := func(t *testing.T, roleID, userID string) {
			waitUntilUserPermissions(t, userID, []*beefcake.AttachedPermission{
				{Id: rolePermID(t, roleID), Permission: testPermission()},
			})
		}

		assertUserHasMembership := func(t *testing.T, roleID, userID string) {
			waitUntilUserMemberships(t, userID, []*beefcake.User_RoleMembership{
				{Id: roleID, Name: testRoleName},
			})
		}

		t.Run("add role, permission, then membership then remove.", func(t *testing.T) {
			t.Parallel()

			userID := testID(t)
			roleID := initialState(t, userID)

			addPermission(t, roleID)
			addMembership(t, roleID, userID)
			assertUserHasPermission(t, roleID, userID)
			assertUserHasMembership(t, roleID, userID)

			removeMembership(t, roleID, userID)
			waitUntilUserPermissions(t, userID, nil)
			waitUntilUserMemberships(t, userID, nil)

			removePermission(t, roleID)
			deleteRole(t, roleID)
		})

		t.Run("add role, membership, permission then remove.", func(t *testing.T) {
			t.Parallel()

			userID := testID(t)
			roleID := initialState(t, userID)

			addMembership(t, roleID, userID)
			assertUserHasMembership(t, roleID, userID)

			addPermission(t, roleID)
			assertUserHasPermission(t, roleID, userID)

			removePermission(t, roleID)
			waitUntilUserPermissions(t, userID, nil)
			assertUserHasMembership(t, roleID, userID)

			removeMembership(t, roleID, userID)
			waitUntilUserPermissions(t, userID, nil)
			waitUntilUserMemberships(t, userID, nil)
			deleteRole(t, roleID)
		})

		t.Run("add membership, permission, then delete role.", func(t *testing.T) {
			t.Parallel()

			userID := testID(t)
			roleID := initialState(t, userID)

			addMembership(t, roleID, userID)
			addPermission(t, roleID)
			assertUserHasMembership(t, roleID, userID)
			assertUserHasPermission(t, roleID, userID)

			deleteRole(t, roleID)
			waitUntilUserPermissions(t, userID, nil)
			waitUntilUserMemberships(t, userID, nil)
		})

		t.Run("Override add then delete role.", func(t *testing.T) {
			t.Parallel()

			roleID := testID(t)
			userID := testID(t)

			_, err := et.Beefcake.OverrideRole(context.Background(), &beefcake.OverrideRoleRequest{
				Id:   roleID,
				Name: testRoleName,
				Permissions: []*beefcake.Permission{
					testPermission(),
				},
				UserMemberships: []*beefcake.Role_UserMembership{
					{UserId: userID},
				},
			})
			require.NoError(t, err)

			assertUserHasMembership(t, roleID, userID)
			assertUserHasPermission(t, roleID, userID)

			_, err = et.Beefcake.OverrideRole(context.Background(), &beefcake.OverrideRoleRequest{
				Id:              roleID,
				Name:            testRoleName,
				Permissions:     []*beefcake.Permission{},
				UserMemberships: []*beefcake.Role_UserMembership{},
			})
			require.NoError(t, err)

			waitUntilUserPermissions(t, userID, nil)
			waitUntilUserMemberships(t, userID, nil)

			deleteRole(t, roleID)
		})
	})

	t.Run("Single role, legacy permission, and membership", func(t *testing.T) {
		t.Parallel()

		const testLegacyPermissionName = "test-legacy-permission-name"
		const testLegacyPermissionDescription = "test-legacy-permission-description"

		initialState := func(t *testing.T, userID string) (roleID string) {
			waitUntilUserPermissions(t, userID, nil)

			role, err := et.Beefcake.CreateRole(context.Background(), &beefcake.CreateRoleRequest{
				Name: testRoleName,
			})
			require.NoError(t, err, "creating role should succeed")
			require.NotEmpty(t, role.Id)
			return role.Id
		}

		createLegacyPermission := func(t *testing.T, legacyPermID string) {
			_, err := et.Beefcake.CreateLegacyPermission(context.Background(), &beefcake.CreateLegacyPermissionRequest{
				Id:          legacyPermID,
				Name:        testLegacyPermissionName,
				Description: testLegacyPermissionDescription,
			})
			require.NoError(t, err, "creating legacy permission should succeed")
		}

		deleteLegacyPermission := func(t *testing.T, legacyPermID string) {
			_, err := et.Beefcake.DeleteLegacyPermission(context.Background(), &beefcake.DeleteLegacyPermissionRequest{
				Id: legacyPermID,
			})
			require.NoError(t, err, "delete legacy permission should succeed")
		}

		addPermission := func(t *testing.T, roleID, legacyPermID string) {
			_, err := et.Beefcake.AddLegacyPermissionToRole(context.Background(), &beefcake.AddLegacyPermissionToRoleRequest{
				RoleId:             roleID,
				LegacyPermissionId: legacyPermID,
			})
			require.NoError(t, err, "adding initial role perms should succeed")
		}

		removePermission := func(t *testing.T, roleID, legacyPermID string) {
			_, err := et.Beefcake.RemoveLegacyPermissionFromRole(context.Background(), &beefcake.RemoveLegacyPermissionFromRoleRequest{
				RoleId:             roleID,
				LegacyPermissionId: legacyPermID,
			})
			require.NoError(t, err, "removing permission should succeed")
		}

		addMembership := func(t *testing.T, roleID, userID string) {
			_, err := et.Beefcake.AddUserToRole(context.Background(), &beefcake.AddUserToRoleRequest{
				RoleId: roleID,
				UserId: userID,
			})
			require.NoError(t, err, "adding user to role should succeed")
		}

		removeMembership := func(t *testing.T, roleID, userID string) {
			_, err := et.Beefcake.RemoveUserFromRole(context.Background(), &beefcake.RemoveUserFromRoleRequest{
				RoleId: roleID,
				UserId: userID,
			})
			require.NoError(t, err, "removing user from role should succeed")
		}

		deleteRole := func(t *testing.T, roleID string) {
			_, err := et.Beefcake.DeleteRole(context.Background(), &beefcake.DeleteRoleRequest{Id: roleID})
			require.NoError(t, err)
		}

		assertLegacyPermissionExists := func(t *testing.T, legacyPermID string) {
			waitUntilCondition(t, func(ctx context.Context) bool {
				res, err := et.Beefcake.GetLegacyPermissions(ctx, &beefcake.GetLegacyPermissionsRequest{})
				require.NoError(t, err)
				for _, p := range res.GetLegacyPermissions() {
					if p.GetId() == legacyPermID {
						return true
					}
				}
				return false
			})

			res, err := et.Beefcake.GetLegacyPermission(context.Background(), &beefcake.GetLegacyPermissionRequest{
				Id: legacyPermID,
			})
			require.NoError(t, err)
			require.Equal(t, res.GetId(), legacyPermID)
		}

		assertUserHasPermission := func(t *testing.T, roleID, userID, legacyPermID string) {
			waitUntilUserPermissions(t, userID, []*beefcake.AttachedPermission{
				{
					Id: "legacy:" + legacyPermID,
					Permission: &beefcake.Permission{
						Value: &beefcake.Permission_Legacy{
							Legacy: &beefcake.Permission_LegacyPermission{
								Id:   legacyPermID,
								Name: testLegacyPermissionName,
							},
						},
					},
				},
			})
		}

		assertUserHasMembership := func(t *testing.T, roleID, userID string) {
			waitUntilUserMemberships(t, userID, []*beefcake.User_RoleMembership{
				{Id: roleID, Name: testRoleName},
			})
		}

		t.Run("add role, permission, then membership then remove.", func(t *testing.T) {
			t.Parallel()

			userID := testID(t)
			legacyPermID := testID(t)

			roleID := initialState(t, userID)
			createLegacyPermission(t, legacyPermID)
			assertLegacyPermissionExists(t, legacyPermID)

			addPermission(t, roleID, legacyPermID)
			addMembership(t, roleID, userID)
			assertUserHasPermission(t, roleID, userID, legacyPermID)
			assertUserHasMembership(t, roleID, userID)

			removeMembership(t, roleID, userID)
			waitUntilUserPermissions(t, userID, nil)
			waitUntilUserMemberships(t, userID, nil)

			deleteRole(t, roleID)
			deleteLegacyPermission(t, legacyPermID)
		})

		t.Run("add role, membership, then permission then remove.", func(t *testing.T) {
			t.Parallel()

			userID := testID(t)
			legacyPermID := testID(t)

			roleID := initialState(t, userID)
			createLegacyPermission(t, legacyPermID)
			assertLegacyPermissionExists(t, legacyPermID)

			addMembership(t, roleID, userID)
			assertUserHasMembership(t, roleID, userID)

			addPermission(t, roleID, legacyPermID)
			assertUserHasMembership(t, roleID, userID)
			assertUserHasPermission(t, roleID, userID, legacyPermID)

			removePermission(t, roleID, legacyPermID)
			waitUntilUserPermissions(t, userID, nil)

			removeMembership(t, roleID, userID)
			waitUntilUserMemberships(t, userID, nil)

			deleteRole(t, roleID)
			deleteLegacyPermission(t, legacyPermID)
		})

		t.Run("add role, membership, then permission then delete role.", func(t *testing.T) {
			t.Parallel()

			userID := testID(t)
			legacyPermID := testID(t)

			roleID := initialState(t, userID)
			createLegacyPermission(t, legacyPermID)
			assertLegacyPermissionExists(t, legacyPermID)

			addPermission(t, roleID, legacyPermID)
			addMembership(t, roleID, userID)
			assertUserHasMembership(t, roleID, userID)
			assertUserHasPermission(t, roleID, userID, legacyPermID)

			deleteRole(t, roleID)
			waitUntilUserPermissions(t, userID, nil)
			waitUntilUserMemberships(t, userID, nil)

			deleteLegacyPermission(t, legacyPermID)
		})

		t.Run("add role, membership, then permission then delete permission.", func(t *testing.T) {
			t.Parallel()

			userID := testID(t)
			legacyPermID := testID(t)

			roleID := initialState(t, userID)
			createLegacyPermission(t, legacyPermID)

			addPermission(t, roleID, legacyPermID)
			addMembership(t, roleID, userID)
			assertUserHasMembership(t, roleID, userID)
			assertUserHasPermission(t, roleID, userID, legacyPermID)

			deleteLegacyPermission(t, legacyPermID)
			waitUntilUserPermissions(t, userID, nil)

			removeMembership(t, roleID, userID)
			waitUntilUserMemberships(t, userID, nil)

			deleteRole(t, roleID)
		})

		t.Run("add role, membership then update for permission.", func(t *testing.T) {
			t.Parallel()

			userID := testID(t)
			legacyPermID := testID(t)

			roleID := initialState(t, userID)
			addMembership(t, roleID, userID)
			createLegacyPermission(t, legacyPermID)

			_, err := et.Beefcake.UpdateRole(context.Background(), &beefcake.UpdateRoleRequest{
				Id:                  roleID,
				Name:                &wrappers.StringValue{Value: testRoleName},
				LegacyPermissionIds: &beefcake.StringArrayValue{Value: []string{legacyPermID}},
			})
			require.NoError(t, err)

			assertUserHasMembership(t, roleID, userID)
			assertUserHasPermission(t, roleID, userID, legacyPermID)

			_, err = et.Beefcake.UpdateRole(context.Background(), &beefcake.UpdateRoleRequest{
				Id:                  roleID,
				Name:                &wrappers.StringValue{Value: testRoleName},
				LegacyPermissionIds: &beefcake.StringArrayValue{Value: []string{}},
			})
			require.NoError(t, err)
			waitUntilUserPermissions(t, userID, nil)

			removeMembership(t, roleID, userID)
			waitUntilUserMemberships(t, userID, nil)

			deleteRole(t, roleID)
		})
	})

	t.Run("Role crud", func(t *testing.T) {
		createRole := func(t *testing.T, name string) (id string) {
			res, err := et.Beefcake.CreateRole(context.Background(), &beefcake.CreateRoleRequest{
				Name: name,
			})
			require.NoError(t, err)
			return res.GetId()
		}

		updateRole := func(t *testing.T, id, name string) {
			_, err := et.Beefcake.UpdateRole(context.Background(), &beefcake.UpdateRoleRequest{
				Id:   id,
				Name: &wrappers.StringValue{Value: name},
			})
			require.NoError(t, err)
		}

		assertRoleDetails := func(t *testing.T, id, name string) {
			waitUntilCondition(t, func(ctx context.Context) bool {
				res, err := et.Beefcake.GetRole(ctx, &beefcake.GetRoleRequest{
					Id: id,
				})
				require.NoError(t, err)
				return res.Name == name
			})
		}

		deleteRole := func(t *testing.T, id string) {
			_, err := et.Beefcake.DeleteRole(context.Background(), &beefcake.DeleteRoleRequest{
				Id: id,
			})
			require.NoError(t, err)
		}

		t.Run("Create, read, update, read, delete", func(t *testing.T) {
			testRoleName1 := "testRoleName1"
			testRoleName2 := "testRoleName2"

			id := createRole(t, testRoleName1)
			assertRoleDetails(t, id, testRoleName1)
			updateRole(t, id, testRoleName2)
			assertRoleDetails(t, id, testRoleName2)
			deleteRole(t, id)
		})
	})

	t.Run("Legacy permission crud", func(t *testing.T) {
		t.Parallel()

		const testName = "test-name"
		const testDescription = "test-description"

		createPerm := func(t *testing.T, id string) {
			_, err := et.Beefcake.CreateLegacyPermission(context.Background(), &beefcake.CreateLegacyPermissionRequest{
				Id:          id,
				Name:        testName,
				Description: testDescription,
			})
			require.NoError(t, err)
		}

		readPerm := func(t *testing.T, id, name, description string) {
			waitUntilCondition(t, func(ctx context.Context) bool {
				p, err := et.Beefcake.GetLegacyPermission(ctx, &beefcake.GetLegacyPermissionRequest{
					Id: id,
				})
				require.NoError(t, err)
				return (p.Name == name) && (p.Description == description)
			})
		}

		updatePerm := func(t *testing.T, id, name, description string) {
			_, err := et.Beefcake.UpdateLegacyPermission(context.Background(), &beefcake.UpdateLegacyPermissionRequest{
				Id:          id,
				Name:        &wrappers.StringValue{Value: name},
				Description: &wrappers.StringValue{Value: description},
			})
			require.NoError(t, err)
		}

		deletePerm := func(t *testing.T, id string) {
			_, err := et.Beefcake.DeleteLegacyPermission(context.Background(), &beefcake.DeleteLegacyPermissionRequest{
				Id: id,
			})
			require.NoError(t, err)
		}

		t.Run("Create, read, update, read, delete.", func(t *testing.T) {
			const newName = "my-new-name"
			const newDescription = "my-new-description"

			legacyPermID := testID(t)
			createPerm(t, legacyPermID)
			readPerm(t, legacyPermID, testName, testDescription)
			updatePerm(t, legacyPermID, newName, newDescription)
			readPerm(t, legacyPermID, newName, newDescription)
			deletePerm(t, legacyPermID)
		})

		t.Run("Update before create should 400", func(t *testing.T) {
			legacyPermID := testID(t)

			_, err := et.Beefcake.UpdateLegacyPermission(context.Background(), &beefcake.UpdateLegacyPermissionRequest{
				Id:          legacyPermID,
				Name:        &wrappers.StringValue{Value: testName},
				Description: &wrappers.StringValue{Value: testDescription},
			})
			assert.Equal(t, twirp.InvalidArgumentError("Id", "legacy permission does not exist"), err)
		})
	})
}
