import 'dart:async';
import 'dart:io';

import 'package:flutter_test/flutter_test.dart';
import 'package:marianum_mobile/api/retry.dart';
import 'package:nextcloud/nextcloud.dart';

void main() {
  // Instant delays so the tests never wait on real timers.
  Duration noDelay(int _) => Duration.zero;

  group('isTransientServerError', () {
    test('5xx DynamiteApiException is transient', () {
      expect(
        isTransientServerError(const DynamiteApiException(500, {}, '')),
        isTrue,
      );
      expect(
        isTransientServerError(const DynamiteApiException(502, {}, '')),
        isTrue,
      );
      expect(
        isTransientServerError(const DynamiteApiException(503, {}, '')),
        isTrue,
      );
    });

    test('4xx DynamiteApiException is not transient', () {
      expect(
        isTransientServerError(const DynamiteApiException(404, {}, '')),
        isFalse,
      );
      expect(
        isTransientServerError(const DynamiteApiException(412, {}, '')),
        isFalse,
      );
      expect(
        isTransientServerError(const DynamiteApiException(429, {}, '')),
        isFalse,
      );
    });

    test('other error types are not transient', () {
      expect(isTransientServerError(TimeoutException('slow')), isFalse);
      expect(isTransientServerError(const SocketException('down')), isFalse);
      expect(isTransientServerError(StateError('boom')), isFalse);
    });
  });

  group('retryOnTransientError', () {
    test('returns the result of a first-try success without retrying', () async {
      var calls = 0;
      final result = await retryOnTransientError(() async {
        calls++;
        return 'ok';
      }, delayFor: noDelay);

      expect(result, 'ok');
      expect(calls, 1);
    });

    test('retries transient failures until an attempt succeeds', () async {
      var calls = 0;
      final result = await retryOnTransientError(() async {
        calls++;
        if (calls < 3) throw const DynamiteApiException(500, {}, '');
        return 'ok';
      }, delayFor: noDelay);

      expect(result, 'ok');
      expect(calls, 3);
    });

    test('rethrows the last error once maxAttempts is exhausted', () async {
      var calls = 0;
      await expectLater(
        retryOnTransientError(() async {
          calls++;
          throw const DynamiteApiException(503, {}, '');
        }, delayFor: noDelay),
        throwsA(
          isA<DynamiteApiException>().having(
            (e) => e.statusCode,
            'statusCode',
            503,
          ),
        ),
      );
      expect(calls, 3);
    });

    test('rethrows non-transient errors immediately', () async {
      var calls = 0;
      await expectLater(
        retryOnTransientError(() async {
          calls++;
          throw const DynamiteApiException(404, {}, '');
        }, delayFor: noDelay),
        throwsA(isA<DynamiteApiException>()),
      );
      expect(calls, 1);
    });

    test('rethrows unrelated exceptions immediately', () async {
      var calls = 0;
      await expectLater(
        retryOnTransientError(() async {
          calls++;
          throw StateError('boom');
        }, delayFor: noDelay),
        throwsStateError,
      );
      expect(calls, 1);
    });

    test('reports upcoming attempts via onRetry and delays via delayFor', () async {
      final retriesSeen = <(int, int)>[];
      final delaysRequested = <int>[];

      await expectLater(
        retryOnTransientError(
          () async => throw const DynamiteApiException(500, {}, ''),
          delayFor: (retry) {
            delaysRequested.add(retry);
            return Duration.zero;
          },
          onRetry: (next, max) => retriesSeen.add((next, max)),
        ),
        throwsA(isA<DynamiteApiException>()),
      );

      expect(retriesSeen, [(2, 3), (3, 3)]);
      expect(delaysRequested, [1, 2]);
    });

    test('respects a custom shouldRetry predicate', () async {
      var calls = 0;
      final result = await retryOnTransientError(
        () async {
          calls++;
          if (calls < 2) throw StateError('flaky');
          return calls;
        },
        delayFor: noDelay,
        shouldRetry: (e) => e is StateError,
      );

      expect(result, 2);
      expect(calls, 2);
    });

    test('respects a custom maxAttempts', () async {
      var calls = 0;
      await expectLater(
        retryOnTransientError(
          () async {
            calls++;
            throw const DynamiteApiException(500, {}, '');
          },
          maxAttempts: 5,
          delayFor: noDelay,
        ),
        throwsA(isA<DynamiteApiException>()),
      );
      expect(calls, 5);
    });
  });
}
