// @vitest-environment node
/**
 * Die Textaufbereitung für die Sprachausgabe.
 *
 * Geprüft wird hier nur, was ohne Stimme prüfbar ist – also alles außer dem
 * Klang: Ersetzungen, Zerlegung, Stimmenwahl. Ob das Betriebssystem eine
 * deutsche Stimme hat, gehört nicht hierher; das entscheidet das Gerät des
 * Nutzers und darf keinen Testlauf zum Scheitern bringen.
 */

import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';

import { describe, expect, it } from 'vitest';

import {
  BUCHSTABIERT,
  HAEPPCHEN_LAENGE,
  SPRECHWEISE,
  fuerSprachausgabe,
  inHaeppchen,
  stimmeWaehlen,
} from '../src/renderer/src/lernen/sprachausgabe';

describe('fuerSprachausgabe', () => {
  it('spricht eine Fundstelle aus, statt sie zu buchstabieren', () => {
    expect(fuerSprachausgabe('§ 12 Abs. 4 Nr. 2 WaffG')).toBe(
      'Paragraf 12 Absatz 4 Nummer 2 Waffengesetz',
    );
  });

  it('kennt den Plural der Paragrafen', () => {
    expect(fuerSprachausgabe('§§ 62, 63 UrhG')).toBe('Paragrafen 62, 63 Urheberrechtsgesetz');
  });

  it('löst „in Verbindung mit“ auf', () => {
    expect(fuerSprachausgabe('§ 2 Abs. 2 i. V. m. Anl. 2')).toBe(
      'Paragraf 2 Absatz 2 in Verbindung mit Anlage 2',
    );
  });

  it('trifft das längere Kürzel zuerst', () => {
    expect(fuerSprachausgabe('BeschussV und BeschG')).toBe('Beschussverordnung und Beschussgesetz');
  });

  it('greift nicht in zusammengesetzte Wörter ein', () => {
    /* „WaffGÄndG“ ist ein eigenes Wort; würde daraus „WaffengesetzÄndG“,
       spräche die Stimme etwas aus, das es nicht gibt. */
    expect(fuerSprachausgabe('WaffGÄndG')).toBe('WaffGÄndG');
    expect(fuerSprachausgabe('Die WBK-Nummer')).toBe('Die WBK-Nummer');
  });

  it('lässt Kürzel stehen, die buchstabiert gehören', () => {
    for (const kuerzel of BUCHSTABIERT) {
      expect(fuerSprachausgabe(`Das ${kuerzel} entscheidet.`)).toContain(kuerzel);
    }
  });

  it('macht aus Aufzählungsstrichen eine Sprechpause', () => {
    expect(fuerSprachausgabe('Voraussetzungen:\n- Sachkunde\n- Bedürfnis')).toBe(
      'Voraussetzungen:\n. Sachkunde\n. Bedürfnis',
    );
  });

  it('lässt eine Datumsangabe unberührt', () => {
    expect(fuerSprachausgabe('bis zum 6. Juli 2017')).toBe('bis zum 6. Juli 2017');
  });

  /*
    Die eine Lückentextfrage des Katalogs (5.01) trägt ihre Lücken als Reihe
    von Unterstrichen – so druckt das amtliche PDF dort eine Schreiblinie.
    Ohne diese Ersetzung hörte man „Notwehr ist diejenige, die ist, um …“:
    einen Satz ohne Lücken, den niemand beantworten kann.
  */
  it('macht aus einer Schreiblinie das Wort „Lücke“', () => {
    expect(fuerSprachausgabe('Notwehr ist diejenige _____, die _____ ist.')).toBe(
      'Notwehr ist diejenige Lücke, die Lücke ist.',
    );
  });

  it('lässt einzelne Unterstriche in Wörtern in Ruhe', () => {
    expect(fuerSprachausgabe('Datei lernstand_alt speichern')).toBe(
      'Datei lernstand_alt speichern',
    );
  });
});

describe('inHaeppchen', () => {
  it('gibt für leeren Text nichts zurück', () => {
    expect(inHaeppchen('   ')).toEqual([]);
  });

  it('lässt einen kurzen Text als ein Stück', () => {
    expect(inHaeppchen('Kurz und gut.')).toEqual(['Kurz und gut.']);
  });

  it('bündelt kurze Sätze, statt bei jedem Punkt abzusetzen', () => {
    const stuecke = inHaeppchen('Eins. Zwei. Drei.');

    expect(stuecke).toEqual(['Eins. Zwei. Drei.']);
  });

  it('hält jedes Stück unter der Höchstlänge', () => {
    const satz = 'Der Erwerb einer Schusswaffe setzt ein Bedürfnis voraus. ';
    const stuecke = inHaeppchen(satz.repeat(40));

    expect(stuecke.length).toBeGreaterThan(5);
    for (const stueck of stuecke) {
      expect(stueck.length).toBeLessThanOrEqual(HAEPPCHEN_LAENGE);
    }
  });

  it('teilt auch einen einzigen überlangen Satz ohne Punkt', () => {
    const stuecke = inHaeppchen('Wort '.repeat(200).trim());

    for (const stueck of stuecke) {
      expect(stueck.length).toBeLessThanOrEqual(HAEPPCHEN_LAENGE);
    }
  });

  it('verliert kein Wort', () => {
    const text =
      'Wer eine Schusswaffe erwerben will, braucht eine Erlaubnis. ' +
      'Diese wird auf Antrag erteilt, wenn Sachkunde, Bedürfnis und ' +
      'Zuverlässigkeit vorliegen; über den Antrag entscheidet die Behörde. ' +
      'Ohne Erlaubnis ist der Erwerb strafbar.';
    const woerter = (satz: string): string[] => satz.split(/\s+/u).filter((w) => w.length > 0);

    expect(woerter(inHaeppchen(text, 60).join(' '))).toEqual(woerter(text));
  });

  it('trennt nicht mitten in einer Datumsangabe', () => {
    const stuecke = inHaeppchen('Die Frist endete am 6. Juli 2017 endgültig.');

    expect(stuecke).toEqual(['Die Frist endete am 6. Juli 2017 endgültig.']);
  });
});

describe('stimmeWaehlen', () => {
  const stimme = (lang: string, name: string, localService = true): Stimmenzeile => ({
    lang,
    name,
    localService,
  });
  interface Stimmenzeile {
    readonly lang: string;
    readonly name: string;
    readonly localService: boolean;
  }

  it('nimmt keine Stimme, wenn keine deutsche dabei ist', () => {
    expect(stimmeWaehlen([stimme('en-US', 'Zira'), stimme('fr-FR', 'Julie')])).toBeNull();
  });

  it('nimmt die deutsche Stimme', () => {
    const gewaehlt = stimmeWaehlen([stimme('en-US', 'Zira'), stimme('de-DE', 'Katja')]);

    expect(gewaehlt?.name).toBe('Katja');
  });

  it('bevorzugt die lokale Stimme vor der aus dem Netz', () => {
    const gewaehlt = stimmeWaehlen([
      stimme('de-DE', 'Cloud-Stimme', false),
      stimme('de-DE', 'Hedda', true),
    ]);

    expect(gewaehlt?.name).toBe('Hedda');
  });

  it('nimmt auch de-AT und de-CH', () => {
    expect(stimmeWaehlen([stimme('de-AT', 'Michael')])?.name).toBe('Michael');
  });
});

describe('Sprechformen und Glossar', () => {
  /*
    Zwei Listen derselben Kürzel driften auseinander – das ist in diesem
    Projekt schon dreimal passiert. Hier fällt es beim ersten neuen
    Glossareintrag auf und nicht erst, wenn eine Stimme etwas buchstabiert.
  */
  it('kennt jedes Kürzel des Glossars', () => {
    const wurzel = fileURLToPath(new URL('..', import.meta.url));
    const glossar = JSON.parse(
      readFileSync(join(wurzel, '..', 'content', 'glossar.json'), 'utf8'),
    ) as { eintraege: { begriff: string; art: string }[] };

    const unbekannt = glossar.eintraege
      .filter((eintrag) => eintrag.art === 'abkuerzung')
      .map((eintrag) => eintrag.begriff)
      .filter((kuerzel) => !(kuerzel in SPRECHWEISE) && !BUCHSTABIERT.includes(kuerzel));

    expect(
      unbekannt,
      'Neue Kürzel gehören in SPRECHWEISE oder – bewusst – in BUCHSTABIERT.',
    ).toEqual([]);
  });

  it('führt kein Kürzel in beiden Listen', () => {
    expect(BUCHSTABIERT.filter((kuerzel) => kuerzel in SPRECHWEISE)).toEqual([]);
  });
});
