/* Copyright 2026 Mozilla Foundation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import {
  convertBlackAndWhiteToRGBA,
  convertToRGBA,
  grayToRGBA,
} from "../../src/shared/image_utils.js";
import { FeatureTest, ImageKind } from "../../src/shared/util.js";

describe("image_utils", function () {
  // Precompute endian-dependent constants once for all tests.
  const isLE = FeatureTest.isLittleEndian;
  const BLACK = isLE ? 0xff000000 : 0x000000ff;
  const WHITE = 0xffffffff;
  const RED = 0xff0000ff;

  describe("convertBlackAndWhiteToRGBA", function () {
    it("converts a single byte (width=8) with alternating bits", function () {
      // 0b10101010: bits 7..0 = 1,0,1,0,1,0,1,0 → W,B,W,B,W,B,W,B
      const src = new Uint8Array([0b10101010]);
      const dest = new Uint8ClampedArray(8 * 4);
      const { srcPos, destPos } = convertBlackAndWhiteToRGBA({
        src,
        dest,
        width: 8,
        height: 1,
      });
      expect(srcPos).toEqual(1);
      expect(destPos).toEqual(8);

      const dest32 = new Uint32Array(dest.buffer);
      expect(dest32[0]).toEqual(WHITE);
      expect(dest32[1]).toEqual(BLACK);
      expect(dest32[2]).toEqual(WHITE);
      expect(dest32[3]).toEqual(BLACK);
      expect(dest32[4]).toEqual(WHITE);
      expect(dest32[5]).toEqual(BLACK);
      expect(dest32[6]).toEqual(WHITE);
      expect(dest32[7]).toEqual(BLACK);
    });

    it("converts two rows (width=8, height=2)", function () {
      // Row 0: 0b10101010 → W,B,W,B,W,B,W,B
      // Row 1: 0b01010101 → B,W,B,W,B,W,B,W
      const src = new Uint8Array([0b10101010, 0b01010101]);
      const dest = new Uint8ClampedArray(16 * 4);
      const { srcPos, destPos } = convertBlackAndWhiteToRGBA({
        src,
        dest,
        width: 8,
        height: 2,
      });
      expect(srcPos).toEqual(2);
      expect(destPos).toEqual(16);

      const dest32 = new Uint32Array(dest.buffer);
      // Row 0
      expect(dest32[0]).toEqual(WHITE);
      expect(dest32[1]).toEqual(BLACK);
      // Row 1
      expect(dest32[8]).toEqual(BLACK);
      expect(dest32[9]).toEqual(WHITE);
    });

    it("handles width not divisible by 8 (width=5)", function () {
      // 0b11100000: bits 7..3 = 1,1,1,0,0 → W,W,W,B,B (only 5 pixels)
      const src = new Uint8Array([0b11100000]);
      const dest = new Uint8ClampedArray(5 * 4);
      const { srcPos, destPos } = convertBlackAndWhiteToRGBA({
        src,
        dest,
        width: 5,
        height: 1,
      });
      expect(srcPos).toEqual(1);
      expect(destPos).toEqual(5);

      const dest32 = new Uint32Array(dest.buffer);
      expect(dest32[0]).toEqual(WHITE);
      expect(dest32[1]).toEqual(WHITE);
      expect(dest32[2]).toEqual(WHITE);
      expect(dest32[3]).toEqual(BLACK);
      expect(dest32[4]).toEqual(BLACK);
    });

    it("handles width=10 spanning two bytes", function () {
      // widthInSource = 1, widthRemainder = 2
      // Byte 0: 0b11111111 → 8 white pixels
      // Byte 1: 0b11000000 → bits 7,6 = 1,1 → W,W (only 2 pixels consumed)
      const src = new Uint8Array([0b11111111, 0b11000000]);
      const dest = new Uint8ClampedArray(10 * 4);
      const { srcPos, destPos } = convertBlackAndWhiteToRGBA({
        src,
        dest,
        width: 10,
        height: 1,
      });
      expect(srcPos).toEqual(2);
      expect(destPos).toEqual(10);

      const dest32 = new Uint32Array(dest.buffer);
      for (let i = 0; i < 10; i++) {
        expect(dest32[i]).withContext(`pixel ${i}`).toEqual(WHITE);
      }
    });

    it("handles srcPos offset", function () {
      // Skip the first 2 bytes; read from byte 2 = 0b10000000 → W,B,B,B,B,B,B,B
      const src = new Uint8Array([0x00, 0x00, 0b10000000]);
      const dest = new Uint8ClampedArray(8 * 4);
      const { srcPos, destPos } = convertBlackAndWhiteToRGBA({
        src,
        srcPos: 2,
        dest,
        width: 8,
        height: 1,
      });
      expect(srcPos).toEqual(3);
      expect(destPos).toEqual(8);

      const dest32 = new Uint32Array(dest.buffer);
      expect(dest32[0]).toEqual(WHITE);
      for (let i = 1; i < 8; i++) {
        expect(dest32[i]).withContext(`pixel ${i}`).toEqual(BLACK);
      }
    });

    it("applies inverseDecode correctly", function () {
      // 0b10101010 normally → W,B,W,B,...
      // With inverseDecode: 1→black, 0→white, so → B,W,B,W,...
      const src = new Uint8Array([0b10101010]);
      const dest = new Uint8ClampedArray(8 * 4);
      convertBlackAndWhiteToRGBA({
        src,
        dest,
        width: 8,
        height: 1,
        inverseDecode: true,
      });

      const dest32 = new Uint32Array(dest.buffer);
      expect(dest32[0]).toEqual(BLACK);
      expect(dest32[1]).toEqual(WHITE);
      expect(dest32[2]).toEqual(BLACK);
      expect(dest32[3]).toEqual(WHITE);
    });

    it("uses nonBlackColor for the one-bits", function () {
      // Custom color for non-black pixels.
      const CUSTOM = isLE ? 0xff0000ff : 0xff0000ff; // red (LE) / different (BE)
      // 0b11110000 → 1,1,1,1,0,0,0,0
      //            → CUSTOM,CUSTOM,CUSTOM,CUSTOM,BLACK,BLACK,BLACK,BLACK
      const src = new Uint8Array([0b11110000]);
      const dest = new Uint8ClampedArray(8 * 4);
      convertBlackAndWhiteToRGBA({
        src,
        dest,
        width: 8,
        height: 1,
        nonBlackColor: CUSTOM,
      });

      const dest32 = new Uint32Array(dest.buffer);
      expect(dest32[0]).toEqual(CUSTOM);
      expect(dest32[1]).toEqual(CUSTOM);
      expect(dest32[2]).toEqual(CUSTOM);
      expect(dest32[3]).toEqual(CUSTOM);
      expect(dest32[4]).toEqual(BLACK);
      expect(dest32[5]).toEqual(BLACK);
      expect(dest32[6]).toEqual(BLACK);
      expect(dest32[7]).toEqual(BLACK);
    });

    it("uses 0xff (all-white byte) when src is shorter than expected", function () {
      // width=10 needs 2 bytes but only 1 provided.
      // widthInSource=1: byte 0 = 0b11110000 → W,W,W,W,B,B,B,B
      // widthRemainder=2: missing byte treated as 0xff → bits 7,6 = 1,1 → W,W
      const src = new Uint8Array([0b11110000]);
      const dest = new Uint8ClampedArray(10 * 4);
      convertBlackAndWhiteToRGBA({ src, dest, width: 10, height: 1 });

      const dest32 = new Uint32Array(dest.buffer);
      expect(dest32[0]).toEqual(WHITE);
      expect(dest32[1]).toEqual(WHITE);
      expect(dest32[2]).toEqual(WHITE);
      expect(dest32[3]).toEqual(WHITE);
      expect(dest32[4]).toEqual(BLACK);
      expect(dest32[5]).toEqual(BLACK);
      expect(dest32[6]).toEqual(BLACK);
      expect(dest32[7]).toEqual(BLACK);
      // Missing second byte → treated as 0xff, so bits 7,6 → W,W
      expect(dest32[8]).toEqual(WHITE);
      expect(dest32[9]).toEqual(WHITE);
    });
  });

  describe("grayToRGBA", function () {
    it("converts black (0), mid-gray (128), and white (255)", function () {
      const src = new Uint8Array([0, 128, 255]);
      const dest = new Uint32Array(3);
      grayToRGBA(src, dest);

      expect(dest[0]).toEqual(BLACK);
      expect(dest[1]).toEqual(isLE ? 0xff808080 : 0x808080ff);
      expect(dest[2]).toEqual(WHITE);
    });

    it("handles an empty input array", function () {
      grayToRGBA(new Uint8Array(0), new Uint32Array(0));
      // No crash, nothing to check beyond reaching here.
    });

    it("alpha channel is always 0xff for every pixel", function () {
      const N = 256;
      const src = new Uint8Array(N);
      const dest = new Uint32Array(N);
      for (let i = 0; i < N; i++) {
        src[i] = i;
      }
      grayToRGBA(src, dest);

      // Extract the alpha byte: high byte in LE, low byte in BE.
      const alphaShift = isLE ? 24 : 0;
      for (let i = 0; i < N; i++) {
        expect((dest[i] >>> alphaShift) & 0xff)
          .withContext(`alpha for value ${i}`)
          .toEqual(0xff);
      }
    });

    it("RGB channels are equal for each gray level", function () {
      const src = new Uint8Array([51, 102, 204]);
      const dest = new Uint32Array(3);
      grayToRGBA(src, dest);

      // In LE: 0xffRRGGBB where RR=GG=BB=value
      // In BE: 0xRRGGBBff where RR=GG=BB=value
      for (let i = 0; i < src.length; i++) {
        const v = src[i];
        const expected = isLE
          ? 0xff000000 | (v << 16) | (v << 8) | v
          : (v << 24) | (v << 16) | (v << 8) | 0xff;
        expect(dest[i])
          .withContext(`gray value ${v}`)
          .toEqual(expected >>> 0);
      }
    });
  });

  describe("convertToRGBA", function () {
    it("dispatches to convertBlackAndWhiteToRGBA for GRAYSCALE_1BPP", function () {
      const src = new Uint8Array([0b11110000]);
      const dest = new Uint8ClampedArray(8 * 4);
      const result = convertToRGBA({
        src,
        dest,
        width: 8,
        height: 1,
        kind: ImageKind.GRAYSCALE_1BPP,
      });
      expect(result).not.toBeNull();
      expect(result.destPos).toEqual(8);

      const dest32 = new Uint32Array(dest.buffer);
      expect(dest32[0]).toEqual(WHITE);
      expect(dest32[4]).toEqual(BLACK);
    });

    it("dispatches to convertRGBToRGBA for RGB_24BPP", function () {
      // Three pixels: white, black, red.
      const src = new Uint8Array([255, 255, 255, 0, 0, 0, 255, 0, 0]);
      const dest = new Uint32Array(3);
      const result = convertToRGBA({
        src,
        dest,
        width: 3,
        height: 1,
        kind: ImageKind.RGB_24BPP,
      });
      expect(result).not.toBeNull();
      expect(result.srcPos).toEqual(9);
      expect(result.destPos).toEqual(3);

      expect(dest[0]).toEqual(WHITE);
      expect(dest[1]).toEqual(BLACK);
      expect(dest[2]).toEqual(RED);
    });

    it("returns null for an unknown kind", function () {
      const result = convertToRGBA({
        src: new Uint8Array(4),
        dest: new Uint32Array(1),
        width: 1,
        height: 1,
        kind: 999,
      });
      expect(result).toBeNull();
    });

    it("handles destPos offset for RGB_24BPP", function () {
      // One red pixel written at destPos=2 in a 4-pixel buffer.
      const src = new Uint8Array([255, 0, 0]);
      const dest = new Uint32Array(4);
      const result = convertToRGBA({
        src,
        dest,
        destPos: 2,
        width: 1,
        height: 1,
        kind: ImageKind.RGB_24BPP,
      });
      expect(result.destPos).toEqual(3);
      expect(dest[0]).toEqual(0); // untouched
      expect(dest[1]).toEqual(0); // untouched
      expect(dest[2]).toEqual(RED); // red
      expect(dest[3]).toEqual(0); // untouched
    });
  });
});
