import {
  getCurrentMailMessageCount,
  getLatestMailMessage,
} from '@affine-test/kit/utils/cloud';
import type { INestApplication } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
import type { TestFn } from 'ava';
import ava from 'ava';

import { AppModule } from '../src/app.module';
import { AuthService } from '../src/core/auth/service';
import { UserService } from '../src/core/user';
import { MailService } from '../src/fundamentals/mailer';
import {
  acceptInviteById,
  createTestingApp,
  createWorkspace,
  getWorkspace,
  inviteUser,
  leaveWorkspace,
  revokeUser,
  signUp,
} from './utils';

const test = ava as TestFn<{
  app: INestApplication;
  client: PrismaClient;
  auth: AuthService;
  mail: MailService;
  user: UserService;
}>;

test.beforeEach(async t => {
  const { app } = await createTestingApp({
    imports: [AppModule],
  });
  t.context.app = app;
  t.context.client = app.get(PrismaClient);
  t.context.auth = app.get(AuthService);
  t.context.mail = app.get(MailService);
  t.context.user = app.get(UserService);
});

test.afterEach.always(async t => {
  await t.context.app.close();
});

test('should invite a user', async t => {
  const { app } = t.context;
  const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1');
  const u2 = await signUp(app, 'u2', 'u2@affine.pro', '1');

  const workspace = await createWorkspace(app, u1.token.token);

  const invite = await inviteUser(
    app,
    u1.token.token,
    workspace.id,
    u2.email,
    'Admin'
  );
  t.truthy(invite, 'failed to invite user');
});

test('should leave a workspace', async t => {
  const { app } = t.context;
  const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1');
  const u2 = await signUp(app, 'u2', 'u2@affine.pro', '1');

  const workspace = await createWorkspace(app, u1.token.token);
  const id = await inviteUser(
    app,
    u1.token.token,
    workspace.id,
    u2.email,
    'Admin'
  );
  await acceptInviteById(app, workspace.id, id, false);

  const leave = await leaveWorkspace(app, u2.token.token, workspace.id);

  t.pass();
  t.true(leave, 'failed to leave workspace');
});

test('should revoke a user', async t => {
  const { app } = t.context;
  const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1');
  const u2 = await signUp(app, 'u2', 'u2@affine.pro', '1');

  const workspace = await createWorkspace(app, u1.token.token);
  await inviteUser(app, u1.token.token, workspace.id, u2.email, 'Admin');

  const currWorkspace = await getWorkspace(app, u1.token.token, workspace.id);
  t.is(currWorkspace.members.length, 2, 'failed to invite user');

  const revoke = await revokeUser(app, u1.token.token, workspace.id, u2.id);
  t.true(revoke, 'failed to revoke user');
});

test('should create user if not exist', async t => {
  const { app, user } = t.context;
  const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1');

  const workspace = await createWorkspace(app, u1.token.token);

  await inviteUser(app, u1.token.token, workspace.id, 'u2@affine.pro', 'Admin');

  const u2 = await user.findUserByEmail('u2@affine.pro');
  t.not(u2, undefined, 'failed to create user');
  t.is(u2?.name, 'u2', 'failed to create user');
});

test('should invite a user by link', async t => {
  const { app } = t.context;
  const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1');
  const u2 = await signUp(app, 'u2', 'u2@affine.pro', '1');

  const workspace = await createWorkspace(app, u1.token.token);

  const invite = await inviteUser(
    app,
    u1.token.token,
    workspace.id,
    u2.email,
    'Admin'
  );

  const accept = await acceptInviteById(app, workspace.id, invite);
  t.true(accept, 'failed to accept invite');

  const invite1 = await inviteUser(
    app,
    u1.token.token,
    workspace.id,
    u2.email,
    'Admin'
  );

  t.is(invite, invite1, 'repeat the invitation must return same id');

  const currWorkspace = await getWorkspace(app, u1.token.token, workspace.id);
  const currMember = currWorkspace.members.find(u => u.email === u2.email);
  t.not(currMember, undefined, 'failed to invite user');
  t.is(currMember?.inviteId, invite, 'failed to check invite id');
});

test('should send email', async t => {
  const { mail, app } = t.context;
  if (mail.hasConfigured()) {
    const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1');
    const u2 = await signUp(app, 'test', 'production@toeverything.info', '1');

    const workspace = await createWorkspace(app, u1.token.token);
    const primitiveMailCount = await getCurrentMailMessageCount();

    const invite = await inviteUser(
      app,
      u1.token.token,
      workspace.id,
      u2.email,
      'Admin',
      true
    );

    const afterInviteMailCount = await getCurrentMailMessageCount();
    t.is(
      primitiveMailCount + 1,
      afterInviteMailCount,
      'failed to send invite email'
    );
    const inviteEmailContent = await getLatestMailMessage();

    t.not(
      inviteEmailContent.To.find((item: any) => {
        return item.Mailbox === 'production';
      }),
      undefined,
      'invite email address was incorrectly sent'
    );

    const accept = await acceptInviteById(app, workspace.id, invite, true);
    t.true(accept, 'failed to accept invite');

    const afterAcceptMailCount = await getCurrentMailMessageCount();
    t.is(
      afterInviteMailCount + 1,
      afterAcceptMailCount,
      'failed to send accepted email to owner'
    );
    const acceptEmailContent = await getLatestMailMessage();
    t.not(
      acceptEmailContent.To.find((item: any) => {
        return item.Mailbox === 'u1';
      }),
      undefined,
      'accept email address was incorrectly sent'
    );

    await leaveWorkspace(app, u2.token.token, workspace.id, true);

    const afterLeaveMailCount = await getCurrentMailMessageCount();
    t.is(
      afterAcceptMailCount + 1,
      afterLeaveMailCount,
      'failed to send leave email to owner'
    );
    const leaveEmailContent = await getLatestMailMessage();
    t.not(
      leaveEmailContent.To.find((item: any) => {
        return item.Mailbox === 'u1';
      }),
      undefined,
      'leave email address was incorrectly sent'
    );
  }
  t.pass();
});

test('should support pagination for member', async t => {
  const { app } = t.context;
  const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1');
  const u2 = await signUp(app, 'u2', 'u2@affine.pro', '1');
  const u3 = await signUp(app, 'u3', 'u3@affine.pro', '1');

  const workspace = await createWorkspace(app, u1.token.token);
  const invite1 = await inviteUser(
    app,
    u1.token.token,
    workspace.id,
    u2.email,
    'Admin'
  );
  const invite2 = await inviteUser(
    app,
    u1.token.token,
    workspace.id,
    u3.email,
    'Admin'
  );

  await acceptInviteById(app, workspace.id, invite1, false);
  await acceptInviteById(app, workspace.id, invite2, false);

  const firstPageWorkspace = await getWorkspace(
    app,
    u1.token.token,
    workspace.id,
    0,
    2
  );
  t.is(firstPageWorkspace.members.length, 2, 'failed to check invite id');
  const secondPageWorkspace = await getWorkspace(
    app,
    u1.token.token,
    workspace.id,
    2,
    2
  );
  t.is(secondPageWorkspace.members.length, 1, 'failed to check invite id');
});

test('should limit member count correctly', async t => {
  const { app } = t.context;
  const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1');
  for (let i = 0; i < 10; i++) {
    const workspace = await createWorkspace(app, u1.token.token);
    await Promise.allSettled(
      Array.from({ length: 10 }).map(async (_, i) =>
        inviteUser(
          app,
          u1.token.token,
          workspace.id,
          `u${i}@affine.pro`,
          'Admin'
        )
      )
    );

    const ws = await getWorkspace(app, u1.token.token, workspace.id);
    t.assert(ws.members.length <= 3, 'failed to check member list');
  }
});
