// @vitest-environment node
/**
 * Die Heilprüfung der laufenden Lernstandsdatei.
 *
 * **Der Anlass.** Für *fremde* Sicherungsdateien lief seit jeher eine
 * neunstufige Prüfkette (`sicherung.ts`, Stufe 4 mit `integrity_check`). Die
 * eigene, laufende Datei bekam nie eine: `vorbereiten()` prüfte die
 * Schemaversion, schaltete WAL ein und wandte das Schema an. Ein Bitfehler,
 * ein defekter Datenträger oder ein abgebrochener Schreibvorgang auf einem
 * USB-Stick blieben unbemerkt, bis irgendeine einzelne Abfrage
 * `SQLITE_CORRUPT` warf – irgendwann mitten in einer Sitzung, mit „Ihr
 * Lernprofil konnte nicht geladen werden“ als einziger Auskunft und ohne
 * jedes Angebot.
 *
 * Hier wird an einer **echt beschädigten Datei** gemessen, nicht an einer
 * Attrappe: Eine gültige Datenbank wird angelegt, geschlossen und danach in
 * ihren Datenseiten überschrieben.
 */

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

import Database from 'better-sqlite3';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';

import {
  istBeschaedigt,
  LERNSTAND_BESCHAEDIGT,
  Lernstand,
  lernstandBeiseitelegen,
} from '../src/main/lernstand';
import type { Katalog } from '../src/shared/katalog';

const KATALOG: Katalog = {
  meta: { titel: 'Test', herausgeber: 'Test', stand: '2024-12-16', quelle: 'Test' },
  kapitel: [{ id: 'I', titel: 'Recht', abschnitte: [{ id: 'I.1', titel: 'Grundlagen' }] }],
  bilder: {},
  fragen: [
    {
      id: 'I.1-1',
      kapitel: 'I',
      abschnitt: 'I.1',
      nummer: '1',
      frage: 'Eine Frage?',
      optionen: [{ label: 'a', text: 'Antwort', richtig: true }],
    },
  ],
} as unknown as Katalog;

let verzeichnis = '';
let pfad = '';

/** Legt eine gültige, geschlossene Lernstandsdatei an. */
function gueltigeDatei(): void {
  const db = new Database(pfad);
  new Lernstand(db, KATALOG);
  db.close();
}

/**
 * Überschreibt die Datenseiten – der Kopf bleibt stehen.
 *
 * Nur so entsteht der Fall, um den es geht: SQLite erkennt die Datei
 * weiterhin als Datenbank und öffnet sie klaglos. Erst eine Prüfung merkt,
 * dass der Inhalt hin ist. Wäre der Kopf mit zerstört, scheiterte schon das
 * Öffnen und die Prüfung wäre überflüssig.
 */
function beschaedigen(): void {
  const inhalt = readFileSync(pfad);
  inhalt.fill(0x7f, 1024, Math.min(inhalt.length, 16384));
  writeFileSync(pfad, inhalt);
}

beforeEach(() => {
  verzeichnis = mkdtempSync(join(tmpdir(), 'wsk-heil-'));
  pfad = join(verzeichnis, 'lernstand.db');
});

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

describe('Heilprüfung beim Öffnen', () => {
  it('lässt eine gültige Datei ohne Weiteres durch', () => {
    gueltigeDatei();

    const db = new Database(pfad);
    expect(() => new Lernstand(db, KATALOG)).not.toThrow();
    db.close();
  });

  it('weist eine beschädigte Datei ab, statt sie in Betrieb zu nehmen', () => {
    gueltigeDatei();
    beschaedigen();

    const db = new Database(pfad);
    try {
      expect(() => new Lernstand(db, KATALOG)).toThrow(/beschädigt/u);
    } finally {
      db.close();
    }
  });

  it('macht den Befund für den Hauptprozess erkennbar', () => {
    /* Der Hauptprozess muss genau diesen Fall von allen anderen
       Öffnungsfehlern unterscheiden können – nur bei ihm gibt es etwas
       anzubieten. */
    gueltigeDatei();
    beschaedigen();

    const db = new Database(pfad);
    try {
      new Lernstand(db, KATALOG);
      expect.unreachable('Die beschädigte Datei hätte abgewiesen werden müssen.');
    } catch (fehler: unknown) {
      expect(istBeschaedigt(fehler)).toBe(true);
      expect((fehler as Error).message).toContain(LERNSTAND_BESCHAEDIGT);
    } finally {
      db.close();
    }
  });

  it('hält andere Öffnungsfehler auseinander', () => {
    expect(istBeschaedigt(new Error('Irgendetwas anderes'))).toBe(false);
    expect(istBeschaedigt('kein Fehlerobjekt')).toBe(false);
    expect(istBeschaedigt(null)).toBe(false);
  });

  it('läuft auch gegen eine Datenbank im Arbeitsspeicher', () => {
    /* Die Tests der Anwendung arbeiten durchweg auf `:memory:`. Eine
       Heilprüfung, die dort anschlüge, legte die halbe Testfläche lahm. */
    const db = new Database(':memory:');
    expect(() => new Lernstand(db, KATALOG)).not.toThrow();
    db.close();
  });
});

describe('Beschädigte Datei beiseitelegen', () => {
  it('benennt um, statt zu löschen', () => {
    gueltigeDatei();
    const vorher = readFileSync(pfad);

    const ziel = lernstandBeiseitelegen(pfad, new Date('2026-08-29T12:00:00.000Z'));

    expect(existsSync(pfad)).toBe(false);
    expect(existsSync(ziel)).toBe(true);
    /* Aus einer beschädigten Datei ist oft noch etwas zu holen – gelöscht
       ist sie endgültig weg. Der Inhalt muss deshalb unangetastet bleiben. */
    expect(readFileSync(ziel)).toEqual(vorher);
  });

  it('trägt einen Zeitstempel, damit ein zweiter Fall den ersten nicht überschreibt', () => {
    gueltigeDatei();
    const erstes = lernstandBeiseitelegen(pfad, new Date('2026-08-29T12:00:00.000Z'));

    gueltigeDatei();
    const zweites = lernstandBeiseitelegen(pfad, new Date('2026-08-30T09:30:00.000Z'));

    expect(erstes).not.toBe(zweites);
    expect(existsSync(erstes)).toBe(true);
    expect(existsSync(zweites)).toBe(true);
  });

  it('nimmt Schreibprotokoll und Sperrdatei mit', () => {
    /* Bleiben `-wal` und `-shm` liegen, findet die frisch angelegte
       Datenbank ein Schreibprotokoll vor, das nicht zu ihr gehört –
       derselbe Fehler wie in docs/stand.md 7.8. */
    gueltigeDatei();
    writeFileSync(`${pfad}-wal`, 'protokoll');
    writeFileSync(`${pfad}-shm`, 'sperre');

    const ziel = lernstandBeiseitelegen(pfad, new Date('2026-08-29T12:00:00.000Z'));

    expect(existsSync(`${pfad}-wal`)).toBe(false);
    expect(existsSync(`${pfad}-shm`)).toBe(false);
    expect(existsSync(`${ziel}-wal`)).toBe(true);
    expect(existsSync(`${ziel}-shm`)).toBe(true);
  });

  it('kommt ohne Nebendateien aus', () => {
    gueltigeDatei();

    expect(() => lernstandBeiseitelegen(pfad, new Date('2026-08-29T12:00:00.000Z'))).not.toThrow();
  });

  it('macht den Weg für einen neuen Lernstand frei', () => {
    gueltigeDatei();
    beschaedigen();
    lernstandBeiseitelegen(pfad, new Date('2026-08-29T12:00:00.000Z'));

    const db = new Database(pfad);
    expect(() => new Lernstand(db, KATALOG)).not.toThrow();
    db.close();
  });
});
