/**
 * Lernstand sichern und einspielen – im echten Fenster, an echten Dateien.
 *
 * Die Einheitentests messen die Prüfkette gegen selbst gebaute Datenbanken.
 * Hier läuft der ganze Weg: Die Anwendung sichert ihren eigenen Lernstand,
 * er wird verändert, die Sicherung wird eingespielt, und danach muss der
 * alte Stand wieder dastehen. Nur dieser Lauf beweist, dass die Reihenfolge
 * beim Ersetzen stimmt.
 *
 * Die Systemdialoge werden im Hauptprozess durch Attrappen ersetzt – dieselbe
 * Vorgehensweise wie in `druck.spec.ts`. Geprüft wird alles, was danach kommt.
 */

import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';

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;
let ablage: string;

test.beforeAll(async () => {
  bauPruefen();
  ({ app, fenster } = await appStarten());
  ablage = mkdtempSync(join(tmpdir(), 'wsk-e2e-sicherung-'));
});

test.afterAll(async () => {
  await app?.close();
  try {
    rmSync(ablage, { recursive: true, force: true });
  } catch {
    /* Aufräumen ist Komfort. */
  }
});

test.describe.configure({ mode: 'serial' });

/** Ersetzt den Speicherdialog. */
async function speicherzielStellen(pfad: string): Promise<void> {
  await app?.evaluate(({ dialog }, ziel) => {
    (dialog as unknown as { showSaveDialog: unknown }).showSaveDialog = () =>
      Promise.resolve({ canceled: false, filePath: ziel });
  }, pfad);
}

/** Ersetzt den Öffnen-Dialog. */
async function oeffnenzielStellen(pfad: string): Promise<void> {
  await app?.evaluate(({ dialog }, ziel) => {
    (dialog as unknown as { showOpenDialog: unknown }).showOpenDialog = () =>
      Promise.resolve({ canceled: false, filePaths: [ziel] });
  }, pfad);
}
/**
 * Wie viele Fragen sind bearbeitet?
 *
 * Aus der Zeile „Schon einmal bearbeitet“ im Lernstand. Die Klasse allein
 * genügt nicht: Der Lernplan benutzt dieselbe für seine eigenen Zahlen.
 */
async function bearbeitet(): Promise<number> {
  const zeile = fenster
    .locator('dt:text-is("Schon einmal bearbeitet")')
    .locator('xpath=following-sibling::dd[1]');
  const text = (await zeile.innerText()).replace(/\./gu, '');
  const treffer = /(\d+) von \d+/u.exec(text);
  return Number(treffer?.[1] ?? -1);
}

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);
  }
}

/** Beantwortet die stehende Frage – gleich welche und wie. */
async function frageBeantworten(): Promise<void> {
  const auswahl = fenster.getByRole('checkbox').or(fenster.getByRole('radio'));
  if ((await auswahl.count()) > 0) {
    await auswahl.first().check();
    await fenster.getByRole('button', { name: /^Antwort bestätigen$/u }).click();
  } else {
    await fenster.getByRole('button', { name: /^Musterantwort anzeigen$/u }).click();
    await fenster.getByRole('button', { name: /^Gewusst$/u }).click();
  }
}

/**
 * Beantwortet so lange Fragen, bis eine davon noch nie bearbeitet war.
 *
 * ## Warum nicht einfach „eine Frage beantworten“
 *
 * „Schon einmal bearbeitet“ zählt **verschiedene** Fragen (`versuche > 0`,
 * `lernstand.ts`). Dieselbe Frage ein zweites Mal zu beantworten lässt die
 * Zahl also stehen – und genau das passierte: Beantwortet wird hier stets die
 * erste Antwortmöglichkeit, die oft falsch ist, und eine falsch beantwortete
 * Frage ist bei der nächsten Wiedervorlage die dringendste. „Weiterlernen“
 * legte damit dieselbe Frage noch einmal vor.
 *
 * Nachgemessen: Im Lauf vom 27.08. blieb die Zahl bei 1 stehen und der Test
 * „zeigt vor dem Ersetzen …“ fiel aus; der Lauf davor war mit derselben
 * Fassung grün. Ein Münzwurf, kein Befund an der Anwendung.
 *
 * Der Ausweg führt **innerhalb** einer Sitzung weiter statt über eine neue:
 * In einer Sitzung kommt keine Frage zweimal vor, nach `vorher + 1` Antworten
 * muss also eine neue dabei gewesen sein. Eine neue Sitzung zu beginnen half
 * nicht – sie legte dieselbe dringende Frage wieder zuoberst.
 */
async function eineNeueFrageBeantworten(): Promise<void> {
  await zumStart();
  const vorher = await bearbeitet();

  await fenster.getByRole('button', { name: /Weiterlernen/u }).click();
  await fenster.getByRole('heading', { level: 1, name: /, Frage \d/u }).waitFor();

  for (let i = 0; i <= vorher; i++) {
    await frageBeantworten();

    const weiter = fenster.getByRole('button', { name: /^Nächste Frage$/u });
    if ((await weiter.count()) === 0) {
      // Die Sitzung ist zu Ende – dann war die letzte Frage die letzte Gelegenheit.
      break;
    }
    await weiter.first().click();
    await fenster.getByRole('heading', { level: 1, name: /, Frage \d/u }).waitFor();
  }

  await zumStart();
}

test.describe('Sicherung', () => {
  const datei = () => join(ablage, 'probe.wsklernstand');

  /* Der Stand zum Zeitpunkt der Sicherung – auf ihn muss das Einspielen
     zurückführen. Ohne diesen Merkposten prüfte der Beweis unten nur, dass
     eine Erfolgsmeldung erscheint. */
  let beiDerSicherung = -1;

  test('schreibt eine einzelne Datei – ohne Nebendateien', async () => {
    /* Der Grund für `VACUUM INTO` statt `db.backup()`: Dessen Ergebnis
       verwandelt sich beim Ansehen in drei Dateien, die liegen bleiben. */
    await eineNeueFrageBeantworten();
    await speicherzielStellen(datei());

    await fenster.getByRole('button', { name: 'Sicherung speichern …' }).scrollIntoViewIfNeeded();
    await fenster.getByRole('button', { name: 'Sicherung speichern …' }).click();

    await expect(fenster.getByText(/Die Sicherung ist geschrieben/u)).toBeVisible();
    beiDerSicherung = await bearbeitet();
    expect(beiDerSicherung).toBeGreaterThan(0);
    expect(existsSync(datei())).toBe(true);
    expect(existsSync(`${datei()}-wal`)).toBe(false);
    expect(existsSync(`${datei()}.teil`)).toBe(false);
  });

  test('zeigt vor dem Ersetzen, was in der Datei steht – und ersetzt noch nichts', async () => {
    const vorher = await bearbeitet();
    await eineNeueFrageBeantworten();
    expect(await bearbeitet()).toBeGreaterThan(vorher);

    await oeffnenzielStellen(datei());
    const knopf = fenster.getByRole('button', { name: 'Sicherung auswählen und ansehen …' });
    await knopf.scrollIntoViewIfNeeded();
    await knopf.click();

    await expect(
      fenster.getByRole('heading', { name: 'Diesen Lernstand durch die Sicherung ersetzen?' }),
    ).toBeVisible();
    /* Die Rückfrage steht da – der Lernstand ist unberührt. */
    expect(await bearbeitet()).toBeGreaterThan(vorher);
  });

  test('stellt beim Ersetzen den gesicherten Stand wirklich wieder her', async () => {
    /*
      Der eigentliche Beweis. Zwischen Sicherung und Einspielen wurde eine
      Frage mehr beantwortet; danach muss die Zahl wieder die der Sicherung
      sein. Wäre die Reihenfolge beim Ersetzen falsch – etwa ein verwaistes
      Schreibprotokoll neben der neuen Datei –, stünde hier der falsche Stand
      bei tadelloser Datenbankprüfung.
    */
    await fenster.getByRole('button', { name: 'Ja, alles ersetzen' }).click();

    await expect(fenster.getByText(/Der Lernstand ist eingelesen/u)).toBeVisible();
    /* Die Sicherheitskopie wird beim Namen genannt – sie ist der einzige Weg
       zurück, falls die eingespielte Datei doch nicht die gewünschte war. */
    await expect(fenster.getByText(/Lernstand-vor-dem-Einspielen-/u)).toBeVisible();

    /* Und jetzt die eigentliche Aussage: Die Zahl steht wieder auf dem Stand
       der Sicherung, nicht auf dem von eben. */
    await expect.poll(bearbeitet).toBe(beiDerSicherung);
  });

  test('weist eine fremde Datei ab, ohne etwas anzufassen', async () => {
    const vorher = await bearbeitet();
    const fremd = join(ablage, 'urlaub.wsklernstand');
    writeFileSync(fremd, 'x'.repeat(2048));

    await oeffnenzielStellen(fremd);
    const knopf = fenster.getByRole('button', { name: 'Sicherung auswählen und ansehen …' });
    await knopf.scrollIntoViewIfNeeded();
    await knopf.click();

    await expect(fenster.getByText(/keine Datenbank/u)).toBeVisible();
    await expect(fenster.getByText(/Es wurde nichts verändert/u)).toBeVisible();
    expect(await bearbeitet()).toBe(vorher);
  });
});
