import { execSync } from "node:child_process";
import { existsSync } from "node:fs";
import { afterAll, beforeAll, describe, expect, it } from "vitest";

/**
 * Auth integration tests against wrangler dev.
 *
 * These verify that the Worker's auth endpoints respond correctly
 * without hitting GitHub OAuth. They catch:
 * - Drizzle schema / D1 mismatches
 * - Better Auth adapter misconfiguration
 * - CORS / routing regressions
 * - Input validation (callbackURL, port, nonce)
 *
 * Requires cloudflare/.dev.vars with GitHub OAuth credentials.
 * Skipped in CI where .dev.vars is not available.
 */

const HAS_DEV_VARS = existsSync("cloudflare/.dev.vars");
const describeAuth = HAS_DEV_VARS ? describe : describe.skip;

const WORKER_URL = "http://localhost:8787";
let wranglerProcess: ReturnType<typeof import("node:child_process").spawn>;

async function waitForWorker(url: string, timeout = 15_000) {
  const start = Date.now();
  while (Date.now() - start < timeout) {
    try {
      const res = await fetch(`${url}/api/replays`);
      if (res.ok) return;
    } catch {
      // not ready yet
    }
    await new Promise((r) => setTimeout(r, 500));
  }
  throw new Error(`Worker not ready after ${timeout}ms`);
}

describeAuth("Auth Worker E2E", () => {
  beforeAll(async () => {
    // Ensure local D1 has the schema (uses Drizzle-managed migrations)
    execSync("pnpm db:migrate:local", {
      cwd: "cloudflare",
      stdio: "pipe",
    });

    // Start wrangler dev in background
    const { spawn } = await import("node:child_process");
    wranglerProcess = spawn("pnpm", ["wrangler", "dev", "--port", "8787"], {
      cwd: "cloudflare",
      stdio: "pipe",
      detached: false,
    });

    await waitForWorker(WORKER_URL);
  }, 20_000);

  afterAll(() => {
    wranglerProcess?.kill();
  });

  // -----------------------------------------------------------------------
  // Existing API not broken
  // -----------------------------------------------------------------------

  it("GET /api/replays still works", async () => {
    const res = await fetch(`${WORKER_URL}/api/replays`);
    expect(res.status).toBe(200);
    const data = await res.json();
    expect(Array.isArray(data)).toBe(true);
  });

  // -----------------------------------------------------------------------
  // Auth session endpoint
  // -----------------------------------------------------------------------

  it("GET /api/auth/get-session returns empty when not logged in", async () => {
    const res = await fetch(`${WORKER_URL}/api/auth/get-session`);
    expect(res.status).toBe(200);
    const data = (await res.json()) as any;
    // Better Auth returns null body or object without session when not logged in
    expect(data?.session?.token).toBeFalsy();
  });

  // -----------------------------------------------------------------------
  // Sign-in endpoint
  // -----------------------------------------------------------------------

  it("POST /api/auth/sign-in/social returns GitHub OAuth URL", async () => {
    const res = await fetch(`${WORKER_URL}/api/auth/sign-in/social`, {
      method: "POST",
      headers: { "Content-Type": "application/json", Origin: WORKER_URL },
      body: JSON.stringify({ provider: "github", callbackURL: "/" }),
    });
    expect(res.status).toBe(200);
    const data = (await res.json()) as any;
    expect(data.url).toContain("github.com/login/oauth/authorize");
    expect(data.url).toContain("client_id=");
    expect(data.url).toContain("code_challenge=");
  });

  // -----------------------------------------------------------------------
  // callbackURL validation
  // -----------------------------------------------------------------------

  it("rejects absolute callbackURL", async () => {
    const res = await fetch(`${WORKER_URL}/api/auth/sign-in/social`, {
      method: "POST",
      headers: { "Content-Type": "application/json", Origin: WORKER_URL },
      body: JSON.stringify({ provider: "github", callbackURL: "https://evil.com" }),
    });
    expect(res.status).toBe(400);
    const data = (await res.json()) as any;
    expect(data.error).toContain("Invalid callbackURL");
  });

  it("rejects protocol-relative callbackURL", async () => {
    const res = await fetch(`${WORKER_URL}/api/auth/sign-in/social`, {
      method: "POST",
      headers: { "Content-Type": "application/json", Origin: WORKER_URL },
      body: JSON.stringify({ provider: "github", callbackURL: "//evil.com" }),
    });
    expect(res.status).toBe(400);
  });

  it("accepts relative callbackURL", async () => {
    const res = await fetch(`${WORKER_URL}/api/auth/sign-in/social`, {
      method: "POST",
      headers: { "Content-Type": "application/json", Origin: WORKER_URL },
      body: JSON.stringify({ provider: "github", callbackURL: "/dashboard" }),
    });
    expect(res.status).toBe(200);
  });

  // -----------------------------------------------------------------------
  // CLI login endpoint validation
  // -----------------------------------------------------------------------

  it("cli-login rejects missing port", async () => {
    const res = await fetch(`${WORKER_URL}/auth/cli-login`);
    expect(res.status).toBe(400);
  });

  it("cli-login rejects non-numeric port", async () => {
    const res = await fetch(
      `${WORKER_URL}/auth/cli-login?port=abc&nonce=00000000-0000-0000-0000-000000000000`,
    );
    expect(res.status).toBe(400);
  });

  it("cli-login rejects port below 1024", async () => {
    const res = await fetch(
      `${WORKER_URL}/auth/cli-login?port=80&nonce=00000000-0000-0000-0000-000000000000`,
    );
    expect(res.status).toBe(400);
  });

  it("cli-login rejects missing nonce", async () => {
    const res = await fetch(`${WORKER_URL}/auth/cli-login?port=3000`);
    expect(res.status).toBe(400);
  });

  it("cli-login accepts valid port and nonce", async () => {
    const res = await fetch(
      `${WORKER_URL}/auth/cli-login?port=3000&nonce=a1b2c3d4-e5f6-7890-abcd-ef1234567890`,
    );
    expect(res.status).toBe(200);
    const html = await res.text();
    expect(html).toContain("Redirecting to GitHub");
  });

  // -----------------------------------------------------------------------
  // Sign-out endpoint
  // -----------------------------------------------------------------------

  it("POST /api/auth/sign-out clears cookies", async () => {
    const res = await fetch(`${WORKER_URL}/api/auth/sign-out`, {
      method: "POST",
      headers: { "Content-Type": "application/json", Origin: WORKER_URL },
      body: "{}",
    });
    expect(res.status).toBe(200);
    const cookies = res.headers.getSetCookie?.() || [];
    const sessionCookie = cookies.find((c) => c.includes("better-auth.session_token"));
    expect(sessionCookie).toContain("Max-Age=0");
  });

  // -----------------------------------------------------------------------
  // Cloud Replays API
  // -----------------------------------------------------------------------

  it("POST /api/cloud-replays requires auth", async () => {
    const res = await fetch(`${WORKER_URL}/api/cloud-replays`, {
      method: "POST",
      headers: { "Content-Type": "application/json", Origin: WORKER_URL },
      body: JSON.stringify({ replay: { meta: {}, scenes: [] } }),
    });
    expect(res.status).toBe(401);
  });

  it("GET /api/cloud-replays requires auth", async () => {
    const res = await fetch(`${WORKER_URL}/api/cloud-replays`);
    expect(res.status).toBe(401);
  });

  it("GET /api/cloud-replays/:id returns 404 for non-existent", async () => {
    const res = await fetch(`${WORKER_URL}/api/cloud-replays/nonexistent1`);
    expect(res.status).toBe(404);
  });

  it("DELETE /api/cloud-replays/:id requires auth", async () => {
    const res = await fetch(`${WORKER_URL}/api/cloud-replays/nonexistent1`, {
      method: "DELETE",
      headers: { Origin: WORKER_URL },
    });
    expect(res.status).toBe(401);
  });

  it("GET /api/cloud-replays/:id rejects invalid ID format", async () => {
    const res = await fetch(`${WORKER_URL}/api/cloud-replays/bad!id`);
    expect(res.status).toBe(400);
  });

  // -----------------------------------------------------------------------
  // Gist API
  // -----------------------------------------------------------------------

  it("POST /api/gists requires auth", async () => {
    const res = await fetch(`${WORKER_URL}/api/gists`, {
      method: "POST",
      headers: { "Content-Type": "application/json", Origin: WORKER_URL },
      body: JSON.stringify({ filename: "test.json", content: "{}" }),
    });
    expect(res.status).toBe(401);
  });

  it("PATCH /api/gists/:id requires auth", async () => {
    const res = await fetch(`${WORKER_URL}/api/gists/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4`, {
      method: "PATCH",
      headers: { "Content-Type": "application/json", Origin: WORKER_URL },
      body: JSON.stringify({ filename: "test.json", content: "{}" }),
    });
    expect(res.status).toBe(401);
  });

  // -----------------------------------------------------------------------
  // CORS — verify new methods are allowed
  // -----------------------------------------------------------------------

  it("CORS preflight allows PATCH and DELETE", async () => {
    const res = await fetch(`${WORKER_URL}/api/cloud-replays`, {
      method: "OPTIONS",
      headers: {
        Origin: WORKER_URL,
        "Access-Control-Request-Method": "DELETE",
      },
    });
    const allowedMethods = res.headers.get("access-control-allow-methods") || "";
    expect(allowedMethods).toContain("DELETE");
    expect(allowedMethods).toContain("PATCH");
  });

  // -----------------------------------------------------------------------
  // Backward compat — old anonymous POST /api/replays
  // -----------------------------------------------------------------------

  it("POST /api/replays rejects invalid gist_id", async () => {
    const res = await fetch(`${WORKER_URL}/api/replays`, {
      method: "POST",
      headers: { "Content-Type": "application/json", Origin: WORKER_URL },
      body: JSON.stringify({ gist_id: "not-hex!" }),
    });
    expect(res.status).toBe(400);
    const data = (await res.json()) as any;
    expect(data.error).toContain("invalid gist_id");
  });

  it("POST /api/replays rejects missing gist_id", async () => {
    const res = await fetch(`${WORKER_URL}/api/replays`, {
      method: "POST",
      headers: { "Content-Type": "application/json", Origin: WORKER_URL },
      body: JSON.stringify({}),
    });
    expect(res.status).toBe(400);
  });

  // -----------------------------------------------------------------------
  // Cloud Replays — additional input validation
  // -----------------------------------------------------------------------

  it("GET /api/cloud-replays/:id rejects too-short ID", async () => {
    const res = await fetch(`${WORKER_URL}/api/cloud-replays/short`);
    expect(res.status).toBe(400);
  });

  it("DELETE /api/cloud-replays/:id rejects invalid ID format", async () => {
    const res = await fetch(`${WORKER_URL}/api/cloud-replays/bad!id!here!`, {
      method: "DELETE",
      headers: { Origin: WORKER_URL },
    });
    // Auth check happens first for DELETE, but invalid ID format may still be caught
    // Either 400 (bad ID) or 401 (no auth) is acceptable
    expect([400, 401]).toContain(res.status);
  });

  // -----------------------------------------------------------------------
  // Cloud Replays — visibility validation
  // -----------------------------------------------------------------------

  it("POST /api/cloud-replays rejects invalid visibility", async () => {
    const res = await fetch(`${WORKER_URL}/api/cloud-replays`, {
      method: "POST",
      headers: { "Content-Type": "application/json", Origin: WORKER_URL },
      body: JSON.stringify({
        replay: { meta: { slug: "test" }, scenes: [{ type: "user_prompt", text: "hi" }] },
        visibility: "secret",
      }),
    });
    // 401 (no auth) takes priority over visibility validation
    expect(res.status).toBe(401);
  });

  it("GET /api/cloud-replays/:id returns 404 for non-existent ID (no existence leak)", async () => {
    // Even for valid ID format, non-existent replays return 404 (not 401/403)
    const res = await fetch(`${WORKER_URL}/api/cloud-replays/aaaaaaaaaaaa`);
    expect(res.status).toBe(404);
  });

  // -----------------------------------------------------------------------
  // Short URL redirect
  // -----------------------------------------------------------------------

  it("GET /r/:id redirects to /view/?cloud=:id for valid format", async () => {
    const res = await fetch(`${WORKER_URL}/r/aaaaaaaaaaaa`, { redirect: "manual" });
    expect(res.status).toBe(302);
    const location = res.headers.get("location") || "";
    expect(location).toContain("/view/?cloud=aaaaaaaaaaaa");
  });

  it("GET /r/:badid returns 404 for invalid format", async () => {
    const res = await fetch(`${WORKER_URL}/r/bad!`, { redirect: "manual" });
    expect(res.status).toBe(404);
  });

  // -----------------------------------------------------------------------
  // Explore API
  // -----------------------------------------------------------------------

  it("GET /api/replays returns array with type field", async () => {
    const res = await fetch(`${WORKER_URL}/api/replays?sort=recent&limit=5`);
    expect(res.status).toBe(200);
    const data = (await res.json()) as any[];
    expect(Array.isArray(data)).toBe(true);
    for (const item of data) {
      if (item.type) {
        expect(["gist", "cloud"]).toContain(item.type);
      }
    }
  });
});
