import { LoginResponseDto, getAllAlbums, getAssetStatistics } from '@immich/sdk';
import { cpSync, readFileSync } from 'node:fs';
import { mkdir, readdir, rm, symlink } from 'node:fs/promises';
import { asKeyAuth, immichCli, specialCharStrings, testAssetDir, utils } from 'src/utils';
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';

interface Test {
  test: string;
  paths: string[];
  files: Record<string, boolean>;
}

const tests: Test[] = [
  {
    test: 'should support globbing with *',
    paths: [`/photos*`],
    files: {
      '/photos1/image1.jpg': true,
      '/photos2/image2.jpg': true,
      '/images/image3.jpg': false,
    },
  },
  {
    test: 'should support paths with an asterisk',
    paths: [`/photos*/image1.jpg`],
    files: {
      '/photos*/image1.jpg': true,
      '/photos*/image2.jpg': false,
      '/images/image3.jpg': false,
    },
  },
  {
    test: 'should support paths with a space',
    paths: [`/my photos/image1.jpg`],
    files: {
      '/my photos/image1.jpg': true,
      '/my photos/image2.jpg': false,
      '/images/image3.jpg': false,
    },
  },
  {
    test: 'should support paths with a single quote',
    paths: [`/photos'/image1.jpg`],
    files: {
      "/photos'/image1.jpg": true,
      "/photos'/image2.jpg": false,
      '/images/image3.jpg': false,
    },
  },
  {
    test: 'should support paths with a double quote',
    paths: [`/photos"/image1.jpg`],
    files: {
      '/photos"/image1.jpg': true,
      '/photos"/image2.jpg': false,
      '/images/image3.jpg': false,
    },
  },
  {
    test: 'should support paths with a comma',
    paths: [`/photos, eh/image1.jpg`],
    files: {
      '/photos, eh/image1.jpg': true,
      '/photos, eh/image2.jpg': false,
      '/images/image3.jpg': false,
    },
  },
  {
    test: 'should support paths with an opening brace',
    paths: [`/photos{/image1.jpg`],
    files: {
      '/photos{/image1.jpg': true,
      '/photos{/image2.jpg': false,
      '/images/image3.jpg': false,
    },
  },
  {
    test: 'should support paths with a closing brace',
    paths: [`/photos}/image1.jpg`],
    files: {
      '/photos}/image1.jpg': true,
      '/photos}/image2.jpg': false,
      '/images/image3.jpg': false,
    },
  },
];

describe(`immich upload`, () => {
  let admin: LoginResponseDto;
  let key: string;

  beforeAll(async () => {
    await utils.resetDatabase();

    admin = await utils.adminSetup();
    key = await utils.cliLogin(admin.accessToken);
  });

  beforeEach(async () => {
    await utils.resetDatabase(['asset', 'album']);
  });

  describe(`immich upload /path/to/file.jpg`, () => {
    it('should upload a single file', async () => {
      const { stderr, stdout, exitCode } = await immichCli(['upload', `${testAssetDir}/albums/nature/silver_fir.jpg`]);
      expect(stderr).toContain('{message}');
      expect(stdout.split('\n')).toEqual(
        expect.arrayContaining([expect.stringContaining('Successfully uploaded 1 new asset')]),
      );
      expect(exitCode).toBe(0);

      const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
      expect(assets.total).toBe(1);
    });

    describe(`should accept special cases`, () => {
      for (const { test, paths, files } of tests) {
        it(test, async () => {
          const baseDir = `/tmp/upload/`;

          const testPaths = Object.keys(files).map((filePath) => `${baseDir}/${filePath}`);
          testPaths.map((filePath) => utils.createImageFile(filePath));

          const commandLine = paths.map((argument) => `${baseDir}/${argument}`);

          const expectedCount = Object.entries(files).filter((entry) => entry[1]).length;

          const { stderr, stdout, exitCode } = await immichCli(['upload', ...commandLine]);
          expect(stderr).toContain('{message}');
          expect(stdout.split('\n')).toEqual(
            expect.arrayContaining([expect.stringContaining(`Successfully uploaded ${expectedCount} new asset`)]),
          );
          expect(exitCode).toBe(0);

          const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
          expect(assets.total).toBe(expectedCount);

          testPaths.map((filePath) => utils.removeImageFile(filePath));
        });
      }
    });

    it.each(specialCharStrings)(`should upload a multiple files from paths containing %s`, async (testString) => {
      // https://github.com/immich-app/immich/issues/12078

      // NOTE: this test must contain more than one path since a related bug is only triggered with multiple paths

      const testPaths = [
        `${testAssetDir}/temp/dir1${testString}name/asset.jpg`,
        `${testAssetDir}/temp/dir2${testString}name/asset.jpg`,
      ];

      cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, testPaths[0]);
      cpSync(`${testAssetDir}/albums/nature/silver_fir.jpg`, testPaths[1]);

      const { stderr, stdout, exitCode } = await immichCli(['upload', ...testPaths]);
      expect(stderr).toContain('{message}');
      expect(stdout.split('\n')).toEqual(
        expect.arrayContaining([expect.stringContaining('Successfully uploaded 2 new assets')]),
      );
      expect(exitCode).toBe(0);

      utils.removeImageFile(testPaths[0]);
      utils.removeImageFile(testPaths[1]);

      const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
      expect(assets.total).toBe(2);
    });

    it('should skip a duplicate file', async () => {
      const first = await immichCli(['upload', `${testAssetDir}/albums/nature/silver_fir.jpg`]);
      expect(first.stderr).toContain('{message}');
      expect(first.stdout.split('\n')).toEqual(
        expect.arrayContaining([expect.stringContaining('Successfully uploaded 1 new asset')]),
      );
      expect(first.exitCode).toBe(0);

      const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
      expect(assets.total).toBe(1);

      const second = await immichCli(['upload', `${testAssetDir}/albums/nature/silver_fir.jpg`]);
      expect(second.stderr).toContain('{message}');
      expect(second.stdout.split('\n')).toEqual(
        expect.arrayContaining([
          expect.stringContaining('Found 0 new files and 1 duplicate'),
          expect.stringContaining('All assets were already uploaded, nothing to do'),
        ]),
      );
      expect(second.exitCode).toBe(0);
    });

    it('should skip files that do not exist', async () => {
      const { stderr, stdout, exitCode } = await immichCli(['upload', `/path/to/file`]);
      expect(stderr).toBe('');
      expect(stdout.split('\n')).toEqual(expect.arrayContaining([expect.stringContaining('No files found, exiting')]));
      expect(exitCode).toBe(0);

      const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
      expect(assets.total).toBe(0);
    });

    it('should have accurate dry run', async () => {
      const { stderr, stdout, exitCode } = await immichCli([
        'upload',
        `${testAssetDir}/albums/nature/silver_fir.jpg`,
        '--dry-run',
      ]);
      expect(stderr).toContain('{message}');
      expect(stdout.split('\n')).toEqual(
        expect.arrayContaining([expect.stringContaining('Would have uploaded 1 asset')]),
      );
      expect(exitCode).toBe(0);

      const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
      expect(assets.total).toBe(0);
    });

    it('dry run should handle duplicates', async () => {
      const first = await immichCli(['upload', `${testAssetDir}/albums/nature/silver_fir.jpg`]);
      expect(first.stderr).toContain('{message}');
      expect(first.stdout.split('\n')).toEqual(
        expect.arrayContaining([expect.stringContaining('Successfully uploaded 1 new asset')]),
      );
      expect(first.exitCode).toBe(0);

      const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
      expect(assets.total).toBe(1);

      const second = await immichCli(['upload', `${testAssetDir}/albums/nature/`, '--dry-run']);
      expect(second.stderr).toContain('{message}');
      expect(second.stdout.split('\n')).toEqual(
        expect.arrayContaining([
          expect.stringContaining('Found 8 new files and 1 duplicate'),
          expect.stringContaining('Would have uploaded 8 assets'),
        ]),
      );
      expect(second.exitCode).toBe(0);
    });
  });

  describe('immich upload --recursive', () => {
    it('should upload a folder recursively', async () => {
      const { stderr, stdout, exitCode } = await immichCli(['upload', `${testAssetDir}/albums/nature/`, '--recursive']);
      expect(stderr).toContain('{message}');
      expect(stdout.split('\n')).toEqual(
        expect.arrayContaining([expect.stringContaining('Successfully uploaded 9 new assets')]),
      );
      expect(exitCode).toBe(0);

      const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
      expect(assets.total).toBe(9);
    });
  });

  describe('immich upload --recursive --album', () => {
    it('should create albums from folder names', async () => {
      const { stderr, stdout, exitCode } = await immichCli([
        'upload',
        `${testAssetDir}/albums/nature/`,
        '--recursive',
        '--album',
      ]);
      expect(stdout.split('\n')).toEqual(
        expect.arrayContaining([
          expect.stringContaining('Successfully uploaded 9 new assets'),
          expect.stringContaining('Successfully created 1 new album'),
          expect.stringContaining('Successfully updated 9 assets'),
        ]),
      );
      expect(stderr).toContain('{message}');
      expect(exitCode).toBe(0);

      const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
      expect(assets.total).toBe(9);

      const albums = await getAllAlbums({}, { headers: asKeyAuth(key) });
      expect(albums.length).toBe(1);
      expect(albums[0].albumName).toBe('nature');
    });

    it('should add existing assets to albums', async () => {
      const response1 = await immichCli(['upload', `${testAssetDir}/albums/nature/`, '--recursive']);
      expect(response1.stdout.split('\n')).toEqual(
        expect.arrayContaining([expect.stringContaining('Successfully uploaded 9 new assets')]),
      );
      expect(response1.stderr).toContain('{message}');
      expect(response1.exitCode).toBe(0);

      const assets1 = await getAssetStatistics({}, { headers: asKeyAuth(key) });
      expect(assets1.total).toBe(9);

      const albums1 = await getAllAlbums({}, { headers: asKeyAuth(key) });
      expect(albums1.length).toBe(0);

      const response2 = await immichCli(['upload', `${testAssetDir}/albums/nature/`, '--recursive', '--album']);
      expect(response2.stdout.split('\n')).toEqual(
        expect.arrayContaining([
          expect.stringContaining('All assets were already uploaded, nothing to do.'),
          expect.stringContaining('Successfully updated 9 assets'),
        ]),
      );
      expect(response2.stderr).toContain('{message}');
      expect(response2.exitCode).toBe(0);

      const assets2 = await getAssetStatistics({}, { headers: asKeyAuth(key) });
      expect(assets2.total).toBe(9);

      const albums2 = await getAllAlbums({}, { headers: asKeyAuth(key) });
      expect(albums2.length).toBe(1);
      expect(albums2[0].albumName).toBe('nature');
    });

    it('should have accurate dry run', async () => {
      const { stderr, stdout, exitCode } = await immichCli([
        'upload',
        `${testAssetDir}/albums/nature/`,
        '--recursive',
        '--album',
        '--dry-run',
      ]);
      expect(stdout.split('\n')).toEqual(
        expect.arrayContaining([
          expect.stringContaining('Would have uploaded 9 assets'),
          expect.stringContaining('Would have created 1 new album'),
          expect.stringContaining('Would have updated albums of 9 assets'),
        ]),
      );
      expect(stderr).toContain('{message}');
      expect(exitCode).toBe(0);

      const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
      expect(assets.total).toBe(0);

      const albums = await getAllAlbums({}, { headers: asKeyAuth(key) });
      expect(albums.length).toBe(0);
    });
  });

  describe('immich upload --recursive --album-name=e2e', () => {
    it('should create a named album', async () => {
      const { stderr, stdout, exitCode } = await immichCli([
        'upload',
        `${testAssetDir}/albums/nature/`,
        '--recursive',
        '--album-name=e2e',
      ]);
      expect(stdout.split('\n')).toEqual(
        expect.arrayContaining([
          expect.stringContaining('Successfully uploaded 9 new assets'),
          expect.stringContaining('Successfully created 1 new album'),
          expect.stringContaining('Successfully updated 9 assets'),
        ]),
      );
      expect(stderr).toContain('{message}');
      expect(exitCode).toBe(0);

      const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
      expect(assets.total).toBe(9);

      const albums = await getAllAlbums({}, { headers: asKeyAuth(key) });
      expect(albums.length).toBe(1);
      expect(albums[0].albumName).toBe('e2e');
    });

    it('should have accurate dry run', async () => {
      const { stderr, stdout, exitCode } = await immichCli([
        'upload',
        `${testAssetDir}/albums/nature/`,
        '--recursive',
        '--album-name=e2e',
        '--dry-run',
      ]);
      expect(stdout.split('\n')).toEqual(
        expect.arrayContaining([
          expect.stringContaining('Would have uploaded 9 assets'),
          expect.stringContaining('Would have created 1 new album'),
          expect.stringContaining('Would have updated albums of 9 assets'),
        ]),
      );
      expect(stderr).toContain('{message}');
      expect(exitCode).toBe(0);

      const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
      expect(assets.total).toBe(0);

      const albums = await getAllAlbums({}, { headers: asKeyAuth(key) });
      expect(albums.length).toBe(0);
    });
  });

  describe('immich upload --delete', () => {
    it('should delete local files if specified', async () => {
      await mkdir(`/tmp/albums/nature`, { recursive: true });
      const filesToLink = await readdir(`${testAssetDir}/albums/nature`);
      for (const file of filesToLink) {
        await symlink(`${testAssetDir}/albums/nature/${file}`, `/tmp/albums/nature/${file}`);
      }

      const { stderr, stdout, exitCode } = await immichCli(['upload', `/tmp/albums/nature`, '--delete']);

      const files = await readdir(`/tmp/albums/nature`);
      await rm(`/tmp/albums/nature`, { recursive: true });
      expect(files).toEqual([]);

      expect(stdout.split('\n')).toEqual(
        expect.arrayContaining([
          expect.stringContaining('Successfully uploaded 9 new assets'),
          expect.stringContaining('Deleting assets that have been uploaded'),
        ]),
      );
      expect(stderr).toContain('{message}');
      expect(exitCode).toBe(0);

      const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
      expect(assets.total).toBe(9);
    });

    it('should have accurate dry run', async () => {
      await mkdir(`/tmp/albums/nature`, { recursive: true });
      const filesToLink = await readdir(`${testAssetDir}/albums/nature`);
      for (const file of filesToLink) {
        await symlink(`${testAssetDir}/albums/nature/${file}`, `/tmp/albums/nature/${file}`);
      }

      const { stderr, stdout, exitCode } = await immichCli(['upload', `/tmp/albums/nature`, '--delete', '--dry-run']);

      const files = await readdir(`/tmp/albums/nature`);
      await rm(`/tmp/albums/nature`, { recursive: true });
      expect(files.length).toBe(9);

      expect(stdout.split('\n')).toEqual(
        expect.arrayContaining([
          expect.stringContaining('Would have uploaded 9 assets'),
          expect.stringContaining('Would have deleted 9 local assets'),
        ]),
      );
      expect(stderr).toContain('{message}');
      expect(exitCode).toBe(0);

      const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
      expect(assets.total).toBe(0);
    });
  });

  describe('immich upload --delete-duplicates', () => {
    it('should delete local duplicate files', async () => {
      const {
        stderr: firstStderr,
        stdout: firstStdout,
        exitCode: firstExitCode,
      } = await immichCli(['upload', `${testAssetDir}/albums/nature/silver_fir.jpg`]);
      expect(firstStderr).toContain('{message}');
      expect(firstStdout.split('\n')).toEqual(
        expect.arrayContaining([expect.stringContaining('Successfully uploaded 1 new asset')]),
      );
      expect(firstExitCode).toBe(0);

      await mkdir(`/tmp/albums/nature`, { recursive: true });
      await symlink(`${testAssetDir}/albums/nature/silver_fir.jpg`, `/tmp/albums/nature/silver_fir.jpg`);

      // Upload with --delete-duplicates flag
      const { stderr, stdout, exitCode } = await immichCli([
        'upload',
        `/tmp/albums/nature/silver_fir.jpg`,
        '--delete-duplicates',
      ]);

      // Check that the duplicate file was deleted
      const files = await readdir(`/tmp/albums/nature`);
      await rm(`/tmp/albums/nature`, { recursive: true });
      expect(files.length).toBe(0);

      expect(stdout.split('\n')).toEqual(
        expect.arrayContaining([
          expect.stringContaining('Found 0 new files and 1 duplicate'),
          expect.stringContaining('All assets were already uploaded, nothing to do'),
        ]),
      );
      expect(stderr).toContain('{message}');
      expect(exitCode).toBe(0);

      // Verify no new assets were uploaded
      const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
      expect(assets.total).toBe(1);
    });

    it('should have accurate dry run with --delete-duplicates', async () => {
      const {
        stderr: firstStderr,
        stdout: firstStdout,
        exitCode: firstExitCode,
      } = await immichCli(['upload', `${testAssetDir}/albums/nature/silver_fir.jpg`]);
      expect(firstStderr).toContain('{message}');
      expect(firstStdout.split('\n')).toEqual(
        expect.arrayContaining([expect.stringContaining('Successfully uploaded 1 new asset')]),
      );
      expect(firstExitCode).toBe(0);

      await mkdir(`/tmp/albums/nature`, { recursive: true });
      await symlink(`${testAssetDir}/albums/nature/silver_fir.jpg`, `/tmp/albums/nature/silver_fir.jpg`);

      // Upload with --delete-duplicates and --dry-run flags
      const { stderr, stdout, exitCode } = await immichCli([
        'upload',
        `/tmp/albums/nature/silver_fir.jpg`,
        '--delete-duplicates',
        '--dry-run',
      ]);

      // Check that the duplicate file was NOT deleted in dry run mode
      const files = await readdir(`/tmp/albums/nature`);
      await rm(`/tmp/albums/nature`, { recursive: true });
      expect(files.length).toBe(1);

      expect(stdout.split('\n')).toEqual(
        expect.arrayContaining([
          expect.stringContaining('Found 0 new files and 1 duplicate'),
          expect.stringContaining('Would have deleted 1 local asset'),
        ]),
      );
      expect(stderr).toContain('{message}');
      expect(exitCode).toBe(0);

      // Verify no new assets were uploaded
      const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
      expect(assets.total).toBe(1);
    });

    it('should work with both --delete and --delete-duplicates flags', async () => {
      // First, upload a file to create a duplicate on the server
      const {
        stderr: firstStderr,
        stdout: firstStdout,
        exitCode: firstExitCode,
      } = await immichCli(['upload', `${testAssetDir}/albums/nature/silver_fir.jpg`]);
      expect(firstStderr).toContain('{message}');
      expect(firstStdout.split('\n')).toEqual(
        expect.arrayContaining([expect.stringContaining('Successfully uploaded 1 new asset')]),
      );
      expect(firstExitCode).toBe(0);

      // Both new and duplicate files
      await mkdir(`/tmp/albums/nature`, { recursive: true });
      await symlink(`${testAssetDir}/albums/nature/silver_fir.jpg`, `/tmp/albums/nature/silver_fir.jpg`); // duplicate
      await symlink(`${testAssetDir}/albums/nature/el_torcal_rocks.jpg`, `/tmp/albums/nature/el_torcal_rocks.jpg`); // new

      // Upload with both --delete and --delete-duplicates flags
      const { stderr, stdout, exitCode } = await immichCli([
        'upload',
        `/tmp/albums/nature`,
        '--delete',
        '--delete-duplicates',
      ]);

      // Check that both files were deleted (new file due to --delete, duplicate due to --delete-duplicates)
      const files = await readdir(`/tmp/albums/nature`);
      await rm(`/tmp/albums/nature`, { recursive: true });
      expect(files.length).toBe(0);

      expect(stdout.split('\n')).toEqual(
        expect.arrayContaining([
          expect.stringContaining('Found 1 new files and 1 duplicate'),
          expect.stringContaining('Successfully uploaded 1 new asset'),
          expect.stringContaining('Deleting assets that have been uploaded'),
        ]),
      );
      expect(stderr).toContain('{message}');
      expect(exitCode).toBe(0);

      // Verify one new asset was uploaded (total should be 2 now)
      const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
      expect(assets.total).toBe(2);
    });

    it('should only delete duplicates when --delete-duplicates is used without --delete', async () => {
      const {
        stderr: firstStderr,
        stdout: firstStdout,
        exitCode: firstExitCode,
      } = await immichCli(['upload', `${testAssetDir}/albums/nature/silver_fir.jpg`]);
      expect(firstStderr).toContain('{message}');
      expect(firstStdout.split('\n')).toEqual(
        expect.arrayContaining([expect.stringContaining('Successfully uploaded 1 new asset')]),
      );
      expect(firstExitCode).toBe(0);

      // Both new and duplicate files
      await mkdir(`/tmp/albums/nature`, { recursive: true });
      await symlink(`${testAssetDir}/albums/nature/silver_fir.jpg`, `/tmp/albums/nature/silver_fir.jpg`); // duplicate
      await symlink(`${testAssetDir}/albums/nature/el_torcal_rocks.jpg`, `/tmp/albums/nature/el_torcal_rocks.jpg`); // new

      // Upload with only --delete-duplicates flag
      const { stderr, stdout, exitCode } = await immichCli(['upload', `/tmp/albums/nature`, '--delete-duplicates']);

      // Check that only the duplicate was deleted, new file should remain
      const files = await readdir(`/tmp/albums/nature`);
      await rm(`/tmp/albums/nature`, { recursive: true });
      expect(files).toEqual(['el_torcal_rocks.jpg']);

      expect(stdout.split('\n')).toEqual(
        expect.arrayContaining([
          expect.stringContaining('Found 1 new files and 1 duplicate'),
          expect.stringContaining('Successfully uploaded 1 new asset'),
        ]),
      );
      expect(stderr).toContain('{message}');
      expect(exitCode).toBe(0);

      // Verify one new asset was uploaded (total should be 2 now)
      const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
      expect(assets.total).toBe(2);
    });
  });

  describe('immich upload --skip-hash', () => {
    it('should skip hashing', async () => {
      const filename = `albums/nature/silver_fir.jpg`;
      await utils.createAsset(admin.accessToken, {
        assetData: {
          bytes: readFileSync(`${testAssetDir}/${filename}`),
          filename: 'silver_fit.jpg',
        },
      });
      const { stderr, stdout, exitCode } = await immichCli(['upload', `${testAssetDir}/${filename}`, '--skip-hash']);

      expect(stderr).toBe('');
      expect(stdout.split('\n')).toEqual(
        expect.arrayContaining([
          'Skipping hash check, assuming all files are new',
          expect.stringContaining('Successfully uploaded 0 new assets'),
          expect.stringContaining('Skipped 1 duplicate asset'),
        ]),
      );
      expect(exitCode).toBe(0);

      const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
      expect(assets.total).toBe(1);
    });

    it('should throw an error if attempting dry run', async () => {
      const { stderr, stdout, exitCode } = await immichCli([
        'upload',
        `${testAssetDir}/albums/nature/`,
        '--skip-hash',
        '--dry-run',
      ]);

      expect(stdout).toBe('');
      expect(stderr).toEqual(`error: option '-n, --dry-run' cannot be used with option '-h, --skip-hash'`);
      expect(exitCode).not.toBe(0);

      const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
      expect(assets.total).toBe(0);
    });
  });

  describe('immich upload --concurrency <number>', () => {
    it('should work', async () => {
      const { stderr, stdout, exitCode } = await immichCli([
        'upload',
        `${testAssetDir}/albums/nature/`,
        '--concurrency',
        '2',
      ]);

      expect(stderr).toContain('{message}');
      expect(stdout.split('\n')).toEqual(
        expect.arrayContaining([
          'Found 9 new files and 0 duplicates',
          expect.stringContaining('Successfully uploaded 9 new assets'),
        ]),
      );
      expect(exitCode).toBe(0);

      const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
      expect(assets.total).toBe(9);
    });

    it('should reject string argument', async () => {
      const { stderr, exitCode } = await immichCli([
        'upload',
        `${testAssetDir}/albums/nature/`,
        '--concurrency string',
      ]);

      expect(stderr).toContain('unknown option');
      expect(exitCode).not.toBe(0);
    });

    it('should reject command without number', async () => {
      const { stderr, exitCode } = await immichCli(['upload', `${testAssetDir}/albums/nature/`, '--concurrency']);

      expect(stderr).toContain('argument missing');
      expect(exitCode).not.toBe(0);
    });
  });

  describe('immich upload --ignore <pattern>', () => {
    it('should work', async () => {
      const { stderr, stdout, exitCode } = await immichCli([
        'upload',
        `${testAssetDir}/albums/nature/`,
        '--ignore',
        'silver_fir.jpg',
      ]);

      expect(stderr).toContain('{message}');
      expect(stdout.split('\n')).toEqual(
        expect.arrayContaining([
          'Found 8 new files and 0 duplicates',
          expect.stringContaining('Successfully uploaded 8 new assets'),
        ]),
      );
      expect(exitCode).toBe(0);

      const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
      expect(assets.total).toBe(8);
    });

    it('should ignore assets matching glob pattern', async () => {
      const { stderr, stdout, exitCode } = await immichCli([
        'upload',
        `${testAssetDir}/albums/nature/`,
        '--ignore',
        '!(*_*_*).jpg',
      ]);

      expect(stderr).toContain('{message}');
      expect(stdout.split('\n')).toEqual(
        expect.arrayContaining([
          'Found 1 new files and 0 duplicates',
          expect.stringContaining('Successfully uploaded 1 new asset'),
        ]),
      );
      expect(exitCode).toBe(0);

      const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
      expect(assets.total).toBe(1);
    });

    it('should have accurate dry run', async () => {
      const { stderr, stdout, exitCode } = await immichCli([
        'upload',
        `${testAssetDir}/albums/nature/`,
        '--ignore',
        'silver_fir.jpg',
        '--dry-run',
      ]);

      expect(stderr).toContain('{message}');
      expect(stdout.split('\n')).toEqual(
        expect.arrayContaining([
          'Found 8 new files and 0 duplicates',
          expect.stringContaining('Would have uploaded 8 assets'),
        ]),
      );
      expect(exitCode).toBe(0);

      const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
      expect(assets.total).toBe(0);
    });
  });
});
