import type { Locator, Page } from "playwright";

/** Options for asserting a CSS class on a Playwright Locator. */
export interface PwAssertClassOptions {
  /** CSS selector string or an existing Playwright Locator. */
  selector: string | Locator;
  /** CSS class to check, without the leading dot (e.g. "border-red"). */
  className: string;
  /** If true (default), asserts the class IS present. If false, asserts it is NOT present. */
  mustHave?: boolean;
  /** Max milliseconds to poll for the condition. Default: 5000. */
  timeoutMs?: number;
}

/**
 * pw_assert_class — asserts a Playwright Locator has (or lacks) a given CSS class.
 *
 * Polls the element up to timeoutMs. Throws with a descriptive message if the
 * condition is not met in time.
 *
 * When selector is a string, resolves via page.locator(selector).first() and
 * delegates to Playwright's native waitForFunction for efficiency.
 * When selector is a Locator, uses a manual polling loop with locator.evaluate()
 * because Locator objects do not serialize across waitForFunction page boundaries.
 */
export async function pw_assert_class(page: Page, opts: PwAssertClassOptions): Promise<void> {
  const { selector, className } = opts;
  const mustHave = opts.mustHave ?? true;
  const timeoutMs = opts.timeoutMs ?? 5000;

  if (typeof selector === "string") {
    // String selector: use waitForFunction for efficient native polling.
    const selectorRepr = selector;
    try {
      await page.waitForFunction(
        ({ sel, cls, must }: { sel: string; cls: string; must: boolean }) => {
          const el = document.querySelector(sel);
          if (!el) return false;
          const has = el.classList.contains(cls);
          return must ? has : !has;
        },
        { sel: selector, cls: className, must: mustHave },
        { timeout: timeoutMs }
      );
    } catch {
      throw new Error(
        `expected ${selectorRepr} to ${mustHave ? "have" : "NOT have"} class ${className} within ${timeoutMs}ms`
      );
    }
  } else {
    // Locator: manual polling loop — Locators don't cross waitForFunction boundary.
    const locator = selector;
    const selectorRepr = "<Locator>";
    const deadline = Date.now() + timeoutMs;
    const pollMs = 100;

    while (true) {
      let conditionMet = false;
      try {
        const has = await locator.evaluate(
          (el: Element, cls: string) => el.classList.contains(cls),
          className
        );
        conditionMet = mustHave ? has : !has;
      } catch {
        // Element may not be attached yet — treat as condition not met.
        conditionMet = false;
      }

      if (conditionMet) return;

      const remaining = deadline - Date.now();
      if (remaining <= 0) {
        throw new Error(
          `expected ${selectorRepr} to ${mustHave ? "have" : "NOT have"} class ${className} within ${timeoutMs}ms`
        );
      }

      await new Promise<void>((resolve) => setTimeout(resolve, Math.min(pollMs, remaining)));
    }
  }
}
