import { randomUUID } from "node:crypto";
import type { Server } from "node:http";
import { createApp } from "./app.js";
import { initDb } from "./db/client.js";

const BASE = "http://127.0.0.1:4599/api/v1";
let passed = 0;
let failed = 0;

function check(label: string, cond: boolean) {
  if (cond) {
    passed++;
    console.log(`  PASS  ${label}`);
  } else {
    failed++;
    console.error(`  FAIL  ${label}`);
  }
}

async function req(
  method: string,
  path: string,
  body?: unknown,
  token?: string,
  cookie?: string,
) {
  const headers: Record<string, string> = { "content-type": "application/json" };
  if (token) headers.authorization = `Bearer ${token}`;
  if (cookie) headers.cookie = cookie;
  const res = await fetch(BASE + path, {
    method,
    headers,
    body: body ? JSON.stringify(body) : undefined,
  });
  const setCookie = res.headers.get("set-cookie") || undefined;
  let json: any = null;
  try {
    json = await res.json();
  } catch {
    /* no body */
  }
  return { status: res.status, json, setCookie };
}

async function main() {
  await initDb();
  const app = createApp();
  const server: Server = await new Promise((resolve) => {
    const s = app.listen(4599, () => resolve(s));
  });

  try {
    // --- A. Register ---
    const email = `u${Date.now()}@test.dev`;
    const profileId = randomUUID();
    const reg = await req("POST", "/auth/register", {
      email,
      password: "supersecret",
      displayName: "Alice",
      desiredId: profileId,
    });
    check("register returns 201", reg.status === 201);
    check("register reuses profile id as account id", reg.json?.user?.id === profileId);
    check("register returns access token", typeof reg.json?.accessToken === "string");
    check("register sets refresh cookie", !!reg.setCookie);
    const tokenA = reg.json.accessToken;

    // --- B. Push data from "device A" ---
    const locId = randomUUID();
    const playerId = randomUUID();
    const sessionId = randomUUID();
    const t0 = Date.now();
    const push1 = await req(
      "POST",
      "/sync",
      {
        locations: [
          { id: locId, name: "Maison", createdAt: t0, updatedAt: t0 },
        ],
        players: [
          { id: playerId, name: "Bob", createdAt: t0, updatedAt: t0 },
        ],
        sessions: [
          {
            id: sessionId,
            gameId: "skyjo",
            dateStart: t0,
            dateEnd: t0,
            players: [{ id: playerId, name: "Bob" }],
            rounds: [],
            status: "finished",
            options: {},
            winnerIds: [playerId],
            locationId: locId,
            updatedAt: t0,
          },
        ],
      },
      tokenA,
    );
    check("push returns 200", push1.status === 200);
    check("push acks location", push1.json?.applied?.locations?.[0]?.id === locId);
    check("push ack has server updatedAt", typeof push1.json?.applied?.sessions?.[0]?.updatedAt === "number");

    // --- C. Second device pulls everything from scratch ---
    const login = await req("POST", "/auth/login", {
      email,
      password: "supersecret",
    });
    check("login returns 200", login.status === 200);
    const tokenB = login.json.accessToken;

    const pullB = await req("GET", "/sync?since=0", undefined, tokenB);
    check("pull returns 200", pullB.status === 200);
    check("device B pulls the location", pullB.json?.locations?.some((l: any) => l.id === locId));
    check("device B pulls the player", pullB.json?.players?.some((p: any) => p.id === playerId));
    check("device B pulls the session", pullB.json?.sessions?.some((s: any) => s.id === sessionId));
    check("pulled session keeps jsonb players", Array.isArray(pullB.json?.sessions?.[0]?.players));
    check("pull returns a server cursor", typeof pullB.json?.serverTime === "number");

    // --- D. Conflict: both devices edit the same location, last push wins ---
    await req("POST", "/sync", {
      locations: [{ id: locId, name: "Maison A", updatedAt: Date.now() }],
    }, tokenA);
    await new Promise((r) => setTimeout(r, 5));
    await req("POST", "/sync", {
      locations: [{ id: locId, name: "Maison B", updatedAt: Date.now() }],
    }, tokenB);

    const pullFinal = await req("GET", "/sync?since=0", undefined, tokenA);
    const finalLoc = pullFinal.json.locations.find((l: any) => l.id === locId);
    check("conflict resolves last-write-wins (Maison B)", finalLoc?.name === "Maison B");

    // --- E. Tombstone: soft delete propagates ---
    await req("POST", "/sync", {
      players: [{ id: playerId, name: "Bob", deletedAt: Date.now(), updatedAt: Date.now() }],
    }, tokenA);
    const pullDel = await req("GET", "/sync?since=0", undefined, tokenB);
    const delPlayer = pullDel.json.players.find((p: any) => p.id === playerId);
    check("tombstone is returned on pull", !!delPlayer?.deletedAt);

    // --- F. Incremental pull excludes old records ---
    const cursor = pullFinal.json.serverTime;
    await new Promise((r) => setTimeout(r, 5));
    const freshLoc = randomUUID();
    await req("POST", "/sync", {
      locations: [{ id: freshLoc, name: "Nouveau", updatedAt: Date.now() }],
    }, tokenA);
    const pullSince = await req("GET", `/sync?since=${cursor}`, undefined, tokenB);
    check("incremental pull includes new record", pullSince.json.locations.some((l: any) => l.id === freshLoc));

    // --- G. Refresh token rotation ---
    const cookie = reg.setCookie!.split(";")[0];
    const refreshed = await req("POST", "/auth/refresh", undefined, undefined, cookie);
    check("refresh returns new access token", typeof refreshed.json?.accessToken === "string");
    // Old refresh token is now revoked (rotation)
    const reuse = await req("POST", "/auth/refresh", undefined, undefined, cookie);
    check("reusing rotated refresh token fails", reuse.status === 401);

    // --- H. Auth required on sync ---
    const noAuth = await req("GET", "/sync?since=0");
    check("sync without token is 401", noAuth.status === 401);

    // --- I. Friends: two accounts befriend each other ---
    const emailF = `friend${Date.now()}@test.dev`;
    const regF = await req("POST", "/auth/register", {
      email: emailF,
      password: "supersecret",
      displayName: "Zoe",
      username: `zoe_${Date.now().toString(36)}`,
    });
    check("second account registers with username", regF.status === 201);
    const tokenF = regF.json.accessToken;
    const zoeUsername = regF.json.user.username;

    // Alice needs a username too so Zoe could find her back (not required here)
    await req("PATCH", "/me", { username: `alice_${Date.now().toString(36)}` }, tokenA);

    // Alice searches for Zoe by username
    const searchRes = await req(
      "GET",
      `/friends/search?q=${zoeUsername}`,
      undefined,
      tokenA,
    );
    check("search finds the user by username", searchRes.json?.results?.some((u: any) => u.id === regF.json.user.id));

    // Alice sends a friend request to Zoe
    const sendRes = await req(
      "POST",
      "/friends/requests",
      { username: zoeUsername },
      tokenA,
    );
    check("friend request created", sendRes.status === 201);
    const friendshipId = sendRes.json.friendshipId;

    // Duplicate request is rejected
    const dupRes = await req(
      "POST",
      "/friends/requests",
      { username: zoeUsername },
      tokenA,
    );
    check("duplicate friend request rejected", dupRes.status === 409);

    // Zoe sees the incoming request
    const zoeReqs = await req("GET", "/friends/requests", undefined, tokenF);
    check("addressee sees incoming request", zoeReqs.json?.incoming?.some((r: any) => r.friendshipId === friendshipId));

    // Alice sees it as outgoing
    const aliceReqs = await req("GET", "/friends/requests", undefined, tokenA);
    check("requester sees outgoing request", aliceReqs.json?.outgoing?.some((r: any) => r.friendshipId === friendshipId));

    // Zoe accepts
    const acceptRes = await req(
      "POST",
      `/friends/requests/${friendshipId}/accept`,
      undefined,
      tokenF,
    );
    check("accept returns ok", acceptRes.status === 200);

    // Both now list each other as friends
    const aliceFriends = await req("GET", "/friends", undefined, tokenA);
    const zoeFriends = await req("GET", "/friends", undefined, tokenF);
    check("requester now has the friend", aliceFriends.json?.friends?.some((f: any) => f.user.id === regF.json.user.id));
    check("addressee now has the friend", zoeFriends.json?.friends?.some((f: any) => f.friendshipId === friendshipId));

    // Unfriend
    const unfriendRes = await req("DELETE", `/friends/${friendshipId}`, undefined, tokenA);
    check("unfriend returns ok", unfriendRes.status === 200);
    const aliceAfter = await req("GET", "/friends", undefined, tokenA);
    check("friend removed after unfriend", !aliceAfter.json?.friends?.some((f: any) => f.friendshipId === friendshipId));
  } finally {
    server.close();
  }

  console.log(`\n${passed} passed, ${failed} failed`);
  process.exit(failed === 0 ? 0 : 1);
}

main().catch((err) => {
  console.error("Smoke test crashed:", err);
  process.exit(1);
});
