/**
 * Request / Upload Size Limits Suite
 *
 * Pure logic — no network, no database. Verifies that the backend refuses
 * oversized requests BEFORE any body is buffered:
 *
 *  1. `contentLengthExceeded` / `guardBodySize` read the Content-Length header
 *     only, so a 2-GB upload never reaches `formData()` / `json()` / `text()`.
 *  2. `readJsonSized` re-measures the serialized body after parsing, which
 *     catches chunked requests without a Content-Length header.
 *  3. The scan route's 413 must fire before any database code runs.
 */

import { describe, test, expect, runAllTests } from "../e2e/runner";
import {
  contentLengthExceeded,
  guardBodySize,
  readJsonSized,
} from "../../src/lib/http/requestSize";
import { MAX_UPLOAD_BYTES, MAX_JSON_BODY_BYTES } from "../../src/lib/limits";
import { issueCsrfToken, CSRF_COOKIE, CSRF_HEADER } from "../../src/lib/auth/csrf";
import { POST as scanPOST } from "../../src/app/api/scan/route";
import type { NextRequest } from "next/server";

const MB = 1024 * 1024;

/** Minimal Request stand-in carrying only the headers the guard reads. */
function headerOnlyRequest(headerName: string, value: string): Request {
  return { headers: new Headers({ [headerName]: value }) } as unknown as Request;
}

/**
 * Builds a scan request that clears the route's CSRF double-submit check so the
 * size guard is actually reached: matching `sr_csrf` cookie + `x-csrf-token`
 * header (origin is absent, which the allow-list accepts for non-browser
 * callers). The size guard runs AFTER the CSRF check and BEFORE any body
 * parsing, so the 413 assertion below exercises exactly that ordering.
 */
function scanRequest(contentLength: string): Request {
  const token = issueCsrfToken();
  return new Request("http://localhost/api/scan", {
    method: "POST",
    headers: {
      "content-length": contentLength,
      cookie: `${CSRF_COOKIE}=${token}`,
      [CSRF_HEADER]: token,
    },
  });
}

describe("Content-Length guard — upload limit (10 MB)", () => {
  test("11 MB content-length is rejected with 413", () => {
    const req = headerOnlyRequest("content-length", String(11 * MB));
    expect(contentLengthExceeded(req, MAX_UPLOAD_BYTES)).toBe(true);
    const res = guardBodySize(req, MAX_UPLOAD_BYTES);
    expect(res).not.toBeNull();
    expect(res!.status).toBe(413);
  });

  test("5 MB content-length passes the guard", () => {
    const req = headerOnlyRequest("content-length", String(5 * MB));
    expect(contentLengthExceeded(req, MAX_UPLOAD_BYTES)).toBe(false);
    expect(guardBodySize(req, MAX_UPLOAD_BYTES)).toBeNull();
  });

  test("exactly 10 MB passes the guard (boundary is exclusive)", () => {
    const req = headerOnlyRequest("content-length", String(MAX_UPLOAD_BYTES));
    expect(contentLengthExceeded(req, MAX_UPLOAD_BYTES)).toBe(false);
    expect(guardBodySize(req, MAX_UPLOAD_BYTES)).toBeNull();
  });

  test("10 MB + 1 byte is rejected", () => {
    const req = headerOnlyRequest("content-length", String(MAX_UPLOAD_BYTES + 1));
    expect(contentLengthExceeded(req, MAX_UPLOAD_BYTES)).toBe(true);
    const res = guardBodySize(req, MAX_UPLOAD_BYTES);
    expect(res).not.toBeNull();
    expect(res!.status).toBe(413);
  });

  test("413 response carries the machine-readable error and limit", async () => {
    const res = guardBodySize(
      headerOnlyRequest("content-length", String(MAX_UPLOAD_BYTES + 1)),
      MAX_UPLOAD_BYTES
    )!;
    const payload = await res.json();
    expect(payload.error).toBe("request_too_large");
    expect(payload.maxBytes).toBe(MAX_UPLOAD_BYTES);
  });

  test("malformed or negative content-length is not treated as exceeded", () => {
    for (const bad of ["abc", "-5", ""]) {
      const req = headerOnlyRequest("content-length", bad);
      expect(contentLengthExceeded(req, MAX_UPLOAD_BYTES)).toBe(false);
      expect(guardBodySize(req, MAX_UPLOAD_BYTES)).toBeNull();
    }
  });

  test("missing content-length header (chunked) passes the header guard", () => {
    const req = { headers: new Headers() } as unknown as Request;
    expect(contentLengthExceeded(req, MAX_UPLOAD_BYTES)).toBe(false);
    expect(guardBodySize(req, MAX_UPLOAD_BYTES)).toBeNull();
  });
});

describe("readJsonSized — JSON body limit (1 MB)", () => {
  test("body larger than 1 MB (serialized length) is rejected with 413", async () => {
    const big = { data: "x".repeat(MAX_JSON_BODY_BYTES + 10) };
    const req = new Request("http://localhost/api/test", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify(big),
    });

    const result = await readJsonSized<unknown>(req, MAX_JSON_BODY_BYTES);
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.response.status).toBe(413);
      const payload = await result.response.json();
      expect(payload.error).toBe("request_too_large");
    }
  });

  test("content-length over 1 MB is rejected before parsing", async () => {
    const req = new Request("http://localhost/api/test", {
      method: "POST",
      headers: { "content-length": String(MAX_JSON_BODY_BYTES + 1) },
    });

    const result = await readJsonSized<unknown>(req, MAX_JSON_BODY_BYTES);
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.response.status).toBe(413);
    }
  });

  test("valid small body returns ok with the parsed body", async () => {
    const req = new Request("http://localhost/api/test", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ email: "timo@example.com", remember: true }),
    });

    const result = await readJsonSized<{ email: string; remember: boolean }>(
      req,
      MAX_JSON_BODY_BYTES
    );
    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.body).toEqual({ email: "timo@example.com", remember: true });
    }
  });

  test("broken JSON returns 400 invalid_json", async () => {
    const req = new Request("http://localhost/api/test", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: "{this is not json",
    });

    const result = await readJsonSized<unknown>(req, MAX_JSON_BODY_BYTES);
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.response.status).toBe(400);
      const payload = await result.response.json();
      expect(payload.error).toBe("invalid_json");
    }
  });
});

describe("Scan route — oversized upload rejected before body/DB access", () => {
  test("POST /api/scan with content-length > 10 MB and no body returns 413", async () => {
    const req = scanRequest(String(MAX_UPLOAD_BYTES + 1));

    // The guard runs before formData()/DB: a 413 here proves the route never
    // tried to buffer the (absent) body and never touched the database.
    const res = await scanPOST(req as unknown as NextRequest);
    expect(res.status).toBe(413);
    const payload = await res.json();
    expect(payload.error).toBe("request_too_large");
  });

  test("POST /api/scan exactly at the 10 MB boundary is not rejected by the header guard", async () => {
    const req = scanRequest(String(MAX_UPLOAD_BYTES));

    const res = await scanPOST(req as unknown as NextRequest);
    // With no multipart body the route must NOT answer 413 — it fails later,
    // inside the formData/validation path (500), which proves the guard did not
    // over-trigger at the exact boundary.
    expect(res.status).not.toBe(413);
  });
});

if (typeof require !== "undefined" && require.main === module) {
  runAllTests()
    .then((ok) => process.exit(ok ? 0 : 1))
    .catch((err) => {
      console.error("Fatal runner crash:", err);
      process.exit(1);
    });
}
