/**
 * Drives the real Server Actions on the running dev server, so the settings
 * screen is measured where people actually meet it. Throwaway.
 *
 *   bun run tmp/settings/e2e.ts closed     the HTTP route is gone
 *   bun run tmp/settings/e2e.ts reach      /settings answers for all three tiers
 *   bun run tmp/settings/e2e.ts change     change, sign in, refuse the old, restore
 *   bun run tmp/settings/e2e.ts limit      ten wrong current passwords, then sign in
 *
 * Action ids are minted by Turbopack and change whenever the module is
 * recompiled, so they are read out of the client chunks rather than pasted in.
 */

const BASE = "http://127.0.0.1:4214"

async function sources(path: string, cookie?: string): Promise<string[]> {
  const html = await fetch(`${BASE}${path}`, {
    headers: cookie ? { cookie } : {},
  }).then((r) => r.text())
  const chunks = [
    ...new Set(
      [...html.matchAll(/\/_next\/static\/[^"'\\ ]+?\.js/g)].map((m) => m[0]),
    ),
  ]
  return Promise.all(
    chunks.map((c) => fetch(`${BASE}${c}`).then((r) => r.text())),
  )
}

async function actionId(
  path: string,
  name: string,
  cookie?: string,
): Promise<string> {
  const pattern = new RegExp(
    `__next_internal_action_entry_do_not_use__ \\[\\{"([0-9a-f]+)":\\{"name":"${name}"\\}\\}`,
  )
  for (const source of await sources(path, cookie)) {
    const found = source.match(pattern)
    if (found) return found[1]!
  }
  throw new Error(`no action id for ${name} on ${path}`)
}

function cookieOf(response: Response): string {
  return response.headers
    .getSetCookie()
    .map((line) => line.split(";")[0]!)
    .join("; ")
}

type Answer = { ok?: boolean; reason?: string; tier?: string; retryAfter?: number }

function answerIn(body: string): Answer {
  const line = body.match(/\{"ok":(?:true|false)[^}]*\}/)
  if (!line) throw new Error(`no answer in: ${body.slice(0, 500)}`)
  return JSON.parse(line[0]) as Answer
}

let SIGN_IN = ""

async function signIn(
  username: string,
  password: string,
): Promise<{ answer: Answer; cookie: string }> {
  SIGN_IN ||= await actionId("/login", "attemptSignIn")
  const response = await fetch(`${BASE}/login`, {
    method: "POST",
    headers: {
      "Next-Action": SIGN_IN,
      "Content-Type": "text/plain;charset=UTF-8",
    },
    body: JSON.stringify([username, password]),
  })
  return { answer: answerIn(await response.text()), cookie: cookieOf(response) }
}

async function change(
  cookie: string,
  current: string,
  next: string,
  again = next,
): Promise<{ answer: Answer; cookie: string }> {
  const id = await actionId("/settings", "changePassword", cookie)
  const response = await fetch(`${BASE}/settings`, {
    method: "POST",
    headers: {
      "Next-Action": id,
      "Content-Type": "text/plain;charset=UTF-8",
      cookie,
    },
    body: JSON.stringify([current, next, again]),
  })
  const fresh = cookieOf(response)
  return {
    answer: answerIn(await response.text()),
    cookie: fresh || cookie,
  }
}

function show(label: string, value: unknown) {
  console.log(`  ${label.padEnd(46)}${JSON.stringify(value)}`)
}

const step = process.argv[2] ?? "all"

if (step === "all" || step === "closed") {
  console.log("--- the HTTP route ---")
  for (const path of [
    "/api/auth/change-password",
    "/api/auth/sign-up/email",
    "/api/auth/update-user",
  ]) {
    const r = await fetch(`${BASE}${path}`, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ currentPassword: "x", newPassword: "yyyyyy" }),
    })
    show(`POST ${path}`, r.status)
  }
  // The one that must still work, so the fix is not "everything is 404".
  const session = await fetch(`${BASE}/api/auth/get-session`)
  show("GET /api/auth/get-session", session.status)
}

if (step === "all" || step === "reach") {
  console.log("\n--- who reaches /settings ---")
  for (const [name, password] of [
    ["narin", "admin1"],
    ["malai", "manager1"],
    ["nattikan", "staff1"],
  ] as const) {
    const { answer, cookie } = await signIn(name, password)
    const page = await fetch(`${BASE}/settings`, {
      headers: { cookie },
      redirect: "manual",
    })
    show(`${name} (${answer.tier})`, page.status)
  }
  const stranger = await fetch(`${BASE}/settings`, { redirect: "manual" })
  show("nobody signed in", stranger.status)
}

if (step === "all" || step === "change") {
  console.log("\n--- nattikan changes their password and puts it back ---")
  const first = await signIn("nattikan", "staff1")
  show("sign in with staff1", first.answer)

  const refusals = await change(first.cookie, "staff1", "abc", "abc")
  show("new password of three characters", refusals.answer)
  const mismatched = await change(refusals.cookie, "staff1", "abcdefg", "abcdefh")
  show("two new ones that differ", mismatched.answer)
  const wrong = await change(mismatched.cookie, "not-it", "cabbage1")
  show("wrong current password", wrong.answer)

  const done = await change(wrong.cookie, "staff1", "cabbage1")
  show("staff1 -> cabbage1", done.answer)

  const withNew = await signIn("nattikan", "cabbage1")
  show("sign in with cabbage1", withNew.answer)
  const withOld = await signIn("nattikan", "staff1")
  show("sign in with staff1", withOld.answer)

  const back = await change(withNew.cookie, "cabbage1", "staff1")
  show("cabbage1 -> staff1", back.answer)
  const restored = await signIn("nattikan", "staff1")
  show("sign in with staff1 again", restored.answer)
}

if (step === "all" || step === "limit") {
  console.log("\n--- the limiter, and the counter it must not share ---")
  const { cookie } = await signIn("nattikan", "staff1")
  let last: Answer = {}
  for (let n = 1; n <= 11; n++) {
    const result = await change(cookie, "definitely-not-it", "cabbage2")
    last = result.answer
    if (n >= 9) show(`wrong current password ${n}`, last)
  }
  const stillSignsIn = await signIn("nattikan", "staff1")
  show("sign in with staff1 while blocked here", stillSignsIn.answer)
  const other = await signIn("malai", "manager1")
  show("malai changes nothing, signs in", other.answer)
  // Wrong on purpose, so malai's password is not touched: what is being read
  // is whether the refusal is "wrong password" rather than "too many attempts".
  const otherChange = await change(other.cookie, "not-it", "cabbage3")
  show("malai is not blocked on this screen", otherChange.answer)
}
