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

const { describe, it } = require('node:test');
const assert = require('node:assert');

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

describe('Rotation', () => {
  ['autoOrient', 'constructor'].forEach((rotateMethod) => {
    describe(`Auto orientation via ${rotateMethod}:`, () => {
      const options = rotateMethod === 'constructor' ? { autoOrient: true } : {};

      ['Landscape', 'Portrait'].forEach((orientation) => {
        [1, 2, 3, 4, 5, 6, 7, 8].forEach((exifTag) => {
          const input = fixtures[`inputJpgWith${orientation}Exif${exifTag}`];
          const expectedOutput = fixtures.expected(`${orientation}_${exifTag}-out.jpg`);
          it(`${orientation} image with EXIF Orientation ${exifTag}: Auto-rotate`, (_t, done) => {
            const [expectedWidth, expectedHeight] = orientation === 'Landscape' ? [600, 450] : [450, 600];

            const img = sharp(input, options);
            rotateMethod === 'autoOrient' && img.autoOrient();

            img.toBuffer((err, data, info) => {
              if (err) throw err;
              assert.strictEqual(info.width, expectedWidth);
              assert.strictEqual(info.height, expectedHeight);
              fixtures.assertSimilar(expectedOutput, data, done);
            });
          });

          it(`${orientation} image with EXIF Orientation ${exifTag}: Auto-rotate then resize`, (_t, done) => {
            const [expectedWidth, expectedHeight] = orientation === 'Landscape' ? [320, 240] : [320, 427];

            const img = sharp(input, options);
            rotateMethod === 'autoOrient' && img.autoOrient();

            img.resize({ width: 320 })
              .toBuffer((err, data, info) => {
                if (err) throw err;
                assert.strictEqual(info.width, expectedWidth);
                assert.strictEqual(info.height, expectedHeight);
                fixtures.assertSimilar(expectedOutput, data, done);
              });
          });

          if (rotateMethod !== 'constructor') {
            it(`${orientation} image with EXIF Orientation ${exifTag}: Resize then auto-rotate`, (_t, done) => {
              const [expectedWidth, expectedHeight] = orientation === 'Landscape'
                ? (exifTag < 5) ? [320, 240] : [320, 240]
                : [320, 427];

              const img = sharp(input, options)
                .resize({ width: 320 });

              rotateMethod === 'autoOrient' && img.autoOrient();
              img.toBuffer((err, data, info) => {
                if (err) throw err;
                assert.strictEqual(info.width, expectedWidth);
                assert.strictEqual(info.height, expectedHeight);
                fixtures.assertSimilar(expectedOutput, data, done);
              });
            });
          }

          [true, false].forEach((doResize) => {
            [90, 180, 270, 45].forEach((angle) => {
              const [inputWidth, inputHeight] = orientation === 'Landscape' ? [600, 450] : [450, 600];
              const expectedOutput = fixtures.expected(`${orientation}_${exifTag}_rotate${angle}-out.jpg`);
              it(`${orientation} image with EXIF Orientation ${exifTag}: Auto-rotate then rotate ${angle} ${doResize ? 'and resize' : ''}`, (_t, done) => {
                const [width, height] = (angle === 45 ? [742, 742] : [inputWidth, inputHeight]).map((x) => doResize ? Math.floor(x / 1.875) : x);
                const [expectedWidth, expectedHeight] = angle % 180 === 0 ? [width, height] : [height, width];

                const img = sharp(input, options);
                rotateMethod === 'autoOrient' && img.autoOrient();

                img.rotate(angle);
                doResize && img.resize(expectedWidth);

                img.toBuffer((err, data, info) => {
                  if (err) throw err;
                  assert.strictEqual(info.width, expectedWidth);
                  assert.strictEqual(info.height, expectedHeight);
                  fixtures.assertSimilar(expectedOutput, data, done);
                });
              });
            });

            [[true, true], [true, false], [false, true]].forEach(([flip, flop]) => {
              const [inputWidth, inputHeight] = orientation === 'Landscape' ? [600, 450] : [450, 600];
              const flipFlopFileName = [flip && 'flip', flop && 'flop'].filter(Boolean).join('_');
              const flipFlopTestName = [flip && 'flip', flop && 'flop'].filter(Boolean).join(' & ');
              it(`${orientation} image with EXIF Orientation ${exifTag}: Auto-rotate then ${flipFlopTestName} ${doResize ? 'and resize' : ''}`, (_t, done) => {
                const expectedOutput = fixtures.expected(`${orientation}_${exifTag}_${flipFlopFileName}-out.jpg`);

                const img = sharp(input, options);

                rotateMethod === 'autoOrient' && img.autoOrient();

                flip && img.flip();
                flop && img.flop();
                doResize && img.resize(orientation === 'Landscape' ? 320 : 240);

                img.toBuffer((err, data, info) => {
                  if (err) throw err;
                  assert.strictEqual(info.width, inputWidth / (doResize ? 1.875 : 1));
                  assert.strictEqual(info.height, inputHeight / (doResize ? 1.875 : 1));
                  fixtures.assertSimilar(expectedOutput, data, done);
                });
              });
            });
          });
        });
      });
    });
  });

  it('Rotate by 30 degrees with semi-transparent background', (_t, done) => {
    sharp(fixtures.inputJpg)
      .resize(320)
      .rotate(30, { background: { r: 255, g: 0, b: 0, alpha: 0.5 } })
      .png()
      .toBuffer((err, data, info) => {
        if (err) throw err;
        assert.strictEqual('png', info.format);
        assert.strictEqual(408, info.width);
        assert.strictEqual(386, info.height);
        fixtures.assertSimilar(fixtures.expected('rotate-transparent-bg.png'), data, done);
      });
  });

  it('Rotate by 30 degrees with solid background', (_t, done) => {
    sharp(fixtures.inputJpg)
      .resize(320)
      .rotate(30, { background: { r: 255, g: 0, b: 0 } })
      .toBuffer((err, data, info) => {
        if (err) throw err;
        assert.strictEqual('jpeg', info.format);
        assert.strictEqual(408, info.width);
        assert.strictEqual(386, info.height);
        fixtures.assertSimilar(fixtures.expected('rotate-solid-bg.jpg'), data, done);
      });
  });

  it('Rotate by 90 degrees, respecting output input size', (_t, done) => {
    sharp(fixtures.inputJpg)
      .rotate(90)
      .resize(320, 240)
      .toBuffer((err, data, info) => {
        if (err) throw err;
        assert.strictEqual(true, data.length > 0);
        assert.strictEqual('jpeg', info.format);
        assert.strictEqual(320, info.width);
        assert.strictEqual(240, info.height);
        done();
      });
  });

  it('Resize then rotate by 30 degrees, respecting output input size', (_t, done) => {
    sharp(fixtures.inputJpg)
      .resize(320, 240)
      .rotate(30)
      .toBuffer((err, data, info) => {
        if (err) throw err;
        assert.strictEqual(true, data.length > 0);
        assert.strictEqual('jpeg', info.format);
        assert.strictEqual(397, info.width);
        assert.strictEqual(368, info.height);
        done();
      });
  });

  [-3690, -450, -90, 90, 450, 3690].forEach((angle) => {
    it(`Rotate by any 90-multiple angle (${angle}deg)`, (_t, done) => {
      sharp(fixtures.inputJpg320x240).rotate(angle).toBuffer((err, _data, info) => {
        if (err) throw err;
        assert.strictEqual(240, info.width);
        assert.strictEqual(320, info.height);
        done();
      });
    });
  });

  [-3750, -510, -150, 30, 390, 3630].forEach((angle) => {
    it(`Rotate by any 30-multiple angle (${angle}deg)`, (_t, done) => {
      sharp(fixtures.inputJpg320x240).rotate(angle).toBuffer((err, _data, info) => {
        if (err) throw err;
        assert.strictEqual(397, info.width);
        assert.strictEqual(368, info.height);
        done();
      });
    });
  });

  [-3780, -540, 0, 180, 540, 3780].forEach((angle) => {
    it(`Rotate by any 180-multiple angle (${angle}deg)`, (_t, done) => {
      sharp(fixtures.inputJpg320x240).rotate(angle).toBuffer((err, _data, info) => {
        if (err) throw err;
        assert.strictEqual(320, info.width);
        assert.strictEqual(240, info.height);
        done();
      });
    });
  });

  it('Rotate by 270 degrees, square output ignoring aspect ratio', (_t, done) => {
    sharp(fixtures.inputJpg)
      .resize(240, 240, { fit: sharp.fit.fill })
      .rotate(270)
      .toBuffer((err, data, info) => {
        if (err) throw err;
        assert.strictEqual(240, info.width);
        assert.strictEqual(240, info.height);
        sharp(data).metadata((err, metadata) => {
          if (err) throw err;
          assert.strictEqual(240, metadata.width);
          assert.strictEqual(240, metadata.height);
          done();
        });
      });
  });

  it('Rotate by 315 degrees, square output ignoring aspect ratio', (_t, done) => {
    sharp(fixtures.inputJpg)
      .resize(240, 240, { fit: sharp.fit.fill })
      .rotate(315)
      .toBuffer((err, data, info) => {
        if (err) throw err;
        assert.strictEqual(339, info.width);
        assert.strictEqual(339, info.height);
        sharp(data).metadata((err, metadata) => {
          if (err) throw err;
          assert.strictEqual(339, metadata.width);
          assert.strictEqual(339, metadata.height);
          done();
        });
      });
  });

  it('Rotate by 270 degrees, rectangular output ignoring aspect ratio', (_t, done) => {
    sharp(fixtures.inputJpg)
      .rotate(270)
      .resize(320, 240, { fit: sharp.fit.fill })
      .toBuffer((err, data, info) => {
        if (err) throw err;
        assert.strictEqual(320, info.width);
        assert.strictEqual(240, info.height);
        sharp(data).metadata((err, metadata) => {
          if (err) throw err;
          assert.strictEqual(320, metadata.width);
          assert.strictEqual(240, metadata.height);
          done();
        });
      });
  });

  it('Auto-rotate by 270 degrees, rectangular output ignoring aspect ratio', (_t, done) => {
    sharp(fixtures.inputJpgWithLandscapeExif8)
      .resize(320, 240, { fit: sharp.fit.fill })
      .rotate()
      .toBuffer((err, data, info) => {
        if (err) throw err;
        assert.strictEqual(320, info.width);
        assert.strictEqual(240, info.height);
        sharp(data).metadata((err, metadata) => {
          if (err) throw err;
          assert.strictEqual(320, metadata.width);
          assert.strictEqual(240, metadata.height);
          done();
        });
      });
  });

  it('Rotate by 30 degrees, rectangular output ignoring aspect ratio', (_t, done) => {
    sharp(fixtures.inputJpg)
      .resize(320, 240, { fit: sharp.fit.fill })
      .rotate(30)
      .toBuffer((err, data, info) => {
        if (err) throw err;
        assert.strictEqual(397, info.width);
        assert.strictEqual(368, info.height);
        sharp(data).metadata((err, metadata) => {
          if (err) throw err;
          assert.strictEqual(397, metadata.width);
          assert.strictEqual(368, metadata.height);
          done();
        });
      });
  });

  it('Input image has Orientation EXIF tag but do not rotate output', (_t, done) => {
    sharp(fixtures.inputJpgWithExif)
      .resize(320)
      .withMetadata()
      .toBuffer((err, data, info) => {
        if (err) throw err;
        assert.strictEqual(true, data.length > 0);
        assert.strictEqual('jpeg', info.format);
        assert.strictEqual(320, info.width);
        assert.strictEqual(427, info.height);
        sharp(data).metadata((err, metadata) => {
          if (err) throw err;
          assert.strictEqual(8, metadata.orientation);
          done();
        });
      });
  });

  it('Input image has Orientation EXIF tag value of 8 (270 degrees), auto-rotate', (_t, done) => {
    sharp(fixtures.inputJpgWithExif)
      .rotate()
      .resize(320)
      .toBuffer((err, data, info) => {
        if (err) throw err;
        assert.strictEqual('jpeg', info.format);
        assert.strictEqual(320, info.width);
        assert.strictEqual(240, info.height);
        fixtures.assertSimilar(fixtures.expected('exif-8.jpg'), data, done);
      });
  });

  it('Override EXIF Orientation tag metadata after auto-rotate', (_t, done) => {
    sharp(fixtures.inputJpgWithExif)
      .rotate()
      .resize(320)
      .withMetadata({ orientation: 3 })
      .toBuffer((err, data, info) => {
        if (err) throw err;
        assert.strictEqual('jpeg', info.format);
        assert.strictEqual(320, info.width);
        assert.strictEqual(240, info.height);
        sharp(data).metadata((err, metadata) => {
          if (err) throw err;
          assert.strictEqual(3, metadata.orientation);
          fixtures.assertSimilar(fixtures.expected('exif-8.jpg'), data, done);
        });
      });
  });

  it('Input image has Orientation EXIF tag value of 5 (270 degrees + flip), auto-rotate', (_t, done) => {
    sharp(fixtures.inputJpgWithExifMirroring)
      .rotate()
      .resize(320)
      .withMetadata()
      .toBuffer((err, data, info) => {
        if (err) throw err;
        assert.strictEqual('jpeg', info.format);
        assert.strictEqual(320, info.width);
        assert.strictEqual(240, info.height);
        sharp(data).metadata((err, metadata) => {
          if (err) throw err;
          assert.strictEqual(1, metadata.orientation);
          fixtures.assertSimilar(fixtures.expected('exif-5.jpg'), data, done);
        });
      });
  });

  it('Attempt to auto-rotate using image that has no EXIF', (_t, done) => {
    sharp(fixtures.inputJpg).rotate().resize(320).toBuffer((err, data, info) => {
      if (err) throw err;
      assert.strictEqual(true, data.length > 0);
      assert.strictEqual('jpeg', info.format);
      assert.strictEqual(320, info.width);
      assert.strictEqual(261, info.height);
      done();
    });
  });

  it('Attempt to auto-rotate image format without EXIF support', (_t, done) => {
    sharp(fixtures.inputPng)
      .rotate()
      .resize(320)
      .jpeg()
      .toBuffer((err, data, info) => {
        if (err) throw err;
        assert.strictEqual(true, data.length > 0);
        assert.strictEqual('jpeg', info.format);
        assert.strictEqual(320, info.width);
        assert.strictEqual(236, info.height);
        done();
      });
  });

  it('Rotate with a string argument, should fail', () => {
    assert.throws(() => {
      sharp(fixtures.inputJpg).rotate('not-a-number');
    });
  });

  it('Animated image rotate-then-extract rejects', () =>
    assert.rejects(() => sharp(fixtures.inputGifAnimated, { animated: true })
      .rotate(1)
      .extract({
        top: 1,
        left: 1,
        width: 10,
        height: 10
      })
      .toBuffer(),
    /Rotate is not supported for multi-page images/
    )
  );

  it('Animated image extract-then-rotate rejects', () =>
    assert.rejects(() => sharp(fixtures.inputGifAnimated, { animated: true })
      .extract({
        top: 1,
        left: 1,
        width: 10,
        height: 10
      })
      .rotate(1)
      .toBuffer(),
    /Rotate is not supported for multi-page images/
    )
  );

  it('Animated image rotate 180', () =>
    assert.doesNotReject(() => sharp(fixtures.inputGifAnimated, { animated: true })
      .rotate(180)
      .toBuffer()
    )
  );

  it('Animated image rotate non-180 rejects', () =>
    assert.rejects(() => sharp(fixtures.inputGifAnimated, { animated: true })
      .rotate(90)
      .toBuffer(),
    /Rotate is not supported for multi-page images/
    )
  );

  it('Multiple rotate emits warning', () => {
    let warningMessage = '';
    const s = sharp();
    s.on('warning', (msg) => { warningMessage = msg; });
    s.rotate(90);
    assert.strictEqual(warningMessage, '');
    s.rotate(180);
    assert.strictEqual(warningMessage, 'ignoring previous rotate options');
  });

  it('Multiple rotate: last one wins (cardinal)', (_t, done) => {
    sharp(fixtures.inputJpg)
      .rotate(45)
      .rotate(90)
      .toBuffer((err, _data, info) => {
        if (err) throw err;
        assert.strictEqual(2225, info.width);
        assert.strictEqual(2725, info.height);
        done();
      });
  });

  it('Multiple rotate: last one wins (non cardinal)', (_t, done) => {
    sharp(fixtures.inputJpg)
      .rotate(90)
      .rotate(45)
      .toBuffer((err, _data, info) => {
        if (err) throw err;
        assert.strictEqual(3500, info.width);
        assert.strictEqual(3500, info.height);
        done();
      });
  });

  it('Flip - vertical', (_t, done) => {
    sharp(fixtures.inputJpg)
      .resize(320)
      .flip()
      .withMetadata()
      .toBuffer((err, data, info) => {
        if (err) throw err;
        assert.strictEqual('jpeg', info.format);
        assert.strictEqual(320, info.width);
        assert.strictEqual(261, info.height);
        sharp(data).metadata((err, metadata) => {
          if (err) throw err;
          assert.strictEqual(1, metadata.orientation);
          fixtures.assertSimilar(fixtures.expected('flip.jpg'), data, done);
        });
      });
  });

  it('Flop - horizontal', (_t, done) => {
    sharp(fixtures.inputJpg)
      .resize(320)
      .flop()
      .withMetadata()
      .toBuffer((err, data, info) => {
        if (err) throw err;
        assert.strictEqual('jpeg', info.format);
        assert.strictEqual(320, info.width);
        assert.strictEqual(261, info.height);
        sharp(data).metadata((err, metadata) => {
          if (err) throw err;
          assert.strictEqual(1, metadata.orientation);
          fixtures.assertSimilar(fixtures.expected('flop.jpg'), data, done);
        });
      });
  });

  it('Flip and flop', (_t, done) => {
    sharp(fixtures.inputJpg)
      .resize(320)
      .flip()
      .flop()
      .toBuffer((err, data, info) => {
        if (err) throw err;
        assert.strictEqual('jpeg', info.format);
        assert.strictEqual(320, info.width);
        assert.strictEqual(261, info.height);
        fixtures.assertSimilar(fixtures.expected('flip-and-flop.jpg'), data, done);
      });
  });

  it('Neither flip nor flop', (_t, done) => {
    sharp(fixtures.inputJpg)
      .resize(320)
      .flip(false)
      .flop(false)
      .toBuffer((err, data, info) => {
        if (err) throw err;
        assert.strictEqual('jpeg', info.format);
        assert.strictEqual(320, info.width);
        assert.strictEqual(261, info.height);
        fixtures.assertSimilar(fixtures.inputJpg, data, done);
      });
  });

  it('Auto-rotate and flip', (_t, done) => {
    sharp(fixtures.inputJpgWithExif)
      .rotate()
      .flip()
      .resize(320)
      .toBuffer((err, data, info) => {
        if (err) throw err;
        assert.strictEqual('jpeg', info.format);
        assert.strictEqual(320, info.width);
        assert.strictEqual(240, info.height);
        fixtures.assertSimilar(fixtures.expected('rotate-and-flip.jpg'), data, done);
      });
  });

  it('Auto-rotate and flop', (_t, done) => {
    sharp(fixtures.inputJpgWithExif)
      .rotate()
      .flop()
      .resize(320)
      .toBuffer((err, data, info) => {
        if (err) throw err;
        assert.strictEqual('jpeg', info.format);
        assert.strictEqual(320, info.width);
        assert.strictEqual(240, info.height);
        fixtures.assertSimilar(fixtures.expected('rotate-and-flop.jpg'), data, done);
      });
  });

  it('Auto-rotate and shrink-on-load', async () => {
    const [r, g, b] = await sharp(fixtures.inputJpgWithLandscapeExif3)
      .rotate()
      .resize(8)
      .raw()
      .toBuffer();

    assert.strictEqual(r, 61);
    assert.strictEqual(g, 74);
    assert.strictEqual(b, 51);
  });

  it('Flip and rotate ordering', async () => {
    const [r, g, b] = await sharp(fixtures.inputJpgWithPortraitExif5)
      .flip()
      .rotate(90)
      .raw()
      .toBuffer();

    assert.strictEqual(r, 55);
    assert.strictEqual(g, 65);
    assert.strictEqual(b, 31);
  });

  it('Flip, rotate and resize ordering', async () => {
    const [r, g, b] = await sharp(fixtures.inputJpgWithPortraitExif5)
      .flip()
      .rotate(90)
      .resize(449)
      .raw()
      .toBuffer();

    assert.strictEqual(r, 54);
    assert.strictEqual(g, 64);
    assert.strictEqual(b, 30);
  });

  it('Resize after affine-based rotation does not overcompute', async () =>
    sharp({
      create: {
        width: 4640,
        height: 2610,
        channels: 3,
        background: 'black'
      }
    })
      .rotate(28)
      .resize({ width: 640, height: 360 })
      .raw()
      .timeout({ seconds: 5 })
      .toBuffer()
  );

  it('Rotate 90 then resize with inside fit', async () => {
    const data = await sharp({ create: { width: 16, height: 8, channels: 3, background: 'red' } })
      .rotate(90)
      .resize({ width: 6, fit: 'inside' })
      .png({ compressionLevel: 0 })
      .toBuffer();

    const { width, height } = await sharp(data).metadata();
    assert.strictEqual(width, 6);
    assert.strictEqual(height, 12);
  });

  it('Resize with inside fit then rotate 90', async () => {
    const data = await sharp({ create: { width: 16, height: 8, channels: 3, background: 'red' } })
      .resize({ width: 6, fit: 'inside' })
      .rotate(90)
      .png({ compressionLevel: 0 })
      .toBuffer();

    const { width, height } = await sharp(data).metadata();
    assert.strictEqual(width, 3);
    assert.strictEqual(height, 6);
  });

  it('Shrink-on-load with autoOrient', async () => {
    const data = await sharp(fixtures.inputJpgWithLandscapeExif6)
      .resize(8)
      .autoOrient()
      .avif({ effort: 0 })
      .toBuffer();

    const { width, height, orientation } = await sharp(data).metadata();
    assert.strictEqual(width, 8);
    assert.strictEqual(height, 6);
    assert.strictEqual(orientation, undefined);
  });

  it('Auto-orient and rotate 45', async () => {
    const data = await sharp(fixtures.inputJpgWithLandscapeExif2, { autoOrient: true })
      .rotate(45)
      .toBuffer();

    const { width, height } = await sharp(data).metadata();
    assert.strictEqual(width, 742);
    assert.strictEqual(height, 742);
  });

  it('Auto-orient, extract and rotate 45', async () => {
    const data = await sharp(fixtures.inputJpgWithLandscapeExif2, { autoOrient: true })
      .extract({ left: 20, top: 20, width: 200, height: 100 })
      .rotate(45)
      .toBuffer();

    const { width, height } = await sharp(data).metadata();
    assert.strictEqual(width, 212);
    assert.strictEqual(height, 212);
  });

  it('Invalid autoOrient throws', () =>
    assert.throws(
      () => sharp({ autoOrient: 'fail' }),
      /Expected boolean for autoOrient but received fail of type string/
    )
  );
});
