import 'package:flutter_test/flutter_test.dart';
import 'package:pyramid/core/settings_prefs.dart';
import 'package:shared_preferences/shared_preferences.dart';

/// Tests für die Storage-Fassade, die laut M2-Modulschnitt zur Pflicht-
/// Schnittstelle für alle Einstellungs-Zugriffe ausgebaut werden soll –
/// dieses Vertragsnetz muss bei dem Umbau stehen bleiben.
void main() {
  TestWidgetsFlutterBinding.ensureInitialized();

  setUp(() {
    // Frischer In-Memory-Store pro Test (setzt auch die gecachte
    // SharedPreferences-Instanz zurück).
    SharedPreferences.setMockInitialValues({});
  });

  group('BoolPref', () {
    test('lädt persistierten Wert und überstimmt damit den Default', () async {
      SharedPreferences.setMockInitialValues({'notif_sound': false});
      final pref = BoolPref('notif_sound', true);
      expect(pref.state, true, reason: 'Default gilt, bis geladen ist');
      await pumpEventQueue();
      expect(pref.state, false, reason: 'persistierter Wert muss gewinnen');
    });

    test(
        'set() sofort nach Erstellung gewinnt gegen den später ladenden '
        'persistierten Wert (Lade-Race)', () async {
      SharedPreferences.setMockInitialValues({'notif_sound': false});
      final pref = BoolPref('notif_sound', false);
      final done = pref.set(true); // bevor _load fertig ist
      await pumpEventQueue();
      await done;
      expect(pref.state, true,
          reason: 'die Nutzeraktion darf nicht vom Lade-Ergebnis '
              'zurücküberschrieben werden');
      final p = await SharedPreferences.getInstance();
      expect(p.getBool('notif_sound'), true);
    });

    test('set/toggle ändern Zustand und persistieren', () async {
      final pref = BoolPref('notif_desktop', true);
      await pumpEventQueue();
      pref.toggle();
      await pumpEventQueue();
      expect(pref.state, false);
      final p = await SharedPreferences.getInstance();
      expect(p.getBool('notif_desktop'), false);
    });
  });

  group('StringSetPref', () {
    test('lädt persistierte Liste als Set, add/remove persistieren', () async {
      SharedPreferences.setMockInitialValues({
        'muted_rooms': ['!a:hs', '!b:hs'],
      });
      final pref = StringSetPref('muted_rooms');
      await pumpEventQueue();
      expect(pref.state, {'!a:hs', '!b:hs'});

      await pref.add('!c:hs');
      await pref.remove('!a:hs');
      final p = await SharedPreferences.getInstance();
      expect(p.getStringList('muted_rooms')!.toSet(), {'!b:hs', '!c:hs'});
    });

    test(
        'add() sofort nach Erstellung verliert keine bereits persistierten '
        'Einträge (Lade-Race)', () async {
      SharedPreferences.setMockInitialValues({
        'muted_rooms': ['!a:hs'],
      });
      final pref = StringSetPref('muted_rooms');
      final done = pref.add('!b:hs'); // bevor _load fertig ist
      await pumpEventQueue();
      await done;
      expect(pref.state, {'!a:hs', '!b:hs'});
      final p = await SharedPreferences.getInstance();
      expect(p.getStringList('muted_rooms')!.toSet(), {'!a:hs', '!b:hs'},
          reason: 'Read-Modify-Write auf ungeladenem State darf keine '
              'persistierten Einträge verlieren');
    });

    test('doppeltes add und remove von Fehlendem sind No-Ops', () async {
      final pref = StringSetPref('muted_rooms');
      await pumpEventQueue();
      await pref.add('!a:hs');
      await pref.add('!a:hs');
      await pref.remove('!gibtsnicht:hs');
      expect(pref.state, {'!a:hs'});
    });
  });

  group('Theme-Prefs', () {
    test('leerer Store liefert die dokumentierten Defaults', () async {
      final t = await loadThemePrefs();
      expect(t.isDark, true);
      expect(t.accentIdx, 0);
      expect(t.radius, 12.0);
      expect(t.motion, 1.0);
    });

    test('Roundtrip: gespeicherte Werte kommen zurück, Teil-Save lässt '
        'den Rest unangetastet', () async {
      await saveThemePrefs(isDark: false, accentIdx: 3, radius: 8, motion: 0.5);
      var t = await loadThemePrefs();
      expect((t.isDark, t.accentIdx, t.radius, t.motion), (false, 3, 8.0, 0.5));

      // Nur ein Feld ändern – die anderen dürfen nicht zurückfallen.
      await saveThemePrefs(accentIdx: 1);
      t = await loadThemePrefs();
      expect((t.isDark, t.accentIdx, t.radius, t.motion), (false, 1, 8.0, 0.5));
    });
  });

  group('Server-Banner', () {
    test('Roundtrip + Entfernen per null/leerem mxcUri', () async {
      await saveServerBanner('https://hs1', 'mxc://hs1/abc');
      await saveServerBanner('https://hs2', 'mxc://hs2/def');
      expect(await loadAllServerBanners(),
          {'https://hs1': 'mxc://hs1/abc', 'https://hs2': 'mxc://hs2/def'});

      await saveServerBanner('https://hs1', null);
      await saveServerBanner('https://hs2', '');
      expect(await loadAllServerBanners(), isEmpty);
    });

    test('korruptes JSON im Store crasht nicht, sondern liefert leere Map',
        () async {
      SharedPreferences.setMockInitialValues(
          {'server_banners': '{kein json'});
      expect(await loadAllServerBanners(), isEmpty);
    });
  });
}
