import { createContext, type Context, type ProblemDetails } from "@wrnexus/core";

export interface TestRequestOptions extends Omit<RequestInit, "body"> {
  body?: BodyInit | Record<string, unknown> | URLSearchParams | FormData | null;
  baseUrl?: string;
}

/** Build a web-standard Request with convenient JSON/FormData handling. */
export function testRequest(path = "/", options: TestRequestOptions = {}): Request {
  const base = options.baseUrl ?? "http://localhost";
  const headers = new Headers(options.headers);
  let body: BodyInit | null | undefined = options.body as BodyInit | null | undefined;
  if (
    body !== null &&
    body !== undefined &&
    typeof body === "object" &&
    !(body instanceof URLSearchParams) &&
    !(body instanceof FormData) &&
    !(body instanceof Blob) &&
    !(body instanceof ArrayBuffer) &&
    !ArrayBuffer.isView(body) &&
    !(body instanceof ReadableStream)
  ) {
    body = JSON.stringify(body);
    if (!headers.has("content-type")) headers.set("content-type", "application/json");
  }
  const method = options.method ?? (body == null ? "GET" : "POST");
  return new Request(new URL(path, base), { ...options, method, headers, body });
}

/** Create a complete Context suitable for middleware and route unit tests. */
export function testContext(path = "/", options: TestRequestOptions = {}): Context {
  const request = testRequest(path, options);
  return createContext(request, new URL(request.url));
}

export interface JsonResponse<T> {
  response: Response;
  body: T;
}

export async function readJsonResponse<T = unknown>(response: Response): Promise<JsonResponse<T>> {
  return { response, body: (await response.json()) as T };
}

export async function expectProblem(response: Response, status?: number): Promise<ProblemDetails> {
  const contentType = response.headers.get("content-type") ?? "";
  if (!contentType.includes("application/problem+json")) {
    throw new Error(`Expected application/problem+json but received '${contentType || "none"}'.`);
  }
  if (status !== undefined && response.status !== status) {
    throw new Error(`Expected status ${status} but received ${response.status}.`);
  }
  const value = (await response.json()) as ProblemDetails;
  if (!value.title || !Number.isInteger(value.status)) {
    throw new Error("Response body is not RFC 9457 Problem Details.");
  }
  return value;
}

export interface Deferred<T> {
  promise: Promise<T>;
  resolve(value: T | PromiseLike<T>): void;
  reject(reason?: unknown): void;
}

export function deferred<T>(): Deferred<T> {
  let resolve!: Deferred<T>["resolve"];
  let reject!: Deferred<T>["reject"];
  const promise = new Promise<T>((resolvePromise, rejectPromise) => {
    resolve = resolvePromise;
    reject = rejectPromise;
  });
  return { promise, resolve, reject };
}

export interface WaitForOptions {
  timeoutMs?: number;
  intervalMs?: number;
  signal?: AbortSignal;
}

/** Poll a condition without depending on fake timers or a browser runtime. */
export async function waitFor(
  condition: () => boolean | Promise<boolean>,
  options: WaitForOptions = {},
): Promise<void> {
  const timeoutMs = options.timeoutMs ?? 1_000;
  const intervalMs = options.intervalMs ?? 10;
  const startedAt = Date.now();
  while (!(await condition())) {
    if (options.signal?.aborted)
      throw options.signal.reason ?? new DOMException("Aborted", "AbortError");
    if (Date.now() - startedAt >= timeoutMs)
      throw new Error(`waitFor timed out after ${timeoutMs}ms`);
    await new Promise<void>((resolve) => setTimeout(resolve, intervalMs));
  }
}

export class MemoryCookieJar {
  readonly #cookies = new Map<string, string>();

  apply(response: Response): void {
    const value = response.headers.get("set-cookie");
    if (!value) return;
    for (const segment of value.split(/,(?=[^;,]+=)/)) {
      const pair = segment.split(";", 1)[0]?.trim();
      if (!pair) continue;
      const index = pair.indexOf("=");
      if (index <= 0) continue;
      this.#cookies.set(pair.slice(0, index), pair.slice(index + 1));
    }
  }

  header(): string {
    return [...this.#cookies].map(([name, value]) => `${name}=${value}`).join("; ");
  }

  request(path: string, options: TestRequestOptions = {}): Request {
    const headers = new Headers(options.headers);
    const cookie = this.header();
    if (cookie) headers.set("cookie", cookie);
    return testRequest(path, { ...options, headers });
  }

  clear(): void {
    this.#cookies.clear();
  }
}
