/*!
  Copyright 2013 Lovell Fuller and others.
  SPDX-License-Identifier: Apache-2.0
*/

const fs = require('node:fs');
const { describe, it } = require('node:test');
const assert = require('node:assert');
const exifReader = require('exif-reader');
const icc = require('icc');

const sharp = require('../../');
const fixtures = require('../fixtures');

const create = { width: 1, height: 1, channels: 3, background: 'red' };

describe('Image metadata', () => {
  it('JPEG', (_t, done) => {
    sharp(fixtures.inputJpg).metadata((err, metadata) => {
      if (err) throw err;
      assert.strictEqual('jpeg', metadata.format);
      assert.strictEqual('undefined', typeof metadata.size);
      assert.strictEqual(2725, metadata.width);
      assert.strictEqual(2225, metadata.height);
      assert.strictEqual('srgb', metadata.space);
      assert.strictEqual(3, metadata.channels);
      assert.strictEqual('uchar', metadata.depth);
      assert.strictEqual(true, ['undefined', 'number'].includes(typeof metadata.density));
      assert.strictEqual('4:2:0', metadata.chromaSubsampling);
      assert.strictEqual(false, metadata.isProgressive);
      assert.strictEqual(false, metadata.hasProfile);
      assert.strictEqual(false, metadata.hasAlpha);
      assert.strictEqual('undefined', typeof metadata.orientation);
      assert.strictEqual('undefined', typeof metadata.exif);
      assert.strictEqual('undefined', typeof metadata.icc);
      done();
    });
  });

  it('JPEG with EXIF/ICC', (_t, done) => {
    sharp(fixtures.inputJpgWithExif).metadata((err, metadata) => {
      if (err) throw err;
      assert.strictEqual('jpeg', metadata.format);
      assert.strictEqual('undefined', typeof metadata.size);
      assert.strictEqual(450, metadata.width);
      assert.strictEqual(600, metadata.height);
      assert.strictEqual('srgb', metadata.space);
      assert.strictEqual(3, metadata.channels);
      assert.strictEqual('uchar', metadata.depth);
      assert.strictEqual(72, metadata.density);
      assert.strictEqual('4:2:0', metadata.chromaSubsampling);
      assert.strictEqual(false, metadata.isProgressive);
      assert.strictEqual(true, metadata.hasProfile);
      assert.strictEqual(false, metadata.hasAlpha);
      assert.strictEqual(8, metadata.orientation);
      // EXIF
      assert.strictEqual('object', typeof metadata.exif);
      assert.strictEqual(true, metadata.exif instanceof Buffer);
      const exif = exifReader(metadata.exif);
      assert.strictEqual('object', typeof exif);
      assert.strictEqual('object', typeof exif.Image);
      assert.strictEqual('number', typeof exif.Image.XResolution);
      // ICC
      assert.strictEqual('object', typeof metadata.icc);
      assert.strictEqual(true, metadata.icc instanceof Buffer);
      const profile = icc.parse(metadata.icc);
      assert.strictEqual('object', typeof profile);
      assert.strictEqual('Generic RGB Profile', profile.description);
      done();
    });
  });

  it('JPEG with IPTC/XMP', (_t, done) => {
    sharp(fixtures.inputJpgWithIptcAndXmp).metadata((err, metadata) => {
      if (err) throw err;
      // IPTC
      assert.strictEqual('object', typeof metadata.iptc);
      assert.strictEqual(true, metadata.iptc instanceof Buffer);
      assert.strictEqual(18250, metadata.iptc.byteLength);
      assert.strictEqual(metadata.iptc.indexOf(Buffer.from('Photoshop')), 0);
      // XMP
      assert.strictEqual('object', typeof metadata.xmp);
      assert.strictEqual(true, metadata.xmp instanceof Buffer);
      assert.strictEqual(12466, metadata.xmp.byteLength);
      assert.strictEqual(metadata.xmp.indexOf(Buffer.from('<?xpacket begin="')), 0);
      assert(metadata.xmpAsString.startsWith('<?xpacket begin="﻿" id="W5M0MpCehiHzreSzNTczkc9d"?>'));
      done();
    });
  });

  it('TIFF', (_t, done) => {
    sharp(fixtures.inputTiff).metadata((err, metadata) => {
      if (err) throw err;
      assert.strictEqual('tiff', metadata.format);
      assert.strictEqual('undefined', typeof metadata.size);
      assert.strictEqual(2464, metadata.width);
      assert.strictEqual(3248, metadata.height);
      assert.strictEqual('b-w', metadata.space);
      assert.strictEqual(1, metadata.channels);
      assert.strictEqual('uchar', metadata.depth);
      assert.strictEqual(300, metadata.density);
      assert.strictEqual('undefined', typeof metadata.chromaSubsampling);
      assert.strictEqual(false, metadata.isProgressive);
      assert.strictEqual(false, metadata.hasProfile);
      assert.strictEqual(false, metadata.hasAlpha);
      assert.strictEqual(1, metadata.orientation);
      assert.strictEqual(2464, metadata.autoOrient.width);
      assert.strictEqual(3248, metadata.autoOrient.height);
      assert.strictEqual('undefined', typeof metadata.exif);
      assert.strictEqual('undefined', typeof metadata.icc);
      assert.strictEqual('undefined', typeof metadata.xmp);
      assert.strictEqual('undefined', typeof metadata.xmpAsString);
      assert.strictEqual('inch', metadata.resolutionUnit);
      done();
    });
  });

  it('Multipage TIFF', (_t, done) => {
    sharp(fixtures.inputTiffMultipage).metadata((err, metadata) => {
      if (err) throw err;
      assert.strictEqual('tiff', metadata.format);
      assert.strictEqual('undefined', typeof metadata.size);
      assert.strictEqual(2464, metadata.width);
      assert.strictEqual(3248, metadata.height);
      assert.strictEqual('b-w', metadata.space);
      assert.strictEqual(1, metadata.channels);
      assert.strictEqual('uchar', metadata.depth);
      assert.strictEqual(300, metadata.density);
      assert.strictEqual('undefined', typeof metadata.chromaSubsampling);
      assert.strictEqual(false, metadata.isProgressive);
      assert.strictEqual(2, metadata.pages);
      assert.strictEqual(false, metadata.hasProfile);
      assert.strictEqual(false, metadata.hasAlpha);
      assert.strictEqual(1, metadata.orientation);
      assert.strictEqual('undefined', typeof metadata.exif);
      assert.strictEqual('undefined', typeof metadata.icc);
      done();
    });
  });

  it('PNG', (_t, done) => {
    sharp(fixtures.inputPng).metadata((err, metadata) => {
      if (err) throw err;
      assert.strictEqual('png', metadata.format);
      assert.strictEqual('undefined', typeof metadata.size);
      assert.strictEqual(2809, metadata.width);
      assert.strictEqual(2074, metadata.height);
      assert.strictEqual('b-w', metadata.space);
      assert.strictEqual(1, metadata.channels);
      assert.strictEqual('uchar', metadata.depth);
      assert.strictEqual(300, metadata.density);
      assert.strictEqual('undefined', typeof metadata.chromaSubsampling);
      assert.strictEqual(false, metadata.isProgressive);
      assert.strictEqual(false, metadata.hasProfile);
      assert.strictEqual(false, metadata.hasAlpha);
      assert.strictEqual('undefined', typeof metadata.orientation);
      assert.strictEqual(2809, metadata.autoOrient.width);
      assert.strictEqual(2074, metadata.autoOrient.height);
      assert.strictEqual('undefined', typeof metadata.exif);
      assert.strictEqual('undefined', typeof metadata.icc);
      done();
    });
  });

  it('PNG with comment', (_t, done) => {
    sharp(fixtures.inputPngTestJoinChannel).metadata((err, metadata) => {
      if (err) throw err;
      assert.strictEqual('png', metadata.format);
      assert.strictEqual('undefined', typeof metadata.size);
      assert.strictEqual(320, metadata.width);
      assert.strictEqual(240, metadata.height);
      assert.strictEqual('b-w', metadata.space);
      assert.strictEqual(1, metadata.channels);
      assert.strictEqual('uchar', metadata.depth);
      assert.strictEqual(72, metadata.density);
      assert.strictEqual('undefined', typeof metadata.chromaSubsampling);
      assert.strictEqual(false, metadata.isProgressive);
      assert.strictEqual(false, metadata.hasProfile);
      assert.strictEqual(false, metadata.hasAlpha);
      assert.strictEqual('undefined', typeof metadata.orientation);
      assert.strictEqual('undefined', typeof metadata.exif);
      assert.strictEqual('undefined', typeof metadata.icc);
      assert.strictEqual(1, metadata.comments.length);
      assert.strictEqual('Comment', metadata.comments[0].keyword);
      assert.strictEqual('Created with GIMP', metadata.comments[0].text);
      done();
    });
  });

  it('Transparent PNG', (_t, done) => {
    sharp(fixtures.inputPngWithTransparency).metadata((err, metadata) => {
      if (err) throw err;
      assert.strictEqual('png', metadata.format);
      assert.strictEqual('undefined', typeof metadata.size);
      assert.strictEqual(2048, metadata.width);
      assert.strictEqual(1536, metadata.height);
      assert.strictEqual('srgb', metadata.space);
      assert.strictEqual(4, metadata.channels);
      assert.strictEqual('uchar', metadata.depth);
      assert.strictEqual(72, metadata.density);
      assert.strictEqual('undefined', typeof metadata.chromaSubsampling);
      assert.strictEqual(false, metadata.isProgressive);
      assert.strictEqual(false, metadata.hasProfile);
      assert.strictEqual(true, metadata.hasAlpha);
      assert.strictEqual('undefined', typeof metadata.orientation);
      assert.strictEqual('undefined', typeof metadata.exif);
      assert.strictEqual('undefined', typeof metadata.icc);
      done();
    });
  });

  it('PNG with greyscale bKGD chunk - 8 bit', async () => {
    const data = await sharp(fixtures.inputPng8BitGreyBackground).metadata();
    assert.deepStrictEqual(data, {
      background: {
        gray: 0
      },
      bitsPerSample: 8,
      channels: 2,
      density: 72,
      depth: 'uchar',
      format: 'png',
      hasAlpha: true,
      hasProfile: false,
      height: 32,
      isPalette: false,
      isProgressive: false,
      space: 'b-w',
      width: 32,
      autoOrient: {
        width: 32,
        height: 32
      }
    });
  });

  it('PNG with greyscale bKGD chunk - 16 bit', async () => {
    const data = await sharp(fixtures.inputPng16BitGreyBackground).metadata();
    assert.deepStrictEqual(data, {
      background: {
        gray: 67
      },
      bitsPerSample: 16,
      channels: 2,
      density: 72,
      depth: 'ushort',
      format: 'png',
      hasAlpha: true,
      hasProfile: false,
      height: 32,
      isPalette: false,
      isProgressive: false,
      space: 'grey16',
      width: 32,
      autoOrient: {
        width: 32,
        height: 32
      }
    });
  });

  it('WebP', (_t, done) => {
    sharp(fixtures.inputWebP).metadata((err, metadata) => {
      if (err) throw err;
      assert.strictEqual('webp', metadata.format);
      assert.strictEqual('undefined', typeof metadata.size);
      assert.strictEqual(1024, metadata.width);
      assert.strictEqual(772, metadata.height);
      assert.strictEqual('srgb', metadata.space);
      assert.strictEqual(3, metadata.channels);
      assert.strictEqual('uchar', metadata.depth);
      assert.strictEqual('undefined', typeof metadata.density);
      assert.strictEqual('undefined', typeof metadata.chromaSubsampling);
      assert.strictEqual(false, metadata.isProgressive);
      assert.strictEqual(false, metadata.hasProfile);
      assert.strictEqual(false, metadata.hasAlpha);
      assert.strictEqual('undefined', typeof metadata.orientation);
      assert.strictEqual('undefined', typeof metadata.exif);
      assert.strictEqual('undefined', typeof metadata.icc);
      done();
    });
  });

  it('Animated WebP', () =>
    sharp(fixtures.inputWebPAnimated)
      .metadata()
      .then(({
        format, width, height, space, channels, depth,
        isProgressive, pages, loop, delay, hasProfile,
        hasAlpha
      }) => {
        assert.strictEqual(format, 'webp');
        assert.strictEqual(width, 80);
        assert.strictEqual(height, 80);
        assert.strictEqual(space, 'srgb');
        assert.strictEqual(channels, 4);
        assert.strictEqual(depth, 'uchar');
        assert.strictEqual(isProgressive, false);
        assert.strictEqual(pages, 9);
        assert.strictEqual(loop, 0);
        assert.deepStrictEqual(delay, [120, 120, 90, 120, 120, 90, 120, 90, 30]);
        assert.strictEqual(hasProfile, false);
        assert.strictEqual(hasAlpha, true);
      })
  );

  it('Animated WebP with all pages', () =>
    sharp(fixtures.inputWebPAnimated, { pages: -1 })
      .metadata()
      .then(({
        format, width, height, space, channels, depth,
        isProgressive, pages, pageHeight, loop, delay,
        hasProfile, hasAlpha
      }) => {
        assert.strictEqual(format, 'webp');
        assert.strictEqual(width, 80);
        assert.strictEqual(height, 720);
        assert.strictEqual(space, 'srgb');
        assert.strictEqual(channels, 4);
        assert.strictEqual(depth, 'uchar');
        assert.strictEqual(isProgressive, false);
        assert.strictEqual(pages, 9);
        assert.strictEqual(pageHeight, 80);
        assert.strictEqual(loop, 0);
        assert.deepStrictEqual(delay, [120, 120, 90, 120, 120, 90, 120, 90, 30]);
        assert.strictEqual(hasProfile, false);
        assert.strictEqual(hasAlpha, true);
      })
  );

  it('Animated WebP with limited looping', () =>
    sharp(fixtures.inputWebPAnimatedLoop3)
      .metadata()
      .then(({
        format, width, height, space, channels, depth,
        isProgressive, pages, loop, delay, hasProfile,
        hasAlpha
      }) => {
        assert.strictEqual(format, 'webp');
        assert.strictEqual(width, 370);
        assert.strictEqual(height, 285);
        assert.strictEqual(space, 'srgb');
        assert.strictEqual(channels, 4);
        assert.strictEqual(depth, 'uchar');
        assert.strictEqual(isProgressive, false);
        assert.strictEqual(pages, 10);
        assert.strictEqual(loop, 3);
        assert.deepStrictEqual(delay, [...Array(9).fill(3000), 15000]);
        assert.strictEqual(hasProfile, false);
        assert.strictEqual(hasAlpha, true);
      })
  );

  it('GIF', (_t, done) => {
    sharp(fixtures.inputGif).metadata((err, metadata) => {
      if (err) throw err;
      assert.strictEqual('gif', metadata.format);
      assert.strictEqual('undefined', typeof metadata.size);
      assert.strictEqual(800, metadata.width);
      assert.strictEqual(533, metadata.height);
      assert.strictEqual(3, metadata.channels);
      assert.strictEqual('uchar', metadata.depth);
      assert.strictEqual('undefined', typeof metadata.density);
      assert.strictEqual('undefined', typeof metadata.chromaSubsampling);
      assert.strictEqual(false, metadata.isProgressive);
      assert.strictEqual(false, metadata.hasProfile);
      assert.strictEqual('undefined', typeof metadata.orientation);
      assert.strictEqual('undefined', typeof metadata.exif);
      assert.strictEqual('undefined', typeof metadata.icc);
      assert.deepStrictEqual(metadata.background, { r: 138, g: 148, b: 102 });
      done();
    });
  });
  it('GIF grey+alpha', (_t, done) => {
    sharp(fixtures.inputGifGreyPlusAlpha).metadata((err, metadata) => {
      if (err) throw err;
      assert.strictEqual('gif', metadata.format);
      assert.strictEqual('undefined', typeof metadata.size);
      assert.strictEqual(2, metadata.width);
      assert.strictEqual(1, metadata.height);
      assert.strictEqual(4, metadata.channels);
      assert.strictEqual('uchar', metadata.depth);
      assert.strictEqual('undefined', typeof metadata.density);
      assert.strictEqual('undefined', typeof metadata.chromaSubsampling);
      assert.strictEqual(false, metadata.isProgressive);
      assert.strictEqual(false, metadata.hasProfile);
      assert.strictEqual('undefined', typeof metadata.orientation);
      assert.strictEqual('undefined', typeof metadata.exif);
      assert.strictEqual('undefined', typeof metadata.icc);
      done();
    });
  });

  it('Animated GIF', () =>
    sharp(fixtures.inputGifAnimated)
      .metadata()
      .then(({
        format, width, height, space, channels, depth,
        isProgressive, pages, loop, delay, background,
        hasProfile, hasAlpha
      }) => {
        assert.strictEqual(format, 'gif');
        assert.strictEqual(width, 80);
        assert.strictEqual(height, 80);
        assert.strictEqual(space, 'srgb');
        assert.strictEqual(channels, 4);
        assert.strictEqual(depth, 'uchar');
        assert.strictEqual(isProgressive, false);
        assert.strictEqual(pages, 30);
        assert.strictEqual(loop, 0);
        assert.deepStrictEqual(delay, Array(30).fill(30));
        assert.deepStrictEqual(background, { r: 0, g: 0, b: 0 });
        assert.strictEqual(hasProfile, false);
        assert.strictEqual(hasAlpha, true);
      })
  );

  it('Animated GIF with limited looping', () =>
    sharp(fixtures.inputGifAnimatedLoop3)
      .metadata()
      .then(({
        format, width, height, space, channels, depth,
        isProgressive, pages, loop, delay, hasProfile,
        hasAlpha
      }) => {
        assert.strictEqual(format, 'gif');
        assert.strictEqual(width, 370);
        assert.strictEqual(height, 285);
        assert.strictEqual(space, 'srgb');
        assert.strictEqual(channels, 4);
        assert.strictEqual(depth, 'uchar');
        assert.strictEqual(isProgressive, false);
        assert.strictEqual(pages, 10);
        assert.strictEqual(loop, 3);
        assert.deepStrictEqual(delay, [...Array(9).fill(3000), 15000]);
        assert.strictEqual(hasProfile, false);
        assert.strictEqual(hasAlpha, true);
      })
  );

  it('vips', () =>
    sharp(fixtures.inputV)
      .metadata()
      .then(metadata => {
        assert.strictEqual('vips', metadata.format);
        assert.strictEqual('undefined', typeof metadata.size);
        assert.strictEqual(70, metadata.width);
        assert.strictEqual(60, metadata.height);
        assert.strictEqual(3, metadata.channels);
        assert.strictEqual('uchar', metadata.depth);
        assert.strictEqual(72, metadata.density);
        assert.strictEqual('undefined', typeof metadata.chromaSubsampling);
        assert.strictEqual(false, metadata.isProgressive);
        assert.strictEqual(false, metadata.hasProfile);
        assert.strictEqual(false, metadata.hasAlpha);
        assert.strictEqual('undefined', typeof metadata.orientation);
        assert.strictEqual('undefined', typeof metadata.exif);
        assert.strictEqual('undefined', typeof metadata.icc);
      })
  );

  it('File in, Promise out', (_t, done) => {
    sharp(fixtures.inputJpg).metadata().then((metadata) => {
      assert.strictEqual('jpeg', metadata.format);
      assert.strictEqual('undefined', typeof metadata.size);
      assert.strictEqual(2725, metadata.width);
      assert.strictEqual(2225, metadata.height);
      assert.strictEqual('srgb', metadata.space);
      assert.strictEqual(3, metadata.channels);
      assert.strictEqual('uchar', metadata.depth);
      assert.strictEqual(true, ['undefined', 'number'].includes(typeof metadata.density));
      assert.strictEqual('4:2:0', metadata.chromaSubsampling);
      assert.strictEqual(false, metadata.isProgressive);
      assert.strictEqual(false, metadata.hasProfile);
      assert.strictEqual(false, metadata.hasAlpha);
      assert.strictEqual('undefined', typeof metadata.orientation);
      assert.strictEqual('undefined', typeof metadata.exif);
      assert.strictEqual('undefined', typeof metadata.icc);
      done();
    });
  });

  it('Non-existent file in, Promise out', async () =>
    assert.rejects(
      () => sharp('fail').metadata(),
      (err) => {
        assert.strictEqual(err.message, 'Input file is missing: fail');
        assert(err.stack.includes('at Sharp.metadata'));
        assert(err.stack.includes(__filename));
        return true;
      }
    )
  );

  it('Invalid stream in, callback out', (_t, done) => {
    fs.createReadStream(__filename).pipe(
      sharp().metadata((err) => {
        assert.strictEqual(err.message, 'Input buffer contains unsupported image format');
        assert(err.stack.includes('at Sharp.metadata'));
        assert(err.stack.includes(__filename));
        done();
      })
    );
  });

  it('Stream in, Promise out', (_t, done) => {
    const readable = fs.createReadStream(fixtures.inputJpg);
    const pipeline = sharp();
    pipeline.metadata().then((metadata) => {
      assert.strictEqual('jpeg', metadata.format);
      assert.strictEqual(829183, metadata.size);
      assert.strictEqual(2725, metadata.width);
      assert.strictEqual(2225, metadata.height);
      assert.strictEqual('srgb', metadata.space);
      assert.strictEqual(3, metadata.channels);
      assert.strictEqual('uchar', metadata.depth);
      assert.strictEqual(true, ['undefined', 'number'].includes(typeof metadata.density));
      assert.strictEqual('4:2:0', metadata.chromaSubsampling);
      assert.strictEqual(false, metadata.isProgressive);
      assert.strictEqual(false, metadata.hasProfile);
      assert.strictEqual(false, metadata.hasAlpha);
      assert.strictEqual('undefined', typeof metadata.orientation);
      assert.strictEqual('undefined', typeof metadata.exif);
      assert.strictEqual('undefined', typeof metadata.icc);
      done();
    }).catch(_t, done);
    readable.pipe(pipeline);
  });

  it('Stream in, rejected Promise out', () => {
    const pipeline = sharp();
    fs
      .createReadStream(__filename)
      .pipe(pipeline);

    return pipeline
      .metadata()
      .then(
        () => Promise.reject(new Error('Expected metadata to reject')),
        err => assert.strictEqual(err.message, 'Input buffer contains unsupported image format')
      );
  });

  it('Stream in, finish event fires before metadata is requested', (_t, done) => {
    const create = { width: 1, height: 1, channels: 3, background: 'red' };
    const image1 = sharp({ create }).png().pipe(sharp());
    const image2 = sharp({ create }).png().pipe(sharp());
    setTimeout(async () => {
      const data1 = await image1.metadata();
      assert.strictEqual('png', data1.format);
      const data2 = await image2.metadata();
      assert.strictEqual('png', data2.format);
      done();
    }, 500);
  });

  it('Stream', (_t, done) => {
    const readable = fs.createReadStream(fixtures.inputJpg);
    const pipeline = sharp().metadata((err, metadata) => {
      if (err) throw err;
      assert.strictEqual('jpeg', metadata.format);
      assert.strictEqual(829183, metadata.size);
      assert.strictEqual(2725, metadata.width);
      assert.strictEqual(2225, metadata.height);
      assert.strictEqual('srgb', metadata.space);
      assert.strictEqual(3, metadata.channels);
      assert.strictEqual('uchar', metadata.depth);
      assert.strictEqual(true, ['undefined', 'number'].includes(typeof metadata.density));
      assert.strictEqual('4:2:0', metadata.chromaSubsampling);
      assert.strictEqual(false, metadata.isProgressive);
      assert.strictEqual(false, metadata.hasProfile);
      assert.strictEqual(false, metadata.hasAlpha);
      assert.strictEqual('undefined', typeof metadata.orientation);
      assert.strictEqual('undefined', typeof metadata.exif);
      assert.strictEqual('undefined', typeof metadata.icc);
      done();
    });
    readable.pipe(pipeline);
  });

  it('Resize to half width using metadata', (_t, done) => {
    const image = sharp(fixtures.inputJpg);
    image.metadata((err, metadata) => {
      if (err) throw err;
      assert.strictEqual('jpeg', metadata.format);
      assert.strictEqual('undefined', typeof metadata.size);
      assert.strictEqual(2725, metadata.width);
      assert.strictEqual(2225, metadata.height);
      assert.strictEqual('srgb', metadata.space);
      assert.strictEqual(3, metadata.channels);
      assert.strictEqual('uchar', metadata.depth);
      assert.strictEqual(true, ['undefined', 'number'].includes(typeof metadata.density));
      assert.strictEqual('4:2:0', metadata.chromaSubsampling);
      assert.strictEqual(false, metadata.isProgressive);
      assert.strictEqual(false, metadata.hasProfile);
      assert.strictEqual(false, metadata.hasAlpha);
      assert.strictEqual('undefined', typeof metadata.orientation);
      assert.strictEqual('undefined', typeof metadata.exif);
      assert.strictEqual('undefined', typeof metadata.icc);
      image.resize(Math.floor(metadata.width / 2)).toBuffer((err, data, info) => {
        if (err) throw err;
        assert.strictEqual(true, data.length > 0);
        assert.strictEqual(1362, info.width);
        assert.strictEqual(1112, info.height);
        done();
      });
    });
  });

  it('Keep EXIF metadata and add sRGB profile after a resize', (_t, done) => {
    sharp(fixtures.inputJpgWithExif)
      .resize(320, 240)
      .withMetadata()
      .toBuffer((err, buffer) => {
        if (err) throw err;
        sharp(buffer).metadata((err, metadata) => {
          if (err) throw err;
          assert.strictEqual(true, metadata.hasProfile);
          assert.strictEqual(8, metadata.orientation);
          assert.strictEqual(320, metadata.width);
          assert.strictEqual(240, metadata.height);
          assert.strictEqual(240, metadata.autoOrient.width);
          assert.strictEqual(320, metadata.autoOrient.height);
          assert.strictEqual('object', typeof metadata.exif);
          assert.strictEqual(true, metadata.exif instanceof Buffer);
          // EXIF
          const exif = exifReader(metadata.exif);
          assert.strictEqual('object', typeof exif);
          assert.strictEqual('object', typeof exif.Image);
          assert.strictEqual('number', typeof exif.Image.XResolution);
          // ICC
          assert.strictEqual('object', typeof metadata.icc);
          assert.strictEqual(true, metadata.icc instanceof Buffer);
          const profile = icc.parse(metadata.icc);
          assert.strictEqual('object', typeof profile);
          assert.strictEqual('RGB', profile.colorSpace);
          assert.strictEqual('Perceptual', profile.intent);
          assert.strictEqual('Monitor', profile.deviceClass);
          done();
        });
      });
  });

  it('keep existing ICC profile', async () => {
    const data = await sharp(fixtures.inputJpgWithExif)
      .keepIccProfile()
      .toBuffer();

    const metadata = await sharp(data).metadata();
    const { description } = icc.parse(metadata.icc);
    assert.strictEqual(description, 'Generic RGB Profile');
  });

  it('keep existing CMYK input profile for CMYK output', async () => {
    const data = await sharp(fixtures.inputJpgWithCmykProfile)
      .keepIccProfile()
      .toColourspace('cmyk')
      .toBuffer();

    const metadata = await sharp(data).metadata();
    assert.strictEqual(metadata.channels, 4);
    const { description } = icc.parse(metadata.icc);
    assert.strictEqual(description, 'U.S. Web Coated (SWOP) v2');
  });

  it('transform with but discard existing RGB input profile for CMYK output', async () => {
    const data = await sharp(fixtures.inputJpgWithExif)
      .keepIccProfile()
      .toColourspace('cmyk')
      .toBuffer();

    const metadata = await sharp(data).metadata();
    assert.strictEqual(metadata.channels, 4);
    assert.strictEqual(metadata.icc, undefined);
  });

  it('keep existing ICC profile, avoid colour transform', async () => {
    const [r, g, b] = await sharp(fixtures.inputPngWithProPhotoProfile)
      .keepIccProfile()
      .raw()
      .toBuffer();

    assert.strictEqual(r, 131);
    assert.strictEqual(g, 141);
    assert.strictEqual(b, 192);
  });

  it('keep existing CMYK ICC profile', async () => {
    const data = await sharp(fixtures.inputJpgWithCmykProfile)
      .pipelineColourspace('cmyk')
      .toColourspace('cmyk')
      .keepIccProfile()
      .toBuffer();

    const metadata = await sharp(data).metadata();
    assert.strictEqual(metadata.channels, 4);
    const { description } = icc.parse(metadata.icc);
    assert.strictEqual(description, 'U.S. Web Coated (SWOP) v2');
  });

  it('transform to ICC profile and attach', async () => {
    const data = await sharp({ create })
      .png()
      .withIccProfile('p3', { attach: true })
      .toBuffer();

    const metadata = await sharp(data).metadata();
    const { description } = icc.parse(metadata.icc);
    assert.strictEqual(description, 'sP3C');
  });

  it('transform to ICC profile but do not attach', async () => {
    const data = await sharp({ create })
      .png()
      .withIccProfile('p3', { attach: false })
      .toBuffer();

    const metadata = await sharp(data).metadata();
    assert.strictEqual(3, metadata.channels);
    assert.strictEqual(undefined, metadata.icc);
  });

  it('transform to invalid ICC profile emits warning', async () => {
    const img = sharp({ create })
      .png()
      .withIccProfile(fixtures.path('invalid-illuminant.icc'));

    const warningsEmitted = [];
    img.on('warning', (warning) => {
      warningsEmitted.push(warning);
    });

    const data = await img.toBuffer();
    assert.strict(warningsEmitted.includes('Invalid profile'));

    const metadata = await sharp(data).metadata();
    assert.strictEqual(3, metadata.channels);
    assert.strictEqual(undefined, metadata.icc);
  });

  it('Apply CMYK output ICC profile', (_t, done) => {
    const output = fixtures.path('output.icc-cmyk.jpg');
    sharp(fixtures.inputJpg)
      .resize(64)
      .withIccProfile('cmyk')
      .toFile(output, (err) => {
        if (err) throw err;
        sharp(output).metadata((err, metadata) => {
          if (err) throw err;
          assert.strictEqual(true, metadata.hasProfile);
          assert.strictEqual('cmyk', metadata.space);
          assert.strictEqual(4, metadata.channels);
          // ICC
          assert.strictEqual('object', typeof metadata.icc);
          assert.strictEqual(true, metadata.icc instanceof Buffer);
          const profile = icc.parse(metadata.icc);
          assert.strictEqual('object', typeof profile);
          assert.strictEqual('CMYK', profile.colorSpace);
          assert.strictEqual('Relative', profile.intent);
          assert.strictEqual('Printer', profile.deviceClass);
        });
        fixtures.assertSimilar(output, fixtures.expected('icc-cmyk.jpg'), { threshold: 1 }, done);
      });
  });

  it('Apply custom output ICC profile', (_t, done) => {
    const output = fixtures.path('output.hilutite.jpg');
    sharp(fixtures.inputJpg)
      .resize(64)
      .withIccProfile(fixtures.path('hilutite.icm'))
      .toFile(output, (err) => {
        if (err) throw err;
        fixtures.assertMaxColourDistance(output, fixtures.expected('hilutite.jpg'), 9);
        done();
      });
  });

  it('Include metadata in output, enabled via empty object', () =>
    sharp(fixtures.inputJpgWithExif)
      .withMetadata({})
      .toBuffer()
      .then((buffer) => sharp(buffer)
        .metadata()
        .then(metadata => {
          assert.strictEqual(true, metadata.hasProfile);
          assert.strictEqual(8, metadata.orientation);
          assert.strictEqual('object', typeof metadata.exif);
          assert.strictEqual(true, metadata.exif instanceof Buffer);
          // EXIF
          const exif = exifReader(metadata.exif);
          assert.strictEqual('object', typeof exif);
          assert.strictEqual('object', typeof exif.Image);
          assert.strictEqual('number', typeof exif.Image.XResolution);
          // ICC
          assert.strictEqual('object', typeof metadata.icc);
          assert.strictEqual(true, metadata.icc instanceof Buffer);
          const profile = icc.parse(metadata.icc);
          assert.strictEqual('object', typeof profile);
          assert.strictEqual('RGB', profile.colorSpace);
          assert.strictEqual('Perceptual', profile.intent);
          assert.strictEqual('Monitor', profile.deviceClass);
        })
      )
  );

  it('Remove EXIF metadata after a resize', (_t, done) => {
    sharp(fixtures.inputJpgWithExif)
      .resize(320, 240)
      .toBuffer((err, buffer) => {
        if (err) throw err;
        sharp(buffer).metadata((err, metadata) => {
          if (err) throw err;
          assert.strictEqual(false, metadata.hasProfile);
          assert.strictEqual('undefined', typeof metadata.orientation);
          assert.strictEqual('undefined', typeof metadata.exif);
          assert.strictEqual('undefined', typeof metadata.icc);
          done();
        });
      });
  });

  it('Remove metadata from PNG output', (_t, done) => {
    sharp(fixtures.inputJpgWithExif)
      .png()
      .toBuffer((err, buffer) => {
        if (err) throw err;
        sharp(buffer).metadata((err, metadata) => {
          if (err) throw err;
          assert.strictEqual(false, metadata.hasProfile);
          assert.strictEqual('undefined', typeof metadata.orientation);
          assert.strictEqual('undefined', typeof metadata.exif);
          assert.strictEqual('undefined', typeof metadata.icc);
          done();
        });
      });
  });

  it('Add EXIF metadata to JPEG', async () => {
    const data = await sharp({ create })
      .jpeg()
      .withMetadata({
        exif: {
          IFD0: { Software: 'sharp' },
          IFD2: { ExposureTime: '0.2' }
        }
      })
      .toBuffer();

    const { exif } = await sharp(data).metadata();
    const parsedExif = exifReader(exif);
    assert.strictEqual(parsedExif.Image.Software, 'sharp');
    assert.strictEqual(parsedExif.Photo.ExposureTime, 0.2);
  });

  it('Set density of JPEG', async () => {
    const data = await sharp({ create })
      .withMetadata({
        density: 300
      })
      .jpeg()
      .toBuffer();

    const { density } = await sharp(data).metadata();
    assert.strictEqual(density, 300);
  });

  it('Set density of PNG', async () => {
    const data = await sharp({ create })
      .withMetadata({
        density: 96
      })
      .png()
      .toBuffer();

    const { density } = await sharp(data).metadata();
    assert.strictEqual(density, 96);
  });

  it('chromaSubsampling 4:4:4:4 CMYK JPEG', () => sharp(fixtures.inputJpgWithCmykProfile)
      .metadata()
      .then((metadata) => {
        assert.strictEqual('4:4:4:4', metadata.chromaSubsampling);
      }));

  it('chromaSubsampling 4:4:4 RGB JPEG', () => sharp(fixtures.inputJpg)
      .resize(10, 10)
      .jpeg({ chromaSubsampling: '4:4:4' })
      .toBuffer()
      .then((data) => sharp(data)
          .metadata()
          .then((metadata) => {
            assert.strictEqual('4:4:4', metadata.chromaSubsampling);
          })));

  it('isProgressive JPEG', () => sharp(fixtures.inputJpg)
      .resize(10, 10)
      .jpeg({ progressive: true })
      .toBuffer()
      .then((data) => sharp(data)
          .metadata()
          .then((metadata) => {
            assert.strictEqual(true, metadata.isProgressive);
          })));

  it('isProgressive PNG', () => sharp(fixtures.inputJpg)
      .resize(10, 10)
      .png({ progressive: true })
      .toBuffer()
      .then((data) => sharp(data)
          .metadata()
          .then((metadata) => {
            assert.strictEqual(true, metadata.isProgressive);
          })));

  it('16-bit TIFF with TIFFTAG_PHOTOSHOP metadata', () =>
    sharp(fixtures.inputTifftagPhotoshop)
      .metadata()
      .then(metadata => {
        assert.strictEqual(metadata.format, 'tiff');
        assert.strictEqual(metadata.width, 317);
        assert.strictEqual(metadata.height, 211);
        assert.strictEqual(metadata.space, 'rgb16');
        assert.strictEqual(metadata.channels, 3);
        assert.strictEqual(typeof metadata.tifftagPhotoshop, 'object');
        assert.strictEqual(metadata.tifftagPhotoshop instanceof Buffer, true);
        assert.strictEqual(metadata.tifftagPhotoshop.length, 6634);
      })
  );

  it('AVIF', async () => {
    const metadata = await sharp(fixtures.inputAvif).metadata();
    assert.deepStrictEqual(metadata, {
      format: 'heif',
      width: 2048,
      height: 858,
      space: 'srgb',
      channels: 3,
      depth: 'uchar',
      isProgressive: false,
      isPalette: false,
      bitsPerSample: 8,
      pages: 1,
      pagePrimary: 0,
      compression: 'av1',
      hasProfile: false,
      hasAlpha: false,
      autoOrient: {
        width: 2048,
        height: 858
      }
    });
  });

  it('withMetadata adds default sRGB profile', async () => {
    const data = await sharp(fixtures.inputJpg)
      .resize(32, 24)
      .withMetadata()
      .toBuffer();

    const metadata = await sharp(data).metadata();
    const { colorSpace, deviceClass, intent } = icc.parse(metadata.icc);
    assert.strictEqual(colorSpace, 'RGB');
    assert.strictEqual(deviceClass, 'Monitor');
    assert.strictEqual(intent, 'Perceptual');
  });

  it('withMetadata adds default sRGB profile to RGB16', async () => {
    const data = await sharp({ create })
      .toColorspace('rgb16')
      .png()
      .withMetadata()
      .toBuffer();

    const metadata = await sharp(data).metadata();
    assert.strictEqual(metadata.depth, 'ushort');

    const { description } = icc.parse(metadata.icc);
    assert.strictEqual(description, 'sRGB');
  });

  it('withMetadata adds P3 profile to 16-bit PNG', async () => {
    const data = await sharp({ create })
      .toColorspace('rgb16')
      .png()
      .withMetadata({ icc: 'p3' })
      .toBuffer();

    const metadata = await sharp(data).metadata();
    assert.strictEqual(metadata.depth, 'ushort');

    const { description } = icc.parse(metadata.icc);
    assert.strictEqual(description, 'sP3C');
  });

  it('File input with corrupt header fails gracefully', (_t, done) => {
    sharp(fixtures.inputJpgWithCorruptHeader)
      .metadata((err) => {
        assert.strictEqual(true, !!err);
        assert.ok(err.message.includes('Input file has corrupt header: VipsJpeg: premature end of'), err);
        done();
      });
  });

  it('Buffer input with corrupt header fails gracefully', (_t, done) => {
    sharp(fs.readFileSync(fixtures.inputJpgWithCorruptHeader))
      .metadata((err) => {
        assert.strictEqual(true, !!err);
        assert.ok(err.message.includes('Input buffer has corrupt header: VipsJpeg: premature end of'), err);
        done();
      });
  });

  it('Lossless JPEG', async () => {
    const metadata = await sharp(fixtures.inputJpgLossless).metadata();
    assert.deepStrictEqual(metadata, {
      format: 'jpeg',
      width: 227,
      height: 149,
      space: 'srgb',
      channels: 3,
      depth: 'uchar',
      density: 72,
      chromaSubsampling: '4:4:4',
      isProgressive: false,
      isPalette: false,
      hasProfile: false,
      hasAlpha: false,
      autoOrient: { width: 227, height: 149 }
    });
  });

  it('keepExif maintains all EXIF metadata', async () => {
    const data1 = await sharp({ create })
      .withExif({
        IFD0: {
          Copyright: 'Test 1',
          Software: 'sharp'
        }
      })
      .jpeg()
      .toBuffer();

    const data2 = await sharp(data1)
      .keepExif()
      .toBuffer();

    const md2 = await sharp(data2).metadata();
    const exif2 = exifReader(md2.exif);
    assert.strictEqual(exif2.Image.Copyright, 'Test 1');
    assert.strictEqual(exif2.Image.Software, 'sharp');
  });

  it('withExif replaces all EXIF metadata', async () => {
    const data1 = await sharp({ create })
      .withExif({
        IFD0: {
          Copyright: 'Test 1',
          Software: 'sharp'
        }
      })
      .jpeg()
      .toBuffer();

    const md1 = await sharp(data1).metadata();
    const exif1 = exifReader(md1.exif);
    assert.strictEqual(exif1.Image.Copyright, 'Test 1');
    assert.strictEqual(exif1.Image.Software, 'sharp');

    const data2 = await sharp(data1)
      .withExif({
        IFD0: {
          Copyright: 'Test 2'
        }
      })
      .toBuffer();

    const md2 = await sharp(data2).metadata();
    const exif2 = exifReader(md2.exif);
    assert.strictEqual(exif2.Image.Copyright, 'Test 2');
    assert.strictEqual(exif2.Image.Software, undefined);
  });

  it('withExifMerge merges all EXIF metadata', async () => {
    const data1 = await sharp({ create })
      .withExif({
        IFD0: {
          Copyright: 'Test 1'
        }
      })
      .jpeg()
      .toBuffer();

    const md1 = await sharp(data1).metadata();
    const exif1 = exifReader(md1.exif);
    assert.strictEqual(exif1.Image.Copyright, 'Test 1');
    assert.strictEqual(exif1.Image.Software, undefined);

    const data2 = await sharp(data1)
      .withExifMerge({
        IFD0: {
          Copyright: 'Test 2',
          Software: 'sharp'

        }
      })
      .toBuffer();

    const md2 = await sharp(data2).metadata();
    const exif2 = exifReader(md2.exif);
    assert.strictEqual(exif2.Image.Copyright, 'Test 2');
    assert.strictEqual(exif2.Image.Software, 'sharp');
  });

  describe('XMP metadata tests', () => {
    it('withMetadata preserves existing XMP metadata from input', async () => {
      const data = await sharp(fixtures.inputJpgWithIptcAndXmp)
        .resize(320, 240)
        .withMetadata()
        .toBuffer();

      const metadata = await sharp(data).metadata();
      assert.strictEqual('object', typeof metadata.xmp);
      assert.strictEqual(true, metadata.xmp instanceof Buffer);
      assert.strictEqual(true, metadata.xmp.length > 0);
      // Check that XMP starts with the expected XML declaration
      assert.strictEqual(metadata.xmp.indexOf(Buffer.from('<?xpacket begin="')), 0);
    });

    it('keepXmp preserves existing XMP metadata from input', async () => {
      const data = await sharp(fixtures.inputJpgWithIptcAndXmp)
        .resize(320, 240)
        .keepXmp()
        .toBuffer();

      const metadata = await sharp(data).metadata();
      assert.strictEqual('object', typeof metadata.xmp);
      assert.strictEqual(true, metadata.xmp instanceof Buffer);
      assert.strictEqual(true, metadata.xmp.length > 0);
      // Check that XMP starts with the expected XML declaration
      assert.strictEqual(metadata.xmp.indexOf(Buffer.from('<?xpacket begin="')), 0);
    });

    it('withXmp with custom XMP replaces existing XMP', async () => {
      const customXmp = '<?xml version="1.0"?><x:xmpmeta xmlns:x="adobe:ns:meta/"><rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><rdf:Description rdf:about="" xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:creator><rdf:Seq><rdf:li>Test Creator</rdf:li></rdf:Seq></dc:creator><dc:title><rdf:Alt><rdf:li xml:lang="x-default">Test Title</rdf:li></rdf:Alt></dc:title></rdf:Description></rdf:RDF></x:xmpmeta>';

      const data = await sharp(fixtures.inputJpgWithIptcAndXmp)
        .resize(320, 240)
        .withXmp(customXmp)
        .toBuffer();

      const metadata = await sharp(data).metadata();
      assert.strictEqual('object', typeof metadata.xmp);
      assert.strictEqual(true, metadata.xmp instanceof Buffer);

      // Check that the XMP contains our custom content
      const xmpString = metadata.xmp.toString();
      assert.strictEqual(true, xmpString.includes('Test Creator'));
      assert.strictEqual(true, xmpString.includes('Test Title'));
    });

    it('withXmp with custom XMP buffer on image without existing XMP', async () => {
      const customXmp = '<?xml version="1.0"?><x:xmpmeta xmlns:x="adobe:ns:meta/"><rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><rdf:Description rdf:about="" xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:description><rdf:Alt><rdf:li xml:lang="x-default">Added via Sharp</rdf:li></rdf:Alt></dc:description></rdf:Description></rdf:RDF></x:xmpmeta>';

      const data = await sharp(fixtures.inputJpg)
        .resize(320, 240)
        .withXmp(customXmp)
        .toBuffer();

      const metadata = await sharp(data).metadata();
      assert.strictEqual('object', typeof metadata.xmp);
      assert.strictEqual(true, metadata.xmp instanceof Buffer);

      // Check that the XMP contains our custom content
      const xmpString = metadata.xmp.toString();
      assert.strictEqual(true, xmpString.includes('Added via Sharp'));
    });

    it('withXmp with valid XMP metadata for different image formats', async () => {
      const customXmp = '<?xml version="1.0"?><x:xmpmeta xmlns:x="adobe:ns:meta/"><rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><rdf:Description rdf:about="" xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:subject><rdf:Bag><rdf:li>test</rdf:li><rdf:li>metadata</rdf:li></rdf:Bag></dc:subject></rdf:Description></rdf:RDF></x:xmpmeta>';

      // Test with JPEG output
      const jpegData = await sharp(fixtures.inputJpg)
        .resize(100, 100)
        .jpeg()
        .withXmp(customXmp)
        .toBuffer();

      const jpegMetadata = await sharp(jpegData).metadata();
      assert.strictEqual('object', typeof jpegMetadata.xmp);
      assert.strictEqual(true, jpegMetadata.xmp instanceof Buffer);
      assert.strictEqual(true, jpegMetadata.xmp.toString().includes('test'));

      // Test with PNG output (PNG should also support XMP metadata)
      const pngData = await sharp(fixtures.inputJpg)
        .resize(100, 100)
        .png()
        .withXmp(customXmp)
        .toBuffer();

      const pngMetadata = await sharp(pngData).metadata();
      // PNG format should preserve XMP metadata when using withXmp
      assert.strictEqual('object', typeof pngMetadata.xmp);
      assert.strictEqual(true, pngMetadata.xmp instanceof Buffer);
      assert.strictEqual(true, pngMetadata.xmp.toString().includes('test'));

      // Test with WebP output (WebP should also support XMP metadata)
      const webpData = await sharp(fixtures.inputJpg)
        .resize(100, 100)
        .webp()
        .withXmp(customXmp)
        .toBuffer();

      const webpMetadata = await sharp(webpData).metadata();
      // WebP format should preserve XMP metadata when using withXmp
      assert.strictEqual('object', typeof webpMetadata.xmp);
      assert.strictEqual(true, webpMetadata.xmp instanceof Buffer);
      assert.strictEqual(true, webpMetadata.xmp.toString().includes('test'));
    });

    it('XMP metadata persists through multiple operations', async () => {
      const customXmp = '<?xml version="1.0"?><x:xmpmeta xmlns:x="adobe:ns:meta/"><rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><rdf:Description rdf:about="" xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:identifier>persistent-test</dc:identifier></rdf:Description></rdf:RDF></x:xmpmeta>';

      const data = await sharp(fixtures.inputJpg)
        .resize(320, 240)
        .withXmp(customXmp)
        .rotate(90)
        .blur(1)
        .sharpen()
        .toBuffer();

      const metadata = await sharp(data).metadata();
      assert.strictEqual('object', typeof metadata.xmp);
      assert.strictEqual(true, metadata.xmp instanceof Buffer);
      assert.strictEqual(true, metadata.xmp.toString().includes('persistent-test'));
    });

    it('withXmp XMP works with WebP format specifically', async () => {
      const webpXmp = '<?xml version="1.0"?><x:xmpmeta xmlns:x="adobe:ns:meta/"><rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><rdf:Description rdf:about="" xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:creator><rdf:Seq><rdf:li>WebP Creator</rdf:li></rdf:Seq></dc:creator><dc:format>image/webp</dc:format></rdf:Description></rdf:RDF></x:xmpmeta>';

      const data = await sharp(fixtures.inputJpg)
        .resize(120, 80)
        .webp({ quality: 80 })
        .withXmp(webpXmp)
        .toBuffer();

      const metadata = await sharp(data).metadata();
      assert.strictEqual('webp', metadata.format);
      assert.strictEqual('object', typeof metadata.xmp);
      assert.strictEqual(true, metadata.xmp instanceof Buffer);

      const xmpString = metadata.xmp.toString();
      assert.strictEqual(true, xmpString.includes('WebP Creator'));
      assert.strictEqual(true, xmpString.includes('image/webp'));
    });

    it('withXmp XMP validation - non-string input', () => {
      assert.throws(
        () => sharp().withXmp(123),
        /Expected non-empty string for xmp but received 123 of type number/
      );
    });

    it('withXmp XMP validation - null input', () => {
      assert.throws(
        () => sharp().withXmp(null),
        /Expected non-empty string for xmp but received null of type object/
      );
    });

    it('withXmp XMP validation - empty string', () => {
      assert.throws(
        () => sharp().withXmp(''),
        /Expected non-empty string for xmp/
      );
    });
  });

  describe('Invalid parameters', () => {
    it('String orientation', () => {
      assert.throws(() => {
        sharp().withMetadata({ orientation: 'zoinks' });
      });
    });
    it('Negative orientation', () => {
      assert.throws(() => {
        sharp().withMetadata({ orientation: -1 });
      });
    });
    it('Zero orientation', () => {
      assert.throws(() => {
        sharp().withMetadata({ orientation: 0 });
      });
    });
    it('Too large orientation', () => {
      assert.throws(() => {
        sharp().withMetadata({ orientation: 9 });
      });
    });
    it('Non-numeric density', () => {
      assert.throws(() => {
        sharp().withMetadata({ density: '1' });
      });
    });
    it('Negative density', () => {
      assert.throws(() => {
        sharp().withMetadata({ density: -1 });
      });
    });
    it('Non string icc', () => {
      assert.throws(() => {
        sharp().withMetadata({ icc: true });
      });
    });
    it('Non object exif', () => {
      assert.throws(() => {
        sharp().withMetadata({ exif: false });
      });
    });
    it('Non string value in object exif', () => {
      assert.throws(() => {
        sharp().withMetadata({ exif: { ifd0: false } });
      });
    });
    it('Non string value in nested object exif', () => {
      assert.throws(() => {
        sharp().withMetadata({ exif: { ifd0: { fail: false } } });
      });
    });
    it('withIccProfile invalid profile', () => {
      assert.throws(
        () => sharp().withIccProfile(false),
        /Expected string for icc but received false of type boolean/
      );
    });
    it('withIccProfile missing attach', () => {
      assert.doesNotThrow(
        () => sharp().withIccProfile('test', {})
      );
    });
    it('withIccProfile invalid attach', () => {
      assert.throws(
        () => sharp().withIccProfile('test', { attach: 1 }),
        /Expected boolean for attach but received 1 of type number/
      );
    });
  });
});
