import { LoginResponseDto, QueueCommand, QueueName, updateConfig } from '@immich/sdk';
import { cpSync, rmSync } from 'node:fs';
import { readFile } from 'node:fs/promises';
import { basename } from 'node:path';
import { errorDto } from 'src/responses';
import { app, asBearerAuth, testAssetDir, utils } from 'src/utils';
import request from 'supertest';
import { afterEach, beforeAll, describe, expect, it } from 'vitest';

describe('/jobs', () => {
  let admin: LoginResponseDto;

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

  describe('PUT /jobs', () => {
    afterEach(async () => {
      await utils.queueCommand(admin.accessToken, QueueName.MetadataExtraction, {
        command: QueueCommand.Resume,
        force: false,
      });

      await utils.queueCommand(admin.accessToken, QueueName.ThumbnailGeneration, {
        command: QueueCommand.Resume,
        force: false,
      });

      await utils.queueCommand(admin.accessToken, QueueName.FaceDetection, {
        command: QueueCommand.Resume,
        force: false,
      });

      await utils.queueCommand(admin.accessToken, QueueName.SmartSearch, {
        command: QueueCommand.Resume,
        force: false,
      });

      await utils.queueCommand(admin.accessToken, QueueName.DuplicateDetection, {
        command: QueueCommand.Resume,
        force: false,
      });

      const config = await utils.getSystemConfig(admin.accessToken);
      config.machineLearning.duplicateDetection.enabled = false;
      config.machineLearning.enabled = false;
      config.metadata.faces.import = false;
      config.machineLearning.clip.enabled = false;
      await updateConfig({ systemConfigDto: config }, { headers: asBearerAuth(admin.accessToken) });
    });

    it('should require authentication', async () => {
      const { status, body } = await request(app).put('/jobs/metadataExtraction');
      expect(status).toBe(401);
      expect(body).toEqual(errorDto.unauthorized);
    });

    it('should queue metadata extraction for missing assets', async () => {
      const path = `${testAssetDir}/formats/raw/Nikon/D700/philadelphia.nef`;

      await utils.queueCommand(admin.accessToken, QueueName.MetadataExtraction, {
        command: QueueCommand.Pause,
        force: false,
      });

      const { id } = await utils.createAsset(admin.accessToken, {
        assetData: { bytes: await readFile(path), filename: basename(path) },
      });

      await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');

      {
        const asset = await utils.getAssetInfo(admin.accessToken, id);

        expect(asset.exifInfo).toBeDefined();
        expect(asset.exifInfo?.make).toBeNull();
      }

      await utils.queueCommand(admin.accessToken, QueueName.MetadataExtraction, {
        command: QueueCommand.Empty,
        force: false,
      });

      await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');

      await utils.queueCommand(admin.accessToken, QueueName.MetadataExtraction, {
        command: QueueCommand.Resume,
        force: false,
      });

      await utils.queueCommand(admin.accessToken, QueueName.MetadataExtraction, {
        command: QueueCommand.Start,
        force: false,
      });

      await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');

      {
        const asset = await utils.getAssetInfo(admin.accessToken, id);

        expect(asset.exifInfo).toBeDefined();
        expect(asset.exifInfo?.make).toBe('NIKON CORPORATION');
      }
    });

    it('should not re-extract metadata for existing assets', async () => {
      const path = `${testAssetDir}/temp/metadata/asset.jpg`;

      cpSync(`${testAssetDir}/formats/raw/Nikon/D700/philadelphia.nef`, path);

      const { id } = await utils.createAsset(admin.accessToken, {
        assetData: { bytes: await readFile(path), filename: basename(path) },
      });

      await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');

      {
        const asset = await utils.getAssetInfo(admin.accessToken, id);

        expect(asset.exifInfo).toBeDefined();
        expect(asset.exifInfo?.model).toBe('NIKON D700');
      }

      cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, path);

      await utils.queueCommand(admin.accessToken, QueueName.MetadataExtraction, {
        command: QueueCommand.Start,
        force: false,
      });

      await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');

      {
        const asset = await utils.getAssetInfo(admin.accessToken, id);

        expect(asset.exifInfo).toBeDefined();
        expect(asset.exifInfo?.model).toBe('NIKON D700');
      }

      rmSync(path);
    });

    it('should queue thumbnail extraction for assets missing thumbs', async () => {
      const path = `${testAssetDir}/albums/nature/tanners_ridge.jpg`;

      await utils.queueCommand(admin.accessToken, QueueName.ThumbnailGeneration, {
        command: QueueCommand.Pause,
        force: false,
      });

      const { id } = await utils.createAsset(admin.accessToken, {
        assetData: { bytes: await readFile(path), filename: basename(path) },
      });

      await utils.waitForQueueFinish(admin.accessToken, QueueName.MetadataExtraction);
      await utils.waitForQueueFinish(admin.accessToken, QueueName.ThumbnailGeneration);

      const assetBefore = await utils.getAssetInfo(admin.accessToken, id);
      expect(assetBefore.thumbhash).toBeNull();

      await utils.queueCommand(admin.accessToken, QueueName.ThumbnailGeneration, {
        command: QueueCommand.Empty,
        force: false,
      });

      await utils.waitForQueueFinish(admin.accessToken, QueueName.MetadataExtraction);
      await utils.waitForQueueFinish(admin.accessToken, QueueName.ThumbnailGeneration);

      await utils.queueCommand(admin.accessToken, QueueName.ThumbnailGeneration, {
        command: QueueCommand.Resume,
        force: false,
      });

      await utils.queueCommand(admin.accessToken, QueueName.ThumbnailGeneration, {
        command: QueueCommand.Start,
        force: false,
      });

      await utils.waitForQueueFinish(admin.accessToken, QueueName.MetadataExtraction);
      await utils.waitForQueueFinish(admin.accessToken, QueueName.ThumbnailGeneration);

      const assetAfter = await utils.getAssetInfo(admin.accessToken, id);
      expect(assetAfter.thumbhash).not.toBeNull();
    });

    it('should not reload existing thumbnail when running thumb job for missing assets', async () => {
      const path = `${testAssetDir}/temp/thumbs/asset1.jpg`;

      cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, path);

      const { id } = await utils.createAsset(admin.accessToken, {
        assetData: { bytes: await readFile(path), filename: basename(path) },
      });

      await utils.waitForQueueFinish(admin.accessToken, QueueName.MetadataExtraction);
      await utils.waitForQueueFinish(admin.accessToken, QueueName.ThumbnailGeneration);

      const assetBefore = await utils.getAssetInfo(admin.accessToken, id);

      cpSync(`${testAssetDir}/albums/nature/notocactus_minimus.jpg`, path);

      await utils.queueCommand(admin.accessToken, QueueName.ThumbnailGeneration, {
        command: QueueCommand.Resume,
        force: false,
      });

      // This runs the missing thumbnail job
      await utils.queueCommand(admin.accessToken, QueueName.ThumbnailGeneration, {
        command: QueueCommand.Start,
        force: false,
      });

      await utils.waitForQueueFinish(admin.accessToken, QueueName.MetadataExtraction);
      await utils.waitForQueueFinish(admin.accessToken, QueueName.ThumbnailGeneration);

      const assetAfter = await utils.getAssetInfo(admin.accessToken, id);

      // Asset 1 thumbnail should be untouched since its thumb should not have been reloaded, even though the file was changed
      expect(assetAfter.thumbhash).toEqual(assetBefore.thumbhash);

      rmSync(path);
    });
  });
});
