import {
  LoginResponseDto,
  QueueName,
  createStack,
  deleteUserAdmin,
  getMyUser,
  getUserAdmin,
  getUserPreferencesAdmin,
  login,
} from '@immich/sdk';
import { Socket } from 'socket.io-client';
import { createUserDto, uuidDto } from 'src/fixtures';
import { errorDto } from 'src/responses';
import { app, asBearerAuth, utils } from 'src/utils';
import request from 'supertest';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';

describe('/admin/users', () => {
  let websocket: Socket;

  let admin: LoginResponseDto;
  let nonAdmin: LoginResponseDto;
  let deletedUser: LoginResponseDto;
  let userToDelete: LoginResponseDto;

  beforeAll(async () => {
    await utils.resetDatabase();
    admin = await utils.adminSetup({ onboarding: false });

    [websocket, nonAdmin, deletedUser, userToDelete] = await Promise.all([
      utils.connectWebsocket(admin.accessToken),
      utils.userSetup(admin.accessToken, createUserDto.user1),
      utils.userSetup(admin.accessToken, createUserDto.user2),
      utils.userSetup(admin.accessToken, createUserDto.user3),
    ]);

    await deleteUserAdmin(
      { id: deletedUser.userId, userAdminDeleteDto: {} },
      { headers: asBearerAuth(admin.accessToken) },
    );
  });

  afterAll(() => {
    utils.disconnectWebsocket(websocket);
  });

  describe('GET /admin/users', () => {
    it('should require authentication', async () => {
      const { status, body } = await request(app).get(`/admin/users`);
      expect(status).toBe(401);
      expect(body).toEqual(errorDto.unauthorized);
    });

    it('should require authorization', async () => {
      const { status, body } = await request(app)
        .get(`/admin/users`)
        .set('Authorization', `Bearer ${nonAdmin.accessToken}`);
      expect(status).toBe(403);
      expect(body).toEqual(errorDto.forbidden);
    });

    it('should hide deleted users by default', async () => {
      const { status, body } = await request(app)
        .get(`/admin/users`)
        .set('Authorization', `Bearer ${admin.accessToken}`);
      expect(status).toBe(200);
      expect(body).toHaveLength(3);
      expect(body).toEqual(
        expect.arrayContaining([
          expect.objectContaining({ email: admin.userEmail }),
          expect.objectContaining({ email: nonAdmin.userEmail }),
          expect.objectContaining({ email: userToDelete.userEmail }),
        ]),
      );
    });

    it('should include deleted users', async () => {
      const { status, body } = await request(app)
        .get(`/admin/users?withDeleted=true`)
        .set('Authorization', `Bearer ${admin.accessToken}`);

      expect(status).toBe(200);
      expect(body).toHaveLength(4);
      expect(body).toEqual(
        expect.arrayContaining([
          expect.objectContaining({ email: admin.userEmail }),
          expect.objectContaining({ email: nonAdmin.userEmail }),
          expect.objectContaining({ email: userToDelete.userEmail }),
          expect.objectContaining({ email: deletedUser.userEmail }),
        ]),
      );
    });
  });

  describe('POST /admin/users', () => {
    it('should require authentication', async () => {
      const { status, body } = await request(app).post(`/admin/users`).send(createUserDto.user1);
      expect(status).toBe(401);
      expect(body).toEqual(errorDto.unauthorized);
    });

    it('should require authorization', async () => {
      const { status, body } = await request(app)
        .post(`/admin/users`)
        .set('Authorization', `Bearer ${nonAdmin.accessToken}`)
        .send(createUserDto.user1);
      expect(status).toBe(403);
      expect(body).toEqual(errorDto.forbidden);
    });

    for (const [key, message] of [
      ['password', 'Invalid input: expected string, received null'],
      ['email', 'Invalid input: expected email, received object'],
      ['name', 'Invalid input: expected string, received null'],
      ['shouldChangePassword', 'Invalid input: expected boolean, received null'],
      ['notify', 'Invalid input: expected boolean, received null'],
    ] as const) {
      it(`should not allow null ${key}`, async () => {
        const { status, body } = await request(app)
          .post(`/admin/users`)
          .set('Authorization', `Bearer ${admin.accessToken}`)
          .send({ ...createUserDto.user1, [key]: null });
        expect(status).toBe(400);
        expect(body).toEqual(errorDto.validationError([{ path: [key], message }]));
      });
    }

    it('should accept `isAdmin`', async () => {
      const { status, body } = await request(app)
        .post(`/admin/users`)
        .send({
          isAdmin: true,
          email: 'user5@immich.cloud',
          password: 'password123',
          name: 'Immich',
        })
        .set('Authorization', `Bearer ${admin.accessToken}`);
      expect(body).toMatchObject({
        email: 'user5@immich.cloud',
        isAdmin: true,
        shouldChangePassword: true,
      });
      expect(status).toBe(201);
    });
  });

  describe('PUT /admin/users/:id', () => {
    it('should require authentication', async () => {
      const { status, body } = await request(app).put(`/admin/users/${uuidDto.notFound}`);
      expect(status).toBe(401);
      expect(body).toEqual(errorDto.unauthorized);
    });

    it('should require authorization', async () => {
      const { status, body } = await request(app)
        .put(`/admin/users/${uuidDto.notFound}`)
        .set('Authorization', `Bearer ${nonAdmin.accessToken}`);
      expect(status).toBe(403);
      expect(body).toEqual(errorDto.forbidden);
    });

    for (const [key, message] of [
      ['password', 'Invalid input: expected string, received null'],
      ['email', 'Invalid input: expected email, received object'],
      ['name', 'Invalid input: expected string, received null'],
      ['shouldChangePassword', 'Invalid input: expected boolean, received null'],
    ] as const) {
      it(`should not allow null ${key}`, async () => {
        const { status, body } = await request(app)
          .put(`/admin/users/${uuidDto.notFound}`)
          .set('Authorization', `Bearer ${admin.accessToken}`)
          .send({ [key]: null });
        expect(status).toBe(400);
        expect(body).toEqual(errorDto.validationError([{ path: [key], message }]));
      });
    }

    it('should allow a non-admin to become an admin', async () => {
      const user = await utils.userSetup(admin.accessToken, createUserDto.create('admin2'));
      const { status, body } = await request(app)
        .put(`/admin/users/${user.userId}`)
        .send({ isAdmin: true })
        .set('Authorization', `Bearer ${admin.accessToken}`);

      expect(status).toBe(200);
      expect(body).toMatchObject({ isAdmin: true });
    });

    it('ignores updates to profileImagePath', async () => {
      const { status, body } = await request(app)
        .put(`/admin/users/${admin.userId}`)
        .send({ profileImagePath: 'invalid.jpg' })
        .set('Authorization', `Bearer ${admin.accessToken}`);

      expect(status).toBe(200);
      expect(body).toMatchObject({ id: admin.userId, profileImagePath: '' });
    });

    it('should update first and last name', async () => {
      const before = await getUserAdmin({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });

      const { status, body } = await request(app)
        .put(`/admin/users/${admin.userId}`)
        .send({ name: 'Name' })
        .set('Authorization', `Bearer ${admin.accessToken}`);

      expect(status).toBe(200);
      expect(body).toEqual({
        ...before,
        updatedAt: expect.any(String),
        name: 'Name',
      });
      expect(before.updatedAt).not.toEqual(body.updatedAt);
    });

    it('should update password', async () => {
      const { status, body } = await request(app)
        .put(`/admin/users/${nonAdmin.userId}`)
        .send({ password: 'super-secret' })
        .set('Authorization', `Bearer ${admin.accessToken}`);

      expect(status).toBe(200);
      expect(body).toMatchObject({ email: nonAdmin.userEmail });

      const token = await login({ loginCredentialDto: { email: nonAdmin.userEmail, password: 'super-secret' } });
      expect(token.accessToken).toBeDefined();

      const user = await getMyUser({ headers: asBearerAuth(token.accessToken) });
      expect(user).toMatchObject({ email: nonAdmin.userEmail });
    });

    it('should update the avatar color', async () => {
      const { status, body } = await request(app)
        .put(`/admin/users/${admin.userId}`)
        .send({ avatarColor: 'orange' })
        .set('Authorization', `Bearer ${admin.accessToken}`);

      expect(status).toBe(200);
      expect(body).toMatchObject({ avatarColor: 'orange' });

      const after = await getUserAdmin({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
      expect(after).toMatchObject({ avatarColor: 'orange' });
    });
  });

  describe('PUT /admin/users/:id/preferences', () => {
    it('should require authentication', async () => {
      const { status, body } = await request(app).put(`/admin/users/${userToDelete.userId}/preferences`);
      expect(status).toBe(401);
      expect(body).toEqual(errorDto.unauthorized);
    });

    it('should update memories enabled', async () => {
      const before = await getUserPreferencesAdmin({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
      expect(before).toMatchObject({ memories: { enabled: true } });

      const { status, body } = await request(app)
        .put(`/admin/users/${admin.userId}/preferences`)
        .send({ memories: { enabled: false } })
        .set('Authorization', `Bearer ${admin.accessToken}`);

      expect(status).toBe(200);
      expect(body).toMatchObject({ memories: { enabled: false } });

      const after = await getUserPreferencesAdmin({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
      expect(after).toMatchObject({ memories: { enabled: false } });
    });

    it('should update download archive size', async () => {
      const { status, body } = await request(app)
        .put(`/admin/users/${admin.userId}/preferences`)
        .send({ download: { archiveSize: 1_234_567 } })
        .set('Authorization', `Bearer ${admin.accessToken}`);

      expect(status).toBe(200);
      expect(body).toMatchObject({ download: { archiveSize: 1_234_567 } });

      const after = await getUserPreferencesAdmin({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
      expect(after).toMatchObject({ download: { archiveSize: 1_234_567 } });
    });
  });

  describe('DELETE /admin/users/:id', () => {
    it('should require authentication', async () => {
      const { status, body } = await request(app).delete(`/admin/users/${userToDelete.userId}`);
      expect(status).toBe(401);
      expect(body).toEqual(errorDto.unauthorized);
    });

    it('should require authorization', async () => {
      const { status, body } = await request(app)
        .delete(`/admin/users/${userToDelete.userId}`)
        .set('Authorization', `Bearer ${nonAdmin.accessToken}`);
      expect(status).toBe(403);
      expect(body).toEqual(errorDto.forbidden);
    });

    it('should delete user', async () => {
      const { status, body } = await request(app)
        .delete(`/admin/users/${userToDelete.userId}`)
        .set('Authorization', `Bearer ${admin.accessToken}`)
        .send({});

      expect(status).toBe(200);
      expect(body).toMatchObject({
        id: userToDelete.userId,
        updatedAt: expect.any(String),
        deletedAt: expect.any(String),
      });
    });

    it('should hard delete a user', async () => {
      const user = await utils.userSetup(admin.accessToken, createUserDto.create('hard-delete-1'));

      const { status, body } = await request(app)
        .delete(`/admin/users/${user.userId}`)
        .send({ force: true })
        .set('Authorization', `Bearer ${admin.accessToken}`);

      expect(status).toBe(200);
      expect(body).toMatchObject({
        id: user.userId,
        updatedAt: expect.any(String),
        deletedAt: expect.any(String),
      });

      await utils.waitForWebsocketEvent({ event: 'userDelete', id: user.userId, timeout: 5000 });
    });

    it('should hard delete a user with stacked assets', async () => {
      const user = await utils.userSetup(admin.accessToken, createUserDto.create('hard-delete-1'));

      const [asset1, asset2] = await Promise.all([
        utils.createAsset(user.accessToken),
        utils.createAsset(user.accessToken),
      ]);

      await createStack(
        { stackCreateDto: { assetIds: [asset1.id, asset2.id] } },
        { headers: asBearerAuth(user.accessToken) },
      );

      await utils.waitForQueueFinish(admin.accessToken, QueueName.BackgroundTask);

      const { status, body } = await request(app)
        .delete(`/admin/users/${user.userId}`)
        .send({ force: true })
        .set('Authorization', `Bearer ${admin.accessToken}`);

      expect(status).toBe(200);
      expect(body).toMatchObject({
        id: user.userId,
        updatedAt: expect.any(String),
        deletedAt: expect.any(String),
      });

      await utils.waitForWebsocketEvent({ event: 'userDelete', id: user.userId, timeout: 5000 });
    });
  });

  describe('POST /admin/users/:id/restore', () => {
    it('should require authentication', async () => {
      const { status, body } = await request(app).post(`/admin/users/${userToDelete.userId}/restore`);
      expect(status).toBe(401);
      expect(body).toEqual(errorDto.unauthorized);
    });

    it('should require authorization', async () => {
      const { status, body } = await request(app)
        .post(`/admin/users/${userToDelete.userId}/restore`)
        .set('Authorization', `Bearer ${nonAdmin.accessToken}`);
      expect(status).toBe(403);
      expect(body).toEqual(errorDto.forbidden);
    });

    it('should restore a user', async () => {
      const user = await utils.userSetup(admin.accessToken, createUserDto.create('restore'));

      await deleteUserAdmin({ id: user.userId, userAdminDeleteDto: {} }, { headers: asBearerAuth(admin.accessToken) });

      const { status, body } = await request(app)
        .post(`/admin/users/${user.userId}/restore`)
        .set('Authorization', `Bearer ${admin.accessToken}`);
      expect(status).toBe(200);
      expect(body).toEqual(
        expect.objectContaining({
          id: user.userId,
          email: user.userEmail,
          status: 'active',
          deletedAt: null,
        }),
      );
    });
  });
});
