// @vitest-environment node
/**
 * Das lokale Fehlerprotokoll.
 *
 * **Der Anlass.** Sämtliche Fehler des Hauptprozesses landeten bis 0.22.0
 * ausschließlich in `console.error` – und die sieht im gepackten Programm
 * niemand, weil es ohne Konsole startet. Der Rückmeldeweg dieser Anwendung
 * ist eine E-Mail; wer „es stürzt manchmal ab“ melden wollte, hatte nichts in
 * der Hand, und wer die Meldung bearbeiten sollte, bekam nichts Greifbares.
 *
 * **Was hier vor allem geprüft wird**, ist nicht das Schreiben, sondern das
 * Weglassen: Eine Datei, die zwar hilft, aber nebenbei den Anmeldenamen
 * mitschreibt, wäre ein schlechterer Tausch als gar keine. Die Anwendung
 * verspricht an drei Stellen, dass nichts Personenbezogenes das Gerät
 * verlässt – diese Datei ist dazu gedacht, von Hand verschickt zu werden.
 */

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

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

import {
  entpersoniert,
  fehlerZeile,
  MAX_BYTES,
  protokollEinrichten,
  protokollieren,
  protokollPfad,
  protokollVergessen,
  PROTOKOLL_DATEI,
} from '../src/main/protokoll';

let verzeichnis = '';

function inhalt(): string {
  const pfad = join(verzeichnis, PROTOKOLL_DATEI);
  return existsSync(pfad) ? readFileSync(pfad, 'utf8') : '';
}

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

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

describe('Personenbezug entfernen', () => {
  it('ersetzt das Heimatverzeichnis, egal mit welchem Trennzeichen', () => {
    /* Node liefert Windows-Pfade mit Rückstrich, viele Meldungen aus
       Chromium und SQLite mit Schrägstrich – beide enthalten den
       Anmeldenamen. */
    const heim = 'C:\\Users\\Erika';

    expect(entpersoniert('EACCES: C:\\Users\\Erika\\AppData\\lernstand.db', heim)).toBe(
      'EACCES: <Benutzer>\\AppData\\lernstand.db',
    );
    expect(entpersoniert('open C:/Users/Erika/AppData/lernstand.db', heim)).toBe(
      'open <Benutzer>/AppData/lernstand.db',
    );
  });

  it('macht aus jeder Meldung genau eine Zeile', () => {
    /* Sonst ließe sich eine Fehlermeldung so wählen, dass sie im Protokoll
       wie mehrere Einträge mit eigenen Zeitstempeln aussieht. */
    expect(entpersoniert('erste Zeile\nzweite Zeile\r\ndritte', 'C:\\Users\\Erika')).toBe(
      'erste Zeile zweite Zeile dritte',
    );
  });

  it('kürzt eine überlange Meldung', () => {
    const lang = entpersoniert('x'.repeat(10_000), 'C:\\Users\\Erika');

    expect(lang.length).toBeLessThanOrEqual(2000);
  });

  it('kommt ohne bekanntes Heimatverzeichnis aus', () => {
    expect(entpersoniert('irgendein Fehler', '')).toBe('irgendein Fehler');
  });
});

describe('Schreiben', () => {
  it('schreibt Zeitpunkt, Bereich und Meldung', () => {
    protokollieren('fenster', 'Renderer beendet (crashed)', new Date('2026-08-29T18:30:00.000Z'));

    expect(inhalt()).toBe('2026-08-29T18:30:00.000Z [fenster] Renderer beendet (crashed)\n');
  });

  it('hängt weitere Einträge an, statt zu überschreiben', () => {
    protokollieren('a', 'erster', new Date('2026-08-29T18:30:00.000Z'));
    protokollieren('b', 'zweiter', new Date('2026-08-29T18:31:00.000Z'));

    expect(inhalt().trimEnd().split('\n')).toHaveLength(2);
  });

  it('nimmt den Anmeldenamen auch beim Schreiben heraus', () => {
    /* Die Reinigung sitzt an genau einer Stelle – hier wird gemessen, dass
       der Schreibweg sie wirklich benutzt und nicht daran vorbeiführt. */
    protokollieren(
      'fehler',
      `EACCES: open ${join(process.env['USERPROFILE'] ?? process.env['HOME'] ?? '', 'geheim.db')}`,
    );

    expect(inhalt()).toContain('<Benutzer>');
  });

  it('schweigt, solange kein Ablageort feststeht', () => {
    /* Ohne Einrichten darf nichts entstehen: Sonst legte ein früh
       auftretender Fehler eine Datei an einer beliebigen Stelle an. */
    protokollVergessen();
    protokollieren('fehler', 'darf nirgends landen');

    expect(existsSync(join(verzeichnis, PROTOKOLL_DATEI))).toBe(false);
  });

  it('bringt die Anwendung nicht zu Fall, wenn nicht geschrieben werden kann', () => {
    /* Ein Protokoll, das den Start verhindert, wäre schlimmer als keines.
       Hier steht ein Verzeichnis an der Stelle der Datei. */
    protokollVergessen();
    const eng = join(verzeichnis, 'eng');
    writeFileSync(join(verzeichnis, 'sperre'), 'x');
    protokollEinrichten(join(verzeichnis, 'sperre'));

    expect(() => {
      protokollieren('fehler', 'irgendetwas');
    }).not.toThrow();
    expect(existsSync(eng)).toBe(false);
  });

  it('nennt seinen Ablageort für den Systemzustand', () => {
    expect(protokollPfad()).toBe(join(verzeichnis, PROTOKOLL_DATEI));

    protokollVergessen();
    expect(protokollPfad()).toBeNull();
  });
});

describe('Obergrenze', () => {
  it('schneidet die ältere Hälfte weg, statt endlos zu wachsen', () => {
    const pfad = join(verzeichnis, PROTOKOLL_DATEI);
    /* Eine überfüllte Datei mit erkennbarem Anfang und Ende. */
    const zeile = `${'a'.repeat(200)}\n`;
    writeFileSync(
      pfad,
      `ALTESTE_ZEILE\n${zeile.repeat(Math.ceil(MAX_BYTES / 200))}JUENGSTE_ZEILE\n`,
    );
    expect(statSync(pfad).size).toBeGreaterThan(MAX_BYTES);

    protokollieren('fehler', 'neuer Eintrag');

    const danach = inhalt();
    expect(statSync(pfad).size).toBeLessThan(MAX_BYTES);
    expect(danach).not.toContain('ALTESTE_ZEILE');
    expect(danach).toContain('JUENGSTE_ZEILE');
    expect(danach).toContain('neuer Eintrag');
    expect(danach).toContain('[gekürzt]');
  });
});

describe('Fehler zu einer Zeile machen', () => {
  it('nennt Meldung und die erste Stelle der Aufrufliste', () => {
    const zeile = fehlerZeile(new Error('Etwas ging schief'));

    expect(zeile).toContain('Etwas ging schief');
  });

  it('kommt auch mit einem geworfenen Nicht-Fehler zurecht', () => {
    expect(fehlerZeile('nur ein Text')).toBe('nur ein Text');
    expect(fehlerZeile(undefined)).toBe('undefined');
  });
});
