/**
 * Transparency Regression Test
 *
 * Exercises the alpha-output pipelines (webm + png-sequence) end-to-end
 * against `tests/transparency-regression/`. Asserts that:
 *
 *   1. Pixels that were transparent in the browser stay transparent in the
 *      output (alpha = 0).
 *   2. Pixels covered by the opaque red `.card` element stay fully opaque
 *      (alpha = 255) and keep their red color.
 *
 * This is intentionally NOT wired into `regression-harness.ts` — the harness
 * compares each fixture against a golden MP4, but transparency requires a
 * different validation strategy (pixel inspection of the alpha channel). Run
 * this script via `bun run --filter @hyperframes/producer test:transparency`
 * or directly via `tsx src/transparency-test.ts` from this package.
 */

import { strict as assert } from "node:assert";
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";

import { decodePng, runFfmpeg } from "@hyperframes/engine";
import { createRenderJob, executeRenderJob } from "./services/renderOrchestrator.js";

const moduleDir = dirname(fileURLToPath(import.meta.url));
const FIXTURE_DIR = resolve(moduleDir, "../tests/transparency-regression");
const FIXTURE_SRC = join(FIXTURE_DIR, "src");

const WIDTH = 200;
const HEIGHT = 200;
const FPS: import("@hyperframes/core").Fps = { num: 30, den: 1 };
const TRANSPARENT_X = 10; // expected fully transparent
const TRANSPARENT_Y = 10;
const OPAQUE_X = 100; // inside the 50–150 red card
const OPAQUE_Y = 100;

function pixelOffset(x: number, y: number, width: number): number {
  return (y * width + x) * 4;
}

function assertAlphaPixel(
  png: { data: Uint8Array; width: number; height: number },
  x: number,
  y: number,
  expectAlpha: "transparent" | "opaque-red",
  label: string,
): void {
  assert.equal(png.width, WIDTH, `${label}: width mismatch`);
  assert.equal(png.height, HEIGHT, `${label}: height mismatch`);
  const off = pixelOffset(x, y, png.width);
  const r = png.data[off + 0];
  const g = png.data[off + 1];
  const b = png.data[off + 2];
  const a = png.data[off + 3];
  if (expectAlpha === "transparent") {
    assert.equal(
      a,
      0,
      `${label}: pixel (${x},${y}) expected fully transparent (alpha=0), got rgba(${r},${g},${b},${a})`,
    );
  } else {
    assert.equal(
      a,
      255,
      `${label}: pixel (${x},${y}) expected fully opaque (alpha=255), got rgba(${r},${g},${b},${a})`,
    );
    assert.ok(
      typeof r === "number" && r >= 240,
      `${label}: pixel (${x},${y}) expected red >= 240, got rgba(${r},${g},${b},${a})`,
    );
    assert.ok(
      typeof g === "number" && g <= 30,
      `${label}: pixel (${x},${y}) expected green <= 30, got rgba(${r},${g},${b},${a})`,
    );
    assert.ok(
      typeof b === "number" && b <= 30,
      `${label}: pixel (${x},${y}) expected blue <= 30, got rgba(${r},${g},${b},${a})`,
    );
  }
}

async function extractFirstFrameFromWebm(webmPath: string, outPng: string): Promise<void> {
  // VP9 alpha is encoded as a separate intra-frame stream inside the WebM,
  // and ffmpeg's default decoder path silently discards it. Forcing the
  // libvpx-vp9 decoder via `-c:v libvpx-vp9` BEFORE `-i` is what engages
  // the alpha-aware decode — without it the captured transparent pixels
  // come out opaque even when the file was correctly encoded as yuva420p.
  // `-update 1` permits writing a single PNG (no `%d` pattern in the path)
  // and silences the otherwise-noisy ffmpeg warning.
  const result = await runFfmpeg(
    [
      "-y",
      "-c:v",
      "libvpx-vp9",
      "-i",
      webmPath,
      "-frames:v",
      "1",
      "-pix_fmt",
      "rgba",
      "-update",
      "1",
      outPng,
    ],
    { timeout: 60_000 },
  );
  if (!result.success) {
    throw new Error(
      `ffmpeg failed extracting frame 0 from ${webmPath}: ${result.stderr.slice(-400)}`,
    );
  }
}

async function runWebmCheck(workRoot: string): Promise<void> {
  console.log("\n[webm] rendering transparency-regression …");
  const outDir = join(workRoot, "webm");
  mkdirSync(outDir, { recursive: true });
  const outPath = join(outDir, "out.webm");

  const job = createRenderJob({
    fps: FPS,
    quality: "draft",
    format: "webm",
  });

  await executeRenderJob(job, FIXTURE_SRC, outPath);
  assert.equal(job.status, "complete", `webm render did not complete: status=${job.status}`);
  assert.ok(existsSync(outPath), `webm output not written to ${outPath}`);
  const size = (await import("node:fs")).statSync(outPath).size;
  assert.ok(size > 0, `webm output ${outPath} is empty`);
  console.log(`[webm] rendered ${outPath} (${size} bytes)`);

  const framePng = join(outDir, "frame-0.png");
  await extractFirstFrameFromWebm(outPath, framePng);
  const decoded = decodePng(readFileSync(framePng));
  assertAlphaPixel(decoded, TRANSPARENT_X, TRANSPARENT_Y, "transparent", "webm");
  assertAlphaPixel(decoded, OPAQUE_X, OPAQUE_Y, "opaque-red", "webm");
  console.log("[webm] PASS — transparent + opaque-red pixels verified");
}

async function runPngSequenceCheck(workRoot: string): Promise<void> {
  console.log("\n[png-sequence] rendering transparency-regression …");
  const outDir = join(workRoot, "pngs");
  // executeRenderJob mkdirs outputPath itself; deliberately leave it absent.

  const job = createRenderJob({
    fps: FPS,
    quality: "draft",
    format: "png-sequence",
  });

  await executeRenderJob(job, FIXTURE_SRC, outDir);
  assert.equal(
    job.status,
    "complete",
    `png-sequence render did not complete: status=${job.status}`,
  );
  assert.ok(existsSync(outDir), `png-sequence output dir missing: ${outDir}`);

  const frames = readdirSync(outDir)
    .filter((name) => name.startsWith("frame_") && name.endsWith(".png"))
    .sort();
  assert.equal(
    frames.length,
    FPS, // 1 second at 30fps = 30 frames
    `png-sequence expected ${FPS} frames, got ${frames.length}: ${frames.join(",")}`,
  );
  assert.equal(frames[0], "frame_000001.png", "first frame should be frame_000001.png");
  assert.equal(
    frames[frames.length - 1],
    `frame_${String(FPS).padStart(6, "0")}.png`,
    `last frame should be frame_${String(FPS).padStart(6, "0")}.png`,
  );
  console.log(`[png-sequence] wrote ${frames.length} frames to ${outDir}`);

  const firstFrame = frames[0];
  if (!firstFrame) throw new Error("png-sequence: first frame missing");
  const decoded = decodePng(readFileSync(join(outDir, firstFrame)));
  assertAlphaPixel(decoded, TRANSPARENT_X, TRANSPARENT_Y, "transparent", "png-sequence");
  assertAlphaPixel(decoded, OPAQUE_X, OPAQUE_Y, "opaque-red", "png-sequence");
  console.log("[png-sequence] PASS — transparent + opaque-red pixels verified");
}

async function main(): Promise<void> {
  if (!existsSync(FIXTURE_SRC)) {
    throw new Error(`Fixture missing: ${FIXTURE_SRC}`);
  }
  const workRoot = join(tmpdir(), `hf-transparency-${process.pid}-${Date.now()}`);
  mkdirSync(workRoot, { recursive: true });
  const keepWork = process.env.KEEP_TEMP === "1";
  console.log(`work dir: ${workRoot}${keepWork ? " (KEEP_TEMP=1)" : ""}`);

  try {
    await runWebmCheck(workRoot);
    await runPngSequenceCheck(workRoot);
    console.log("\nAll transparency assertions passed.");
  } finally {
    if (!keepWork) {
      try {
        rmSync(workRoot, { recursive: true, force: true });
      } catch {
        // best-effort cleanup
      }
    }
  }
}

main().catch((err) => {
  console.error("\nTransparency regression test FAILED:");
  console.error(err);
  process.exitCode = 1;
});
