/**
 * Verification battery for the sensitive-path / directory-listing hardening
 * (src/lib/http/sensitivePaths.ts + its wiring in src/middleware.ts).
 *
 * Run:  node --import ./scripts/register-next-server-resolve.mjs scripts/verify_sensitive_paths.mjs
 *   (or: npx tsx scripts/verify_sensitive_paths.mjs)
 *
 * Asserts against the REAL implementation (imported pure function + wrapper),
 * not a re-implementation:
 *   - blocked paths   -> isSensitivePath() === true  and blockSensitivePath()
 *                        returns a 404 NextResponse with {error:"Not found"}
 *   - allowed paths   -> isSensitivePath() === false and blockSensitivePath()
 *                        returns undefined (request processing continues)
 *   - encoded bypasses-> blocked by the wrapper's raw-path check
 * Exits 0 on success, 1 on any failure.
 */
import { isSensitivePath, blockSensitivePath } from "../src/lib/http/sensitivePaths.ts";
import { NextRequest } from "next/server";

let failures = 0;
let checks = 0;

function expect(condition, label) {
  checks++;
  if (!condition) {
    failures++;
    console.error(`  FAIL: ${label}`);
  }
}

function describe(label, fn) {
  console.log(`\n${label}`);
  return fn();
}

const BLOCKED_PATHS = [
  // dotfiles / dot-directories
  "/.env",
  "/.env.local",
  "/.env.example",
  "/.git/config",
  "/.gitignore",
  "/.dockerignore",
  "/.npmrc",
  "/.next/BUILD_ID",
  "/.next-corrupt-20260815-2345/foo",
  "/.next-corrupt-20260815-2350/server.js",
  "/api/.env", // dotfile at depth
  // non-web-facing directories
  "/node_modules/x",
  "/node_modules/next/dist/server.js",
  "/drizzle/0000_init.sql",
  "/scripts/generate_demo_images.mjs",
  "/foo/node_modules/x",
  // project / build / config files (root + at depth)
  "/docker-compose.yml",
  "/docker-compose.override.yml",
  "/Dockerfile",
  "/deep/dir/Dockerfile",
  "/build_err.txt",
  "/tsconfig.json",
  "/tsconfig.tsbuildinfo",
  "/next.config.ts",
  "/drizzle.config.ts",
  "/package.json",
  "/package-lock.json",
  "/foo/package.json",
  // markdown documentation
  "/PROJECT.md",
  "/ORIGINAL_REQUEST.md",
  "/PROMPT_GOAL.md",
  "/DESIGN (1).md",
  "/AUTH_SETUP_GUIDE.md",
  "/STRIPE_SETUP_GUIDE.md",
  "/TEST_INFRA.md",
  "/receipt_scanner_to_excel_blueprint.md",
  "/deep/dir/notes.md",
  // sensitive extensions
  "/deep/dir/secret.pem",
  "/deep/dir/private.key",
  "/deep/dir/cert.crt",
  "/deep/dir/app.log",
  // path traversal / malformed input (defensive)
  "/foo/../bar",
  "/foo/./bar",
  "no-leading-slash",
  "",
];

const ALLOWED_PATHS = [
  "/",
  "/de",
  "/en",
  "/dashboard",
  "/api/receipts",
  "/api/receipts/123",
  "/api/auth/login",
  "/api/auth/callback",
  "/showcase/01_synthwave_cyberpunk_sunset.png",
  "/showcase/05_watchmaker_cinematic_photo.jpg",
  "/app-icon.jpg",
  "/app-icon-original.png",
  "/demo/01_aral_tankbeleg_muenchen.jpg",
  "/favicon.ico",
  "/icon.png",
  "/apple-icon.png",
  "/robots.txt",
  "/sitemap.xml",
  "/admin",
  "/admin/users",
  "/admin/waitlist",
  "/admin/system",
  "/terms",
  "/privacy",
  "/impressum",
  "/datenschutz",
  "/agb",
  "/_next/static/chunks/app/layout.js", // excluded by matcher anyway; policy must pass it
];

const ENCODED_CASES = [
  // [url, expectBlocked, note]
  [
    "https://example.com/%2eenv",
    true,
    "encoded leading dot survives URL parsing -> raw %2e check blocks",
  ],
  [
    "https://example.com/%252eenv",
    true,
    "double-encoded dot -> raw %25 check blocks",
  ],
  [
    "https://example.com/%2e%2e/%2eenv",
    true,
    "encoded .. collapses but %2eenv remains -> raw %2e check blocks",
  ],
  [
    "https://example.com/foo%5c..%5c.env",
    true,
    "encoded backslashes survive URL parsing -> raw %5c check blocks",
  ],
  [
    "https://example.com/%2e%2e/%2e%2e/etc/passwd",
    false,
    "URL parser fully normalizes encoded .. -> neutralized upstream (/etc/passwd is not a route)",
  ],
  [
    "https://example.com/foo\\bar",
    false,
    "URL parser normalizes raw backslash to '/' -> neutralized upstream",
  ],
];

/**
 * Wrapper-level subset: every blocked path the wrapper can actually see.
 * Paths with literal `.`/`..` segments and non-"/" inputs are pure-level
 * concerns — NextRequest URL parsing normalizes them away (or the middleware
 * never produces them), so they are asserted separately below.
 */
const WRAPPER_BLOCKED_PATHS = BLOCKED_PATHS.filter(
  (p) =>
    p.startsWith("/") &&
    !p.includes("/../") &&
    !p.includes("/./") &&
    p !== ".."
);

function mockRequest(url) {
  return new NextRequest(url);
}

async function main() {
  await describe("Pure predicate - blocked paths (expect isSensitivePath() === true)", () => {
    for (const p of BLOCKED_PATHS) {
      expect(isSensitivePath(p) === true, `should block ${JSON.stringify(p)}`);
    }
  });

  await describe("Pure predicate - allowed paths (expect isSensitivePath() === false)", () => {
    for (const p of ALLOWED_PATHS) {
      expect(isSensitivePath(p) === false, `should allow ${JSON.stringify(p)}`);
    }
  });

  await describe("Wrapper - blocked paths return 404 {error:'Not found'}", async () => {
    for (const p of WRAPPER_BLOCKED_PATHS) {
      const res = blockSensitivePath(mockRequest(`https://example.com${p}`));
      expect(res !== undefined, `should return a response for ${JSON.stringify(p)}`);
      if (res) {
        expect(res.status === 404, `status 404 for ${JSON.stringify(p)} (got ${res.status})`);
        const body = await res.json().catch(() => null);
        expect(
          body && typeof body === "object" && body.error === "Not found",
          `body {error:'Not found'} for ${JSON.stringify(p)} (got ${JSON.stringify(body)})`
        );
      }
    }
  });

  await describe("Wrapper - allowed paths return undefined (processing continues)", () => {
    for (const p of ALLOWED_PATHS) {
      const res = blockSensitivePath(mockRequest(`https://example.com${p}`));
      expect(res === undefined, `should continue for ${JSON.stringify(p)}`);
    }
  });

  await describe("Wrapper - literal traversal segments are neutralized by URL parsing", () => {
    // NextRequest uses the WHATWG URL parser, which collapses literal
    // `.`/`..` segments and drops non-"/" inputs before middleware code runs.
    // The pure predicate still blocks them if they ever appear (defense
    // against a proxy that forwards un-normalized paths).
    const neutralized = ["/foo/../bar", "/foo/./bar", "no-leading-slash", ""];
    for (const p of neutralized) {
      expect(isSensitivePath(p) === true, `pure predicate still blocks ${JSON.stringify(p)}`);
      const res = blockSensitivePath(mockRequest(`https://example.com${p}`));
      expect(
        res === undefined,
        `wrapper returns undefined for ${JSON.stringify(p)} (URL parser normalized it; router 404s the result)`
      );
    }
  });

  await describe("Wrapper - percent-encoded traversal bypasses", () => {
    for (const [url, expectBlocked, note] of ENCODED_CASES) {
      const req = mockRequest(url);
      // informational: show what nextUrl.pathname looked like after URL parsing
      console.log(`    nextUrl.pathname of ${url} -> ${JSON.stringify(req.nextUrl.pathname)}`);
      const res = blockSensitivePath(req);
      if (expectBlocked) {
        expect(res !== undefined, `should block ${url} (${note})`);
        if (res) expect(res.status === 404, `status 404 for ${url}`);
      } else {
        expect(res === undefined, `neutralized upstream (returns undefined): ${url} (${note})`);
      }
    }
  });
}

main().then(
  () => {
    console.log(`\n${checks} checks, ${failures} failure(s)`);
    if (failures > 0) process.exitCode = 1;
  },
  (err) => {
    console.error("Battery crashed:", err);
    process.exitCode = 1;
  }
);
