import { describe, it, expect, beforeAll } from "vitest";
import { readFile, writeFile, rm } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { convertFont, execCommand } from "../src/services/font.js";
import { ProcessingError } from "../src/middleware/error-handler.js";

const FIXTURES = join(import.meta.dirname, "fixtures");

let ttfData: Buffer;
let otfData: Buffer;
let woffData: Buffer;
let woff2Data: Buffer;

beforeAll(async () => {
  [ttfData, otfData, woffData, woff2Data] = await Promise.all([
    readFile(join(FIXTURES, "font-1.ttf")),
    readFile(join(FIXTURES, "font-1.otf")),
    readFile(join(FIXTURES, "font-1.woff")),
    readFile(join(FIXTURES, "font-1.woff2")),
  ]);
});

describe("convertFont", () => {
  describe("sourceType=ttf", () => {
    it("ttf→ttf returns the input buffer unchanged", async () => {
      const result = await convertFont(ttfData, "font/ttf", "font/ttf");
      expect(result).toBe(ttfData);
    });

    it("ttf→otf returns non-null Buffer", async () => {
      const result = await convertFont(ttfData, "font/ttf", "font/otf");
      expect(result).toBeInstanceOf(Buffer);
      expect(result!.length).toBeGreaterThan(0);
    });

    it("ttf→woff returns non-null Buffer", async () => {
      const result = await convertFont(ttfData, "font/ttf", "font/woff");
      expect(result).toBeInstanceOf(Buffer);
      expect(result!.length).toBeGreaterThan(0);
    });
  });

  describe("sourceType=otf", () => {
    it("otf→otf returns the input buffer unchanged", async () => {
      const result = await convertFont(otfData, "font/otf", "font/otf");
      expect(result).toBe(otfData);
    });

    it("otf→ttf returns non-null Buffer", async () => {
      const result = await convertFont(otfData, "font/otf", "font/ttf");
      expect(result).toBeInstanceOf(Buffer);
      expect(result!.length).toBeGreaterThan(0);
    });

    it("otf→woff returns non-null Buffer", async () => {
      const result = await convertFont(otfData, "font/otf", "font/woff");
      expect(result).toBeInstanceOf(Buffer);
      expect(result!.length).toBeGreaterThan(0);
    });
  });

  describe("sourceType=woff", () => {
    it("woff→woff returns the input buffer unchanged", async () => {
      const result = await convertFont(woffData, "font/woff", "font/woff");
      expect(result).toBe(woffData);
    });

    it("woff→ttf returns non-null Buffer", async () => {
      const result = await convertFont(woffData, "font/woff", "font/ttf");
      expect(result).toBeInstanceOf(Buffer);
      expect(result!.length).toBeGreaterThan(0);
    });

    it("woff→otf returns Buffer or null (FontForge limitation)", async () => {
      const result = await convertFont(woffData, "font/woff", "font/otf");
      // FontForge may fail to convert TTF-based WOFF to OTF for some fonts.
      // The backend handles null gracefully (variant is just absent).
      if (result !== null) {
        expect(result).toBeInstanceOf(Buffer);
        expect(result.length).toBeGreaterThan(0);
      }
    });
  });

  describe("sourceType=woff2", () => {
    it("woff2→ttf returns non-null Buffer", async () => {
      const result = await convertFont(woff2Data, "font/woff2", "font/ttf");
      expect(result).toBeInstanceOf(Buffer);
      expect(result!.length).toBeGreaterThan(0);
    });

    it("woff2→otf returns non-null Buffer", async () => {
      const result = await convertFont(woff2Data, "font/woff2", "font/otf");
      expect(result).toBeInstanceOf(Buffer);
      expect(result!.length).toBeGreaterThan(0);
    });

    it("woff2→woff returns non-null Buffer", async () => {
      const result = await convertFont(woff2Data, "font/woff2", "font/woff");
      expect(result).toBeInstanceOf(Buffer);
      expect(result!.length).toBeGreaterThan(0);
    });
  });

  describe("invalid input", () => {
    it("woff with garbage data throws ProcessingError", async () => {
      const garbage = Buffer.from("not a font at all");
      await expect(convertFont(garbage, "font/woff", "font/ttf")).rejects.toThrow(ProcessingError);
    });

    it("woff2 with garbage data throws ProcessingError", async () => {
      const garbage = Buffer.from("not a font at all");
      await expect(convertFont(garbage, "font/woff2", "font/ttf")).rejects.toThrow(ProcessingError);
    });

    it("sfnt with garbage data throws validation error", async () => {
      const garbage = Buffer.from("not a font at all");
      try {
        await convertFont(garbage, "font/woff", "font/ttf");
        expect.fail("should have thrown");
      } catch (err) {
        expect(err).toBeInstanceOf(ProcessingError);
        const pe = err as import("../src/middleware/error-handler.js").ProcessingError;
        expect(pe.statusCode).toBe(400);
        expect(pe.errorBody.code).toBe("invalid-font");
      }
    });

    it("ttf→ttf with invalid magic bytes throws validation error", async () => {
      const invalidTtf = Buffer.from("00000000", "hex");
      try {
        await convertFont(invalidTtf, "font/ttf", "font/ttf");
        expect.fail("should have thrown");
      } catch (err) {
        expect(err).toBeInstanceOf(ProcessingError);
        const pe = err as import("../src/middleware/error-handler.js").ProcessingError;
        expect(pe.statusCode).toBe(400);
        expect(pe.errorBody.code).toBe("invalid-font");
      }
    });

    it("otf→otf with invalid magic bytes throws validation error", async () => {
      const invalidOtf = Buffer.from("00000000", "hex");
      try {
        await convertFont(invalidOtf, "font/otf", "font/otf");
        expect.fail("should have thrown");
      } catch (err) {
        expect(err).toBeInstanceOf(ProcessingError);
        const pe = err as import("../src/middleware/error-handler.js").ProcessingError;
        expect(pe.statusCode).toBe(400);
        expect(pe.errorBody.code).toBe("invalid-font");
      }
    });

    it("woff→woff with invalid magic bytes throws validation error", async () => {
      const invalidWoff = Buffer.from("00000000", "hex");
      try {
        await convertFont(invalidWoff, "font/woff", "font/woff");
        expect.fail("should have thrown");
      } catch (err) {
        expect(err).toBeInstanceOf(ProcessingError);
        const pe = err as import("../src/middleware/error-handler.js").ProcessingError;
        expect(pe.statusCode).toBe(400);
        expect(pe.errorBody.code).toBe("invalid-font");
      }
    });

    it("woff2→woff2 with invalid magic bytes throws validation error", async () => {
      const invalidWoff2 = Buffer.from("00000000", "hex");
      try {
        await convertFont(invalidWoff2, "font/woff2", "font/woff2");
        expect.fail("should have thrown");
      } catch (err) {
        expect(err).toBeInstanceOf(ProcessingError);
        const pe = err as import("../src/middleware/error-handler.js").ProcessingError;
        expect(pe.statusCode).toBe(400);
        expect(pe.errorBody.code).toBe("invalid-font");
      }
    });
  });

  describe("data integrity", () => {
    it("ttf→otf produces valid font buffer", async () => {
      const result = await convertFont(ttfData, "font/ttf", "font/otf");
      expect(result).toBeInstanceOf(Buffer);
      expect(result!.length).toBeGreaterThan(0);
    });

    it("otf→ttf produces valid font buffer", async () => {
      const result = await convertFont(otfData, "font/otf", "font/ttf");
      expect(result).toBeInstanceOf(Buffer);
      expect(result!.length).toBeGreaterThan(0);
    });

    it("woff→ttf produces valid SFNT with correct magic bytes", async () => {
      const result = await convertFont(woffData, "font/woff", "font/ttf");
      expect(result).toBeInstanceOf(Buffer);
      expect(result!.length).toBeGreaterThan(0);
      // SFNT magic: 00010000 (TTF) or 4f54544f (OTF/CFF)
      const magic = result!.subarray(0, 4).toString("hex");
      expect(["00010000", "4f54544f"]).toContain(magic);
    });

    it("woff2→ttf produces valid font buffer", async () => {
      const result = await convertFont(woff2Data, "font/woff2", "font/ttf");
      expect(result).toBeInstanceOf(Buffer);
      expect(result!.length).toBeGreaterThan(0);
    });
  });

  describe("file path input", () => {
    it("ttf→otf with file path returns non-null Buffer", async () => {
      const tempPath = join(tmpdir(), `test-font-${Date.now()}.ttf`);
      try {
        await writeFile(tempPath, ttfData);
        const result = await convertFont(tempPath, "font/ttf", "font/otf");
        expect(result).toBeInstanceOf(Buffer);
        expect(result!.length).toBeGreaterThan(0);
      } finally {
        await rm(tempPath, { force: true });
      }
    });

    it("ttf→ttf with file path returns file contents as Buffer", async () => {
      const tempPath = join(tmpdir(), `test-font-${Date.now()}.ttf`);
      try {
        await writeFile(tempPath, ttfData);
        const result = await convertFont(tempPath, "font/ttf", "font/ttf");
        expect(result).toBeInstanceOf(Buffer);
        expect(result!.length).toBe(ttfData.length);
      } finally {
        await rm(tempPath, { force: true });
      }
    });

    it("woff→ttf with file path returns non-null Buffer", async () => {
      const tempPath = join(tmpdir(), `test-font-${Date.now()}.woff`);
      try {
        await writeFile(tempPath, woffData);
        const result = await convertFont(tempPath, "font/woff", "font/ttf");
        expect(result).toBeInstanceOf(Buffer);
        expect(result!.length).toBeGreaterThan(0);
      } finally {
        await rm(tempPath, { force: true });
      }
    });

    it("ttf→woff with file path returns non-null Buffer", async () => {
      const tempPath = join(tmpdir(), `test-font-${Date.now()}.ttf`);
      try {
        await writeFile(tempPath, ttfData);
        const result = await convertFont(tempPath, "font/ttf", "font/woff");
        expect(result).toBeInstanceOf(Buffer);
        expect(result!.length).toBeGreaterThan(0);
      } finally {
        await rm(tempPath, { force: true });
      }
    });

    it("woff2→ttf with file path returns non-null Buffer", async () => {
      const tempPath = join(tmpdir(), `test-font-${Date.now()}.woff2`);
      try {
        await writeFile(tempPath, woff2Data);
        const result = await convertFont(tempPath, "font/woff2", "font/ttf");
        expect(result).toBeInstanceOf(Buffer);
        expect(result!.length).toBeGreaterThan(0);
      } finally {
        await rm(tempPath, { force: true });
      }
    });
  });

  describe("path validation", () => {
    it("rejects string input path outside tmpdir", async () => {
      const outsidePath = "/etc/passwd";
      try {
        await convertFont(outsidePath, "font/ttf", "font/otf");
        expect.fail("should have thrown");
      } catch (err) {
        const error = err as Error;
        expect(error.message).toContain("Font processing denied: input path is outside expected directory");
      }
    });
  });
});

describe("execCommand", () => {
  it("preserves killed and signal properties from child process errors", async () => {
    try {
      await execCommand("false", []);
      expect.fail("should have thrown");
    } catch (err) {
      const error = err as Error & { killed?: boolean; signal?: string; code?: number };
      expect(error).toBeInstanceOf(Error);
      expect(error.message).toContain("Command failed");
    }
  });

  it("preserves error properties when command is killed by signal", async () => {
    try {
      await execCommand("sh", ["-c", "kill -KILL $$"], 5000);
      expect.fail("should have thrown");
    } catch (err) {
      const error = err as Error & { killed?: boolean; signal?: string };
      expect(error).toBeInstanceOf(Error);
      expect(error.signal).toBe("SIGKILL");
    }
  });
});
