import sharp from "sharp";
import { beforeAll, describe, expect, it } from "vitest";
import { processImage } from "../src/engine.js";
import { detectFormat } from "../src/formats/detect.js";
import { brightness } from "../src/operations/brightness.js";
import { colorChannels } from "../src/operations/color-channels.js";
import { compress } from "../src/operations/compress.js";
import { contrast } from "../src/operations/contrast.js";
import { convert } from "../src/operations/convert.js";
import { crop } from "../src/operations/crop.js";
import { flip } from "../src/operations/flip.js";
import { grayscale } from "../src/operations/grayscale.js";
import { invert } from "../src/operations/invert.js";
import { resize } from "../src/operations/resize.js";
import { rotate } from "../src/operations/rotate.js";
import { saturation } from "../src/operations/saturation.js";
import { sepia } from "../src/operations/sepia.js";
import { stripMetadata } from "../src/operations/strip-metadata.js";
import { getImageInfo } from "../src/utils/metadata.js";
import { extToMime, formatToExt, formatToMime, mimeToExt } from "../src/utils/mime.js";

// Generate a 100x100 red PNG buffer for testing
let testBuffer: Buffer;
let testImage: () => sharp.Sharp;

beforeAll(async () => {
  testBuffer = await sharp({
    create: {
      width: 100,
      height: 100,
      channels: 3,
      background: { r: 255, g: 0, b: 0 },
    },
  })
    .png()
    .toBuffer();

  testImage = () => sharp(testBuffer);
});

describe("resize", () => {
  it("should resize to 50x50", async () => {
    const result = await resize(testImage(), { width: 50, height: 50 });
    const buf = await result.toBuffer();
    const meta = await sharp(buf).metadata();
    expect(meta.width).toBe(50);
    expect(meta.height).toBe(50);
  });

  it("should resize by percentage", async () => {
    const result = await resize(testImage(), { percentage: 50 });
    const buf = await result.toBuffer();
    const meta = await sharp(buf).metadata();
    expect(meta.width).toBe(50);
    expect(meta.height).toBe(50);
  });

  it("should throw on zero width", async () => {
    await expect(resize(testImage(), { width: 0 })).rejects.toThrow();
  });

  it("should throw when no dimensions provided", async () => {
    await expect(resize(testImage(), {})).rejects.toThrow();
  });
});

describe("crop", () => {
  it("should crop 25x25 at (10,10)", async () => {
    const result = await crop(testImage(), {
      left: 10,
      top: 10,
      width: 25,
      height: 25,
    });
    const buf = await result.toBuffer();
    const meta = await sharp(buf).metadata();
    expect(meta.width).toBe(25);
    expect(meta.height).toBe(25);
  });

  it("should throw on out-of-bounds crop", async () => {
    await expect(crop(testImage(), { left: 90, top: 90, width: 20, height: 20 })).rejects.toThrow();
  });

  it("should throw on zero dimensions", async () => {
    await expect(crop(testImage(), { left: 0, top: 0, width: 0, height: 10 })).rejects.toThrow();
  });
});

describe("rotate", () => {
  it("should rotate 90 degrees and swap dimensions", async () => {
    // Create a non-square image to verify dimension swap
    const rectBuffer = await sharp({
      create: {
        width: 100,
        height: 50,
        channels: 3,
        background: { r: 255, g: 0, b: 0 },
      },
    })
      .png()
      .toBuffer();

    const result = await rotate(sharp(rectBuffer), { angle: 90 });
    const buf = await result.toBuffer();
    const meta = await sharp(buf).metadata();
    expect(meta.width).toBe(50);
    expect(meta.height).toBe(100);
  });

  it("should rotate non-90 angle with background", async () => {
    const result = await rotate(testImage(), {
      angle: 45,
      background: "#FF0000",
    });
    const meta = await result.metadata();
    expect(meta.width).toBeGreaterThan(0);
    expect(meta.height).toBeGreaterThan(0);
  });
});

describe("flip", () => {
  it("should flip horizontally without error", async () => {
    const result = await flip(testImage(), { horizontal: true });
    const buf = await result.toBuffer();
    expect(buf.length).toBeGreaterThan(0);
  });

  it("should flip vertically without error", async () => {
    const result = await flip(testImage(), { vertical: true });
    const buf = await result.toBuffer();
    expect(buf.length).toBeGreaterThan(0);
  });

  it("should flip both directions", async () => {
    const result = await flip(testImage(), { horizontal: true, vertical: true });
    const buf = await result.toBuffer();
    expect(buf.length).toBeGreaterThan(0);
  });

  it("should throw when neither direction specified", async () => {
    await expect(flip(testImage(), {})).rejects.toThrow();
  });
});

describe("convert", () => {
  it("should convert to webp", async () => {
    const result = await convert(testImage(), { format: "webp" });
    const buf = await result.toBuffer();
    const meta = await sharp(buf).metadata();
    expect(meta.format).toBe("webp");
  });

  it("should convert to jpg with quality", async () => {
    const result = await convert(testImage(), { format: "jpg", quality: 80 });
    const buf = await result.toBuffer();
    const meta = await sharp(buf).metadata();
    expect(meta.format).toBe("jpeg");
  });

  it("should throw on invalid quality", async () => {
    await expect(convert(testImage(), { format: "png", quality: 0 })).rejects.toThrow();
  });
});

describe("compress", () => {
  it("should compress at quality 50 and produce smaller output", async () => {
    // Use a larger image for better compression ratio visibility
    const largeBuffer = await sharp({
      create: {
        width: 500,
        height: 500,
        channels: 3,
        background: { r: 255, g: 128, b: 0 },
      },
    })
      .jpeg({ quality: 100 })
      .toBuffer();

    const result = await compress(sharp(largeBuffer), {
      quality: 50,
      format: "jpg",
    });
    const buf = await result.toBuffer();
    expect(buf.length).toBeLessThan(largeBuffer.length);
  });

  it("should throw on invalid quality", async () => {
    await expect(compress(testImage(), { quality: 0 })).rejects.toThrow();
  });

  it("should compress to target size", async () => {
    const largeBuffer = await sharp({
      create: {
        width: 500,
        height: 500,
        channels: 3,
        background: { r: 255, g: 128, b: 0 },
      },
    })
      .jpeg({ quality: 100 })
      .toBuffer();

    const targetSize = Math.round(largeBuffer.length * 0.5);
    const result = await compress(sharp(largeBuffer), {
      targetSizeBytes: targetSize,
      format: "jpg",
    });
    const buf = await result.toBuffer();
    // Should be reasonably close to target (within 50% tolerance for small images)
    expect(buf.length).toBeLessThan(largeBuffer.length);
  });
});

describe("grayscale", () => {
  it("should convert to grayscale", async () => {
    const result = await grayscale(testImage());
    const buf = await result.toBuffer();
    const meta = await sharp(buf).metadata();
    // Grayscale PNG may still report channels as 3 or 1 depending on output
    expect(buf.length).toBeGreaterThan(0);
    // The image should have no color variation
    expect(meta.width).toBe(100);
  });
});

describe("sepia", () => {
  it("should apply sepia tone without error", async () => {
    const result = await sepia(testImage());
    const buf = await result.toBuffer();
    expect(buf.length).toBeGreaterThan(0);
  });
});

describe("invert", () => {
  it("should invert colors without error", async () => {
    const result = await invert(testImage());
    const buf = await result.toBuffer();
    expect(buf.length).toBeGreaterThan(0);
  });
});

describe("brightness", () => {
  it("should adjust brightness +50 without error", async () => {
    const result = await brightness(testImage(), { value: 50 });
    const buf = await result.toBuffer();
    expect(buf.length).toBeGreaterThan(0);
  });

  it("should throw on out-of-range value", async () => {
    await expect(brightness(testImage(), { value: 150 })).rejects.toThrow();
  });
});

describe("contrast", () => {
  it("should adjust contrast +50 without error", async () => {
    const result = await contrast(testImage(), { value: 50 });
    const buf = await result.toBuffer();
    expect(buf.length).toBeGreaterThan(0);
  });

  it("should throw on out-of-range value", async () => {
    await expect(contrast(testImage(), { value: -150 })).rejects.toThrow();
  });
});

describe("saturation", () => {
  it("should adjust saturation -50 without error", async () => {
    const result = await saturation(testImage(), { value: -50 });
    const buf = await result.toBuffer();
    expect(buf.length).toBeGreaterThan(0);
  });

  it("should throw on out-of-range value", async () => {
    await expect(saturation(testImage(), { value: 200 })).rejects.toThrow();
  });
});

describe("colorChannels", () => {
  it("should adjust color channels without error", async () => {
    const result = await colorChannels(testImage(), {
      red: 150,
      green: 100,
      blue: 50,
    });
    const buf = await result.toBuffer();
    expect(buf.length).toBeGreaterThan(0);
  });

  it("should throw on out-of-range values", async () => {
    await expect(colorChannels(testImage(), { red: 250, green: 100, blue: 100 })).rejects.toThrow();
  });
});

describe("stripMetadata", () => {
  it("should strip metadata without error", async () => {
    const result = await stripMetadata(testImage());
    const buf = await result.toBuffer();
    expect(buf.length).toBeGreaterThan(0);
  });
});

describe("processImage (engine)", () => {
  it("should apply multiple operations in sequence", async () => {
    const result = await processImage(
      testBuffer,
      [
        { type: "resize", options: { width: 50, height: 50 } },
        { type: "grayscale", options: {} },
      ],
      "png",
    );

    expect(result.info.width).toBe(50);
    expect(result.info.height).toBe(50);
    expect(result.buffer.length).toBeGreaterThan(0);
  });

  it("should throw on unknown operation", async () => {
    await expect(processImage(testBuffer, [{ type: "unknown-op", options: {} }])).rejects.toThrow(
      "Unknown operation",
    );
  });

  it("should convert output format", async () => {
    const result = await processImage(testBuffer, [], "webp");
    expect(result.info.format).toBe("webp");
  });
});

describe("detectFormat", () => {
  it("should detect PNG format", async () => {
    const format = await detectFormat(testBuffer);
    expect(format).toBe("png");
  });

  it("should detect JPEG format", async () => {
    const jpegBuffer = await sharp(testBuffer).jpeg().toBuffer();
    const format = await detectFormat(jpegBuffer);
    expect(format).toBe("jpeg");
  });
});

describe("getImageInfo", () => {
  it("should return correct image info", async () => {
    const info = await getImageInfo(testBuffer);
    expect(info.width).toBe(100);
    expect(info.height).toBe(100);
    expect(info.format).toBe("png");
    expect(info.channels).toBe(3);
    expect(info.size).toBeGreaterThan(0);
    expect(info.hasAlpha).toBe(false);
  });
});

describe("mime utilities", () => {
  it("should map extension to MIME type", () => {
    expect(extToMime("jpg")).toBe("image/jpeg");
    expect(extToMime("png")).toBe("image/png");
    expect(extToMime("webp")).toBe("image/webp");
    expect(extToMime(".jpg")).toBe("image/jpeg");
    expect(extToMime("unknown")).toBe("application/octet-stream");
  });

  it("should map MIME type to extension", () => {
    expect(mimeToExt("image/jpeg")).toBe("jpg");
    expect(mimeToExt("image/png")).toBe("png");
    expect(mimeToExt("application/unknown")).toBe("bin");
  });

  it("should map format to MIME type", () => {
    expect(formatToMime("jpeg")).toBe("image/jpeg");
    expect(formatToMime("png")).toBe("image/png");
  });

  it("should map format to extension", () => {
    expect(formatToExt("jpeg")).toBe("jpg");
    expect(formatToExt("png")).toBe("png");
  });
});
