import { describe, expect, it } from "vitest";
import {
  buildDockerRunArgs,
  resolveDockerPlatform,
  type DockerRenderOptions,
} from "./dockerRunArgs.js";

const BASE: DockerRenderOptions = {
  fps: { num: 30, den: 1 },
  quality: "standard",
  format: "mp4",
  gpu: false,
  browserGpu: false,
  hdrMode: "auto",
  crf: undefined,
  videoBitrate: undefined,
  quiet: false,
};

const FIXED_INPUT = {
  imageTag: "hyperframes-renderer:0.0.0-test",
  projectDir: "/abs/proj",
  outputDir: "/abs/out",
  outputFilename: "out.mp4",
  // Pin platform in tests so snapshots are arch-independent (otherwise they
  // flip between linux/amd64 and linux/arm64 depending on the host running
  // the test).
  platform: "linux/amd64",
};

describe("buildDockerRunArgs", () => {
  it("matches snapshot for the default render", () => {
    expect(buildDockerRunArgs({ ...FIXED_INPUT, options: BASE })).toMatchInlineSnapshot(`
      [
        "run",
        "--rm",
        "--platform",
        "linux/amd64",
        "--shm-size=2g",
        "-v",
        "/abs/proj:/project:ro",
        "-v",
        "/abs/out:/output",
        "hyperframes-renderer:0.0.0-test",
        "/project",
        "--output",
        "/output/out.mp4",
        "--fps",
        "30",
        "--quality",
        "standard",
        "--format",
        "mp4",
        "--no-browser-gpu",
      ]
    `);
  });

  it("omits --workers when auto sizing should happen inside the container", () => {
    const args = buildDockerRunArgs({ ...FIXED_INPUT, options: BASE });
    expect(args).not.toContain("--workers");
  });

  it("matches snapshot when every renderer flag is enabled", () => {
    expect(
      buildDockerRunArgs({
        ...FIXED_INPUT,
        options: {
          ...BASE,
          gpu: true,
          hdrMode: "force-hdr",
          crf: 18,
          videoBitrate: undefined,
          quiet: true,
        },
      }),
    ).toMatchInlineSnapshot(`
      [
        "run",
        "--rm",
        "--platform",
        "linux/amd64",
        "--shm-size=2g",
        "--gpus",
        "all",
        "-v",
        "/abs/proj:/project:ro",
        "-v",
        "/abs/out:/output",
        "hyperframes-renderer:0.0.0-test",
        "/project",
        "--output",
        "/output/out.mp4",
        "--fps",
        "30",
        "--quality",
        "standard",
        "--format",
        "mp4",
        "--crf",
        "18",
        "--quiet",
        "--gpu",
        "--no-browser-gpu",
        "--hdr",
      ]
    `);
  });

  // Regression for the original PR feedback: --hdr was silently dropped from
  // the docker arg array. Keep this assertion explicit (in addition to the
  // snapshot above) so the failure message points directly at the flag.
  it("forwards --hdr to the container when hdrMode is force-hdr", () => {
    const args = buildDockerRunArgs({
      ...FIXED_INPUT,
      options: { ...BASE, hdrMode: "force-hdr" },
    });
    expect(args).toContain("--hdr");
    expect(args).not.toContain("--sdr");
  });

  it("forwards --sdr to the container when hdrMode is force-sdr", () => {
    const args = buildDockerRunArgs({
      ...FIXED_INPUT,
      options: { ...BASE, hdrMode: "force-sdr" },
    });
    expect(args).toContain("--sdr");
    expect(args).not.toContain("--hdr");
  });

  it("omits --hdr and --sdr when hdrMode is auto", () => {
    const args = buildDockerRunArgs({ ...FIXED_INPUT, options: BASE });
    expect(args).not.toContain("--hdr");
    expect(args).not.toContain("--sdr");
  });

  it("requests host GPU passthrough only when gpu is enabled", () => {
    const off = buildDockerRunArgs({ ...FIXED_INPUT, options: BASE });
    expect(off).not.toContain("--gpus");
    expect(off).not.toContain("--gpu");

    const on = buildDockerRunArgs({
      ...FIXED_INPUT,
      options: { ...BASE, gpu: true },
    });
    // `--gpus all` is a docker run flag (host passthrough); `--gpu` is the
    // hyperframes CLI flag forwarded into the container — both must be set.
    expect(on).toContain("--gpus");
    expect(on).toContain("all");
    expect(on).toContain("--gpu");
  });

  it("forces software browser capture inside Docker", () => {
    const args = buildDockerRunArgs({ ...FIXED_INPUT, options: BASE });
    expect(args).toContain("--no-browser-gpu");
  });

  it("forwards every renderer-shaped option (regression tripwire for silent drops)", () => {
    const args = buildDockerRunArgs({
      ...FIXED_INPUT,
      options: {
        fps: { num: 60, den: 1 },
        quality: "high",
        format: "webm",
        workers: 8,
        gpu: true,
        browserGpu: false,
        hdrMode: "force-hdr",
        crf: 16,
        vp9CpuUsed: 2,
        videoBitrate: undefined,
        videoFrameFormat: "png",
        quiet: true,
        entryFile: "compositions/intro.html",
      },
    });
    // Each value must reach the container exactly once. If a future option
    // is added but only wired through to renderLocal, this test forces the
    // author to update buildDockerRunArgs (and add a check here) too.
    expect(args).toContain("60");
    expect(args).toContain("high");
    expect(args).toContain("webm");
    expect(args).toContain("8");
    expect(args).toContain("--crf");
    expect(args).toContain("16");
    expect(args).toContain("--vp9-cpu-used");
    expect(args).toContain("2");
    expect(args).toContain("--video-frame-format");
    expect(args).toContain("png");
    expect(args).toContain("--quiet");
    expect(args).toContain("--gpu");
    expect(args).toContain("--no-browser-gpu");
    expect(args).toContain("--hdr");
    expect(args).toContain("--composition");
    expect(args).toContain("compositions/intro.html");
  });

  it("forwards --format png-sequence to the container", () => {
    const args = buildDockerRunArgs({
      ...FIXED_INPUT,
      outputFilename: "frames",
      options: { ...BASE, format: "png-sequence" },
    });
    const formatIdx = args.indexOf("--format");
    expect(formatIdx).toBeGreaterThanOrEqual(0);
    expect(args[formatIdx + 1]).toBe("png-sequence");
  });

  it("forwards --format gif and --gif-loop to the container", () => {
    const args = buildDockerRunArgs({
      ...FIXED_INPUT,
      outputFilename: "demo.gif",
      options: { ...BASE, format: "gif", gifLoop: 0 },
    });
    const formatIdx = args.indexOf("--format");
    const loopIdx = args.indexOf("--gif-loop");
    expect(formatIdx).toBeGreaterThanOrEqual(0);
    expect(args[formatIdx + 1]).toBe("gif");
    expect(loopIdx).toBeGreaterThanOrEqual(0);
    expect(args[loopIdx + 1]).toBe("0");
  });

  it("forwards --video-bitrate to the container when set", () => {
    const args = buildDockerRunArgs({
      ...FIXED_INPUT,
      options: { ...BASE, videoBitrate: "10M" },
    });
    expect(args).toContain("--video-bitrate");
    expect(args).toContain("10M");
    expect(args).not.toContain("--crf");
  });

  it("forwards --video-frame-format to the container when set to png", () => {
    const args = buildDockerRunArgs({
      ...FIXED_INPUT,
      options: { ...BASE, videoFrameFormat: "png" },
    });
    expect(args).toContain("--video-frame-format");
    expect(args).toContain("png");
  });

  it("omits --video-frame-format when it is auto or unset", () => {
    expect(buildDockerRunArgs({ ...FIXED_INPUT, options: BASE })).not.toContain(
      "--video-frame-format",
    );
    expect(
      buildDockerRunArgs({
        ...FIXED_INPUT,
        options: { ...BASE, videoFrameFormat: "auto" },
      }),
    ).not.toContain("--video-frame-format");
  });

  it("forwards --variables JSON to the container when set", () => {
    const args = buildDockerRunArgs({
      ...FIXED_INPUT,
      options: { ...BASE, variables: { title: "Hello", n: 3 } },
    });
    const idx = args.indexOf("--variables");
    expect(idx).toBeGreaterThan(-1);
    expect(args[idx + 1]).toBe('{"title":"Hello","n":3}');
  });

  it("omits --variables when none provided", () => {
    const args = buildDockerRunArgs({ ...FIXED_INPUT, options: BASE });
    expect(args).not.toContain("--variables");
  });

  it("omits --variables when payload is empty", () => {
    const args = buildDockerRunArgs({
      ...FIXED_INPUT,
      options: { ...BASE, variables: {} },
    });
    expect(args).not.toContain("--variables");
  });

  it("forwards --composition to the container when entryFile is set", () => {
    const args = buildDockerRunArgs({
      ...FIXED_INPUT,
      options: { ...BASE, entryFile: "compositions/intro.html" },
    });
    const idx = args.indexOf("--composition");
    expect(idx).toBeGreaterThan(-1);
    expect(args[idx + 1]).toBe("compositions/intro.html");
  });

  it("omits --composition when entryFile is not set", () => {
    const args = buildDockerRunArgs({ ...FIXED_INPUT, options: BASE });
    expect(args).not.toContain("--composition");
  });

  it("forwards --browser-timeout in seconds when pageNavigationTimeoutMs is set", () => {
    const args = buildDockerRunArgs({
      ...FIXED_INPUT,
      options: { ...BASE, pageNavigationTimeoutMs: 180_000 },
    });
    const idx = args.indexOf("--browser-timeout");
    expect(idx).toBeGreaterThan(-1);
    // CLI flag takes seconds; engine takes ms — the docker bridge converts
    // back to seconds so the in-container CLI re-parses it consistently.
    expect(args[idx + 1]).toBe("180");
  });

  it("omits --browser-timeout when pageNavigationTimeoutMs is not set", () => {
    const args = buildDockerRunArgs({ ...FIXED_INPUT, options: BASE });
    expect(args).not.toContain("--browser-timeout");
  });

  it("forwards rational --fps verbatim (NTSC 30000/1001)", () => {
    // Regression for the fps fraction-syntax feature: the rational form must
    // survive the host → container hop as a single `30000/1001` argument so
    // the in-container CLI re-parses it as exact NTSC, not 29.97 decimal.
    const args = buildDockerRunArgs({
      ...FIXED_INPUT,
      options: { ...BASE, fps: { num: 30000, den: 1001 } },
    });
    const fpsIdx = args.indexOf("--fps");
    expect(fpsIdx).toBeGreaterThanOrEqual(0);
    expect(args[fpsIdx + 1]).toBe("30000/1001");
  });

  it("forwards integer --fps as a bare integer string", () => {
    const args = buildDockerRunArgs({
      ...FIXED_INPUT,
      options: { ...BASE, fps: { num: 60, den: 1 } },
    });
    const fpsIdx = args.indexOf("--fps");
    expect(fpsIdx).toBeGreaterThanOrEqual(0);
    expect(args[fpsIdx + 1]).toBe("60");
  });

  it("forwards --resolution to the container when outputResolution is set", () => {
    const args = buildDockerRunArgs({
      ...FIXED_INPUT,
      options: { ...BASE, outputResolution: "landscape-4k" },
    });
    const idx = args.indexOf("--resolution");
    expect(idx).toBeGreaterThan(-1);
    expect(args[idx + 1]).toBe("landscape-4k");
  });

  it("omits --resolution when outputResolution is not set", () => {
    const args = buildDockerRunArgs({ ...FIXED_INPUT, options: BASE });
    expect(args).not.toContain("--resolution");
  });

  it("forwards --no-page-side-compositing when pageSideCompositing is false", () => {
    const args = buildDockerRunArgs({
      ...FIXED_INPUT,
      options: { ...BASE, pageSideCompositing: false },
    });
    expect(args).toContain("--no-page-side-compositing");
  });

  it("omits --no-page-side-compositing when pageSideCompositing is not explicitly false", () => {
    const args = buildDockerRunArgs({ ...FIXED_INPUT, options: BASE });
    expect(args).not.toContain("--no-page-side-compositing");
  });

  // Regression for #1193: an arm64 host (Apple Silicon) was being pinned to
  // linux/amd64, which forced qemu emulation of chrome-headless-shell and
  // produced either navigation timeouts or chrome SEGVs. Each host arch must
  // land in its native --platform value.
  it("emits linux/arm64 when host platform is arm64", () => {
    const args = buildDockerRunArgs({
      imageTag: "hyperframes-renderer:0.0.0-test",
      projectDir: "/abs/proj",
      outputDir: "/abs/out",
      outputFilename: "out.mp4",
      platform: "linux/arm64",
      options: BASE,
    });
    const idx = args.indexOf("--platform");
    expect(idx).toBeGreaterThanOrEqual(0);
    expect(args[idx + 1]).toBe("linux/arm64");
  });

  it("emits linux/amd64 when platform is explicitly amd64", () => {
    const args = buildDockerRunArgs({ ...FIXED_INPUT, options: BASE });
    const idx = args.indexOf("--platform");
    expect(idx).toBeGreaterThanOrEqual(0);
    expect(args[idx + 1]).toBe("linux/amd64");
  });
});

describe("resolveDockerPlatform", () => {
  it("maps arm64 hosts to linux/arm64", () => {
    expect(resolveDockerPlatform("arm64", {})).toBe("linux/arm64");
  });

  it("maps x64 hosts to linux/amd64", () => {
    expect(resolveDockerPlatform("x64", {})).toBe("linux/amd64");
  });

  it("treats unknown architectures as linux/amd64 (safe default)", () => {
    expect(resolveDockerPlatform("riscv64", {})).toBe("linux/amd64");
  });

  // Regression guard: the production call site is `resolveDockerPlatform()`
  // with no args. If a refactor drops either default parameter, every other
  // arch-mapping test would still pass — this one fails loudly.
  it("uses process.arch and process.env when called with no arguments", () => {
    const result = resolveDockerPlatform();
    // Must equal the explicit-arg form (env override notwithstanding, which
    // wouldn't be set in the test runner unless deliberately stubbed).
    const expected = process.env.HYPERFRAMES_DOCKER_PLATFORM
      ? process.env.HYPERFRAMES_DOCKER_PLATFORM
      : resolveDockerPlatform(process.arch, {});
    expect(result).toBe(expected);
  });

  it("honors HYPERFRAMES_DOCKER_PLATFORM override on an arm64 host (Rosetta-Node / parity-regen escape hatch)", () => {
    expect(resolveDockerPlatform("arm64", { HYPERFRAMES_DOCKER_PLATFORM: "linux/amd64" })).toBe(
      "linux/amd64",
    );
  });

  it("honors HYPERFRAMES_DOCKER_PLATFORM override on an amd64 host", () => {
    expect(resolveDockerPlatform("x64", { HYPERFRAMES_DOCKER_PLATFORM: "linux/arm64" })).toBe(
      "linux/arm64",
    );
  });

  it("trims whitespace from HYPERFRAMES_DOCKER_PLATFORM and ignores empty override", () => {
    expect(resolveDockerPlatform("arm64", { HYPERFRAMES_DOCKER_PLATFORM: "  linux/amd64  " })).toBe(
      "linux/amd64",
    );
    // Empty/whitespace-only override falls back to arch detection — important
    // for shells where `export FOO=""` would otherwise pin platform to "".
    expect(resolveDockerPlatform("arm64", { HYPERFRAMES_DOCKER_PLATFORM: "" })).toBe("linux/arm64");
    expect(resolveDockerPlatform("arm64", { HYPERFRAMES_DOCKER_PLATFORM: "   " })).toBe(
      "linux/arm64",
    );
  });
});
