/**
 * Die Volltextsuche im echten Fenster.
 *
 * Der Suchkern ist in `tests/fragensuche.test.ts` gegen den echten Katalog
 * gemessen, die Oberfläche in `tests/renderer-suche.test.tsx` gegen eine
 * Attrappe. Hier läuft beides zusammen – und zwar an dem Bestand, den der
 * Nutzer tatsächlich vor sich hat: 575 amtliche Fragen.
 *
 * Zwei Dinge lassen sich nur hier prüfen: der Durchstich von der Suche über
 * eine Sitzung zurück in dieselbe Trefferliste, und die Bedienbarkeit
 * ausschließlich mit der Tastatur.
 */

import { type ElectronApplication, type Page } from '@playwright/test';

import { amStart, appStarten, bauPruefen } from './electron-hilfe';
import { expect, test } from './konsolenwache';

let app: ElectronApplication | undefined;
let fenster: Page;

test.beforeAll(async () => {
  bauPruefen();
  ({ app, fenster } = await appStarten());
});

test.afterAll(async () => {
  await app?.close();
});

function feld() {
  return fenster.getByLabel('Suchbegriff');
}

/** Die Überschriften der Trefferkarten. */
function treffer() {
  return fenster.locator('.treffer__frage');
}

async function zumStart(): Promise<void> {
  for (let i = 0; i < 4; i++) {
    if (await amStart(fenster)) {
      return;
    }
    const zurueck = fenster.getByRole('button', { name: /^(Zum Start|Sitzung beenden)$/u });
    if ((await zurueck.count()) === 0) {
      return;
    }
    await zurueck.first().click();
    await fenster.waitForTimeout(250);
  }
}

async function sucheOeffnen(): Promise<void> {
  await zumStart();
  await fenster.getByRole('button', { name: /Fragen durchsuchen/u }).click();
  await fenster.getByRole('heading', { name: 'Fragen durchsuchen', level: 1 }).waitFor();
}

test.describe('Suche – am echten Katalog', () => {
  test('findet über Umlaute, ß und Zusammensetzungen hinweg', async () => {
    await sucheOeffnen();

    /*
      Die drei Fälle, an denen SQLite FTS5 nachweislich scheitert – siehe
      docs/entscheidung-volltextsuche.md. Hier müssen sie stimmen, sonst war
      die Abweichung vom Plan umsonst.
    */
    const faelle: readonly (readonly [string, string])[] = [
      ['schiessen', 'Fragen gefunden'],
      ['schießen', 'Fragen gefunden'],
      ['besitzkarte', 'Fragen gefunden'],
      ['buechse', 'Fragen gefunden'],
    ];

    for (const [eingabe, erwartet] of faelle) {
      await feld().fill(eingabe);
      await fenster.waitForTimeout(500);
      await expect(fenster.locator('.suche__anzahl'), `„${eingabe}“ findet nichts`).toContainText(
        erwartet,
      );
      expect(await treffer().count(), `„${eingabe}“ ohne Trefferkarte`).toBeGreaterThan(0);
    }
  });

  test('liefert für beide Schreibweisen dieselbe Zahl', async () => {
    await sucheOeffnen();
    await feld().fill('schiessen');
    await fenster.waitForTimeout(500);
    const mitSS = (await fenster.locator('.suche__anzahl').textContent()) ?? '';

    await feld().fill('schießen');
    await fenster.waitForTimeout(500);
    expect(await fenster.locator('.suche__anzahl').textContent()).toBe(mitSS);
  });

  test('zeigt bei leerem Feld den ganzen Bereich – der Fragen-Browser', async () => {
    await sucheOeffnen();
    await feld().fill('');
    await fenster.getByRole('radio', { name: /I\.5 – Notwehr und Notstand \(43\)/u }).check();
    await fenster.waitForTimeout(500);
    await expect(fenster.locator('.suche__anzahl')).toContainText('43 Fragen');
  });

  test('schlägt eine Frage über ihre amtliche Nummer nach', async () => {
    await sucheOeffnen();
    await fenster.getByRole('radio', { name: /Alle Bereiche/u }).check();
    await feld().fill('2.150');
    await fenster.waitForTimeout(400);
    await expect(fenster.getByText('Über die Fragennummer gefunden:')).toBeVisible();
  });
});

test.describe('Suche – der Durchstich', () => {
  test('führt von der Suche ins Üben und zurück in dieselbe Trefferliste', async () => {
    await sucheOeffnen();
    /* Eingeschränkt, und das ist seit Fassung 0.26.0 der eigentliche Punkt:
       Bis dahin kam nur der Suchtext zurück, und die Liste stand danach über
       dem ganzen Katalog. „Dieselbe Trefferliste“, wie dieser Prüflauf seit
       jeher heißt, war sie damit nicht. */
    const bereich = fenster.getByRole('radio', { name: /^I\.1 /u }).first();
    await bereich.check();
    await feld().fill('Anscheinswaffe');
    await fenster.waitForTimeout(500);

    const ersterKnopf = fenster.getByRole('button', { name: /^Frage [\d.]+ üben$/u }).first();
    const beschriftung = (await ersterKnopf.textContent())?.trim() ?? '';
    await ersterKnopf.click();

    // In der Sitzung: genau eine Frage.
    await expect(fenster.getByText(/Frage 1 von 1/u)).toBeVisible();

    await fenster.getByRole('button', { name: 'Sitzung beenden' }).click();
    await fenster.getByRole('button', { name: 'Zurück zur Suche' }).click();

    await fenster.getByRole('heading', { name: 'Fragen durchsuchen', level: 1 }).waitFor();
    /* Der Suchtext steht wieder da – das ist der Punkt der ganzen Übung. Wer
       eine Frage aus einer Suche übt, will danach die übrigen Treffer sehen
       und nicht denselben Begriff noch einmal tippen. */
    await expect(feld()).toHaveValue('Anscheinswaffe');
    await expect(bereich).toBeChecked();
    /* Und die Zählzeile nennt den Bereich – der sichtbare Stand IST die
       Ansage, eine eigene Meldung „wiederhergestellt“ gibt es bewusst
       nicht. */
    await expect(fenster.locator('.suche__anzahl')).toContainText('I.1');
    expect(beschluesselt(beschriftung)).toBe(true);
  });

  test('vergisst die Anfrage auf dem Weg über den Startbildschirm', async () => {
    /* Die Wache über die Verfallsregel. Die Anfrage reist mit dem
       Sitzungsauftrag und stirbt mit ihm; wer über den Startbildschirm geht,
       beginnt von vorn. Deckt zugleich ab, dass zwischen zwei Prüfläufen
       nichts wandert. */
    await zumStart();
    await fenster.getByRole('button', { name: /Fragen durchsuchen/u }).click();
    await fenster.getByRole('heading', { name: 'Fragen durchsuchen', level: 1 }).waitFor();

    await expect(feld()).toHaveValue('');
    await expect(fenster.getByRole('radio', { name: /Alle Bereiche/u })).toBeChecked();
  });
});

/** Der Knopf trägt seinen vollen Namen (WCAG 2.4.6), nicht nur „Üben“. */
function beschluesselt(beschriftung: string): boolean {
  return /^Frage [\d.]+ üben$/u.test(beschriftung);
}

test.describe('Suche – ohne Maus', () => {
  test('ist vollständig mit der Tastatur bedienbar', async () => {
    await sucheOeffnen();

    /* Leeren, bevor getippt wird, und seit Fassung 0.26.0 aus einem anderen
       Grund als vorher: Das Feld ist nach `sucheOeffnen()` leer, weil der
       Weg über den Startbildschirm führt und eine Suchanfrage nur den
       Rückweg aus einer Sitzung überlebt. Das Leeren bleibt als
       Unabhängigkeit vom vorigen Lauf stehen — Playwright fährt die Läufe
       dieser Datei nacheinander im selben Fenster. */
    await feld().fill('');
    await feld().focus();
    await fenster.keyboard.type('Notwehr');
    await fenster.waitForTimeout(500);
    expect(await treffer().count()).toBeGreaterThan(0);

    /*
      Die Bereichsgruppe ist genau EIN Tabulatorhalt – das ist der Grund für
      die Radiogruppe statt zwölf einzelner Halte. Vom Feld aus ein Tab, dann
      steht der Fokus in der Gruppe und die Pfeiltasten wählen.
    */
    await fenster.keyboard.press('Tab');
    const inGruppe = await fenster.evaluate(
      () => document.activeElement?.closest('.suche__bereiche') !== null,
    );
    expect(inGruppe, 'Nach einem Tab steht der Fokus nicht in der Bereichsgruppe.').toBe(true);
  });

  test('setzt den Fokus nach dem Nachladen auf den ersten neuen Treffer', async () => {
    await sucheOeffnen();
    await feld().fill('waffe');
    await fenster.waitForTimeout(500);

    const vorher = await treffer().count();
    expect(vorher).toBe(25);

    await fenster.getByRole('button', { name: /Weitere \d+ anzeigen/u }).click();
    await fenster.waitForTimeout(300);

    /* Die neuen Karten stehen im Baum VOR dem Knopf. Bliebe der Fokus dort,
       würden sie beim Weitertabben vollständig übersprungen. */
    const aufNeuem = await fenster.evaluate(
      () => document.activeElement?.classList.contains('treffer__frage') ?? false,
    );
    expect(aufNeuem, 'Der Fokus steht nicht auf dem ersten neuen Treffer.').toBe(true);
    expect(await treffer().count()).toBe(50);
  });
});

test.describe('Suche – verrät die Lösung nicht', () => {
  test('nennt bei einer offenen Frage die Musterantwort nicht', async () => {
    await sucheOeffnen();
    /* „5.01“ ist der Lückentext; seine Musterantwort beginnt mit „Lösung:“ –
       ein Wort, das in der Trefferliste nichts zu suchen hat. */
    await feld().fill('5.01');
    await fenster.waitForTimeout(400);

    const inhalt = (await fenster.locator('main').textContent()) ?? '';
    expect(inhalt).not.toContain('Lösung:');
  });

  test('weist im DOM nirgends aus, welche Antwort richtig ist', async () => {
    await sucheOeffnen();
    await feld().fill('Anscheinswaffe');
    await fenster.waitForTimeout(500);
    await fenster
      .getByText(/Antwortmöglichkeiten anzeigen/u)
      .first()
      .click();

    const verraeter = await fenster.evaluate(() => {
      const bereich = document.querySelector('.suche');
      if (bereich === null) {
        return ['kein Suchbereich'];
      }
      const gefunden: string[] = [];
      for (const knoten of bereich.querySelectorAll('*')) {
        for (const attribut of knoten.attributes) {
          if (/korrekt|richtig/iu.test(attribut.name) || /korrekt|richtig/iu.test(attribut.value)) {
            gefunden.push(`${knoten.tagName}[${attribut.name}="${attribut.value}"]`);
          }
        }
      }
      return gefunden;
    });
    expect(verraeter).toEqual([]);
  });
});
