// @vitest-environment node
/**
 * Die Persistenz der Nutzereinstellungen.
 *
 * Anlass ist eine Randnotiz aus der Abnahme (`docs/abnahme-befunde.md`,
 * Befund 13): `einstellungenBereinigen` füllt fehlende Felder mit den
 * Standardwerten auf. Legte man eine Teiländerung – etwa nur das Farbschema –
 * unmittelbar zugrunde, setzte jede Umschaltung alle übrigen Einstellungen
 * zurück. Damals war das folgenlos, weil kein Renderer-Code die anderen
 * Felder schrieb.
 *
 * Seit „Antwortmöglichkeiten mischen“ und „Vorlesen“ wirklich benutzt werden,
 * ist es das nicht mehr. Das Zusammenlegen in `einstellungenSchreiben` gibt
 * es zwar schon; geprüft wurde es nie. Diese Datei holt das nach – damit die
 * Zusammenlegung nicht eines Tages als überflüssig entfernt wird.
 */

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

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

/* `app.getPath('userData')` ist die einzige Electron-Abhängigkeit des
   Moduls – ein Verzeichnis je Test hält die Läufe voneinander getrennt. */
let verzeichnis = '';

vi.mock('electron', () => ({
  app: {
    getPath: (): string => verzeichnis,
  },
}));

const { einstellungenBereinigen, einstellungenLesen, einstellungenSchreiben } =
  await import('../src/main/einstellungen');

function gespeichert(): Record<string, unknown> {
  return JSON.parse(readFileSync(join(verzeichnis, 'einstellungen.json'), 'utf8')) as Record<
    string,
    unknown
  >;
}

beforeEach(() => {
  verzeichnis = mkdtempSync(join(tmpdir(), 'wsk-einstellungen-'));
});

afterEach(() => {
  rmSync(verzeichnis, { recursive: true, force: true });
});

describe('einstellungenBereinigen', () => {
  it('nimmt für alles Unbrauchbare die Standardwerte', () => {
    /* Beide Schwierigkeitsregler stehen ab Werk auf „aus“: Ohne eigene
       Angabe steht die Anwendung so nah am amtlichen Katalog wie möglich. */
    expect(einstellungenBereinigen(null)).toEqual({
      thema: 'system',
      optionenMischen: false,
      antwortzahlVerbergen: false,
      vorlesen: false,
      vorlesenAutomatisch: false,
      vorlesenErklaerungBeiFehler: false,
      zuschnittGefragt: false,
      anzeigegroesse: 100,
      textabstand: 'normal',
      sprechtempo: 1,
    });
    expect(einstellungenBereinigen({ thema: 'lila', vorlesen: 'ja' })).toEqual({
      thema: 'system',
      optionenMischen: false,
      antwortzahlVerbergen: false,
      vorlesen: false,
      vorlesenAutomatisch: false,
      vorlesenErklaerungBeiFehler: false,
      zuschnittGefragt: false,
      anzeigegroesse: 100,
      textabstand: 'normal',
      sprechtempo: 1,
    });
  });

  it('übernimmt gültige Werte unverändert', () => {
    expect(
      einstellungenBereinigen({
        thema: 'dunkel',
        optionenMischen: true,
        antwortzahlVerbergen: true,
        vorlesen: true,
        vorlesenAutomatisch: false,
        vorlesenErklaerungBeiFehler: false,
        zuschnittGefragt: false,
        profilId: 3,
      }),
    ).toEqual({
      thema: 'dunkel',
      optionenMischen: true,
      antwortzahlVerbergen: true,
      vorlesen: true,
      vorlesenAutomatisch: false,
      vorlesenErklaerungBeiFehler: false,
      zuschnittGefragt: false,
      anzeigegroesse: 100,
      textabstand: 'normal',
      sprechtempo: 1,
      profilId: 3,
    });
  });
});

describe('Tastenkürzel', () => {
  /*
    Dreiwertig, und das ist der ganze Punkt: Bis 0.21.0 lag diese Wahl im
    `localStorage` des Renderers. Beim Umzug in die Datei darf sie nicht zu
    einem gewöhnlichen Schalter mit Standardwert werden – das Fehlen des
    Feldes bedeutet „noch nicht entschieden“ und steuert die Voreinstellung
    nach erkanntem Hilfsmittel.
  */
  it('erfindet keine Entscheidung, wo keine getroffen wurde', () => {
    expect(einstellungenBereinigen({}).tastenkuerzel).toBeUndefined();
    expect('tastenkuerzel' in einstellungenBereinigen({})).toBe(false);
  });

  it('hält beide ausdrücklichen Entscheidungen fest', () => {
    /* Besonders `false`: Ein Rückfallwert `?? true` schluckte genau die
       Entscheidung, die jemand bewusst getroffen hat. */
    expect(einstellungenBereinigen({ tastenkuerzel: false }).tastenkuerzel).toBe(false);
    expect(einstellungenBereinigen({ tastenkuerzel: true }).tastenkuerzel).toBe(true);
  });

  it('verwirft, was keine Entscheidung ist', () => {
    expect(einstellungenBereinigen({ tastenkuerzel: 'aus' }).tastenkuerzel).toBeUndefined();
    expect(einstellungenBereinigen({ tastenkuerzel: null }).tastenkuerzel).toBeUndefined();
  });

  it('überlebt einen Neustart', () => {
    einstellungenSchreiben({ tastenkuerzel: false });

    expect(einstellungenLesen().tastenkuerzel).toBe(false);
    expect(gespeichert()['tastenkuerzel']).toBe(false);
  });
});

describe('Anzeigegröße', () => {
  it('bereinigt eine Größe, die es nicht gibt', () => {
    /* Ein Wert aus einer früheren Fassung oder von Hand in die Datei
       geschrieben darf die Anwendung nicht in eine unbedienbare Größe
       zwingen. */
    expect(einstellungenBereinigen({ anzeigegroesse: 5000 }).anzeigegroesse).toBe(400);
    expect(einstellungenBereinigen({ anzeigegroesse: 1 }).anzeigegroesse).toBe(80);
    expect(einstellungenBereinigen({ anzeigegroesse: 'gross' }).anzeigegroesse).toBe(100);
  });

  it('hält eine gültige Stufe fest', () => {
    expect(einstellungenBereinigen({ anzeigegroesse: 150 }).anzeigegroesse).toBe(150);
  });
});

describe('einstellungenSchreiben', () => {
  it('legt eine Teiländerung über den gespeicherten Stand', () => {
    einstellungenSchreiben({
      optionenMischen: true,
      antwortzahlVerbergen: true,
      vorlesen: true,
      vorlesenAutomatisch: false,
      vorlesenErklaerungBeiFehler: false,
      zuschnittGefragt: false,
      profilId: 2,
    });

    /* Genau der Aufruf, den `useThema` macht: nur das Farbschema. */
    const danach = einstellungenSchreiben({ thema: 'dunkel' });

    /* Bewusst mit den vom Standard ABWEICHENDEN Werten geprüft: Nur so
       zeigt sich, dass die Teiländerung sie stehen lässt, statt sie
       stillschweigend auf die Standardwerte zurückzusetzen. */
    expect(danach).toEqual({
      thema: 'dunkel',
      optionenMischen: true,
      antwortzahlVerbergen: true,
      vorlesen: true,
      vorlesenAutomatisch: false,
      vorlesenErklaerungBeiFehler: false,
      zuschnittGefragt: false,
      anzeigegroesse: 100,
      textabstand: 'normal',
      sprechtempo: 1,
      profilId: 2,
    });
    expect(gespeichert()).toEqual(danach);
  });

  it('überlebt einen Neustart', () => {
    einstellungenSchreiben({ vorlesen: true, optionenMischen: false });

    expect(einstellungenLesen()).toMatchObject({ vorlesen: true, optionenMischen: false });
  });

  it('meldet, wenn nicht geschrieben werden konnte', () => {
    /* Ein Pfad, unter dem sich nichts anlegen lässt: Die Datei tritt an die
       Stelle des Verzeichnisses. Stiller Verlust wäre hier das Schlimmste –
       die Einstellung gälte bis zum Beenden und wäre danach weg. */
    einstellungenSchreiben({ thema: 'hell' });
    verzeichnis = join(verzeichnis, 'einstellungen.json');

    expect(() => einstellungenSchreiben({ thema: 'dunkel' })).toThrow(
      /konnten nicht gespeichert werden/u,
    );
  });
});

/*
  Geschrieben wird über eine Nebendatei und ein `rename`.

  Vorher ging es geradewegs in die Zieldatei: Ein Abbruch mitten im Schreiben
  – Stromausfall, voller Datenträger nach dem halben Puffer – hinterließ eine
  verstümmelte Datei. Gelesen wurde sie danach zwar sauber abgefangen, aber
  der Nutzer verlor wortlos Farbschema, Anzeigegröße, Vorlesen, Tastenkürzel
  und das zuletzt benutzte Profil. Es war der einzige nicht unteilbare
  Schreibweg der Anwendung.
*/
describe('einstellungenSchreiben – unteilbar', () => {
  const ziel = (): string => join(verzeichnis, 'einstellungen.json');
  const neben = (): string => `${ziel()}.neu`;

  it('lässt nach dem Schreiben keine Nebendatei zurück', () => {
    einstellungenSchreiben({ thema: 'dunkel' });

    expect(existsSync(ziel())).toBe(true);
    expect(existsSync(neben())).toBe(false);
  });

  it('lässt den alten Stand unversehrt, wenn das Schreiben scheitert', () => {
    einstellungenSchreiben({ thema: 'hell', anzeigegroesse: 250 });

    /* Der Abbruch mitten im Vorgang, ohne Attrappe: Ein Verzeichnis an der
       Stelle der Nebendatei lässt `writeFileSync` scheitern. Ginge es
       geradewegs in die Zieldatei, träfe derselbe Fehler sie – und zwar
       erst, nachdem sie geöffnet und geleert wurde. */
    mkdirSync(neben(), { recursive: true });

    expect(() => einstellungenSchreiben({ thema: 'dunkel' })).toThrow(
      /konnten nicht gespeichert werden/u,
    );

    rmSync(neben(), { recursive: true, force: true });
    expect(einstellungenLesen()).toMatchObject({ thema: 'hell', anzeigegroesse: 250 });
  });

  it('räumt die Nebendatei weg, wenn das Umbenennen scheitert', () => {
    einstellungenSchreiben({ thema: 'hell' });

    /* Ein nicht leeres Verzeichnis an der Stelle der Zieldatei: Schreiben
       gelingt, Umbenennen nicht. Bliebe die Nebendatei liegen, stünde ein
       halber Stand neben dem gültigen und belegte Platz, den es gerade
       nicht gab. */
    rmSync(ziel(), { force: true });
    mkdirSync(join(ziel(), 'sperre'), { recursive: true });

    expect(() => einstellungenSchreiben({ thema: 'dunkel' })).toThrow(
      /konnten nicht gespeichert werden/u,
    );

    expect(existsSync(neben())).toBe(false);
  });
});
