import { v4 as generateUUID } from 'uuid';
import nconf from 'nconf';
import {
  createAndPopulateGroup,
  generateUser,
  translate as t,
} from '../../../../helpers/api-integration/v3';

const INVITES_LIMIT = 100;
const PARTY_LIMIT_MEMBERS = 30;
const MAX_EMAIL_INVITES_BY_USER = 200;

describe('Post /groups/:groupId/invite', () => {
  let inviter;
  let group;
  const groupName = 'Test Party';

  beforeEach(async () => {
    inviter = await generateUser({ balance: 4 });
    group = await inviter.post('/groups', {
      name: groupName,
      type: 'party',
    });
  });

  describe('username invites', () => {
    it('returns an error when invited user is not found', async () => {
      const fakeID = 'fakeuserid';

      await expect(inviter.post(`/groups/${group._id}/invite`, {
        usernames: [fakeID],
      }))
        .to.eventually.be.rejected.and.eql({
          code: 404,
          error: 'NotFound',
          message: t('userWithUsernameNotFound', { username: fakeID }),
        });
    });

    it('returns an error when inviting yourself to a group', async () => {
      await expect(inviter.post(`/groups/${group._id}/invite`, {
        usernames: [inviter.auth.local.lowerCaseUsername],
      }))
        .to.eventually.be.rejected.and.eql({
          code: 400,
          error: 'BadRequest',
          message: t('cannotInviteSelfToGroup'),
        });
    });

    it('returns error when recipient has blocked the senders', async () => {
      const inviterNoBlocks = await inviter.updateOne({ 'inbox.blocks': [] });
      const userWithBlockedInviter = await generateUser({ 'inbox.blocks': [inviter._id] });
      await expect(inviterNoBlocks.post(`/groups/${group._id}/invite`, {
        usernames: [userWithBlockedInviter.auth.local.lowerCaseUsername],
      }))
        .to.eventually.be.rejected.and.eql({
          code: 401,
          error: 'NotAuthorized',
          message: t('notAuthorizedToSendMessageToThisUser'),
        });
    });

    it('invites a user to a group by username', async () => {
      const userToInvite = await generateUser();

      const response = await inviter.post(`/groups/${group._id}/invite`, {
        usernames: [userToInvite.auth.local.lowerCaseUsername],
      });
      expect(response).to.be.an('Array');
      expect(response[0]).to.have.all.keys(['_id', 'id', 'name', 'inviter']);
      expect(response[0]._id).to.be.a('String');
      expect(response[0].id).to.eql(group._id);
      expect(response[0].name).to.eql(groupName);
      expect(response[0].inviter).to.eql(inviter._id);

      await expect(userToInvite.get('/user'))
        .to.eventually.have.nested.property('invitations.parties[0].id', group._id);
    });

    it('invites multiple users to a group by uuid', async () => {
      const userToInvite = await generateUser();
      const userToInvite2 = await generateUser();

      const response = await (inviter.post(`/groups/${group._id}/invite`, {
        usernames: [
          userToInvite.auth.local.lowerCaseUsername,
          userToInvite2.auth.local.lowerCaseUsername,
        ],
      }));
      expect(response).to.be.an('Array');
      expect(response[0]).to.have.all.keys(['_id', 'id', 'name', 'inviter']);
      expect(response[0]._id).to.be.a('String');
      expect(response[0].id).to.eql(group._id);
      expect(response[0].name).to.eql(groupName);
      expect(response[0].inviter).to.eql(inviter._id);
      expect(response[1]).to.have.all.keys(['_id', 'id', 'name', 'inviter']);
      expect(response[1]._id).to.be.a('String');
      expect(response[1].id).to.eql(group._id);
      expect(response[1].name).to.eql(groupName);
      expect(response[1].inviter).to.eql(inviter._id);

      await expect(userToInvite.get('/user')).to.eventually.have.nested.property('invitations.parties[0].id', group._id);
      await expect(userToInvite2.get('/user')).to.eventually.have.nested.property('invitations.parties[0].id', group._id);
    });
  });

  describe('user id invites', () => {
    it('returns an error when inviter has no chat privileges', async () => {
      const inviterMuted = await inviter.updateOne({ 'flags.chatRevoked': true });
      const userToInvite = await generateUser();
      await expect(inviterMuted.post(`/groups/${group._id}/invite`, {
        uuids: [userToInvite._id],
      }))
        .to.eventually.be.rejected.and.eql({
          code: 401,
          error: 'NotAuthorized',
          message: t('chatPrivilegesRevoked'),
        });
    });

    it('returns an error when invited user is not found', async () => {
      const fakeID = generateUUID();

      await expect(inviter.post(`/groups/${group._id}/invite`, {
        uuids: [fakeID],
      }))
        .to.eventually.be.rejected.and.eql({
          code: 404,
          error: 'NotFound',
          message: t('userWithIDNotFound', { userId: fakeID }),
        });
    });

    it('returns an error when inviting yourself to a group', async () => {
      await expect(inviter.post(`/groups/${group._id}/invite`, {
        uuids: [inviter._id],
      }))
        .to.eventually.be.rejected.and.eql({
          code: 400,
          error: 'BadRequest',
          message: t('cannotInviteSelfToGroup'),
        });
    });

    it('returns an error when uuids is not an array', async () => {
      const fakeID = generateUUID();

      await expect(inviter.post(`/groups/${group._id}/invite`, {
        uuids: { fakeID },
      }))
        .to.eventually.be.rejected.and.eql({
          code: 400,
          error: 'BadRequest',
          message: t('uuidsMustBeAnArray'),
        });
    });

    it('returns an error when uuids and emails are empty', async () => {
      await expect(inviter.post(`/groups/${group._id}/invite`, {
        emails: [],
        uuids: [],
      }))
        .to.eventually.be.rejected.and.eql({
          code: 400,
          error: 'BadRequest',
          message: t('inviteMustNotBeEmpty'),
        });
    });

    it('returns an error when uuids is empty and emails is not passed', async () => {
      await expect(inviter.post(`/groups/${group._id}/invite`, {
        uuids: [],
      }))
        .to.eventually.be.rejected.and.eql({
          code: 400,
          error: 'BadRequest',
          message: t('inviteMustNotBeEmpty'),
        });
    });

    it('returns an error when there are more than INVITES_LIMIT uuids', async () => {
      const uuids = [];

      for (let i = 0; i < 101; i += 1) {
        uuids.push(generateUUID());
      }

      await expect(inviter.post(`/groups/${group._id}/invite`, {
        uuids,
      }))
        .to.eventually.be.rejected.and.eql({
          code: 400,
          error: 'BadRequest',
          message: t('canOnlyInviteMaxInvites', { maxInvites: INVITES_LIMIT }),
        });
    });

    it('returns error when recipient has blocked the senders', async () => {
      const inviterNoBlocks = await inviter.updateOne({ 'inbox.blocks': [] });
      const userWithBlockedInviter = await generateUser({ 'inbox.blocks': [inviter._id] });
      await expect(inviterNoBlocks.post(`/groups/${group._id}/invite`, {
        uuids: [userWithBlockedInviter._id],
      }))
        .to.eventually.be.rejected.and.eql({
          code: 401,
          error: 'NotAuthorized',
          message: t('notAuthorizedToSendMessageToThisUser'),
        });
    });

    it('invites a user to a group by uuid', async () => {
      const userToInvite = await generateUser();

      const response = await inviter.post(`/groups/${group._id}/invite`, {
        uuids: [userToInvite._id],
      });
      expect(response).to.be.an('Array');
      expect(response[0]).to.have.all.keys(['_id', 'id', 'name', 'inviter']);
      expect(response[0]._id).to.be.a('String');
      expect(response[0].id).to.eql(group._id);
      expect(response[0].name).to.eql(groupName);
      expect(response[0].inviter).to.eql(inviter._id);

      await expect(userToInvite.get('/user'))
        .to.eventually.have.nested.property('invitations.parties[0].id', group._id);
    });

    it('invites multiple users to a group by uuid', async () => {
      const userToInvite = await generateUser();
      const userToInvite2 = await generateUser();

      const response = await inviter.post(`/groups/${group._id}/invite`, {
        uuids: [userToInvite._id, userToInvite2._id],
      });

      expect(response).to.be.an('Array');
      expect(response[0]).to.have.all.keys(['_id', 'id', 'name', 'inviter']);
      expect(response[0]._id).to.be.a('String');
      expect(response[0].id).to.eql(group._id);
      expect(response[0].name).to.eql(groupName);
      expect(response[0].inviter).to.eql(inviter._id);
      expect(response[1]).to.have.all.keys(['_id', 'id', 'name', 'inviter']);
      expect(response[1]._id).to.be.a('String');
      expect(response[1].id).to.eql(group._id);
      expect(response[1].name).to.eql(groupName);
      expect(response[1].inviter).to.eql(inviter._id);

      await expect(userToInvite.get('/user')).to.eventually.have.nested.property('invitations.parties[0].id', group._id);
      await expect(userToInvite2.get('/user')).to.eventually.have.nested.property('invitations.parties[0].id', group._id);
    });

    it('returns an error when inviting multiple users and a user is not found', async () => {
      const userToInvite = await generateUser();
      const fakeID = generateUUID();

      await expect(inviter.post(`/groups/${group._id}/invite`, {
        uuids: [userToInvite._id, fakeID],
      }))
        .to.eventually.be.rejected.and.eql({
          code: 404,
          error: 'NotFound',
          message: t('userWithIDNotFound', { userId: fakeID }),
        });
    });
  });

  describe('email invites', () => {
    const testInvite = { name: 'test', email: 'test@habitica.com' };

    it('returns an error when inviter has no chat privileges', async () => {
      const inviterMuted = await inviter.updateOne({ 'flags.chatRevoked': true });
      await expect(inviterMuted.post(`/groups/${group._id}/invite`, {
        emails: [testInvite],
        inviter: 'inviter name',
      }))
        .to.eventually.be.rejected.and.eql({
          code: 401,
          error: 'NotAuthorized',
          message: t('chatPrivilegesRevoked'),
        });
    });

    it('returns an error when invite is missing an email', async () => {
      await expect(inviter.post(`/groups/${group._id}/invite`, {
        emails: [{ name: 'test' }],
      }))
        .to.eventually.be.rejected.and.eql({
          code: 400,
          error: 'BadRequest',
          message: t('inviteMissingEmail'),
        });
    });

    it('returns an error when emails is not an array', async () => {
      await expect(inviter.post(`/groups/${group._id}/invite`, {
        emails: { testInvite },
      }))
        .to.eventually.be.rejected.and.eql({
          code: 400,
          error: 'BadRequest',
          message: t('emailsMustBeAnArray'),
        });
    });

    it('returns an error when emails is empty and uuids is not passed', async () => {
      await expect(inviter.post(`/groups/${group._id}/invite`, {
        emails: [],
      }))
        .to.eventually.be.rejected.and.eql({
          code: 400,
          error: 'BadRequest',
          message: t('inviteMustNotBeEmpty'),
        });
    });

    it('returns an error when there are more than INVITES_LIMIT emails', async () => {
      const emails = [];

      for (let i = 0; i < 101; i += 1) {
        emails.push(`${generateUUID()}@habitica.com`);
      }

      await expect(inviter.post(`/groups/${group._id}/invite`, {
        emails,
      }))
        .to.eventually.be.rejected.and.eql({
          code: 400,
          error: 'BadRequest',
          message: t('canOnlyInviteMaxInvites', { maxInvites: INVITES_LIMIT }),
        });
    });

    it('returns an error when a user has sent the max number of email invites', async () => {
      const inviterWithMax = await generateUser({
        invitesSent: MAX_EMAIL_INVITES_BY_USER,
        balance: 4,
      });

      await expect(inviterWithMax.post(`/groups/${group._id}/invite`, {
        emails: [testInvite],
        inviter: 'inviter name',
      }))
        .to.eventually.be.rejected.and.eql({
          code: 401,
          error: 'NotAuthorized',
          message: t('inviteLimitReached', { techAssistanceEmail: nconf.get('EMAILS_TECH_ASSISTANCE_EMAIL') }),
        });
    });

    it('invites a user to a group by email', async () => {
      const res = await inviter.post(`/groups/${group._id}/invite`, {
        emails: [testInvite],
        inviter: 'inviter name',
      });

      const updatedUser = await inviter.sync();

      expect(res).to.exist;
      expect(updatedUser.invitesSent).to.eql(1);
    });

    it('invites multiple users to a group by email', async () => {
      const res = await inviter.post(`/groups/${group._id}/invite`, {
        emails: [testInvite, { name: 'test2', email: 'test2@habitica.com' }],
      });

      const updatedUser = await inviter.sync();

      expect(res).to.exist;
      expect(updatedUser.invitesSent).to.eql(2);
    });
  });

  describe('user and email invites', () => {
    it('returns an error when emails and uuids are not provided', async () => {
      await expect(inviter.post(`/groups/${group._id}/invite`))
        .to.eventually.be.rejected.and.eql({
          code: 400,
          error: 'BadRequest',
          message: t('canOnlyInviteEmailUuid'),
        });
    });

    it('returns an error when there are more than INVITES_LIMIT uuids and emails', async () => {
      const emails = [];
      const uuids = [];

      for (let i = 0; i < 50; i += 1) {
        emails.push(`${generateUUID()}@habitica.com`);
      }

      for (let i = 0; i < 51; i += 1) {
        uuids.push(generateUUID());
      }

      await expect(inviter.post(`/groups/${group._id}/invite`, {
        emails,
        uuids,
      }))
        .to.eventually.be.rejected.and.eql({
          code: 400,
          error: 'BadRequest',
          message: t('canOnlyInviteMaxInvites', { maxInvites: INVITES_LIMIT }),
        });
    });

    it('invites users to a group by uuid and email', async () => {
      const newUser = await generateUser();
      const invite = await inviter.post(`/groups/${group._id}/invite`, {
        uuids: [newUser._id],
        emails: [{ name: 'test', email: 'test@habitica.com' }],
      });
      const invitedUser = await newUser.get('/user');

      expect(invitedUser.invitations.parties[0].id).to.equal(group._id);
      expect(invite).to.exist;
    });

    it('invites user to group with cancelled plan', async () => {
      let cancelledPlanGroup;
      ({ group: cancelledPlanGroup, groupLeader: inviter } = await createAndPopulateGroup({
        upgradeToGroupPlan: true,
      }));
      await cancelledPlanGroup.createCancelledSubscription();

      const newUser = await generateUser();
      const invite = await inviter.post(`/groups/${cancelledPlanGroup._id}/invite`, {
        uuids: [newUser._id],
        emails: [{ name: 'test', email: 'test@habitica.com' }],
      });
      const invitedUser = await newUser.get('/user');

      expect(invitedUser.invitations.parties[0].id).to.equal(cancelledPlanGroup._id);
      expect(invitedUser.invitations.parties[0].cancelledPlan).to.be.true;
      expect(invite).to.exist;
    });
  });

  describe('party invites', () => {
    it('returns an error when inviter has no chat privileges', async () => {
      const inviterMuted = await inviter.updateOne({ 'flags.chatRevoked': true });
      const userToInvite = await generateUser();
      await expect(inviterMuted.post(`/groups/${group._id}/invite`, {
        uuids: [userToInvite._id],
      }))
        .to.eventually.be.rejected.and.eql({
          code: 401,
          error: 'NotAuthorized',
          message: t('chatPrivilegesRevoked'),
        });
    });

    it('returns an error when invited user has a pending invitation to the party', async () => {
      const userToInvite = await generateUser();
      await inviter.post(`/groups/${group._id}/invite`, {
        uuids: [userToInvite._id],
      });

      await expect(inviter.post(`/groups/${group._id}/invite`, {
        uuids: [userToInvite._id],
      }))
        .to.eventually.be.rejected.and.eql({
          code: 401,
          error: 'NotAuthorized',
          message: t('userAlreadyPendingInvitation', { userId: userToInvite._id, username: userToInvite.profile.name }),
        });
    });

    it('returns an error when invited user is already in a party of more than 1 member', async () => {
      const userToInvite = await generateUser();
      const userToInvite2 = await generateUser();
      await inviter.post(`/groups/${group._id}/invite`, {
        uuids: [userToInvite._id, userToInvite2._id],
      });
      await userToInvite.post(`/groups/${group._id}/join`);
      await userToInvite2.post(`/groups/${group._id}/join`);

      await expect(inviter.post(`/groups/${group._id}/invite`, {
        uuids: [userToInvite._id],
      }))
        .to.eventually.be.rejected.and.eql({
          code: 401,
          error: 'NotAuthorized',
          message: t('userAlreadyInAParty', { userId: userToInvite._id, username: userToInvite.profile.name }),
        });
    });

    it('allows inviting a user to 2 different parties', async () => {
      // Create another inviter
      const inviter2 = await generateUser();

      // Create user to invite
      const userToInvite = await generateUser();

      // Create second group
      const party2 = await inviter2.post('/groups', {
        name: 'Test Party 2',
        type: 'party',
      });

      // Invite to first party
      await inviter.post(`/groups/${group._id}/invite`, {
        uuids: [userToInvite._id],
      });

      // Invite to second party
      await inviter2.post(`/groups/${party2._id}/invite`, {
        uuids: [userToInvite._id],
      });

      // Get updated user
      const invitedUser = await userToInvite.get('/user');

      expect(invitedUser.invitations.parties.length).to.equal(2);
      expect(invitedUser.invitations.parties[0].id).to.equal(group._id);
      expect(invitedUser.invitations.parties[1].id).to.equal(party2._id);
    });

    it('allows inviting a user if party id is not associated with a real party', async () => {
      const userToInvite = await generateUser({
        party: { _id: generateUUID() },
      });

      await inviter.post(`/groups/${group._id}/invite`, {
        uuids: [userToInvite._id],
      });
      expect((await userToInvite.get('/user')).invitations.parties[0].id).to.equal(group._id);
    });
  });

  describe('party size limits', () => {
    let partyLeader;

    beforeEach(async () => {
      ({ group, groupLeader: partyLeader } = await createAndPopulateGroup({
        groupDetails: {
          name: 'Test Party',
          type: 'party',
          privacy: 'private',
        },
        // Generate party with 20 members
        members: PARTY_LIMIT_MEMBERS - 10,
      }));
    });

    it('allows 30 members in a party', async () => {
      const invitesToGenerate = [];
      // Generate 10 new invites
      for (let i = 1; i < 10; i += 1) {
        invitesToGenerate.push(generateUser());
      }
      const generatedInvites = await Promise.all(invitesToGenerate);
      // Invite users
      expect(await partyLeader.post(`/groups/${group._id}/invite`, {
        uuids: generatedInvites.map(invite => invite._id),
      })).to.be.an('array');
    }).timeout(10000);

    it('does not allow >30 members in a party', async () => {
      const invitesToGenerate = [];
      // Generate 11 invites
      for (let i = 1; i < 11; i += 1) {
        invitesToGenerate.push(generateUser());
      }
      const generatedInvites = await Promise.all(invitesToGenerate);
      // Invite users
      await expect(partyLeader.post(`/groups/${group._id}/invite`, {
        uuids: generatedInvites.map(invite => invite._id),
      }))
        .to.eventually.be.rejected.and.eql({
          code: 400,
          error: 'BadRequest',
          message: t('partyExceedsMembersLimit', { maxMembersParty: PARTY_LIMIT_MEMBERS }),
        });
    }).timeout(10000);
  });
});
