import { createHash } from "node:crypto";
import {
  copyFileSync,
  existsSync,
  mkdirSync,
  mkdtempSync,
  readFileSync,
  readdirSync,
  rmSync,
  statSync,
  writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { basename, dirname, join, relative, resolve } from "node:path";

import { afterEach, describe, expect, it } from "vitest";

import { compileAssetArchive } from "../../packages/asset-compiler/src/index.js";

const temporaryRoots: string[] = [];

afterEach(() => {
  for (const root of temporaryRoots.splice(0)) rmSync(root, { force: true, recursive: true });
});

function json(path: string, value: unknown) {
  mkdirSync(dirname(path), { recursive: true });
  writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`);
}

function csv(path: string, rows: Array<Record<string, string>>) {
  const headers = [...new Set(rows.flatMap((row) => Object.keys(row)))];
  const encode = (value: string) => `"${value.replaceAll('"', '""')}"`;
  mkdirSync(dirname(path), { recursive: true });
  writeFileSync(path, `${headers.map(encode).join(",")}\n${rows.map((row) => headers.map((header) => encode(row[header] ?? "")).join(",")).join("\n")}\n`);
}

function hash(path: string) {
  return createHash("sha256").update(readFileSync(path)).digest("hex").toUpperCase();
}

function filesBelow(root: string): string[] {
  return readdirSync(root, { withFileTypes: true }).flatMap((entry) => {
    const path = join(root, entry.name);
    return entry.isDirectory() ? filesBelow(path) : [path];
  });
}

function snapshot(roots: string[]) {
  return roots.flatMap((root) => filesBelow(root).map((path) => {
    const stats = statSync(path);
    return { path: `${basename(root)}/${relative(root, path).replaceAll("\\", "/")}`, sha256: hash(path), size: stats.size, mtime_ms: stats.mtimeMs };
  })).sort((left, right) => left.path.localeCompare(right.path));
}

interface Fixture {
  evidenceTrap: string;
  handoffRoot: string;
  manifestPath: string;
  outputRoot: string;
  root: string;
  sourceRoot: string;
}

function createFixture(): Fixture {
  const root = mkdtempSync(join(tmpdir(), "dada-wp5-01-"));
  temporaryRoots.push(root);
  const handoffRoot = join(root, "handoff");
  const sourceRoot = join(root, "sources");
  const outputRoot = join(root, "output");
  const evidenceTrap = join(root, "evidence", "executed.txt");
  mkdirSync(handoffRoot, { recursive: true });
  writeFileSync(join(handoffRoot, "HANDOFF.md"), "fixture handoff\n");
  json(join(handoffRoot, "validation.json"), { all_valid: true, schema_version: 1 });

  const textRoot = join(sourceRoot, "text");
  const fixtureFontBytes = Buffer.from("dada-test-font-package");
  const fixtureFontSha256 = createHash("sha256").update(fixtureFontBytes).digest("hex").toUpperCase();
  csv(join(textRoot, "flower", "catalog.csv"), [{
    canonical_dir: "templates/FLOWER001", canonical_id: "FLOWER001", category: "flower", default_text: "春日,计划",
    display_name: "春日,计划", family: "text_template", font_paths: "fonts/original.ztf", preview_path: "preview.png",
    resource_class: "zip_template",
  }]);
  json(join(textRoot, "flower", "templates", "FLOWER001", "metadata.json"), {
    canonical_id: "FLOWER001", category: "flower", default_text: "春日,计划", display_name: "春日,计划",
    dynamic_keys: [], family: "text_template",
    files: { fonts: ["fonts/original.ztf"], layer: "layer.pb", package: "package", preview: "preview.png" },
    resource_class: "zip_template", runtime: { editable_text: true }, schema_version: 1,
    source: { archive: join(root, "evidence", "historical") },
  });
  mkdirSync(join(textRoot, "flower", "templates", "FLOWER001", "fonts"), { recursive: true });
  writeFileSync(join(textRoot, "flower", "templates", "FLOWER001", "fonts", "original.ztf"), fixtureFontBytes);
  writeFileSync(join(textRoot, "unreferenced-source.bin"), Buffer.alloc(128 * 1024, 7));

  const fontRoot = join(sourceRoot, "fonts");
  const fontDirectory = join(fontRoot, "resources", "font_packages", "FONT001_Test");
  csv(join(fontRoot, "reports", "font_panel_catalog.csv"), [{
    candidate_id: "FONT001", display_name: "Test Font", font_family: "Dada Test", local_sha256: fixtureFontSha256,
    panel_order: "1", resource_dir: fontDirectory, resource_status: "verified_extracted",
  }]);
  json(join(fontDirectory, "metadata.json"), {
    candidate_id: "FONT001", display_name: "Test Font", font_family: "Dada Test", font_file_count: 1,
    local_sha256: fixtureFontSha256, panel_order: 1, resource_status: "verified_extracted",
  });

  const colorRoot = join(sourceRoot, "colors");
  csv(join(colorRoot, "catalog.csv"), [{
    canonical_dir: "styles/COLOR001", canonical_id: "COLOR001", category: "color", default_text: "",
    display_name: "style_01", family: "color_card", preview_path: "preview.png", resource_class: "parameter_renderer",
  }]);
  json(join(colorRoot, "styles", "COLOR001", "metadata.json"), {
    canonical_id: "COLOR001", category: "color", default_text: "", display_name: "style_01", dynamic_keys: [],
    family: "color_card", files: { preview: "preview.png", renderer_source: "renderer_source.py" },
    resource_class: "parameter_renderer", runtime: { stable_style_id: "style_01" }, schema_version: 1,
  });

  const dynamicRoot = join(sourceRoot, "dynamic");
  csv(join(dynamicRoot, "catalog.csv"), [{
    canonical_dir: "templates/DYN001", canonical_id: "DYN001", category: "location", default_text: "",
    display_name: "location", dynamic_keys: "title", family: "interactive_sticker", resource_class: "dynamic_resource",
  }]);
  const dynamicDirectory = join(dynamicRoot, "templates", "DYN001");
  json(join(dynamicDirectory, "metadata.json"), {
    canonical_id: "DYN001", category: "location", default_text: "", display_name: "location", dynamic_keys: ["title"],
    family: "interactive_sticker", files: { lua: ["resource/trap.lua"], prefab: ["resource/trap.prefab"] },
    resource_class: "dynamic_resource", runtime: { has_dynamic_binding: true }, schema_version: 1,
  });
  mkdirSync(join(dynamicDirectory, "resource"), { recursive: true });
  writeFileSync(join(dynamicDirectory, "resource", "trap.lua"), `io.open([[${evidenceTrap}]], "w")`);
  writeFileSync(join(dynamicDirectory, "resource", "trap.prefab"), "must remain conversion evidence only");

  const manifestPath = join(handoffRoot, "sticker_web_catalog_manifest.json");
  json(manifestPath, {
    collections: [
      { catalogs: { flower: relative(textRoot, join(textRoot, "flower", "catalog.csv")) }, id: "text_templates", root: relative(handoffRoot, textRoot) },
      { catalog: relative(fontRoot, join(fontRoot, "reports", "font_panel_catalog.csv")), id: "font_panel", root: relative(handoffRoot, fontRoot) },
      { catalog: "catalog.csv", id: "color_cards", root: relative(handoffRoot, colorRoot) },
      { catalog: "catalog.csv", id: "interactive_stickers", root: relative(handoffRoot, dynamicRoot) },
    ],
    schema_version: 1,
    validation: "validation.json",
    web_handoff: "HANDOFF.md",
  });
  return { evidenceTrap, handoffRoot, manifestPath, outputRoot, root, sourceRoot };
}

describe("TDD-WP5-MAN-001 readonly asset compiler", () => {
  it("produces declarative, redacted, content-addressed output without changing or copying source", () => {
    const fixture = createFixture();
    const before = snapshot([fixture.handoffRoot, fixture.sourceRoot]);
    const result = compileAssetArchive({ manifestPath: fixture.manifestPath, outputDirectory: fixture.outputRoot, releaseVersion: "fixture-v1" });
    const after = snapshot([fixture.handoffRoot, fixture.sourceRoot]);

    expect(after).toEqual(before);
    expect(existsSync(fixture.evidenceTrap)).toBe(false);
    expect(result.report).toMatchObject({ copied_source_files: 0, executed_source_files: 0, source_mutations: 0 });

    const manifestText = readFileSync(join(fixture.outputRoot, "output-manifest.json"), "utf8");
    const outputManifest = JSON.parse(manifestText) as { items: Array<Record<string, unknown>>; schema_version: string };
    expect(outputManifest.schema_version).toBe("asset-release-compiler/v1");
    expect(outputManifest.items.map((item) => item.canonical_id)).toEqual(["COLOR001", "DYN001", "FLOWER001", "FONT001"]);
    expect(outputManifest.items.every((item) => item.release_status === "imported" && item.validation_status === "metadata_validated")).toBe(true);

    const allOutput = filesBelow(fixture.outputRoot);
    expect(allOutput.every((path) => path.endsWith(".json"))).toBe(true);
    expect(allOutput.some((path) => path.endsWith(".lua") || path.endsWith(".prefab") || path.endsWith(".bin"))).toBe(false);
    const serializedOutput = allOutput.map((path) => readFileSync(path, "utf8")).join("\n");
    expect(serializedOutput).not.toContain(resolve(fixture.root));
    expect(serializedOutput).not.toContain("file://");
    expect(serializedOutput).not.toContain("historical");
    expect(serializedOutput).not.toContain("unreferenced-source.bin");

    const evidenceDirectory = process.env.DADA_EVIDENCE_DIR_ASSET_COMPILER;
    if (evidenceDirectory) {
      mkdirSync(evidenceDirectory, { recursive: true });
      for (const file of ["compiler-report.json", "source-before.json", "source-after.json", "output-manifest.json"]) {
        copyFileSync(join(fixture.outputRoot, file), join(evidenceDirectory, file));
      }
    }
  });

  it("is reproducible and reuses existing derived JSON by content hash", () => {
    const fixture = createFixture();
    const secondOutput = join(fixture.root, "second-output");
    compileAssetArchive({ manifestPath: fixture.manifestPath, outputDirectory: fixture.outputRoot, releaseVersion: "fixture-v1" });
    compileAssetArchive({ manifestPath: fixture.manifestPath, outputDirectory: secondOutput, releaseVersion: "fixture-v1" });
    expect(readFileSync(join(fixture.outputRoot, "output-manifest.json"), "utf8")).toBe(readFileSync(join(secondOutput, "output-manifest.json"), "utf8"));

    const repeated = compileAssetArchive({ manifestPath: fixture.manifestPath, outputDirectory: fixture.outputRoot, releaseVersion: "fixture-v1" });
    expect(repeated.report.derived_files).toEqual({ created: 0, reused: 4, total: 4 });
  });

  it("safely relocates legacy absolute font package paths after an archive move", () => {
    const fixture = createFixture();
    const catalogPath = join(fixture.sourceRoot, "fonts", "reports", "font_panel_catalog.csv");
    const metadata = JSON.parse(readFileSync(join(fixture.sourceRoot, "fonts", "resources", "font_packages", "FONT001_Test", "metadata.json"), "utf8")) as { local_sha256: string };
    csv(catalogPath, [{
      candidate_id: "FONT001",
      display_name: "Test Font",
      font_family: "Dada Test",
      local_sha256: metadata.local_sha256,
      panel_order: "1",
      resource_dir: "C:/Users/legacy/Desktop/sticker_text/fonts/resources/font_packages/FONT001_Test",
      resource_status: "verified_extracted",
    }]);

    expect(() => compileAssetArchive({
      manifestPath: fixture.manifestPath,
      outputDirectory: fixture.outputRoot,
      releaseVersion: "fixture-v1",
    })).not.toThrow();
  });

  it("rejects evidence collections, traversal and output inside a source root", () => {
    const fixture = createFixture();
    const manifest = JSON.parse(readFileSync(fixture.manifestPath, "utf8")) as { collections: unknown[] };
    manifest.collections.push({ catalog: "../evidence/catalog.csv", id: "evidence", root: "../evidence" });
    json(fixture.manifestPath, manifest);
    expect(() => compileAssetArchive({ manifestPath: fixture.manifestPath, outputDirectory: fixture.outputRoot, releaseVersion: "fixture-v1" })).toThrow(/unsupported collection/i);

    const clean = createFixture();
    const catalogPath = join(clean.sourceRoot, "dynamic", "catalog.csv");
    const row = { canonical_dir: "../../evidence", canonical_id: "DYN001", category: "location", default_text: "", display_name: "location", dynamic_keys: "title", family: "interactive_sticker", resource_class: "dynamic_resource" };
    csv(catalogPath, [row]);
    expect(() => compileAssetArchive({ manifestPath: clean.manifestPath, outputDirectory: clean.outputRoot, releaseVersion: "fixture-v1" })).toThrow(/unsafe relative path|outside.*source root/i);

    const nested = createFixture();
    expect(() => compileAssetArchive({ manifestPath: nested.manifestPath, outputDirectory: join(nested.sourceRoot, "text", "derived"), releaseVersion: "fixture-v1" })).toThrow(/output.*source root/i);
  });
});
