'use strict';

const _ = require('lodash');

const { createStrapiInstance } = require('../../../../../test/helpers/strapi');
const { createAuthRequest } = require('../../../../../test/helpers/request');

const edition = process.env.STRAPI_DISABLE_EE === 'true' ? 'CE' : 'EE';

const data = {
  rolesWithUsers: [],
  rolesWithoutUsers: [],
  users: [],
  deleteRolesIds: [],
  superAdminRole: undefined,
  authorRole: undefined,
  editorRole: undefined,
};

const omitTimestamps = obj => _.omit(obj, ['updatedAt', 'createdAt']);

describe('Role CRUD End to End', () => {
  let rq;
  let strapi;

  beforeAll(async () => {
    strapi = await createStrapiInstance();
    rq = await createAuthRequest({ strapi });
  });

  afterAll(async () => {
    await strapi.destroy();
  });

  describe('Default roles', () => {
    test('Default roles are created', async () => {
      const defaultsRoles = [
        {
          name: 'Super Admin',
          code: 'strapi-super-admin',
          description: 'Super Admins can access and manage all features and settings.',
          usersCount: 1,
        },
        {
          name: 'Editor',
          code: 'strapi-editor',
          description: 'Editors can manage and publish contents including those of other users.',
          usersCount: 0,
        },
        {
          name: 'Author',
          code: 'strapi-author',
          description: 'Authors can manage the content they have created.',
          usersCount: 0,
        },
      ];

      const res = await rq({
        url: '/admin/roles',
        method: 'GET',
      });

      expect(res.statusCode).toBe(200);
      expect(res.body.data).toHaveLength(3);
      expect(res.body.data).toEqual(
        expect.arrayContaining([
          expect.objectContaining(defaultsRoles[0]),
          expect.objectContaining(defaultsRoles[1]),
          expect.objectContaining(defaultsRoles[2]),
        ])
      );
      data.superAdminRole = res.body.data.find(r => r.code === 'strapi-super-admin');
      data.authorRole = res.body.data.find(r => r.code === 'strapi-author');
      data.editorRole = res.body.data.find(r => r.code === 'strapi-editor');
    });

    test('Author have admin::is-creator condition for every permission', async () => {
      const res = await rq({
        url: `/admin/roles/${data.authorRole.id}/permissions`,
        method: 'GET',
      });

      expect(res.statusCode).toBe(200);
      expect(Array.isArray(res.body.data)).toBe(true);
      expect(res.body.data).toHaveLength(5);
      res.body.data
        .filter(p => !p.action.includes('plugin::upload'))
        .forEach(permission => {
          expect(permission.conditions).toEqual(['admin::is-creator']);
        });
    });

    test("Editor's permissions don't have any conditions", async () => {
      const res = await rq({
        url: `/admin/roles/${data.editorRole.id}/permissions`,
        method: 'GET',
      });

      expect(res.statusCode).toBe(200);
      expect(Array.isArray(res.body.data)).toBe(true);
      expect(res.body.data).toHaveLength(5);
      res.body.data
        .filter(p => !p.action.includes('plugin::upload'))
        .forEach(permission => {
          expect(permission.conditions).toEqual([]);
        });
    });

    if (edition === 'EE') {
      const newPermissions = [
        {
          action: 'plugin::users-permissions.roles.update',
        },
        {
          action: 'plugin::content-manager.explorer.create',
          subject: 'plugin::users-permissions.user',
          properties: { fields: ['username'], locales: [] },
          conditions: ['admin::is-creator'],
        },
      ];

      test('Conditions of editors and author can be modified', async () => {
        let res = await rq({
          url: `/admin/roles/${data.editorRole.id}/permissions`,
          method: 'PUT',
          body: { permissions: newPermissions },
        });

        expect(res.statusCode).toBe(200);
        expect(res.body.data).toHaveLength(2);
        expect(res.body).toEqual({
          data: expect.arrayContaining([
            expect.objectContaining({
              action: 'plugin::users-permissions.roles.update',
              properties: {},
              conditions: [],
              subject: null,
            }),
            expect.objectContaining({
              action: 'plugin::content-manager.explorer.create',
              subject: 'plugin::users-permissions.user',
              properties: { fields: ['username'], locales: [] },
              conditions: ['admin::is-creator'],
            }),
          ]),
        });

        res = await rq({
          url: `/admin/roles/${data.authorRole.id}/permissions`,
          method: 'PUT',
          body: { permissions: newPermissions },
        });

        expect(res.statusCode).toBe(200);
        expect(res.body.data).toHaveLength(2);
        expect(res.body).toEqual({
          data: expect.arrayContaining([
            expect.objectContaining({
              action: 'plugin::users-permissions.roles.update',
              properties: {},
              conditions: [],
              subject: null,
            }),
            expect.objectContaining({
              action: 'plugin::content-manager.explorer.create',
              subject: 'plugin::users-permissions.user',
              properties: { fields: ['username'], locales: [] },
              conditions: ['admin::is-creator'],
            }),
          ]),
        });
      });
    } else if (edition === 'CE') {
      const newPermissions = [
        {
          action: 'plugin::users-permissions.roles.update',
        },
        {
          action: 'plugin::users-permissions.roles.read',
          conditions: ['admin::is-creator'],
        },
        {
          action: 'plugin::content-manager.explorer.create',
          subject: 'plugin::users-permissions.user',
          properties: { fields: ['username'], locales: [] },
          conditions: ['admin::is-creator'],
        },
        {
          action: 'plugin::content-manager.explorer.update',
          subject: 'plugin::users-permissions.user',
          properties: { fields: ['username'], locales: [] },
          conditions: ['admin::is-creator'],
        },
        {
          action: 'plugin::content-manager.explorer.delete',
          subject: 'plugin::users-permissions.user',
          properties: { locales: [] },
          conditions: ['admin::is-creator'],
        },
        {
          action: 'plugin::content-manager.explorer.read',
          subject: 'plugin::users-permissions.user',
          properties: { fields: ['username'], locales: [] },
          conditions: ['admin::is-creator'],
        },
      ];

      test("Conditions of editors and author can't be modified", async () => {
        let res = await rq({
          url: `/admin/roles/${data.editorRole.id}/permissions`,
          method: 'PUT',
          body: { permissions: newPermissions },
        });

        expect(res.statusCode).toBe(200);
        expect(res.body.data).toHaveLength(6);
        expect(res.body).toEqual({
          data: expect.arrayContaining(
            newPermissions
              .slice(3, 6)
              .map(p => ({ ...p, conditions: [] }))
              .map(perm => expect.objectContaining(perm))
          ),
        });

        res = await rq({
          url: `/admin/roles/${data.authorRole.id}/permissions`,
          method: 'PUT',
          body: { permissions: newPermissions },
        });

        expect(res.statusCode).toBe(200);
        expect(res.body.data).toHaveLength(6);
        expect(res.body).toEqual({
          data: expect.arrayContaining(
            newPermissions
              .slice(3, 6)
              .map(p => ({ ...p, conditions: ['admin::is-creator'] }))
              .map(expect.objectContaining)
          ),
        });
      });
    }
  });

  if (edition === 'EE') {
    describe('Create some roles', () => {
      const rolesToCreate = [
        [{ name: 'new role 0', description: 'description' }],
        [{ name: 'new role 1', description: 'description' }],
        [{ name: 'new role 2', description: 'description' }],
        [{ name: 'new role 3', description: 'description' }],
        [{ name: 'new role 4', description: 'description' }],
        [{ name: 'new role 5', description: 'description' }],
      ];

      test.each(rolesToCreate)('can create %p', async role => {
        let res = await rq({
          url: '/admin/roles',
          method: 'POST',
          body: role,
        });

        expect(res.statusCode).toBe(201);
        expect(res.body.data).toMatchObject({
          id: expect.anything(),
          name: role.name,
          description: role.description,
        });
        data.rolesWithoutUsers.push(res.body.data);
      });
      test('Cannot create a role already existing', async () => {
        const role = _.pick(data.rolesWithoutUsers[0], ['name', 'description']);
        const res = await rq({
          url: '/admin/roles',
          method: 'POST',
          body: role,
        });

        expect(res.statusCode).toBe(400);
        expect(res.body).toMatchObject({
          data: null,
          error: {
            details: {},
            message: 'The name must be unique and a role with name `new role 0` already exists.',
            name: 'ApplicationError',
            status: 400,
          },
        });
      });
      test('Can create a user with a role', async () => {
        const user = {
          email: 'new-user@strapi.io',
          firstname: 'New',
          lastname: 'User',
          roles: [data.rolesWithoutUsers[5].id],
        };

        const res = await rq({
          url: '/admin/users',
          method: 'POST',
          body: user,
        });

        expect(res.statusCode).toBe(201);

        data.users.push(res.body.data);
        data.rolesWithUsers.push(data.rolesWithoutUsers[5]);
        data.rolesWithoutUsers.splice(5, 1);
      });
    });

    describe('Find a role', () => {
      test('Can find a role successfully', async () => {
        const res = await rq({
          url: `/admin/roles/${data.rolesWithoutUsers[0].id}`,
          method: 'GET',
        });

        expect(res.statusCode).toBe(200);
        expect(res.body.data).toMatchObject({
          id: data.rolesWithoutUsers[0].id,
          name: data.rolesWithoutUsers[0].name,
          description: data.rolesWithoutUsers[0].description,
          usersCount: 0,
          code: expect.anything(),
        });
        expect(res.body.data.code.startsWith('new-role-0')).toBe(true);
      });
    });

    describe('Find all roles', () => {
      test('Can find all roles successfully', async () => {
        const expectedRolesWithoutUser = data.rolesWithoutUsers.map(r => ({ ...r, usersCount: 0 }));
        const expectedRolesWithUser = data.rolesWithUsers.map(r => ({ ...r, usersCount: 1 }));
        const expectedRoles = expectedRolesWithoutUser.concat(expectedRolesWithUser);

        const res = await rq({
          url: '/admin/roles',
          method: 'GET',
        });

        expect(res.statusCode).toBe(200);
        expectedRoles.forEach(role => {
          expect(res.body.data).toEqual(
            expect.arrayContaining([
              expect.objectContaining({
                id: role.id,
                name: role.name,
                description: role.description,
                usersCount: role.usersCount,
                code: expect.anything(),
              }),
            ])
          );
        });
      });
    });

    describe('Update a role', () => {
      test('Can update name and description of a role successfully', async () => {
        const updates = {
          name: 'new name - Cannot update the name of a role',
          description: 'new description - Can update a role successfully',
        };
        const res = await rq({
          url: `/admin/roles/${data.rolesWithoutUsers[0].id}`,
          method: 'PUT',
          body: updates,
        });

        expect(res.statusCode).toBe(200);
        expect(omitTimestamps(res.body.data)).toMatchObject({
          ...omitTimestamps(data.rolesWithoutUsers[0]),
          ...updates,
        });
        data.rolesWithoutUsers[0] = res.body.data;
      });

      test('Can update description of a role successfully', async () => {
        const updates = {
          name: 'new name - Cannot update the name of a role',
          description: 'new description - Can update description of a role successfully',
        };
        const res = await rq({
          url: `/admin/roles/${data.rolesWithoutUsers[0].id}`,
          method: 'PUT',
          body: updates,
        });

        expect(res.statusCode).toBe(200);
        expect(omitTimestamps(res.body.data)).toMatchObject({
          ...omitTimestamps(data.rolesWithoutUsers[0]),
          ...updates,
        });
        data.rolesWithoutUsers[0] = res.body.data;
      });

      test('Cannot update the name of a role if already exists', async () => {
        const updates = {
          name: data.rolesWithoutUsers[0].name,
          description: 'new description - Cannot update the name of a role if already exists',
        };
        const res = await rq({
          url: `/admin/roles/${data.rolesWithoutUsers[1].id}`,
          method: 'PUT',
          body: updates,
        });

        expect(res.statusCode).toBe(400);
        expect(res.body).toMatchObject({
          data: null,
          error: {
            details: {},
            message:
              'The name must be unique and a role with name `new name - Cannot update the name of a role` already exists.',
            name: 'ApplicationError',
            status: 400,
          },
        });
      });

      test('Cannot update super admin role', async () => {
        const updates = {
          name: 'new name - Cannot update the name of a role',
          description: 'new description - Can update a role successfully',
        };
        const res = await rq({
          url: `/admin/roles/${data.superAdminRole.id}`,
          method: 'PUT',
          body: updates,
        });

        expect(res.statusCode).toBe(400);
      });
    });

    describe('Delete roles', () => {
      describe('batch-delete', () => {
        test("Don't delete the roles if some still have assigned users", async () => {
          const roles = [data.rolesWithUsers[0], data.rolesWithUsers[0]];
          const rolesIds = roles.map(r => r.id);
          let res = await rq({
            url: '/admin/roles/batch-delete',
            method: 'POST',
            body: { ids: rolesIds },
          });

          expect(res.statusCode).toBe(400);
          expect(res.body).toMatchObject({
            data: null,
            error: {
              details: {
                errors: [
                  {
                    message: 'Some roles are still assigned to some users',
                    name: 'ValidationError',
                    path: ['ids'],
                  },
                ],
              },
              message: 'Some roles are still assigned to some users',
              name: 'ValidationError',
              status: 400,
            },
          });

          for (let role of roles) {
            res = await rq({
              url: `/admin/roles/${role.id}`,
              method: 'GET',
            });
            expect(res.statusCode).toBe(200);
            expect(res.body.data).toMatchObject(role);
          }
        });

        test('Can delete a role', async () => {
          let res = await rq({
            url: '/admin/roles/batch-delete',
            method: 'POST',
            body: { ids: [data.rolesWithoutUsers[0].id] },
          });
          expect(res.statusCode).toBe(200);
          expect(res.body.data).toMatchObject([data.rolesWithoutUsers[0]]);

          res = await rq({
            url: `/admin/roles/${data.rolesWithoutUsers[0].id}`,
            method: 'GET',
          });
          expect(res.statusCode).toBe(404);

          data.deleteRolesIds.push(data.rolesWithoutUsers[0].id);
          data.rolesWithoutUsers.shift();
        });

        test('Can delete two roles', async () => {
          const roles = data.rolesWithoutUsers.slice(0, 2);
          const rolesIds = roles.map(r => r.id);

          let res = await rq({
            url: '/admin/roles/batch-delete',
            method: 'POST',
            body: { ids: rolesIds },
          });
          expect(res.statusCode).toBe(200);
          expect(res.body.data).toMatchObject(roles);

          for (let roleId of rolesIds) {
            res = await rq({
              url: `/admin/roles/${roleId}`,
              method: 'GET',
            });
            expect(res.statusCode).toBe(404);
            data.deleteRolesIds.push(data.rolesWithoutUsers[0].id);
            data.rolesWithoutUsers.shift();
          }
        });
      });

      describe('simple delete', () => {
        test('Can delete a role', async () => {
          let res = await rq({
            url: `/admin/roles/${data.rolesWithoutUsers[0].id}`,
            method: 'DELETE',
          });
          expect(res.statusCode).toBe(200);
          expect(res.body.data).toMatchObject(data.rolesWithoutUsers[0]);

          res = await rq({
            url: `/admin/roles/${data.rolesWithoutUsers[0].id}`,
            method: 'GET',
          });
          expect(res.statusCode).toBe(404);

          data.deleteRolesIds.push(data.rolesWithoutUsers[0].id);
          data.rolesWithoutUsers.shift();
        });

        test("Don't delete a role if it still has assigned users", async () => {
          let res = await rq({
            url: `/admin/roles/${data.rolesWithUsers[0].id}`,
            method: 'DELETE',
          });

          expect(res.statusCode).toBe(400);
          expect(res.body).toMatchObject({
            data: null,
            error: {
              details: {
                errors: [
                  {
                    message: 'Some roles are still assigned to some users',
                    name: 'ValidationError',
                    path: ['id'],
                  },
                ],
              },
              message: 'Some roles are still assigned to some users',
              name: 'ValidationError',
              status: 400,
            },
          });

          res = await rq({
            url: `/admin/roles/${data.rolesWithUsers[0].id}`,
            method: 'GET',
          });
          expect(res.statusCode).toBe(200);
          expect(res.body.data).toMatchObject(data.rolesWithUsers[0]);
        });

        test("Can't delete super admin role", async () => {
          let res = await rq({
            url: `/admin/roles/${data.superAdminRole.id}`,
            method: 'DELETE',
          });

          expect(res.statusCode).toBe(400);
          expect(res.body).toMatchObject({
            data: null,
            error: {
              details: {
                errors: [
                  {
                    message: 'You cannot delete the super admin role',
                    name: 'ValidationError',
                    path: ['id'],
                  },
                ],
              },
              message: 'You cannot delete the super admin role',
              name: 'ValidationError',
              status: 400,
            },
          });

          res = await rq({
            url: `/admin/roles/${data.rolesWithUsers[0].id}`,
            method: 'GET',
          });
          expect(res.statusCode).toBe(200);
          expect(res.body.data).toMatchObject(data.rolesWithUsers[0]);
        });
      });
    });

    describe("Roles don't exist", () => {
      test("Cannot update a role if it doesn't exist", async () => {
        const updates = {
          name: "new name - Cannot update a role if it doesn't exist",
          description: "new description - Cannot update a role if it doesn't exist",
        };
        const res = await rq({
          url: `/admin/roles/${data.deleteRolesIds[0]}`,
          method: 'PUT',
          body: updates,
        });

        expect(res.statusCode).toBe(404);
        expect(res.body).toMatchObject({
          error: {
            details: {},
            message: 'role.notFound',
            name: 'NotFoundError',
            status: 404,
          },
        });
      });

      test("Simple delete - No error if deleting a role that doesn't exist", async () => {
        const res = await rq({
          url: `/admin/roles/${data.deleteRolesIds[0]}`,
          method: 'DELETE',
        });

        expect(res.statusCode).toBe(200);
        expect(res.body.data).toEqual(null);
      });
      test("Batch Delete - No error if deleting a role that doesn't exist", async () => {
        const res = await rq({
          url: '/admin/roles/batch-delete',
          method: 'POST',
          body: { ids: [data.deleteRolesIds[0]] },
        });

        expect(res.statusCode).toBe(200);
        expect(res.body.data).toEqual([]);
      });
    });

    describe('get & update Permissions', () => {
      test('get permissions on empty role', async () => {
        const res = await rq({
          url: `/admin/roles/${data.rolesWithoutUsers[0].id}/permissions`,
          method: 'GET',
        });

        expect(res.statusCode).toBe(200);
        expect(res.body).toEqual({
          data: [],
        });
      });

      test('assign permissions on role', async () => {
        const res = await rq({
          url: `/admin/roles/${data.rolesWithoutUsers[0].id}/permissions`,
          method: 'PUT',
          body: {
            permissions: [
              {
                action: 'plugin::users-permissions.roles.update',
              },
              {
                action: 'plugin::content-manager.explorer.create',
                subject: 'plugin::users-permissions.user',
                properties: { fields: ['username'], locales: [] },
                conditions: ['admin::is-creator'],
              },
            ],
          },
        });

        expect(res.statusCode).toBe(200);
        expect(res.body.data.length > 0).toBe(true);
        res.body.data.forEach(permission => {
          expect(permission).toMatchObject({
            id: expect.anything(),
            action: expect.any(String),
            subject: expect.stringOrNull(),
          });

          if (permission.conditions.length > 0) {
            expect(permission.conditions).toEqual(expect.arrayContaining([expect.any(String)]));
          }
          if (permission.fields && permission.fields.length > 0) {
            expect(permission.fields).toEqual(expect.arrayContaining([expect.any(String)]));
          }
        });
      });

      test('assign permissions on role with an unknown condition', async () => {
        const permissions = [
          {
            action: 'plugin::users-permissions.roles.update',
          },
          {
            action: 'plugin::content-manager.explorer.create',
            subject: 'plugin::users-permissions.user',
            properties: { fields: ['username'], locales: [] },
            conditions: ['admin::is-creator'],
          },
        ];
        const res = await rq({
          url: `/admin/roles/${data.rolesWithoutUsers[0].id}/permissions`,
          method: 'PUT',
          body: {
            permissions: [
              permissions[0],
              {
                ...permissions[1],
                conditions: [...permissions[1].conditions, 'unknown-condition'],
              },
            ],
          },
        });

        expect(res.statusCode).toBe(200);
        expect(res.body.data).toHaveLength(1);
        expect(res.body.data[0]).toMatchObject(permissions[1]);
      });

      test("can't assign non-existing permissions on role", async () => {
        const res = await rq({
          url: `/admin/roles/${data.rolesWithoutUsers[0].id}/permissions`,
          method: 'PUT',
          body: {
            permissions: [
              {
                action: 'non.existing.action',
              },
            ],
          },
        });

        expect(res.statusCode).toBe(400);
        expect(res.body).toMatchObject({
          data: null,
          error: {
            details: {
              errors: [
                {
                  message: 'action is not an existing permission action',
                  name: 'ValidationError',
                  path: ['permissions', '0', 'action'],
                },
              ],
            },
            message: 'action is not an existing permission action',
            name: 'ValidationError',
            status: 400,
          },
        });
      });

      test('get permissions role', async () => {
        const res = await rq({
          url: `/admin/roles/${data.rolesWithoutUsers[0].id}/permissions`,
          method: 'GET',
        });

        expect(res.statusCode).toBe(200);
        expect(res.body.data.length > 0).toBe(true);
        res.body.data.forEach(permission => {
          expect(permission).toMatchObject({
            id: expect.anything(),
            action: expect.any(String),
            subject: expect.stringOrNull(),
          });

          if (permission.conditions.length > 0) {
            expect(permission.conditions).toEqual(expect.arrayContaining([expect.any(String)]));
          }
          if (permission.fields && permission.fields.length > 0) {
            expect(permission.fields).toEqual(expect.arrayContaining([expect.any(String)]));
          }
        });
      });
    });
  }

  if (edition === 'CE') {
    describe('Cannot create a new role', () => {
      test('Cannot create a role successfully', async () => {
        const role = {
          name: 'new role',
          description: 'Description of new role',
        };

        const res = await rq({
          url: '/admin/roles',
          method: 'POST',
          body: role,
        });

        expect(res.statusCode).toBe(405);
      });
    });
  }
});
