import { createHash } from "node:crypto";
import {
  existsSync,
  lstatSync,
  mkdirSync,
  mkdtempSync,
  readFileSync,
  readdirSync,
  rmSync,
  symlinkSync,
  writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { createRequire } from "node:module";

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

import {
  DATA_TRANSFER_POLICY,
  defaultInstanceConfigPath,
  defaultLocalDataRoot,
  initializeLocalDataRoot,
  inspectInitializedLocalDataRoot,
  readConfiguredAssetRoot,
  resolvePathWithinRoot,
  validateLocalDataRoot,
  validateReadOnlyAssetRoot,
  type LocalDataRootBoundaries,
} from "../../apps/api/src/local-data-root.js";

const requireFromApi = createRequire(new URL("../../apps/api/package.json", import.meta.url));
const Database = requireFromApi("better-sqlite3");
const temporaryDirectories: string[] = [];

function temporaryDirectory() {
  const directory = mkdtempSync(join(tmpdir(), "dada-wp0-04-"));
  temporaryDirectories.push(directory);
  return directory;
}

function fixture() {
  const base = temporaryDirectory();
  const projectRoot = join(base, "project");
  const downloadsRoot = join(base, "Downloads");
  const assetRoot = join(base, "read-only-assets");
  for (const directory of [projectRoot, downloadsRoot, assetRoot]) mkdirSync(directory);
  const boundaries: LocalDataRootBoundaries = {
    downloadsRoot,
    programRoot: process.cwd(),
    projectRoots: [projectRoot],
    readOnlyAssetRoots: [assetRoot],
    repositoryRoot: process.cwd(),
  };
  return { assetRoot, base, boundaries, downloadsRoot, projectRoot };
}

function snapshot(directory: string): string[] {
  if (!existsSync(directory)) return [];
  return readdirSync(directory, { recursive: true }).map(String).sort();
}

afterEach(() => {
  for (const directory of temporaryDirectories.splice(0)) {
    if (resolve(directory).startsWith(resolve(tmpdir()))) rmSync(directory, { force: true, recursive: true });
  }
});

describe("TDD-WP0-DATA-001-root-validation", () => {
  it("reads only an absolute configured read-only asset root", () => {
    const base = temporaryDirectory();
    const configFile = join(base, "instance.json");
    const assetRoot = join(base, "runtime-assets");
    writeFileSync(configFile, JSON.stringify({ asset_root: assetRoot }));

    expect(readConfiguredAssetRoot(configFile)).toBe(resolve(assetRoot));
    writeFileSync(configFile, JSON.stringify({ asset_root: "relative-assets" }));
    expect(() => readConfiguredAssetRoot(configFile)).toThrow("asset_root_configuration_invalid");
  });

  it("derives default data and configuration paths from LOCALAPPDATA without a hardcoded user", () => {
    const localAppData = join(temporaryDirectory(), "LocalAppData");
    expect(defaultLocalDataRoot({ LOCALAPPDATA: localAppData })).toBe(join(localAppData, "Dada", "P0A", "data"));
    expect(defaultInstanceConfigPath({ LOCALAPPDATA: localAppData })).toBe(
      join(localAppData, "Dada", "P0A", "config", "instance.json"),
    );
    expect(() => defaultLocalDataRoot({})).toThrow("local_app_data_unavailable");
  });

  it("rejects repository, project, Downloads, read-only asset, junction and escaped paths without writes", () => {
    const { assetRoot, base, boundaries, downloadsRoot, projectRoot } = fixture();
    const outside = join(base, "junction-target");
    const junction = join(base, "junction-root");
    mkdirSync(outside);
    symlinkSync(outside, junction, "junction");
    expect(lstatSync(junction).isSymbolicLink()).toBe(true);

    const cases = [
      { path: join(process.cwd(), ".dada-local", "forbidden"), reason: "repository_root" },
      { path: join(projectRoot, "data"), reason: "project_root" },
      { path: join(downloadsRoot, "data"), reason: "downloads_root" },
      { path: join(assetRoot, "data"), reason: "read_only_asset_root" },
      { path: join(junction, "data"), reason: "symbolic_link" },
    ] as const;
    const before = snapshot(base);
    for (const [index, candidate] of cases.entries()) {
      expect(validateLocalDataRoot(candidate.path, boundaries)).toEqual({
        ok: false,
        reason: candidate.reason,
      });
      expect(() => initializeLocalDataRoot({
        boundaries,
        configFile: join(base, `invalid-config-${index}`, "instance.json"),
        dataRoot: candidate.path,
      })).toThrow(candidate.reason);
    }
    expect(() => resolvePathWithinRoot(join(base, "safe"), "../escaped/file.png")).toThrow("path_escape");
    const after = snapshot(base);
    expect(after).toEqual(before);
    const evidenceDirectory = process.env.DADA_EVIDENCE_DIR_DATA_ROOT;
    if (evidenceDirectory) {
      mkdirSync(evidenceDirectory, { recursive: true });
      writeFileSync(join(evidenceDirectory, "fs-before.json"), `${JSON.stringify({ entries: before }, null, 2)}\n`);
    }
  });

  it("creates the fixed layout and SQLite only during explicit initialization", () => {
    const { base, boundaries } = fixture();
    const dataRoot = join(base, "compliant-data");
    const configFile = join(base, "config", "instance.json");
    const initialized = initializeLocalDataRoot({ boundaries, configFile, dataRoot });

    expect(initialized.status).toBe("ready");
    for (const relativePath of [
      "db/dada.sqlite3",
      "content/references",
      "content/generated",
      "content/exports",
      "managed-assets",
      "derived-assets",
      "staging",
      "logs/api",
      "logs/worker",
      "logs/supervisor",
    ]) {
      expect(existsSync(join(dataRoot, relativePath))).toBe(true);
    }

    const database = new Database(join(dataRoot, "db", "dada.sqlite3"), { readonly: true });
    expect(database.prepare("select schema_version from instance_metadata").get()).toEqual({ schema_version: 1 });
    database.close();
    expect(JSON.parse(readFileSync(configFile, "utf8"))).toMatchObject({ initialized: true, schema_version: 1 });
    const evidenceDirectory = process.env.DADA_EVIDENCE_DIR_DATA_ROOT;
    if (evidenceDirectory) {
      mkdirSync(evidenceDirectory, { recursive: true });
      writeFileSync(join(evidenceDirectory, "config-result.json"), `${JSON.stringify({
        database: "db/dada.sqlite3",
        fixed_layout: true,
        path_exposed: false,
        status: initialized.status,
      }, null, 2)}\n`);
    }
  });

  it("reports data_missing without recreating a deleted initialized root or database", () => {
    const { base, boundaries } = fixture();
    const dataRoot = join(base, "initialized-data");
    const configFile = join(base, "config", "instance.json");
    initializeLocalDataRoot({ boundaries, configFile, dataRoot });
    rmSync(join(dataRoot, "db", "dada.sqlite3"));

    expect(inspectInitializedLocalDataRoot({ boundaries, configFile })).toMatchObject({
      database_exists: false,
      root_exists: true,
      status: "data_missing",
    });
    expect(existsSync(join(dataRoot, "db", "dada.sqlite3"))).toBe(false);

    rmSync(dataRoot, { recursive: true });
    expect(inspectInitializedLocalDataRoot({ boundaries, configFile })).toMatchObject({
      database_exists: false,
      root_exists: false,
      status: "data_missing",
    });
    expect(existsSync(dataRoot)).toBe(false);
    const evidenceDirectory = process.env.DADA_EVIDENCE_DIR_DATA_ROOT;
    if (evidenceDirectory) {
      mkdirSync(evidenceDirectory, { recursive: true });
      writeFileSync(join(evidenceDirectory, "fs-after.json"), `${JSON.stringify({
        database_recreated: false,
        root_recreated: false,
        status: "data_missing",
      }, null, 2)}\n`);
    }
  });

  it("validates a read-only manifest by hash without copying or writing its source root", () => {
    const { assetRoot, base } = fixture();
    const manifest = join(assetRoot, "catalog.json");
    writeFileSync(manifest, "{\"assets\":[]}\n");
    const expectedSha256 = createHash("sha256").update(readFileSync(manifest)).digest("hex");
    const before = snapshot(assetRoot);

    expect(validateReadOnlyAssetRoot({
      dataRoot: join(base, "data"),
      expectedSha256,
      manifestRelativePath: "catalog.json",
      root: assetRoot,
      rootRef: "fixture_assets",
    })).toMatchObject({ ok: true, root_ref: "fixture_assets" });
    expect(snapshot(assetRoot)).toEqual(before);
  });
});

describe("TDD-WP0-DATA-002-no-backup-migration", () => {
  it("does not recover deleted data or treat retained downloads as migration input", () => {
    const { base, boundaries } = fixture();
    const dataRoot = join(base, "instance-a");
    const configFile = join(base, "config-a", "instance.json");
    initializeLocalDataRoot({ boundaries, configFile, dataRoot });
    const businessMarker = join(dataRoot, "content", "generated", "private-record.json");
    mkdirSync(dirname(businessMarker), { recursive: true });
    writeFileSync(businessMarker, "private business data");
    const retainedDownload = join(base, "user-downloads", "finished.png");
    mkdirSync(dirname(retainedDownload));
    writeFileSync(retainedDownload, "downloaded image");

    rmSync(dataRoot, { recursive: true });
    expect(inspectInitializedLocalDataRoot({ boundaries, configFile }).status).toBe("data_missing");
    expect(existsSync(dataRoot)).toBe(false);
    expect(readFileSync(retainedDownload, "utf8")).toBe("downloaded image");

    const replacementRoot = join(base, "instance-b");
    initializeLocalDataRoot({
      boundaries,
      configFile: join(base, "config-b", "instance.json"),
      dataRoot: replacementRoot,
    });
    expect(existsSync(join(replacementRoot, "content", "generated", "private-record.json"))).toBe(false);
    expect(DATA_TRANSFER_POLICY).toEqual({
      allowed_downloads: ["original_generation", "jpg", "png"],
      application_backup: false,
      business_import: false,
      editable_project_archive: false,
      p0b_migration: false,
      recovery: false,
    });
  });
});
