import 'package:fake_async/fake_async.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:marianum_mobile/utils/debouncer.dart';

void main() {
  // Each test is wrapped in fakeAsync so timers fire deterministically.
  group('Debouncer.debounce', () {
    test(
      'runs the action once after the delay elapses without further calls',
      () {
        fakeAsync((async) {
          var calls = 0;
          Debouncer.debounce(
            'tag',
            const Duration(milliseconds: 100),
            () => calls++,
          );

          async.elapse(const Duration(milliseconds: 99));
          expect(calls, 0);

          async.elapse(const Duration(milliseconds: 1));
          expect(calls, 1);
        });
      },
    );

    test('subsequent calls within the delay reset the timer (coalesce)', () {
      fakeAsync((async) {
        var calls = 0;
        void schedule() => Debouncer.debounce(
          'tag',
          const Duration(milliseconds: 100),
          () => calls++,
        );

        schedule();
        async.elapse(const Duration(milliseconds: 80));
        schedule(); // resets
        async.elapse(const Duration(milliseconds: 80));
        schedule(); // resets
        async.elapse(const Duration(milliseconds: 80));
        expect(calls, 0, reason: 'each schedule() resets the timer');

        async.elapse(const Duration(milliseconds: 100));
        expect(calls, 1);
      });
    });

    test('different tags are independent', () {
      fakeAsync((async) {
        var aCalls = 0;
        var bCalls = 0;
        Debouncer.debounce(
          'a',
          const Duration(milliseconds: 100),
          () => aCalls++,
        );
        Debouncer.debounce(
          'b',
          const Duration(milliseconds: 100),
          () => bCalls++,
        );

        async.elapse(const Duration(milliseconds: 100));
        expect(aCalls, 1);
        expect(bCalls, 1);
      });
    });
  });

  group('Debouncer.throttle', () {
    test(
      'first call runs immediately, subsequent calls within window are dropped',
      () {
        fakeAsync((async) {
          var calls = 0;
          Debouncer.throttle(
            'tag',
            const Duration(milliseconds: 100),
            () => calls++,
          );
          expect(
            calls,
            1,
            reason: 'throttle fires the first call synchronously',
          );

          Debouncer.throttle(
            'tag',
            const Duration(milliseconds: 100),
            () => calls++,
          );
          Debouncer.throttle(
            'tag',
            const Duration(milliseconds: 100),
            () => calls++,
          );
          expect(
            calls,
            1,
            reason: 'subsequent calls within the gate are ignored',
          );

          async.elapse(const Duration(milliseconds: 100));
          Debouncer.throttle(
            'tag',
            const Duration(milliseconds: 100),
            () => calls++,
          );
          expect(
            calls,
            2,
            reason: 'after the window elapses, throttle fires again',
          );
        });
      },
    );

    test('different tags throttle independently', () {
      fakeAsync((async) {
        var aCalls = 0;
        var bCalls = 0;
        Debouncer.throttle(
          'a',
          const Duration(milliseconds: 100),
          () => aCalls++,
        );
        Debouncer.throttle(
          'b',
          const Duration(milliseconds: 100),
          () => bCalls++,
        );
        expect(aCalls, 1);
        expect(bCalls, 1);

        async.elapse(const Duration(milliseconds: 100));
      });
    });
  });

  group('Debouncer.cancel', () {
    test('cancels a pending debounce so the action never runs', () {
      fakeAsync((async) {
        var calls = 0;
        Debouncer.debounce(
          'tag',
          const Duration(milliseconds: 100),
          () => calls++,
        );
        Debouncer.cancel('tag');

        async.elapse(const Duration(milliseconds: 200));
        expect(calls, 0);
      });
    });

    test(
      'cancels an active throttle gate so the next call fires immediately',
      () {
        fakeAsync((async) {
          var calls = 0;
          Debouncer.throttle(
            'tag',
            const Duration(milliseconds: 100),
            () => calls++,
          );
          expect(calls, 1);

          Debouncer.cancel('tag');
          Debouncer.throttle(
            'tag',
            const Duration(milliseconds: 100),
            () => calls++,
          );
          expect(
            calls,
            2,
            reason: 'cancel removed the gate so the next throttle fires again',
          );

          async.elapse(const Duration(milliseconds: 100));
        });
      },
    );
  });
}
