import { createHash, randomUUID } from "node:crypto";
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";

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

import { createApp } from "../../apps/api/src/app.js";
import {
  createPublicAssetResolver,
  validateReadOnlyAssetRoot,
} from "../../apps/api/src/local-data-root.js";

const temporaryDirectories: string[] = [];

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 resource boundary", () => {
  it("serves a manifest resource by stable ID without exposing the absolute root or object key", async () => {
    const base = mkdtempSync(join(tmpdir(), "dada-wp0-04-api-"));
    temporaryDirectories.push(base);
    const assetRoot = join(base, "read-only-assets");
    const relativePath = "images/source.png";
    const bytes = Buffer.from("synthetic png fixture");
    const assetId = "STK001";
    mkdirSync(join(assetRoot, "images"), { recursive: true });
    writeFileSync(join(assetRoot, relativePath), bytes);
    const manifest = JSON.stringify({ assets: [{ asset_id: assetId, relative_path: relativePath }] });
    writeFileSync(join(assetRoot, "catalog.json"), manifest);
    const validatedRoot = validateReadOnlyAssetRoot({
      dataRoot: join(base, "data"),
      expectedSha256: createHash("sha256").update(manifest).digest("hex"),
      manifestRelativePath: "catalog.json",
      root: assetRoot,
      rootRef: "fixture_assets",
    });
    if (!validatedRoot.ok) throw new Error(validatedRoot.reason);
    const publicAssets = createPublicAssetResolver({
      entries: [{
        assetId,
        mimeType: "image/png",
        relativePath,
        resourceVersion: "fixture-v1",
        rootRef: "fixture_assets",
        sha256: createHash("sha256").update(bytes).digest("hex"),
      }],
      roots: [validatedRoot],
    });
    const app = await createApp({ browserGate: false, publicAssets });

    try {
      const resourceUrl = `/api/v1/assets/public/fixture-v1/${assetId}`;
      const response = await app.inject({
        headers: { host: "127.0.0.1:43121" },
        method: "GET",
        url: resourceUrl,
      });
      expect(response.statusCode).toBe(200);
      expect(response.headers["content-type"]).toContain("image/png");
      expect(response.rawPayload).toEqual(bytes);
      const wrongVersion = await app.inject({
        headers: { host: "127.0.0.1:43121" },
        method: "GET",
        url: `/api/v1/assets/public/other-version/${assetId}`,
      });
      expect(wrongVersion.statusCode).toBe(404);
      const browserVisible = JSON.stringify({
        headers: response.headers,
        resource_id: assetId,
        url: resourceUrl,
      });
      expect(browserVisible).not.toContain(assetRoot);
      expect(browserVisible).not.toContain(relativePath);
      expect(browserVisible).not.toMatch(/[A-Za-z]:\\/);

      const evidenceDirectory = process.env.DADA_EVIDENCE_DIR_DATA_ROOT;
      if (evidenceDirectory) {
        mkdirSync(evidenceDirectory, { recursive: true });
        writeFileSync(join(evidenceDirectory, "response.json"), `${JSON.stringify({
          content_type: response.headers["content-type"],
          path_exposed: false,
          resource_id: assetId,
          status: "passed",
        }, null, 2)}\n`);
      }
    } finally {
      await app.close();
    }
  });

  it("does not resolve unknown IDs or path-like route parameters", async () => {
    const app = await createApp({ browserGate: false, publicAssets: createPublicAssetResolver({ entries: [], roots: [] }) });
    try {
      for (const target of [randomUUID(), "..%2F..%2Fsecret", "C:%5CUsers%5Csecret"]) {
        const response = await app.inject({
          headers: { host: "127.0.0.1:43121" },
          method: "GET",
          url: `/api/v1/assets/public/fixture-v1/${target}`,
        });
        expect(response.statusCode).toBe(404);
        expect(response.body).not.toMatch(/[A-Za-z]:\\/);
      }
    } finally {
      await app.close();
    }
  });
});

describe("TDD-WP0-DATA-002-no-backup-migration route inventory", () => {
  it("contains no product backup, archive, business import, migration or recovery route", async () => {
    const app = await createApp({ browserGate: false });
    try {
      await app.ready();
      const routes = Object.keys(app.swagger().paths ?? {}).map((route) => route.toLowerCase());
      for (const forbidden of ["backup", "archive", "project-package", "business-import", "migration", "recovery"]) {
        expect(routes.some((route) => route.includes(forbidden))).toBe(false);
      }
      expect(routes.filter((route) => route.includes("restore"))).toEqual(["/api/v1/projects/{projectid}/restore"]);
      const evidenceDirectory = process.env.DADA_EVIDENCE_DIR_NO_TRANSFER;
      if (evidenceDirectory) {
        mkdirSync(evidenceDirectory, { recursive: true });
        writeFileSync(join(evidenceDirectory, "route-inventory.json"), `${JSON.stringify({
          forbidden_routes: [],
          status: "passed",
        }, null, 2)}\n`);
      }
    } finally {
      await app.close();
    }
  });
});
