import { AuthController } from 'src/controllers/auth.controller';
import { LoginResponseDto } from 'src/dtos/auth.dto';
import { AuthService } from 'src/services/auth.service';
import request from 'supertest';
import { mediumFactory } from 'test/medium.factory';
import { errorDto } from 'test/medium/responses';
import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils';

describe(AuthController.name, () => {
  let ctx: ControllerContext;
  const service = mockBaseService(AuthService);

  beforeAll(async () => {
    ctx = await controllerSetup(AuthController, [{ provide: AuthService, useValue: service }]);
    return () => ctx.close();
  });

  beforeEach(() => {
    service.resetAllMocks();
    ctx.reset();
  });

  describe('POST /auth/admin-sign-up', () => {
    const name = 'admin';
    const email = 'admin@immich.cloud';
    const password = 'password';

    it('should require an email address', async () => {
      const { status, body } = await request(ctx.getHttpServer()).post('/auth/admin-sign-up').send({ name, password });
      expect(status).toEqual(400);
      expect(body).toEqual(errorDto.badRequest());
    });

    it('should require a password', async () => {
      const { status, body } = await request(ctx.getHttpServer()).post('/auth/admin-sign-up').send({ name, email });
      expect(status).toEqual(400);
      expect(body).toEqual(errorDto.badRequest());
    });

    it('should require a name', async () => {
      const { status, body } = await request(ctx.getHttpServer()).post('/auth/admin-sign-up').send({ email, password });
      expect(status).toEqual(400);
      expect(body).toEqual(errorDto.badRequest());
    });

    it('should require a valid email', async () => {
      const { status, body } = await request(ctx.getHttpServer())
        .post('/auth/admin-sign-up')
        .send({ name, email: 'immich', password });
      expect(status).toEqual(400);
      expect(body).toEqual(errorDto.badRequest());
    });

    it('should transform email to lower case', async () => {
      service.adminSignUp.mockReset();
      const { status } = await request(ctx.getHttpServer())
        .post('/auth/admin-sign-up')
        .send({ name: 'admin', password: 'password', email: 'aDmIn@IMMICH.cloud' });
      expect(status).toEqual(201);
      expect(service.adminSignUp).toHaveBeenCalledWith(expect.objectContaining({ email: 'admin@immich.cloud' }));
    });

    it('should accept an email with a local domain', async () => {
      const { status } = await request(ctx.getHttpServer())
        .post('/auth/admin-sign-up')
        .send({ name: 'admin', password: 'password', email: 'admin@local' });
      expect(status).toEqual(201);
    });
  });

  describe('POST /auth/login', () => {
    it(`should require an email and password`, async () => {
      const { status, body } = await request(ctx.getHttpServer()).post('/auth/login').send({ name: 'admin' });
      expect(status).toBe(400);
      expect(body).toEqual(
        errorDto.badRequest([
          'email should not be empty',
          'email must be an email',
          'password should not be empty',
          'password must be a string',
        ]),
      );
    });

    it(`should not allow null email`, async () => {
      const { status, body } = await request(ctx.getHttpServer())
        .post('/auth/login')
        .send({ name: 'admin', email: null, password: 'password' });
      expect(status).toBe(400);
      expect(body).toEqual(errorDto.badRequest(['email should not be empty', 'email must be an email']));
    });

    it(`should not allow null password`, async () => {
      const { status, body } = await request(ctx.getHttpServer())
        .post('/auth/login')
        .send({ name: 'admin', email: 'admin@immich.cloud', password: null });
      expect(status).toBe(400);
      expect(body).toEqual(errorDto.badRequest(['password should not be empty', 'password must be a string']));
    });

    it('should reject an invalid email', async () => {
      service.login.mockResolvedValue({ accessToken: 'access-token' } as LoginResponseDto);

      const { status, body } = await request(ctx.getHttpServer())
        .post('/auth/login')
        .send({ name: 'admin', email: [], password: 'password' });

      expect(status).toBe(400);
      expect(body).toEqual(errorDto.badRequest(['email must be an email']));
    });

    it('should transform the email to all lowercase', async () => {
      service.login.mockResolvedValue({ accessToken: 'access-token' } as LoginResponseDto);

      const { status } = await request(ctx.getHttpServer())
        .post('/auth/login')
        .send({ name: 'admin', email: 'aDmIn@iMmIcH.ApP', password: 'password' });

      expect(status).toBe(201);
      expect(service.login).toHaveBeenCalledWith(
        expect.objectContaining({ email: 'admin@immich.app' }),
        expect.anything(),
      );
    });

    it('should accept an email with a local domain', async () => {
      service.login.mockResolvedValue({ accessToken: 'access-token' } as LoginResponseDto);

      const { status } = await request(ctx.getHttpServer())
        .post('/auth/login')
        .send({ name: 'admin', email: 'admin@local', password: 'password' });

      expect(status).toEqual(201);
      expect(service.login).toHaveBeenCalledWith(expect.objectContaining({ email: 'admin@local' }), expect.anything());
    });

    it('should auth cookies on a secure connection', async () => {
      const loginResponse = mediumFactory.loginResponse();
      service.login.mockResolvedValue(loginResponse);
      const { status, body, headers } = await request(ctx.getHttpServer())
        .post('/auth/login')
        .send({ name: 'admin', email: 'admin@local', password: 'password' });

      expect(status).toEqual(201);
      expect(body).toEqual(loginResponse);

      const cookies = headers['set-cookie'];
      expect(cookies).toHaveLength(3);
      expect(cookies[0].split(';').map((item) => item.trim())).toEqual([
        `immich_access_token=${loginResponse.accessToken}`,
        'Max-Age=34560000',
        'Path=/',
        expect.stringContaining('Expires='),
        'HttpOnly',
        'SameSite=Lax',
      ]);
      expect(cookies[1].split(';').map((item) => item.trim())).toEqual([
        'immich_auth_type=password',
        'Max-Age=34560000',
        'Path=/',
        expect.stringContaining('Expires='),
        'HttpOnly',
        'SameSite=Lax',
      ]);
      expect(cookies[2].split(';').map((item) => item.trim())).toEqual([
        'immich_is_authenticated=true',
        'Max-Age=34560000',
        'Path=/',
        expect.stringContaining('Expires='),
        'SameSite=Lax',
      ]);
    });
  });

  describe('POST /auth/logout', () => {
    it('should be an authenticated route', async () => {
      await request(ctx.getHttpServer()).post('/auth/logout');
      expect(ctx.authenticate).toHaveBeenCalled();
    });
  });

  describe('POST /auth/change-password', () => {
    it('should be an authenticated route', async () => {
      await request(ctx.getHttpServer())
        .post('/auth/change-password')
        .send({ password: 'password', newPassword: 'Password1234', invalidateSessions: false });
      expect(ctx.authenticate).toHaveBeenCalled();
    });
  });

  describe('POST /auth/pin-code', () => {
    it('should be an authenticated route', async () => {
      await request(ctx.getHttpServer()).post('/auth/pin-code').send({ pinCode: '123456' });
      expect(ctx.authenticate).toHaveBeenCalled();
    });

    it('should reject 5 digits', async () => {
      const { status, body } = await request(ctx.getHttpServer()).post('/auth/pin-code').send({ pinCode: '12345' });
      expect(status).toEqual(400);
      expect(body).toEqual(errorDto.badRequest(['pinCode must be a 6-digit numeric string']));
    });

    it('should reject 7 digits', async () => {
      const { status, body } = await request(ctx.getHttpServer()).post('/auth/pin-code').send({ pinCode: '1234567' });
      expect(status).toEqual(400);
      expect(body).toEqual(errorDto.badRequest(['pinCode must be a 6-digit numeric string']));
    });

    it('should reject non-numbers', async () => {
      const { status, body } = await request(ctx.getHttpServer()).post('/auth/pin-code').send({ pinCode: 'A12345' });
      expect(status).toEqual(400);
      expect(body).toEqual(errorDto.badRequest(['pinCode must be a 6-digit numeric string']));
    });
  });

  describe('PUT /auth/pin-code', () => {
    it('should be an authenticated route', async () => {
      await request(ctx.getHttpServer()).put('/auth/pin-code').send({ pinCode: '123456', newPinCode: '654321' });
      expect(ctx.authenticate).toHaveBeenCalled();
    });
  });

  describe('DELETE /auth/pin-code', () => {
    it('should be an authenticated route', async () => {
      await request(ctx.getHttpServer()).delete('/auth/pin-code').send({ pinCode: '123456' });
      expect(ctx.authenticate).toHaveBeenCalled();
    });
  });

  describe('GET /auth/status', () => {
    it('should be an authenticated route', async () => {
      await request(ctx.getHttpServer()).get('/auth/status');
      expect(ctx.authenticate).toHaveBeenCalled();
    });
  });
});
