import { createRequire } from "node:module";
import path from "node:path";
import { beforeAll, describe, expect, it } from "vitest";

const require = createRequire(
  path.resolve(__dirname, "../../../packages/image-engine/src/index.ts"),
);
const sharp = require("sharp") as typeof import("sharp").default;
const exifReader = require(
  path.resolve(__dirname, "../../../packages/image-engine/node_modules/exif-reader"),
) as typeof import("exif-reader").default;

import { editMetadata } from "@snapotter/image-engine";
import { fixtures, readFixture } from "../../fixtures/index.js";

let jpgWithExif: Buffer;
let png200x150: Buffer;

beforeAll(() => {
  jpgWithExif = readFixture(fixtures.image.exifGps);
  png200x150 = readFixture(fixtures.image.base.png200);
});

async function getExif(img: sharp.Sharp) {
  const buf = await img.toBuffer();
  const meta = await sharp(buf).metadata();
  if (!meta.exif) return null;
  return exifReader(meta.exif);
}

describe("editMetadata", () => {
  // -- No-op cases -----------------------------------------------------------

  it("returns image unchanged when no edits or removals are specified", async () => {
    const img = sharp(jpgWithExif);
    const result = await editMetadata(img, {});
    const buf = await result.toBuffer();
    expect(buf.length).toBeGreaterThan(0);
  });

  it("returns image unchanged with empty options object", async () => {
    const img = sharp(jpgWithExif);
    const result = await editMetadata(img);
    const buf = await result.toBuffer();
    expect(buf.length).toBeGreaterThan(0);
  });

  // -- Writing fields --------------------------------------------------------

  it("writes artist field to EXIF", async () => {
    const img = sharp(jpgWithExif);
    const result = await editMetadata(img, { artist: "New Artist" });
    const exif = await getExif(result);
    expect(exif?.Image?.Artist).toBe("New Artist");
  });

  it("writes copyright field to EXIF", async () => {
    const img = sharp(jpgWithExif);
    const result = await editMetadata(img, { copyright: "2026 Test" });
    const exif = await getExif(result);
    expect(exif?.Image?.Copyright).toBe("2026 Test");
  });

  it("writes imageDescription to EXIF", async () => {
    const img = sharp(jpgWithExif);
    const result = await editMetadata(img, { imageDescription: "A test image" });
    const exif = await getExif(result);
    expect(exif?.Image?.ImageDescription).toBe("A test image");
  });

  it("writes software field to EXIF", async () => {
    const img = sharp(jpgWithExif);
    const result = await editMetadata(img, { software: "SnapOtter v1" });
    const exif = await getExif(result);
    expect(exif?.Image?.Software).toBe("SnapOtter v1");
  });

  it("writes dateTime field to IFD0", async () => {
    const img = sharp(jpgWithExif);
    const result = await editMetadata(img, { dateTime: "2026:01:15 10:30:00" });
    const exif = await getExif(result);
    expect(exif?.Image?.DateTime).toBeDefined();
  });

  it("writes dateTimeOriginal to IFD2", async () => {
    const img = sharp(jpgWithExif);
    const result = await editMetadata(img, { dateTimeOriginal: "2025:06:01 12:00:00" });
    const exif = await getExif(result);
    expect(exif?.Photo?.DateTimeOriginal).toBeDefined();
  });

  it("writes multiple fields at once", async () => {
    const img = sharp(jpgWithExif);
    const result = await editMetadata(img, {
      artist: "Multi Writer",
      copyright: "2026 Multi",
      software: "TestApp",
    });
    const exif = await getExif(result);
    expect(exif?.Image?.Artist).toBe("Multi Writer");
    expect(exif?.Image?.Copyright).toBe("2026 Multi");
    expect(exif?.Image?.Software).toBe("TestApp");
  });

  // -- Ignoring empty strings ------------------------------------------------

  it("ignores empty string values for fields", async () => {
    const img = sharp(jpgWithExif);
    const result = await editMetadata(img, { artist: "", copyright: "" });
    // No edits + no removals = keepMetadata path
    const buf = await result.toBuffer();
    expect(buf.length).toBeGreaterThan(0);
  });

  // -- Removing fields -------------------------------------------------------

  it("removes specified fields from EXIF", async () => {
    const img = sharp(jpgWithExif);
    const result = await editMetadata(img, { fieldsToRemove: ["Artist"] });
    const exif = await getExif(result);
    // Artist should be removed
    expect(exif?.Image?.Artist).toBeUndefined();
  });

  it("filters out unsafe round-trip keys from fieldsToRemove", async () => {
    const img = sharp(jpgWithExif);
    // MakerNote is in the UNSAFE_ROUND_TRIP_KEYS set
    const result = await editMetadata(img, { fieldsToRemove: ["MakerNote"] });
    // Should not throw, and image should be returned
    const buf = await result.toBuffer();
    expect(buf.length).toBeGreaterThan(0);
  });

  it("does not remove a field that is also being written", async () => {
    const img = sharp(jpgWithExif);
    // Write Artist and also try to remove it - write takes precedence
    const result = await editMetadata(img, {
      artist: "Keep Me",
      fieldsToRemove: ["Artist"],
    });
    const exif = await getExif(result);
    expect(exif?.Image?.Artist).toBe("Keep Me");
  });

  // -- clearGps --------------------------------------------------------------

  it("clearGps removes GPS data", async () => {
    const img = sharp(jpgWithExif);
    const result = await editMetadata(img, { clearGps: true });
    const buf = await result.toBuffer();
    expect(buf.length).toBeGreaterThan(0);
  });

  it("removes a Photo (IFD2) field via fieldsToRemove", async () => {
    const img = sharp(jpgWithExif);
    // DateTimeOriginal is in the Photo (IFD2) section
    const result = await editMetadata(img, {
      fieldsToRemove: ["DateTimeOriginal"],
    });
    const exif = await getExif(result);
    expect(exif?.Photo?.DateTimeOriginal).toBeUndefined();
    // Other IFD0 fields should be preserved
    expect(exif?.Image?.Artist).toBe("Test Artist");
  });

  // -- Combined edit + remove ------------------------------------------------

  it("handles both edits and removals together", async () => {
    const img = sharp(jpgWithExif);
    const result = await editMetadata(img, {
      artist: "New Creator",
      fieldsToRemove: ["Copyright"],
    });
    const exif = await getExif(result);
    expect(exif?.Image?.Artist).toBe("New Creator");
    expect(exif?.Image?.Copyright).toBeUndefined();
  });

  // -- Write-only path (withExifMerge) ---------------------------------------

  it("uses merge path when only writing fields (no removals)", async () => {
    const img = sharp(jpgWithExif);
    const result = await editMetadata(img, { artist: "Merge Writer" });
    const exif = await getExif(result);
    expect(exif?.Image?.Artist).toBe("Merge Writer");
    // Copyright should still exist from original EXIF
    expect(exif?.Image?.Copyright).toBeDefined();
  });

  // -- Image without EXIF ---------------------------------------------------

  it("writes EXIF to image that had no EXIF before", async () => {
    const img = sharp(png200x150);
    const result = await editMetadata(img, { artist: "PNG Artist" });
    // Convert to JPEG first since PNG doesn't support EXIF natively
    const jpgBuf = await result.jpeg().toBuffer();
    const meta = await sharp(jpgBuf).metadata();
    if (meta.exif) {
      const exif = exifReader(meta.exif);
      expect(exif?.Image?.Artist).toBe("PNG Artist");
    }
  });

  it("handles removal on image without existing EXIF gracefully", async () => {
    const img = sharp(png200x150);
    const result = await editMetadata(img, {
      fieldsToRemove: ["Artist"],
      artist: "FallbackWriter",
    });
    const buf = await result.toBuffer();
    expect(buf.length).toBeGreaterThan(0);
  });

  it("survives corrupt EXIF during removal (catch branch)", async () => {
    // Corrupt the byte order marker in the EXIF so exif-reader throws,
    // but sharp still sees the EXIF segment as present.
    const rawBuf = Buffer.from(jpgWithExif);
    // Find the 'Exif' header in the raw JPEG
    let exifStart = -1;
    for (let i = 0; i < rawBuf.length - 4; i++) {
      if (
        rawBuf[i] === 0x45 &&
        rawBuf[i + 1] === 0x78 &&
        rawBuf[i + 2] === 0x69 &&
        rawBuf[i + 3] === 0x66
      ) {
        exifStart = i;
        break;
      }
    }
    expect(exifStart).toBeGreaterThanOrEqual(0);
    // Corrupt the byte order marker (offset +6 after 'Exif\0\0')
    const corruptBuf = Buffer.from(rawBuf);
    corruptBuf[exifStart + 6] = 0xde;
    corruptBuf[exifStart + 7] = 0xad;

    const img = sharp(corruptBuf);
    // fieldsToRemove triggers the removal path which tries to parse EXIF
    const result = await editMetadata(img, {
      fieldsToRemove: ["Software"],
      artist: "Survivor",
    });
    const buf = await result.toBuffer();
    expect(buf.length).toBeGreaterThan(0);
  });
});
