import { Page, Locator } from "@playwright/test"

/**
 * Root cause (confirmed via a repro script capturing `page.on("pageerror")`): every load of
 * these pages throws a genuine React hydration error ("Minified React error #418" - text
 * content mismatch between server and client render) - it is NOT intermittent. What IS
 * unpredictable is its effect: hydration recovery blanks the placeholder text of a random
 * subset of that page's Select triggers, but leaves everything else (the sibling <Label>/
 * <FieldLabel>, the trigger's role="combobox" attribute, the DOM structure) intact. So a
 * reload-until-clean strategy never terminates (confirmed: reloading never once produced a
 * "clean" load), and a bare `getByRole("combobox", { name: ... })` is unreliable because the
 * accessible name it depends on is exactly what gets blanked.
 *
 * The fix is to stop depending on that name at all: every Select trigger in this app sits as
 * an immediate sibling of a stable, always-intact label element, so `comboboxByLabel()` finds
 * the trigger via that label + role="combobox" alone. `retryClick`/`selectOption`/
 * `clickToReveal` remain useful as defense-in-depth for ordinary timing races (dialogs
 * mounting, popups opening) that are unrelated to this hydration bug.
 */
export function comboboxByLabel(scope: Page | Locator, labelText: string): Locator {
  return scope.getByText(labelText, { exact: true }).locator("..").getByRole("combobox").first()
}

/**
 * `trigger.click()` gets an explicit, short per-attempt timeout deliberately: without one, a
 * momentarily-disabled/not-yet-actionable button (e.g. a trigger that's disabled for one tick
 * after navigation before client state settles) lets a SINGLE click() call sit and retry
 * internally for the whole remaining test timeout, so this loop never reaches a second attempt
 * - confirmed happening on "Cancel run" right after starting a run. A short click timeout lets
 * the loop actually cycle through multiple real attempts within the test's time budget.
 */
export async function retryClick(trigger: Locator, verify: () => Promise<void>, attempts = 5) {
  let lastErr: unknown
  for (let i = 0; i < attempts; i++) {
    try {
      await trigger.click({ timeout: 3000 })
    } catch (e) {
      lastErr = e
      continue
    }
    try {
      await verify()
      return
    } catch (e) {
      lastErr = e
    }
  }
  throw lastErr
}

/**
 * Opens a Select trigger and picks an option by name. Deliberately a single click, not a
 * retryClick loop: re-clicking an already-open Select trigger toggles it shut, and if the
 * option name is ever wrong the popup's portal can end up overlaying the trigger, making
 * Playwright's actionability check for a second click hang indefinitely (confirmed while
 * testing this file) instead of failing fast. `comboboxByLabel` already makes the trigger
 * lookup itself reliable, so a plain click here is both simpler and safer.
 */
export async function selectOption(page: Page, trigger: Locator, optionName: string | RegExp) {
  // .first() covers callers that intentionally pass a name matching multiple options (e.g. "pick
  // any scrap reason") - for the common single-match case it's a no-op.
  const option = page.getByRole("option", { name: optionName }).first()
  await trigger.click()
  await option.click()
}

/** Clicks a trigger that's expected to reveal `target` (a dialog, a newly-mounted control), retrying the click. */
export async function clickToReveal(trigger: Locator, target: Locator) {
  await retryClick(trigger, () => target.waitFor({ state: "visible", timeout: 1500 }))
}

/**
 * Same as `clickToReveal`, but escalates to a full `page.reload()` between rounds when the
 * trigger itself never becomes actionable within a round - not just "the popup didn't open"
 * but "the trigger stayed disabled" or "never rendered at all". Confirmed on the production run
 * detail page's "Cancel run"/"Return leftover" buttons (RunActions.tsx, gated on
 * `run.status === "InProgress"`): occasionally that gate/enabled-state renders wrong for the
 * rest of a page's life - the same class of one-shot render corruption as the hydration bug
 * documented above, just hitting a component's disabled/mounted state instead of a Select's
 * placeholder text. A reload gets a fresh render attempt; `trigger`/`target` are re-queried
 * live each round since Playwright locators aren't tied to a specific DOM snapshot.
 */
export async function clickToRevealWithReload(page: Page, trigger: Locator, target: Locator, reloadAttempts = 3) {
  let lastErr: unknown
  for (let i = 0; i < reloadAttempts; i++) {
    try {
      await clickToReveal(trigger, target)
      return
    } catch (e) {
      lastErr = e
      if (i < reloadAttempts - 1) await page.reload()
    }
  }
  throw lastErr
}

/**
 * Every stage-action / document-action button in this app fires an async POST and only updates
 * the DOM once the response comes back (`onActed()`/`onSuccess()` refetch pattern) - Playwright's
 * `.click()` resolves as soon as the click event dispatches, NOT once that request settles. A
 * test that clicks "Approve & receive" and immediately reads stock through a separate API call
 * can race ahead of the backend commit and observe pre-action state (confirmed: production run
 * stock checks reading 0 immediately after a click the UI later shows as successful). Wrapping
 * the click in `page.waitForResponse` for the specific endpoint makes the helper actually wait
 * for the request that matters, not just the DOM event.
 */
export async function submitAndWait(page: Page, trigger: Locator, urlIncludes: string, method: "POST" | "PUT" = "POST") {
  const [response] = await Promise.all([
    page.waitForResponse((res) => res.url().includes(urlIncludes) && res.request().method() === method),
    trigger.click(),
  ])
  return response
}
