import { ok } from 'node:assert';
import { randomUUID } from 'node:crypto';

import { Transformer } from '@napi-rs/image';
import type { INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import { hashSync } from '@node-rs/argon2';
import { type User } from '@prisma/client';
import ava, { type TestFn } from 'ava';
import type { Express } from 'express';
import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs';
import request from 'supertest';

import { AppModule } from '../src/app';
import { FeatureManagementService } from '../src/modules/features';
import { PrismaService } from '../src/prisma/service';

const gql = '/graphql';

const test = ava as TestFn<{
  app: INestApplication;
}>;

class FakePrisma {
  fakeUser: User = {
    id: randomUUID(),
    name: 'Alex Yang',
    avatarUrl: '',
    email: 'alex.yang@example.org',
    password: hashSync('123456'),
    emailVerified: new Date(),
    createdAt: new Date(),
  };
  get user() {
    // eslint-disable-next-line @typescript-eslint/no-this-alias
    const prisma = this;
    return {
      async findFirst() {
        return prisma.fakeUser;
      },
      async findUnique() {
        return this.findFirst();
      },
      async update() {
        return this.findFirst();
      },
    };
  }
  get newFeaturesWaitingList() {
    return {
      async findUnique() {
        return null;
      },
    };
  }
}

test.beforeEach(async t => {
  const module = await Test.createTestingModule({
    imports: [AppModule],
  })
    .overrideProvider(PrismaService)
    .useClass(FakePrisma)
    .overrideProvider(FeatureManagementService)
    .useValue({ canEarlyAccess: () => true })
    .compile();
  t.context.app = module.createNestApplication({
    cors: true,
    bodyParser: true,
  });
  t.context.app.use(
    graphqlUploadExpress({
      maxFileSize: 10 * 1024 * 1024,
      maxFiles: 5,
    })
  );
  await t.context.app.init();
});

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

test('should init app', async t => {
  t.is(typeof t.context.app, 'object');
  await request(t.context.app.getHttpServer())
    .post(gql)
    .send({
      query: `
          query {
            error
          }
        `,
    })
    .expect(400);

  const { token } = await createToken(t.context.app);

  await request(t.context.app.getHttpServer())
    .post(gql)
    .auth(token, { type: 'bearer' })
    .send({
      query: `
      query {
        __typename
      }
    `,
    })
    .expect(200)
    .expect(res => {
      t.is(res.body.data.__typename, 'Query');
    });
});

test('should find default user', async t => {
  const { token } = await createToken(t.context.app);
  await request(t.context.app.getHttpServer())
    .post(gql)
    .auth(token, { type: 'bearer' })
    .send({
      query: `
      query {
        user(email: "alex.yang@example.org") {
          email
          avatarUrl
        }
      }
    `,
    })
    .expect(200)
    .expect(res => {
      t.is(res.body.data.user.email, 'alex.yang@example.org');
    });
});

test('should be able to upload avatar and remove it', async t => {
  const { token, id } = await createToken(t.context.app);
  const png = await Transformer.fromRgbaPixels(
    Buffer.alloc(400 * 400 * 4).fill(255),
    400,
    400
  ).png();

  await request(t.context.app.getHttpServer())
    .post(gql)
    .auth(token, { type: 'bearer' })
    .field(
      'operations',
      JSON.stringify({
        name: 'uploadAvatar',
        query: `mutation uploadAvatar($avatar: Upload!) {
          uploadAvatar(avatar: $avatar) {
            id
            name
            avatarUrl
            email
          }
        }
        `,
        variables: { id, avatar: null },
      })
    )
    .field('map', JSON.stringify({ '0': ['variables.avatar'] }))
    .attach('0', png, 'avatar.png')
    .expect(200)
    .expect(res => {
      t.is(res.body.data.uploadAvatar.id, id);
    });

  await request(t.context.app.getHttpServer())
    .post(gql)
    .auth(token, { type: 'bearer' })
    .set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
    .send({
      query: `
          mutation removeAvatar {
            removeAvatar {
              success
            }
          }
        `,
    })
    .expect(200)
    .expect(res => {
      t.is(res.body.data.removeAvatar.success, true);
    });
});

async function createToken(app: INestApplication<Express>): Promise<{
  id: string;
  token: string;
}> {
  let token;
  let id;
  await request(app.getHttpServer())
    .post(gql)
    .send({
      query: `
          mutation {
            signIn(email: "alex.yang@example.org", password: "123456") {
              id
              token {
                token
              }
            }
          }
        `,
    })
    .expect(200)
    .expect(res => {
      id = res.body.data.signIn.id;
      ok(
        typeof res.body.data.signIn.token.token === 'string',
        'res.body.data.signIn.token.token is not a string'
      );
      token = res.body.data.signIn.token.token;
    });
  return { token: token!, id: id! };
}
