package beefcakeserver

import (
	"context"
	"testing"

	"code.justin.tv/beefcake/server/internal/legacyperm"
	"code.justin.tv/beefcake/server/internal/legacyperm/legacypermmocks"
	"code.justin.tv/beefcake/server/internal/optional"
	"code.justin.tv/beefcake/server/internal/perm"
	"code.justin.tv/beefcake/server/internal/role"
	"code.justin.tv/beefcake/server/internal/role/rolemocks"
	"code.justin.tv/beefcake/server/internal/user"
	"code.justin.tv/beefcake/server/internal/user/usermocks"
	"code.justin.tv/beefcake/server/rpc/beefcake"
	"github.com/golang/protobuf/ptypes/wrappers"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"
	"github.com/twitchtv/twirp"
)

type serverTest struct {
	Server            *Server
	LegacyPermissions *legacypermmocks.LegacyPermissionsAPI
	Roles             *rolemocks.RolesAPI
	Users             *usermocks.UsersAPI
}

func newServerTest() *serverTest {
	legacyPermissions := new(legacypermmocks.LegacyPermissionsAPI)
	roles := new(rolemocks.RolesAPI)
	users := new(usermocks.UsersAPI)
	return &serverTest{
		LegacyPermissions: legacyPermissions,
		Roles:             roles,
		Users:             users,
		Server: &Server{
			LegacyPermissions: legacyPermissions,
			Roles:             roles,
			Users:             users,
		},
	}
}

func (st *serverTest) Teardown(t *testing.T) {
	st.LegacyPermissions.AssertExpectations(t)
	st.Roles.AssertExpectations(t)
	st.Users.AssertExpectations(t)
}

func TestServer(t *testing.T) {
	testPermission := func() *beefcake.Permission {
		return &beefcake.Permission{Value: &beefcake.Permission_Legacy{
			Legacy: &beefcake.Permission_LegacyPermission{
				Id:   "p-id",
				Name: "p-name",
			},
		}}
	}

	t.Run("CreateRole", func(t *testing.T) {
		const testRoleID = "testRoleID"
		const testRoleName = "testRoleName"

		t.Run("success", func(t *testing.T) {
			st := newServerTest()
			defer st.Teardown(t)

			st.Roles.
				On("Create", mock.Anything, testRoleName).
				Return(&role.Role{ID: testRoleID, Name: testRoleName}, nil)

			res, err := st.Server.CreateRole(context.Background(), &beefcake.CreateRoleRequest{
				Name: testRoleName,
			})
			require.NoError(t, err)
			assert.Equal(t, &beefcake.CreateRoleResponse{
				Id:   testRoleID,
				Name: testRoleName,
			}, res)
		})
	})

	t.Run("AddRolePermission", func(t *testing.T) {
		testRoleID := func() string { return "testRoleID" }

		t.Run("success", func(t *testing.T) {
			st := newServerTest()
			defer st.Teardown(t)

			st.Roles.
				On("AddPermission", mock.Anything, testRoleID(), testPermission()).
				Return(nil)

			res, err := st.Server.AddRolePermission(context.Background(), &beefcake.AddRolePermissionRequest{
				RoleId:     testRoleID(),
				Permission: testPermission(),
			})
			require.NoError(t, err)
			assert.Equal(t, &beefcake.AddRolePermissionResponse{}, res)
		})

		t.Run("legacy permission should 400", func(t *testing.T) {
			st := newServerTest()
			defer st.Teardown(t)

			st.Roles.
				On("AddPermission", mock.Anything, testRoleID(), testPermission()).
				Return(role.ErrCannotAddLegacyPermission)

			_, err := st.Server.AddRolePermission(context.Background(), &beefcake.AddRolePermissionRequest{
				RoleId:     testRoleID(),
				Permission: testPermission(),
			})
			assert.Equal(t, twirp.InvalidArgumentError("Permission", "cannot be a legacy permission"), err)
		})
	})

	t.Run("AddLegacyPermissionToRole", func(t *testing.T) {
		const testRoleID = "testRoleID"
		const testLegacyPermID = "testLegacyPermID"

		t.Run("success", func(t *testing.T) {
			st := newServerTest()
			defer st.Teardown(t)

			st.Roles.
				On("AddLegacyPermission", mock.Anything, testRoleID, testLegacyPermID).
				Return(nil)

			res, err := st.Server.AddLegacyPermissionToRole(context.Background(), &beefcake.AddLegacyPermissionToRoleRequest{
				RoleId:             testRoleID,
				LegacyPermissionId: testLegacyPermID,
			})
			require.NoError(t, err)
			assert.Equal(t, &beefcake.AddLegacyPermissionToRoleResponse{}, res)
		})
	})

	t.Run("RemoveLegacyPermissionFromRole", func(t *testing.T) {
		const testRoleID = "testRoleID"
		const testLegacyPermID = "testLegacyPermID"

		t.Run("success", func(t *testing.T) {
			st := newServerTest()
			defer st.Teardown(t)

			st.Roles.
				On("RemoveLegacyPermission", mock.Anything, testRoleID, testLegacyPermID).
				Return(nil)

			res, err := st.Server.RemoveLegacyPermissionFromRole(context.Background(), &beefcake.RemoveLegacyPermissionFromRoleRequest{
				RoleId:             testRoleID,
				LegacyPermissionId: testLegacyPermID,
			})
			require.NoError(t, err)
			assert.Equal(t, &beefcake.RemoveLegacyPermissionFromRoleResponse{}, res)
		})
	})

	t.Run("GetRole", func(t *testing.T) {
		const testRoleID = "testRoleID"
		const testRoleName = "testRoleName"
		const testRolePermissionID = "testRolePermissionID"
		testMembership := func() *beefcake.Role_UserMembership {
			return &beefcake.Role_UserMembership{
				UserId: "my-user-id",
			}
		}
		testMemberships := func() role.UserMemberships {
			return role.UserMemberships([]*beefcake.Role_UserMembership{
				testMembership(),
			})
		}

		t.Run("success", func(t *testing.T) {
			st := newServerTest()
			defer st.Teardown(t)

			st.Roles.
				On("Get", mock.Anything, testRoleID).
				Return(&role.Role{
					ID:   testRoleID,
					Name: testRoleName,
					Permissions: perm.AttachedPermissions([]*beefcake.AttachedPermission{
						{Id: testRolePermissionID, Permission: testPermission()},
					}),
					UserMemberships: testMemberships(),
				}, nil)

			res, err := st.Server.GetRole(context.Background(), &beefcake.GetRoleRequest{
				Id: testRoleID,
			})
			require.NoError(t, err)
			assert.Equal(t, &beefcake.Role{
				Id:              testRoleID,
				Name:            testRoleName,
				UserMemberships: testMemberships(),
				Permissions: []*beefcake.AttachedPermission{
					{Id: testRolePermissionID, Permission: testPermission()},
				},
			}, res)
		})
	})

	t.Run("UpdateRole", func(t *testing.T) {
		const testRoleID = "testRoleID"
		const testRoleName = "testRoleName"
		const testLegacyPermissionID = "testLegacyPermissionID"

		mockUpdate := func(st *serverTest) *mock.Call {
			return st.Roles.
				On("Update", mock.Anything, testRoleID, role.UpdateOptions{
					Name:                optional.String(testRoleName),
					LegacyPermissionIDs: optional.StringSlice([]string{testLegacyPermissionID}),
				})
		}

		updateRole := func(st *serverTest) (*beefcake.UpdateRoleResponse, error) {
			return st.Server.UpdateRole(context.Background(), &beefcake.UpdateRoleRequest{
				Id:   testRoleID,
				Name: &wrappers.StringValue{Value: testRoleName},
				LegacyPermissionIds: &beefcake.StringArrayValue{
					Value: []string{testLegacyPermissionID},
				},
			})
		}

		t.Run("success", func(t *testing.T) {
			st := newServerTest()
			defer st.Teardown(t)

			mockUpdate(st).
				Return(nil)

			res, err := updateRole(st)
			require.NoError(t, err)
			assert.Equal(t, &beefcake.UpdateRoleResponse{}, res)
		})

		t.Run("missing should be 404", func(t *testing.T) {
			st := newServerTest()
			defer st.Teardown(t)

			mockUpdate(st).
				Return(role.ErrRoleDoesNotExist{RoleID: testRoleID})

			_, err := updateRole(st)
			assert.Equal(t, twirp.InvalidArgumentError("Id", "role does not exist"), err)
		})
	})

	t.Run("RemoveRolePermission", func(t *testing.T) {
		testRoleID := func() string { return "testRoleID" }
		testRolePermissionID := func() string { return "testRolePermissionID" }

		t.Run("success", func(t *testing.T) {
			st := newServerTest()
			defer st.Teardown(t)

			st.Roles.
				On("RemovePermission", mock.Anything, testRoleID(), testRolePermissionID()).
				Return(nil)

			res, err := st.Server.RemoveRolePermission(context.Background(), &beefcake.RemoveRolePermissionRequest{
				RoleId:           testRoleID(),
				RolePermissionId: testRolePermissionID(),
			})
			require.NoError(t, err)
			assert.Equal(t, &beefcake.RemoveRolePermissionResponse{}, res)
		})
	})

	t.Run("AddUserToRole", func(t *testing.T) {
		testRoleID := func() string { return "testRoleID" }
		testUserID := func() string { return "testUserID" }

		t.Run("success", func(t *testing.T) {
			st := newServerTest()
			defer st.Teardown(t)

			st.Roles.
				On("AddUserMembership", mock.Anything, testRoleID(), &beefcake.Role_UserMembership{
					UserId: testUserID(),
				}).
				Return(nil)

			res, err := st.Server.AddUserToRole(context.Background(), &beefcake.AddUserToRoleRequest{
				RoleId: testRoleID(),
				UserId: testUserID(),
			})
			require.NoError(t, err)
			assert.Equal(t, &beefcake.AddUserToRoleResponse{}, res)
		})
	})

	t.Run("RemoveUserFromRole", func(t *testing.T) {
		testRoleID := func() string { return "testRoleID" }
		testUserID := func() string { return "testUserID" }

		t.Run("success", func(t *testing.T) {
			st := newServerTest()
			defer st.Teardown(t)

			st.Roles.
				On("RemoveUserMembership", mock.Anything, testRoleID(), testUserID()).
				Return(nil)

			res, err := st.Server.RemoveUserFromRole(context.Background(), &beefcake.RemoveUserFromRoleRequest{
				RoleId: testRoleID(),
				UserId: testUserID(),
			})
			require.NoError(t, err)
			assert.Equal(t, &beefcake.RemoveUserFromRoleResponse{}, res)
		})
	})

	t.Run("DeleteRole", func(t *testing.T) {
		testRoleID := func() string { return "testRoleID" }

		t.Run("success", func(t *testing.T) {
			st := newServerTest()
			defer st.Teardown(t)

			st.Roles.
				On("Delete", mock.Anything, testRoleID()).
				Return(nil)

			res, err := st.Server.DeleteRole(context.Background(), &beefcake.DeleteRoleRequest{
				Id: testRoleID(),
			})
			require.NoError(t, err)
			assert.Equal(t, &beefcake.DeleteRoleResponse{}, res)
		})
	})

	t.Run("GetUser", func(t *testing.T) {
		testRole := func() *beefcake.User_RoleMembership { return &beefcake.User_RoleMembership{Id: "my-id"} }
		testUserID := func() string { return "testUserID" }
		testUserPermID := func() string { return "testUserPermID" }

		t.Run("success", func(t *testing.T) {
			st := newServerTest()
			defer st.Teardown(t)

			st.Users.
				On("Get", mock.Anything, testUserID()).
				Return(&user.User{
					ID: testUserID(),
					RoleMemberships: user.RoleMemberships([]*beefcake.User_RoleMembership{
						testRole(),
					}),
					Permissions: perm.AttachedPermissions([]*beefcake.AttachedPermission{
						{Id: testUserPermID(), Permission: testPermission()},
					}),
				}, nil)

			st.Users.
				On("UpdateAccessTime", mock.Anything, testUserID(), mock.Anything).
				Return(nil)

			res, err := st.Server.GetUser(context.Background(), &beefcake.GetUserRequest{
				Id: testUserID(),
			})
			require.NoError(t, err)
			assert.Equal(t, &beefcake.User{
				Id: testUserID(),
				RoleMemberships: []*beefcake.User_RoleMembership{
					testRole(),
				},
				Permissions: []*beefcake.AttachedPermission{
					{Id: testUserPermID(), Permission: testPermission()},
				},
			}, res)
		})
	})

	t.Run("Override", func(t *testing.T) {
		const testRoleID = "testRoleID"
		const testRoleName = "testRoleName"
		testMembership := func() *beefcake.Role_UserMembership {
			return &beefcake.Role_UserMembership{
				UserId: "my-user-id",
			}
		}

		t.Run("success", func(t *testing.T) {
			st := newServerTest()
			defer st.Teardown(t)

			st.Roles.
				On(
					"Override",
					mock.Anything,
					testRoleID,
					testRoleName,
					[]*beefcake.Permission{
						testPermission(),
					},
					[]*beefcake.Role_UserMembership{
						testMembership(),
					},
				).
				Return(nil)

			res, err := st.Server.OverrideRole(context.Background(), &beefcake.OverrideRoleRequest{
				Id:   testRoleID,
				Name: testRoleName,
				Permissions: []*beefcake.Permission{
					testPermission(),
				},
				UserMemberships: []*beefcake.Role_UserMembership{
					testMembership(),
				},
			})
			require.NoError(t, err)
			assert.Equal(t, &beefcake.OverrideRoleResponse{}, res)
		})
	})

	t.Run("CreateLegacyPermission", func(t *testing.T) {
		const testID = "testID"
		const testName = "testName"
		const testDescription = "testDescription"

		t.Run("success", func(t *testing.T) {
			st := newServerTest()
			defer st.Teardown(t)

			st.LegacyPermissions.
				On("Create", mock.Anything, testID, testName, testDescription).
				Return(nil)

			res, err := st.Server.CreateLegacyPermission(context.Background(), &beefcake.CreateLegacyPermissionRequest{
				Id:          testID,
				Name:        testName,
				Description: testDescription,
			})
			require.NoError(t, err)
			assert.Equal(t, &beefcake.CreateLegacyPermissionResponse{}, res)
		})
	})

	t.Run("GetLegacyPermissions", func(t *testing.T) {
		const testID = "testID"
		const testName = "testName"
		const testDescription = "testDescription"

		t.Run("success", func(t *testing.T) {
			st := newServerTest()
			defer st.Teardown(t)

			st.LegacyPermissions.
				On("All", mock.Anything).
				Return([]*beefcake.GetLegacyPermissionsResponse_LegacyPermission{
					{
						Id:          testID,
						Name:        testName,
						Description: testDescription,
					},
				}, nil)

			res, err := st.Server.GetLegacyPermissions(context.Background(), &beefcake.GetLegacyPermissionsRequest{})
			require.NoError(t, err)
			assert.Equal(t, &beefcake.GetLegacyPermissionsResponse{
				LegacyPermissions: []*beefcake.GetLegacyPermissionsResponse_LegacyPermission{
					{Id: testID, Name: testName, Description: testDescription},
				},
			}, res)
		})
	})

	t.Run("GetLegacyPermission", func(t *testing.T) {
		const testID = "testID"
		const testName = "testName"
		const testDescription = "testDescription"
		const testRoleID = "role-id"
		const testRoleName = "role-name"

		t.Run("success", func(t *testing.T) {
			st := newServerTest()
			defer st.Teardown(t)

			st.LegacyPermissions.
				On("Get", mock.Anything, testID).
				Return(&legacyperm.LegacyPermission{
					ID:          testID,
					Name:        testName,
					Description: testDescription,
					Roles: legacyperm.Roles([]*beefcake.LegacyPermission_Role{
						{Id: testRoleID, Name: testRoleName},
					}),
				}, nil)

			res, err := st.Server.GetLegacyPermission(context.Background(), &beefcake.GetLegacyPermissionRequest{
				Id: testID,
			})
			require.NoError(t, err)
			assert.Equal(t, &beefcake.LegacyPermission{
				Id:          testID,
				Name:        testName,
				Description: testDescription,
				Roles: []*beefcake.LegacyPermission_Role{
					{Id: testRoleID, Name: testRoleName},
				},
			}, res)
		})
	})

	t.Run("UpdateLegacyPermission", func(t *testing.T) {
		const testID = "testID"
		const testName = "testName"
		const testDescription = "testDescription"

		mockUpdate := func(st *serverTest) *mock.Call {
			return st.LegacyPermissions.
				On("Update", mock.Anything, testID, legacyperm.UpdateOptions{
					Name:        optional.String(testName),
					Description: optional.String(testDescription),
				})
		}

		mockCall := func(st *serverTest) (*beefcake.UpdateLegacyPermissionResponse, error) {
			return st.Server.UpdateLegacyPermission(context.Background(), &beefcake.UpdateLegacyPermissionRequest{
				Id:          testID,
				Name:        &wrappers.StringValue{Value: testName},
				Description: &wrappers.StringValue{Value: testDescription},
			})
		}

		t.Run("success", func(t *testing.T) {
			st := newServerTest()
			defer st.Teardown(t)

			mockUpdate(st).
				Return(nil)

			res, err := mockCall(st)
			require.NoError(t, err)
			assert.Equal(t, &beefcake.UpdateLegacyPermissionResponse{}, res)
		})

		t.Run("missing should 400", func(t *testing.T) {
			st := newServerTest()
			defer st.Teardown(t)

			mockUpdate(st).
				Return(legacyperm.ErrLegacyPermissionDoesNotExist{ID: testID})

			_, err := mockCall(st)
			assert.Equal(t, twirp.InvalidArgumentError("Id", "legacy permission does not exist"), err)
		})
	})

	t.Run("DeleteLegacyPermission", func(t *testing.T) {
		const testID = "testID"

		t.Run("success", func(t *testing.T) {
			st := newServerTest()
			defer st.Teardown(t)

			st.LegacyPermissions.
				On("Delete", mock.Anything, testID).
				Return(nil)

			res, err := st.Server.DeleteLegacyPermission(context.Background(), &beefcake.DeleteLegacyPermissionRequest{
				Id: testID,
			})
			require.NoError(t, err)
			assert.Equal(t, &beefcake.DeleteLegacyPermissionResponse{}, res)
		})
	})
}
