import type { Application, CreateApplication } from '@logto/schemas';
import { ApplicationType } from '@logto/schemas';

import { mockApplication } from '#src/__mocks__/index.js';
import { findApplicationById } from '#src/queries/application.js';
import { createRequester } from '#src/utils/test-utils.js';

import applicationRoutes from './application.js';

jest.mock('#src/queries/application.js', () => ({
  findTotalNumberOfApplications: jest.fn(async () => ({ count: 10 })),
  findAllApplications: jest.fn(async () => [mockApplication]),
  findApplicationById: jest.fn(async () => mockApplication),
  deleteApplicationById: jest.fn(),
  insertApplication: jest.fn(
    async (body: CreateApplication): Promise<Application> => ({
      ...mockApplication,
      ...body,
      oidcClientMetadata: {
        ...mockApplication.oidcClientMetadata,
        ...body.oidcClientMetadata,
      },
    })
  ),
  updateApplicationById: jest.fn(
    async (_, data: Partial<CreateApplication>): Promise<Application> => ({
      ...mockApplication,
      ...data,
    })
  ),
}));

jest.mock('@logto/shared', () => ({
  // eslint-disable-next-line unicorn/consistent-function-scoping
  buildIdGenerator: jest.fn(() => () => 'randomId'),
  buildApplicationSecret: jest.fn(() => 'randomId'),
}));

const customClientMetadata = {
  corsAllowedOrigins: ['http://localhost:5000', 'http://localhost:5001', 'https://silverhand.com'],
  idTokenTtl: 999_999,
  refreshTokenTtl: 100_000_000,
};

const customOidcClientMetadata = {
  redirectUris: [],
  postLogoutRedirectUris: [],
};

describe('application route', () => {
  const applicationRequest = createRequester({ authedRoutes: applicationRoutes });

  it('GET /applications', async () => {
    const response = await applicationRequest.get('/applications');
    expect(response.status).toEqual(200);
    expect(response.body).toEqual([mockApplication]);
    expect(response.header).toHaveProperty('total-number', '10');
  });

  it('POST /applications', async () => {
    const name = 'FooApplication';
    const description = 'FooDescription';
    const type = ApplicationType.Traditional;

    const response = await applicationRequest
      .post('/applications')
      .send({ name, type, description });
    expect(response.status).toEqual(200);
    expect(response.body).toEqual({
      ...mockApplication,
      id: 'randomId',
      secret: 'randomId',
      name,
      description,
      type,
    });
  });

  it('POST /applications with custom client metadata', async () => {
    const name = 'FooApplication';
    const type = ApplicationType.Traditional;

    const response = await applicationRequest
      .post('/applications')
      .send({ name, type, customClientMetadata });
    expect(response.status).toEqual(200);
    expect(response.body).toEqual({
      ...mockApplication,
      id: 'randomId',
      name,
      type,
      customClientMetadata,
    });
  });

  it('POST /applications should throw with invalid input body', async () => {
    const name = 'FooApplication';
    const description = 'FooDescription';
    const type = ApplicationType.Traditional;

    await expect(applicationRequest.post('/applications')).resolves.toHaveProperty('status', 400);
    await expect(
      applicationRequest.post('/applications').send({ customClientMetadata })
    ).resolves.toHaveProperty('status', 400);
    await expect(
      applicationRequest.post('/applications').send({ name, description, customClientMetadata })
    ).resolves.toHaveProperty('status', 400);
    await expect(
      applicationRequest.post('/applications').send({ type, description, customClientMetadata })
    ).resolves.toHaveProperty('status', 400);
    await expect(
      applicationRequest.post('/applications').send({
        name,
        type,
        customClientMetadata: {
          ...customClientMetadata,
          corsAllowedOrigins: [''],
        },
      })
    ).resolves.toHaveProperty('status', 400);
  });

  it('GET /applications/:id', async () => {
    const response = await applicationRequest.get('/applications/foo');

    expect(response.status).toEqual(200);
    expect(response.body).toEqual(mockApplication);
  });

  it('PATCH /applications/:applicationId', async () => {
    const name = 'FooApplication';
    const description = 'FooDescription';

    const response = await applicationRequest
      .patch('/applications/foo')
      .send({ name, description, customClientMetadata });
    expect(response.status).toEqual(200);
    expect(response.body).toEqual({ ...mockApplication, name, description, customClientMetadata });
  });

  it('PATCH /applications/:applicationId expect to throw with invalid properties', async () => {
    await expect(
      applicationRequest.patch('/applications/doo').send({ type: 'node' })
    ).resolves.toHaveProperty('status', 400);
    await expect(
      applicationRequest.patch('/applications/doo').send({
        customClientMetadata: {
          ...customClientMetadata,
          corsAllowedOrigins: [''],
        },
      })
    ).resolves.toHaveProperty('status', 400);
  });

  it('PATCH /applications/:applicationId should save the formatted URIs as per RFC', async () => {
    await expect(
      applicationRequest.patch('/applications/foo').send({
        oidcClientMetadata: {
          redirectUris: [
            'https://example.com/callback?auth=true',
            'https://Example.com',
            'http://127.0.0.1',
            'http://localhost:3002',
          ],
        },
      })
    ).resolves.toHaveProperty('status', 200);
  });

  it('PATCH /application/:applicationId expect to throw with invalid redirectURI', async () => {
    await expect(
      applicationRequest.patch('/applications/foo').send({
        oidcClientMetadata: {
          redirectUris: ['www.example.com', 'com.example://callback'],
        },
      })
    ).resolves.toHaveProperty('status', 400);
  });

  it('PATCH /application/:applicationId should save the formatted custom scheme URIs for native apps', async () => {
    await expect(
      applicationRequest.patch('/applications/foo').send({
        type: ApplicationType.Native,
        oidcClientMetadata: {
          redirectUris: [
            'com.example://demo-app/callback',
            'com.example://callback',
            'io.logto://Abc123',
          ],
        },
      })
    ).resolves.toHaveProperty('status', 200);
  });

  it('PATCH /application/:applicationId expect to throw with invalid custom scheme for native apps', async () => {
    await expect(
      applicationRequest.patch('/applications/foo').send({
        type: ApplicationType.Native,
        oidcClientMetadata: {
          redirectUris: ['https://www.example.com', 'com.example/callback'],
        },
      })
    ).resolves.toHaveProperty('status', 400);
  });

  it('DELETE /applications/:applicationId', async () => {
    await expect(applicationRequest.delete('/applications/foo')).resolves.toHaveProperty(
      'status',
      204
    );
  });

  it('DELETE /applications/:applicationId', async () => {
    await expect(applicationRequest.delete('/applications/foo')).resolves.toHaveProperty(
      'status',
      204
    );
  });

  it('DELETE /applications/:applicationId should throw if application not found', async () => {
    const mockFindApplicationById = findApplicationById as jest.Mock;
    mockFindApplicationById.mockRejectedValueOnce(new Error(' '));

    await expect(applicationRequest.delete('/applications/foo')).resolves.toHaveProperty(
      'status',
      500
    );
  });
});
