import { SourceAdapter } from './base.mjs';
import { log } from '../logger.mjs';
import { join } from 'node:path';
import { ensureDir, LOG_DIR, fileExistsAtAnyLayout, safeFilename } from '../paths.mjs';

// CapTrader = white-labeled Interactive Brokers Account Management.
// Login-URL ist per-Account konfigurierbar, weil partnerID je nach Account-Vintage variiert
// (z.B. CapTrader1_mig fuer migrierte Accounts).
const DEFAULT_LOGIN_URL = 'https://www.clientam.com/sso/Login?partnerID=CapTrader1_mig';

// Default-Filter: nur die Dokumente, die der DE-Steuerberater wirklich braucht.
// Alles andere (Formular 1042S = US-Quellensteuer, FX-Arbeitsblatt, Allgemeiner Bericht)
// wird nur runtergeladen wenn account.allTaxForms === true.
const DEFAULT_TAX_TITLES = [
  /Informativer Steuerbericht/i,
  /Dividendenbericht/i,
  /Quellensteuerbescheinigung/i,
];

export class CaptraderAdapter extends SourceAdapter {
  static key = 'captrader';
  static label = 'CapTrader';

  async login() {
    const { page } = this;
    const loginUrl = this.account.loginUrl || DEFAULT_LOGIN_URL;
    await page.goto(loginUrl, { waitUntil: 'domcontentloaded' });
    log.info(`[captrader] Page geladen: url=${page.url()}`);

    // Form wird per JS injected (IBSSO.XYZ.inject in .loginformWrapper).
    // Selektoren auf die IBSSO-Klassen, die ueberlebenmiteminem.
    const user = page.locator('input.xyz-username').first();
    const pw = page.locator('input.xyz-password').first();
    // Submit ist initial display:none, erscheint nach Username-Eingabe.
    // Fallback: Enter im Passwort-Feld.
    const submit = page.locator('button.xyz-button-login').first();

    log.info('[captrader] Warte auf Login-Form ...');
    const formReady = pw.waitFor({ state: 'visible', timeout: 60_000 }).then(() => 'form').catch(() => null);
    const urlChanged = page.waitForURL((u) => !/\/sso\/Login/i.test(new URL(u).pathname + new URL(u).search), { timeout: 60_000 })
      .then(() => 'redirect').catch(() => null);
    const what = await Promise.race([formReady, urlChanged]);

    if (what === 'redirect') {
      log.info(`[captrader] Session noch aktiv (URL: ${page.url()})`);
      return;
    }
    if (what !== 'form') {
      const shot = join(ensureDir(LOG_DIR), `captrader-no-form-${Date.now()}.png`);
      await page.screenshot({ path: shot, fullPage: true }).catch(() => {});
      throw new Error(`[captrader] Login-Form erschien nicht innerhalb 60s. URL=${page.url()} Screenshot=${shot}`);
    }

    if (await this.dismissConsent()) log.info('[captrader] Consent-Banner bestätigt');

    log.info('[captrader] Fülle Login-Felder');
    await user.fill(this.account.credentials.username);
    await pw.fill(this.account.credentials.password);

    log.info('[captrader] Drücke Anmelden');
    if (await submit.isVisible({ timeout: 2_000 }).catch(() => false)) {
      await submit.click({ timeout: 10_000 });
    } else {
      // Submit-Button ist initial display:none; Enter im Passwort-Feld submitted die Form.
      await pw.press('Enter');
    }

    // Multi-2FA: wenn der User mehrere Verfahren registriert hat (Handy Key + Auth App),
    // erscheint .xyz-multipleselect. Wenn account.twofaMethod gesetzt ist, waehlen wir
    // automatisch vor (z.B. "4" = Mobile Authenticator App, "5.2a" = Handy Key).
    // Ansonsten waehlt der User in Ruhe selbst.
    if (this.account.twofaMethod) {
      const multi = page.locator('select.xyz-multipleselect').first();
      if (await multi.isVisible({ timeout: 5_000 }).catch(() => false)) {
        log.info(`[captrader] Auto-Select 2FA-Methode: ${this.account.twofaMethod}`);
        await multi.selectOption(this.account.twofaMethod).catch((e) => {
          log.warn(`[captrader] selectOption fehlgeschlagen: ${e.message}`);
        });
      }
    }

    // 2FA: User bestaetigt in IBKR Mobile App oder gibt Code aus Authenticator-App ein.
    // Wir warten bis die URL weg von /sso/Login wechselt, bis zu 3 Min.
    log.info('[captrader] Warte auf 2FA-Bestätigung (bis zu 3 Min) ...');
    try {
      await page.waitForURL((url) => !/\/sso\/Login/i.test(new URL(url).pathname + new URL(url).search), { timeout: 180_000 });
    } catch (e) {
      const shot = join(ensureDir(LOG_DIR), `captrader-no-redirect-${Date.now()}.png`);
      await page.screenshot({ path: shot, fullPage: true }).catch(() => {});
      log.warn(`[captrader] Kein Redirect nach 3 Min. Screenshot: ${shot}`);
      throw e;
    }
    log.info(`[captrader] Login erfolgreich (URL: ${page.url()})`);
  }

  // Steuerformulare/-berichte aus AccountManagement holen.
  // Ablauf: Reports & Statements aufrufen, je Steuerjahr alle <reporting-tax-form> einsammeln,
  // pro Form den ersten Format-Button (PDF bevorzugt, sonst HTML/CSV) klicken und den Download
  // via page.on('download') fangen. Die PDF kommt als Blob-URL ueber AngularJS — der echte
  // Backend-Endpoint sitzt unter /AccountManagement/TaxForms/* und ist nicht stabil dokumentiert,
  // deshalb klicken wir UI statt direkt zu request.get()-en.
  async *iterateStatements() {
    const { page } = this;

    // Direkter Sprung auf /AccountManagement/?action=RM_STATEMENTS killt die Session
    // (alter Servlet vs. neue Portal-Session, kein Auth-Bridging). Stattdessen
    // klicken wir uns durch das Portal-Menue: "Performance & Berichte" >
    // "Steuerdokumente" > "Berichte". Selektoren auf accessible Names statt
    // CSS-Klassen — robust gegen Icon-Spans und Whitespace im Menue-Markup.
    await page.waitForLoadState('networkidle', { timeout: 30_000 }).catch(() => {});

    const clickMenu = async (label) => {
      const labelRe = new RegExp(label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i');
      // Role-Selektoren zuerst — die treffen immer ein klick-bares Ziel.
      // getByText als allerletzter Fallback gestrichen: matcht zu locker (Footer/Tooltip-
      // Treffer), was bei mehrdeutigen Labels (z.B. "Steuerdokumente" auch im Footer)
      // den falschen Klick ausloest. Wenn keine Role matched, ist das Menue offenbar
      // anders strukturiert → screenshot + fail statt blind klicken.
      const candidates = [
        page.getByRole('button', { name: labelRe }),
        page.getByRole('link', { name: labelRe }),
        page.getByRole('menuitem', { name: labelRe }),
        page.getByRole('treeitem', { name: labelRe }),
      ];
      for (const cand of candidates) {
        const first = cand.first();
        const visible = await first.waitFor({ state: 'visible', timeout: 2_000 }).then(() => true).catch(() => false);
        if (visible) {
          await first.click({ timeout: 10_000 });
          return true;
        }
      }
      return false;
    };

    const menuPath = ['Performance & Berichte', 'Steuerdokumente', 'Berichte'];
    for (const label of menuPath) {
      log.info(`[captrader] Menü-Klick: "${label}"`);
      const ok = await clickMenu(label);
      if (!ok) {
        const safeLabel = label.replace(/[^\w]/g, '_');
        const shot = join(ensureDir(LOG_DIR), `captrader-menu-fail-${safeLabel}-${Date.now()}.png`);
        await page.screenshot({ path: shot, fullPage: true }).catch(() => {});
        log.error(`[captrader] Menü-Item "${label}" nicht gefunden. URL=${page.url()} Screenshot=${shot}`);
        return;
      }
      // Submenu/Page-Render abwarten — Portal animiert Slide-Outs, sonst klicken wir
      // ins Leere bevor das nachgeladene Item da ist.
      await page.waitForTimeout(800);
    }

    log.info(`[captrader] Menü durchklickt — URL=${page.url()}`);
    await page.waitForLoadState('networkidle', { timeout: 30_000 }).catch(() => {});

    // Year-Select erscheint, sobald TaxFormsCtrl die Liste der Jahre geladen hat.
    // Wichtig: locator.waitFor() — Locator.isVisible() ohne Wait kommt sofort zurueck,
    // der "timeout"-Parameter dort ist nur Locator-Resolution, kein Visibility-Wait.
    const yearSelect = page.locator('select[aria-label*="Steuerjahr"]').first();
    try {
      await yearSelect.waitFor({ state: 'visible', timeout: 30_000 });
    } catch {
      const shot = join(ensureDir(LOG_DIR), `captrader-no-yearselect-${Date.now()}.png`);
      await page.screenshot({ path: shot, fullPage: true }).catch(() => {});
      log.warn(`[captrader] Steuerjahr-Auswahl nicht sichtbar nach 30s — URL=${page.url()} Screenshot=${shot}`);
      return;
    }

    // Alle Jahre aus dem Dropdown ziehen. Werte sehen aus wie "string:2025" (AngularJS),
    // Labels sind "2025", "2024" etc. Wir nehmen die Labels als Year und die rohe Value
    // fuer selectOption.
    const yearChoices = await yearSelect.evaluate((sel) => {
      return Array.from(sel.options)
        .filter((o) => o.value && o.value !== '')
        .map((o) => ({ value: o.value, label: (o.label || o.text || '').trim() }));
    });
    log.info(`[captrader] Steuerjahre: ${yearChoices.map((y) => y.label).join(', ') || '(keine)'}`);
    if (!yearChoices.length) return;

    for (const yc of yearChoices) {
      const yearNum = Number(yc.label);
      if (!Number.isFinite(yearNum)) {
        log.warn(`[captrader] Year-Label nicht parsebar: "${yc.label}" — übersprungen`);
        continue;
      }

      // Year-Switch. Bei nur einem Jahr ist das Select disabled — dann ist es eh schon vorausgewaehlt.
      const isDisabled = await yearSelect.isDisabled().catch(() => false);
      if (!isDisabled) {
        await yearSelect.selectOption({ value: yc.value }).catch(async () => {
          await yearSelect.selectOption({ label: yc.label });
        });
        log.info(`[captrader] Steuerjahr ${yc.label} ausgewählt`);
        // Loading-Overlay erscheint kurz; warten bis es weg ist UND mind. ein form-bordered da.
        await page.waitForTimeout(500);
      }

      // Auf gerenderte Forms warten (loading-overlay weg, .form-bordered sichtbar).
      await page.waitForFunction(() => {
        const overlays = document.querySelectorAll('loading-overlay');
        for (const o of overlays) {
          if (!o.classList.contains('ng-hide')) return false;
        }
        return document.querySelectorAll('reporting-tax-form').length > 0;
      }, null, { timeout: 30_000 }).catch(() => {
        log.warn(`[captrader] ${yc.label}: keine Forms nach 30s — leeres Jahr?`);
      });

      const formCount = await page.locator('reporting-tax-form').count();
      log.info(`[captrader] ${yc.label}: ${formCount} Steuerformulare gefunden`);

      const wantAll = this.account.allTaxForms === true;
      const titleFilter = this.account.taxFormTitles
        ? this.account.taxFormTitles.map((s) => new RegExp(s, 'i'))
        : DEFAULT_TAX_TITLES;

      // Bewusst KEIN "Alle herunterladen"-ZIP-Fastpath: ein ZIP mit konstantem Filename
      // verhindert Per-Form-Change-Detection — verspaetet erscheinende Steuerunterlagen
      // (z.B. Informativer Steuerbericht 31.04.) wuerden bei einem schon vorhandenen
      // ZIP geskippt. Einzeldownloads haben pro Doc einen deterministischen Filename,
      // sodass file-existence-checks neue Doks automatisch erkennen. DocuDown packt
      // im Mail-Feature spaeter eh selbst per Source-uebergreifend.

      // Pre-Snapshot der Form-Titles: AngularJS kann die Form-Liste zwischen Iterationen
      // umsortieren (z.B. nach Recovery oder _ensureYear-Re-Render). Wenn wir dann per
      // numerischem Index iterieren, wuerden wir spaetere i auf falsche Forms anwenden.
      // Stattdessen iterieren wir per Title und holen den aktuellen Index pro Iteration neu.
      const titleSnapshot = [];
      for (let k = 0; k < formCount; k++) {
        const t = (await page.locator('reporting-tax-form').nth(k).locator('p').first().innerText().catch(() => '')).trim();
        if (t) titleSnapshot.push(t);
      }

      for (const title of titleSnapshot) {
        // Aktuellen Index per Title holen — robust gegen zwischenzeitlich Re-Order.
        const i = await this._findFormIndexByTitle(title);
        if (i === -1) {
          log.warn(`[captrader] ${yc.label}: Form "${title}" nicht mehr in der Liste — uebersprungen`);
          continue;
        }
        const formEl = page.locator('reporting-tax-form').nth(i);

        // Default-Filter: nur Steuerberater-relevante Dokumente. Override via
        // account.allTaxForms=true oder account.taxFormTitles=["regex", ...].
        if (!wantAll && !titleFilter.some((re) => re.test(title))) {
          log.debug(`[captrader] ${yc.label} skip (Filter): "${title}"`);
          continue;
        }

        // Alle angebotenen Format-Buttons (PDF/CSV/HTML). Jedes Format = eigenes File
        // (z.B. PDF + CSV beim FX-Arbeitsblatt — beide nuetzlich: PDF fuer Lesbarkeit,
        // CSV fuer Steuersoftware-Import). Per-Format-Skip via filename-existence.
        const buttons = formEl.locator('a.btn');
        const btnCount = await buttons.count();
        if (!btnCount) {
          log.warn(`[captrader] "${title}" (${yc.label}): keine Format-Buttons`);
          continue;
        }
        const btnTexts = [];
        for (let j = 0; j < btnCount; j++) {
          btnTexts.push((await buttons.nth(j).innerText().catch(() => '')).trim());
        }
        // Reihenfolge: PDF zuerst (loest meist Download-Event sauber ohne Inline-Nav),
        // dann CSV, dann HTML (HTML triggert oft Inline-View → Recovery noetig).
        // Buttons ohne erkennbaren Format-Text (nur Icon, leere innerText) wurden
        // frueher als 'bin' geparkt → Files endeten als ".bin", was die DocuDown-
        // Filename-Konvention verletzte. Stattdessen ueberspringen + warnen.
        const formatOrder = btnTexts
          .map((t, idx) => {
            const cleaned = t.toLowerCase().replace(/[^a-z0-9]/g, '');
            return { idx, fmt: cleaned || null };
          })
          .filter((entry) => {
            if (!entry.fmt) {
              log.warn(`[captrader] "${title}" (${yc.label}): Format-Button #${entry.idx} ohne erkennbaren Text — uebersprungen`);
              return false;
            }
            return true;
          })
          .sort((a, b) => {
            const rank = (f) => f === 'pdf' ? 0 : f === 'csv' ? 1 : f === 'html' ? 2 : 3;
            return rank(a.fmt) - rank(b.fmt);
          });

        const subfolder = 'Steuer';
        let formNeedsRecovery = false;
        let anyClickAttempted = false;
        // formIndex kann sich nach Recovery aendern, falls AngularJS Form-Order rotiert.
        let formIndex = i;

        for (const { idx: btnIdx, fmt } of formatOrder) {
          // HTML-Format wird als PDF gespeichert (newPage.pdf()), weil das nackte
          // newPage.content() ohne CSS/Bilder fuer den Steuerberater unbrauchbar ist.
          // Filename-Extension entsprechend angepasst — alte .html-Files (vor diesem Fix)
          // werden NICHT migriert, koexistieren als Leftover.
          const ext = fmt === 'html' ? 'pdf' : fmt;
          const filename = safeFilename(`${yearNum}-12-31 ${title}.${ext}`);

          if (this.cfg) {
            const existing = fileExistsAtAnyLayout(this.cfg, this.account.folder, yearNum, subfolder, filename);
            if (existing) {
              log.info(`[${this.account.id}] skip: ${existing}`);
              yield { kind: 'skip', path: existing };
              continue;
            }
          }

          // Falls vorheriger Klick die Page weggeschoben hat: erst Recovery vor naechstem Klick.
          if (formNeedsRecovery) {
            const ok = await this._recoverToFormsList(yc);
            if (!ok) {
              log.warn(`[captrader] ${yc.label}: Recovery vor naechstem Format ${fmt.toUpperCase()} fehlgeschlagen — abbrechen`);
              break;
            }
            formNeedsRecovery = false;
            // Re-validate Form-Index per Title — nach Recovery kann AngularJS die Reihenfolge
            // gerotated haben (z.B. wenn _ensureYear ein Re-Render triggert).
            const reIdx = await this._findFormIndexByTitle(title);
            if (reIdx === -1) {
              log.warn(`[captrader] Form "${title}" nach Recovery nicht mehr gefunden — Rest der Form abbrechen`);
              break;
            }
            if (reIdx !== formIndex) {
              log.info(`[captrader] Form-Index hat sich verschoben: ${formIndex} → ${reIdx}`);
              formIndex = reIdx;
            }
          }

          log.info(`[captrader] Download "${title}" (${yc.label}, ${fmt.toUpperCase()})`);
          anyClickAttempted = true;
          try {
            const captured = await this._clickAndCapture(formIndex, btnIdx);
            if (captured.kind === 'download') {
              yield { filename, year: yearNum, subfolder, download: captured.download };
            } else if (captured.kind === 'page') {
              // HTML-Inline-View → als PDF rendern (sonst CSS/Bilder verloren).
              const newPage = captured.page;
              await newPage.waitForLoadState('domcontentloaded', { timeout: 15_000 }).catch(() => {});
              await newPage.waitForLoadState('networkidle', { timeout: 5_000 }).catch(() => {});
              const buffer = await newPage.pdf({ format: 'A4', printBackground: true });
              await newPage.close().catch(() => {});
              yield { filename, year: yearNum, subfolder, buffer };
            } else if (captured.kind === 'nav') {
              // Aktuelle Page hat zur blob:-URL navigiert (Inline-Nav statt new tab).
              // PDF aus der aktuellen Page rendern, dann Recovery.
              const buffer = await page.pdf({ format: 'A4', printBackground: true });
              yield { filename, year: yearNum, subfolder, buffer };
            } else {
              throw new Error('Kein Download/Page/Nav-Event innerhalb 30s — anderer Render-Pfad');
            }
          } catch (e) {
            const shot = join(ensureDir(LOG_DIR), `captrader-dl-fail-${yearNum}-${formIndex}-${fmt}-${Date.now()}.png`);
            await page.screenshot({ path: shot, fullPage: true }).catch(() => {});
            log.error(`[captrader] Download fehlgeschlagen für "${title}" (${yc.label}, ${fmt.toUpperCase()}): ${e.message}. Screenshot=${shot}`);
          }
          formNeedsRecovery = true;
        }

        // Nur recovern wenn ueberhaupt geklickt wurde — sonst sparen wir 1.5s pro reinem Skip-Form.
        if (anyClickAttempted) {
          const recovered = await this._recoverToFormsList(yc);
          if (!recovered) {
            log.warn(`[captrader] ${yc.label}: Konnte nach "${title}" nicht zur Forms-Liste zurueck — Rest des Jahres uebersprungen`);
            break;
          }
        }
      }
    }
  }

  // Klickt einen Format-Button und captured das Ergebnis ohne Listener-Leaks.
  // Drei moegliche Outcomes:
  //   - Download-Event (PDF/CSV → Browser laedt mit Content-Disposition)
  //   - Neue Page (HTML via window.open auf blob:-URL)
  //   - Inline-Navigation der aktuellen Page auf blob:-URL (selten)
  // Wir wiren manuelle one-shot Listener (kein page.waitForEvent), damit nach Auflosung
  // GENAU EIN Outcome zaehlt und die Listener garantiert removed werden — kein Risiko, dass
  // der "verlierende" Listener im naechsten Click-Cycle ungewollt feuert.
  async _clickAndCapture(formIndex, btnIdx, timeoutMs = 30_000) {
    const { page } = this;
    const ctx = this.context;
    let resolveOuter;
    let settled = false;
    const outerP = new Promise((r) => { resolveOuter = r; });
    const settle = (val) => { if (!settled) { settled = true; resolveOuter(val); } };

    const onDownload = (d) => settle({ kind: 'download', download: d });
    const onPage = (p) => settle({ kind: 'page', page: p });
    const onFrameNav = (frame) => {
      if (frame === page.mainFrame() && frame.url().startsWith('blob:')) {
        settle({ kind: 'nav' });
      }
    };

    page.on('download', onDownload);
    ctx.on('page', onPage);
    page.on('framenavigated', onFrameNav);

    let timeoutHandle;
    try {
      const freshFormEl = page.locator('reporting-tax-form').nth(formIndex);
      await freshFormEl.locator('a.btn').nth(btnIdx).click();
      const timeoutP = new Promise((resolve) => {
        timeoutHandle = setTimeout(() => resolve(null), timeoutMs);
      });
      const winner = await Promise.race([outerP, timeoutP]);
      return winner ?? { kind: 'timeout' };
    } finally {
      // Race-Timer aufraeumen, sonst leakt ein 30s-Handle pro Click und blockiert den
      // Process-Exit.
      if (timeoutHandle) clearTimeout(timeoutHandle);
      page.off('download', onDownload);
      ctx.off('page', onPage);
      page.off('framenavigated', onFrameNav);
    }
  }

  async _findFormIndexByTitle(title) {
    const { page } = this;
    const count = await page.locator('reporting-tax-form').count();
    for (let k = 0; k < count; k++) {
      const t = (await page.locator('reporting-tax-form').nth(k).locator('p').first().innerText().catch(() => '')).trim();
      if (t === title) return k;
    }
    return -1;
  }

  // Stellt sicher, dass wir wieder auf der Forms-Liste fuer das angegebene Jahr stehen.
  // Reihenfolge: (1) noch direkt sichtbar? -> ok. (2) Breadcrumb "Steuerberichte" klicken.
  // (3) Fallback goBack max 2x. Danach ggf. Year neu setzen, falls SPA auf Default
  // zurueckgefallen ist.
  async _recoverToFormsList(yc) {
    const { page } = this;
    const formsVisible = async () => {
      try {
        await page.locator('reporting-tax-form').first().waitFor({ state: 'visible', timeout: 1_500 });
        return true;
      } catch { return false; }
    };

    if (await formsVisible()) return await this._ensureYear(yc);

    const breadcrumb = page.getByRole('link', { name: /Steuerberichte/i }).first();
    const hasBreadcrumb = await breadcrumb.waitFor({ state: 'visible', timeout: 2_000 }).then(() => true).catch(() => false);
    if (hasBreadcrumb) {
      log.info('[captrader] Recovery: Breadcrumb "Steuerberichte" klicken');
      await breadcrumb.click({ timeout: 5_000 }).catch(() => {});
      // 800ms statt 500ms — Portal-Slide-Animation kann laenger dauern, sonst sehen wir
      // mit 1.5s formsVisible nichts und gehen unnoetig in goBack.
      await page.waitForTimeout(800);
      if (await formsVisible()) return await this._ensureYear(yc);
    }

    for (let i = 0; i < 2; i++) {
      log.info(`[captrader] Recovery: goBack #${i + 1}`);
      await page.goBack({ waitUntil: 'domcontentloaded' }).catch(() => {});
      await page.waitForTimeout(800);
      if (await formsVisible()) return await this._ensureYear(yc);
    }
    return false;
  }

  // Setzt das Steuerjahr neu, falls die Recovery die Auswahl auf den Default zurueckgesetzt
  // hat. Wartet danach bis die Forms wieder gerendert sind.
  async _ensureYear(yc) {
    const { page } = this;
    const yearSelect = page.locator('select[aria-label*="Steuerjahr"]').first();
    const isDisabled = await yearSelect.isDisabled().catch(() => false);
    if (isDisabled) {
      // Disabled allein heisst nicht "alles ok" — nur wenn auch das richtige Jahr
      // ausgewaehlt ist, sind wir am Ziel. Sonst sind wir auf einer fremden Page.
      const currentValue = await yearSelect.inputValue().catch(() => '');
      return currentValue === yc.value;
    }

    const currentValue = await yearSelect.inputValue().catch(() => '');
    if (currentValue === yc.value) return true;

    await yearSelect.selectOption({ value: yc.value }).catch(async () => {
      await yearSelect.selectOption({ label: yc.label });
    });
    log.info(`[captrader] Recovery: Year ${yc.label} re-selected`);
    await page.waitForFunction(() => {
      const overlays = document.querySelectorAll('loading-overlay');
      for (const o of overlays) if (!o.classList.contains('ng-hide')) return false;
      return document.querySelectorAll('reporting-tax-form').length > 0;
    }, null, { timeout: 15_000 }).catch(() => {});
    return await page.locator('reporting-tax-form').count() > 0;
  }
}
