/**
 * Voreinstellung der Tastenkürzel bei erkanntem Hilfsmittel – und ihr Ort.
 *
 * Hintergrund der Voreinstellung: Im Lesemodus von NVDA und JAWS sind die
 * Ziffern 1 bis 6 für die Navigation nach Überschriftenebenen belegt. Kürzel,
 * die dieselben Tasten belegen, gehen dort ins Leere oder stören –
 * ausgerechnet beim Beantworten einer Frage, dem wichtigsten Ablauf der
 * Anwendung.
 *
 * Hintergrund des Ortes: Bis 0.21.0 lag die Entscheidung im `localStorage`
 * des Renderers, also außerhalb von Einstellungen und Profil. Seither geht
 * sie über `einstellungen:lesen`/`einstellungen:schreiben` nach
 * `einstellungen.json` – denselben Weg wie „Vorlesen“ und die Anzeigegröße.
 * Geprüft wird der echte Hook, nicht eine Nachbildung seiner Regel.
 */
import { act, renderHook, waitFor } from '@testing-library/react';
import { afterEach, describe, expect, it, vi } from 'vitest';

import { useKuerzelEinstellung } from '../src/renderer/src/hooks/useTastenkuerzel';
import { EINSTELLUNGEN_STANDARD, type Einstellungen } from '../src/shared/ipc';

/** Der frühere Ablageort – nur noch für die einmalige Übernahme. */
const ALTER_SCHLUESSEL = 'waffensachkunde.tastenkuerzel';

/**
 * Bildet den Anwendungskern nach: eine „Datei“, die gelesen und beschrieben
 * wird. `schreibfehler` stellt einen vollen Datenträger nach.
 *
 * Zurück kommt die Liste der Teiländerungen, die den Kern erreicht haben –
 * daran zeigt sich, ob wirklich `einstellungen.json` bedient wird.
 */
function kern(
  gespeichert: Partial<Einstellungen> = {},
  { schreibfehler = false }: { schreibfehler?: boolean } = {},
): { readonly geschrieben: Partial<Einstellungen>[] } {
  let stand: Einstellungen = { ...EINSTELLUNGEN_STANDARD, ...gespeichert };
  const geschrieben: Partial<Einstellungen>[] = [];

  vi.stubGlobal('lernApp', {
    einstellungenLesen: vi.fn(() => Promise.resolve(stand)),
    einstellungenSchreiben: vi.fn((aenderung: Partial<Einstellungen>) => {
      if (schreibfehler) {
        return Promise.reject(new Error('Die Einstellungen konnten nicht gespeichert werden.'));
      }
      geschrieben.push(aenderung);
      stand = { ...stand, ...aenderung };
      return Promise.resolve(stand);
    }),
  });

  return { geschrieben };
}

/* `tests/setup.ts` leert vor jedem Test den localStorage; die Brücke muss
   hier weg, sonst sähe der Test „ohne Anwendungskern“ die Attrappe des
   vorigen. */
afterEach(() => {
  vi.unstubAllGlobals();
});

describe('Voreinstellung der Tastenkürzel', () => {
  it('lässt die Kürzel an, solange kein Hilfsmittel läuft', async () => {
    kern();
    const { result } = renderHook(() => useKuerzelEinstellung(false));

    await waitFor(() => {
      expect(result.current.aktiv).toBe(true);
    });
    expect(result.current.wegenHilfsmittelAus).toBe(false);
  });

  it('schaltet sie ohne eigene Entscheidung ab, wenn ein Hilfsmittel läuft', async () => {
    kern();
    const { result } = renderHook(() => useKuerzelEinstellung(true));

    await waitFor(() => {
      expect(result.current.wegenHilfsmittelAus).toBe(true);
    });
    expect(result.current.aktiv).toBe(false);
  });

  it('achtet eine ausdrückliche Zusage auch bei laufendem Hilfsmittel', async () => {
    // Wer die Kürzel bewusst eingeschaltet hat, soll sie behalten.
    kern({ tastenkuerzel: true });
    const { result } = renderHook(() => useKuerzelEinstellung(true));

    await waitFor(() => {
      expect(result.current.aktiv).toBe(true);
    });
    // Die Begründung „wegen Hilfsmittel“ wäre hier schlicht falsch.
    expect(result.current.wegenHilfsmittelAus).toBe(false);
  });

  it('achtet eine ausdrückliche Absage auch ohne Hilfsmittel', async () => {
    kern({ tastenkuerzel: false });
    const { result } = renderHook(() => useKuerzelEinstellung(false));

    await waitFor(() => {
      expect(result.current.aktiv).toBe(false);
    });
    expect(result.current.wegenHilfsmittelAus).toBe(false);
  });

  it('begründet nichts, solange der Anwendungskern noch antwortet', () => {
    /* Sonst blitzte der Hinweis „Es läuft ein Hilfsmittel …“ am Schalter
       auf, um gleich wieder zu verschwinden – bei jemandem, der die Kürzel
       längst eingeschaltet hat. */
    kern({ tastenkuerzel: true });
    const { result } = renderHook(() => useKuerzelEinstellung(true));

    expect(result.current.wegenHilfsmittelAus).toBe(false);
  });

  it('überschreibt eine eigene Entscheidung nicht, wenn später ein Hilfsmittel startet', async () => {
    kern();
    const { result, rerender } = renderHook(
      ({ hilfsmittel }: { hilfsmittel: boolean }) => useKuerzelEinstellung(hilfsmittel),
      { initialProps: { hilfsmittel: false } },
    );

    await waitFor(() => {
      expect(result.current.aktiv).toBe(true);
    });

    act(() => {
      result.current.setzen(true);
    });
    expect(result.current.aktiv).toBe(true);

    // Screenreader startet mitten in der Sitzung.
    rerender({ hilfsmittel: true });

    expect(result.current.aktiv).toBe(true);
    expect(result.current.wegenHilfsmittelAus).toBe(false);
  });

  it('greift die Voreinstellung auf, wenn das Hilfsmittel ohne eigene Wahl startet', async () => {
    kern();
    const { result, rerender } = renderHook(
      ({ hilfsmittel }: { hilfsmittel: boolean }) => useKuerzelEinstellung(hilfsmittel),
      { initialProps: { hilfsmittel: false } },
    );
    await waitFor(() => {
      expect(result.current.aktiv).toBe(true);
    });

    rerender({ hilfsmittel: true });

    expect(result.current.aktiv).toBe(false);
    expect(result.current.wegenHilfsmittelAus).toBe(true);
  });
});

describe('Ablage der Entscheidung', () => {
  it('schreibt die Wahl in die Einstellungen, nicht in den localStorage', async () => {
    const { geschrieben } = kern();
    const { result } = renderHook(() => useKuerzelEinstellung(false));
    await waitFor(() => {
      expect(result.current.aktiv).toBe(true);
    });

    act(() => {
      result.current.setzen(false);
    });

    await waitFor(() => {
      expect(geschrieben).toEqual([{ tastenkuerzel: false }]);
    });
    expect(window.localStorage.getItem(ALTER_SCHLUESSEL)).toBeNull();
  });

  it('liegt nach einem Neustart wieder an', async () => {
    const { geschrieben } = kern();
    const erster = renderHook(() => useKuerzelEinstellung(false));
    await waitFor(() => {
      expect(erster.result.current.aktiv).toBe(true);
    });
    act(() => {
      erster.result.current.setzen(false);
    });
    await waitFor(() => {
      expect(geschrieben).toHaveLength(1);
    });
    erster.unmount();

    /* Neustart: dieselbe Attrappe, also dieselbe „Datei“ – ein neuer Hook
       liest sie erneut. */
    const { result } = renderHook(() => useKuerzelEinstellung(false));

    await waitFor(() => {
      expect(result.current.aktiv).toBe(false);
    });
  });

  it('meldet, wenn die Wahl nicht gesichert werden konnte', async () => {
    /* Der Schalter springt trotzdem um – aber dass er die Sitzung nicht
       überlebt, muss gesagt werden statt verschwiegen. */
    vi.spyOn(console, 'warn').mockImplementation(() => undefined);
    kern({}, { schreibfehler: true });
    const { result } = renderHook(() => useKuerzelEinstellung(false));
    await waitFor(() => {
      expect(result.current.aktiv).toBe(true);
    });

    act(() => {
      result.current.setzen(false);
    });

    expect(result.current.aktiv).toBe(false);
    await waitFor(() => {
      expect(result.current.meldung).toMatch(/nicht dauerhaft gespeichert/u);
    });
  });
});

describe('Übernahme der bisherigen Wahl aus dem localStorage', () => {
  it('übernimmt einen alten Wert beim ersten Start nach der Umstellung', async () => {
    /* Wer die Kürzel vor der Umstellung abgeschaltet hat, soll sie nicht
       beim nächsten Start wieder eingeschaltet vorfinden. */
    window.localStorage.setItem(ALTER_SCHLUESSEL, 'aus');
    const { geschrieben } = kern();

    const { result } = renderHook(() => useKuerzelEinstellung(false));

    await waitFor(() => {
      expect(result.current.aktiv).toBe(false);
    });
    await waitFor(() => {
      expect(geschrieben).toEqual([{ tastenkuerzel: false }]);
    });
    // Danach ist der alte Eintrag weg und wird nie wieder gelesen.
    expect(window.localStorage.getItem(ALTER_SCHLUESSEL)).toBeNull();
  });

  it('übernimmt auch ein ausdrückliches „an“', async () => {
    window.localStorage.setItem(ALTER_SCHLUESSEL, 'an');
    const { geschrieben } = kern();

    const { result } = renderHook(() => useKuerzelEinstellung(true));

    await waitFor(() => {
      expect(result.current.aktiv).toBe(true);
    });
    expect(geschrieben).toEqual([{ tastenkuerzel: true }]);
    expect(result.current.wegenHilfsmittelAus).toBe(false);
  });

  it('lässt die Datei gewinnen, sobald sie einen Wert hat', async () => {
    /* Nach der Übernahme wird der alte Ort nicht mehr gelesen – auch dann
       nicht, wenn dort noch etwas steht (etwa nach einem gescheiterten
       Löschversuch oder auf einem zweiten Gerät). */
    window.localStorage.setItem(ALTER_SCHLUESSEL, 'aus');
    const { geschrieben } = kern({ tastenkuerzel: true });

    const { result } = renderHook(() => useKuerzelEinstellung(false));

    await waitFor(() => {
      expect(result.current.aktiv).toBe(true);
    });
    expect(geschrieben).toEqual([]);
    expect(window.localStorage.getItem(ALTER_SCHLUESSEL)).toBeNull();
  });

  it('behält den alten Wert, wenn das Übernehmen scheitert', async () => {
    /* Löschen vor dem erfolgreichen Schreiben wäre der eine Fehler, der die
       Entscheidung endgültig vernichtet. Der nächste Start versucht es
       stattdessen erneut. */
    vi.spyOn(console, 'warn').mockImplementation(() => undefined);
    window.localStorage.setItem(ALTER_SCHLUESSEL, 'aus');
    kern({}, { schreibfehler: true });

    const { result } = renderHook(() => useKuerzelEinstellung(false));

    await waitFor(() => {
      expect(result.current.aktiv).toBe(false);
    });
    await waitFor(() => {
      expect(window.localStorage.getItem(ALTER_SCHLUESSEL)).toBe('aus');
    });
  });

  it('kommt ohne Anwendungskern mit der Voreinstellung aus', () => {
    /* Kein Preload (Vitest ohne Attrappe, Vorschau im Browser): Dann gibt es
       nichts nachzuladen, und der Anfangswert ist schon der Endwert. */
    const { result } = renderHook(() => useKuerzelEinstellung(true));

    expect(result.current.aktiv).toBe(false);
    expect(result.current.wegenHilfsmittelAus).toBe(true);
  });
});
