// Copyright 2013 The Flutter Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';

import 'package:fake_async/fake_async.dart';
import 'package:gcloud/storage.dart';
import 'package:googleapis/storage/v1.dart';
import 'package:googleapis_auth/auth_io.dart';
import 'package:metrics_center/src/constants.dart';
import 'package:metrics_center/src/gcs_lock.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';

import 'common.dart';
import 'gcs_lock_test.mocks.dart';
import 'utility.dart';

enum TestPhase { run1, run2 }

@GenerateMocks(
  <Type>[AuthClient, StorageApi],
  customMocks: <MockSpec<dynamic>>[
    MockSpec<ObjectsResource>(onMissingStub: OnMissingStub.returnDefault),
  ],
)
void main() {
  const kDelayStep = Duration(milliseconds: 10);
  final Map<String, dynamic>? credentialsJson = getTestGcpCredentialsJson();

  test('GcsLock prints warnings for long waits', () {
    // Capture print to verify error messages.
    final prints = <String>[];
    final spec = ZoneSpecification(
      print: (_, __, ___, String msg) => prints.add(msg),
    );

    Zone.current.fork(specification: spec).run<void>(() {
      fakeAsync((FakeAsync fakeAsync) {
        final mockClient = MockAuthClient();
        final lock = GcsLock(StorageApi(mockClient), 'mockBucket');
        when(mockClient.send(any)).thenThrow(DetailedApiRequestError(412, ''));
        final Future<void> runFinished = lock.protectedRun(
          'mock.lock',
          () async {},
        );
        fakeAsync.elapse(const Duration(seconds: 10));
        when(mockClient.send(any)).thenThrow(AssertionError('Stop!'));
        runFinished.catchError((dynamic e) {
          final error = e as AssertionError;
          expect(error.message, 'Stop!');
          // TODO(goderbauer): We should not be printing from a test.
          // ignore: avoid_print
          print('${error.message}');
        });
        fakeAsync.elapse(const Duration(seconds: 20));
      });
    });

    const kExpectedErrorMessage =
        'The lock is waiting for a long time: '
        '0:00:10.240000. If the lock file mock.lock in bucket mockBucket '
        'seems to be stuck (i.e., it was created a long time ago and no one '
        'seems to be owning it currently), delete it manually to unblock this.';
    expect(prints, equals(<String>[kExpectedErrorMessage, 'Stop!']));
  });

  test(
    'GcsLock integration test: single protectedRun is successful',
    () async {
      final AutoRefreshingAuthClient client = await clientViaServiceAccount(
        ServiceAccountCredentials.fromJson(credentialsJson),
        Storage.SCOPES,
      );
      final lock = GcsLock(StorageApi(client), kTestBucketName);
      var testValue = 0;
      await lock.protectedRun('test.lock', () async {
        testValue = 1;
      });
      expect(testValue, 1);
    },
    skip: credentialsJson == null,
  );

  test(
    'GcsLock integration test: protectedRun is exclusive',
    () async {
      final AutoRefreshingAuthClient client = await clientViaServiceAccount(
        ServiceAccountCredentials.fromJson(credentialsJson),
        Storage.SCOPES,
      );
      final lock1 = GcsLock(StorageApi(client), kTestBucketName);
      final lock2 = GcsLock(StorageApi(client), kTestBucketName);

      TestPhase phase = TestPhase.run1;
      final started1 = Completer<void>();
      final Future<void> finished1 = lock1.protectedRun('test.lock', () async {
        started1.complete();
        while (phase == TestPhase.run1) {
          await Future<void>.delayed(kDelayStep);
        }
      });

      await started1.future;

      final started2 = Completer<void>();
      final Future<void> finished2 = lock2.protectedRun('test.lock', () async {
        started2.complete();
      });

      // started2 should not be set even after a long wait because lock1 is
      // holding the GCS lock file.
      await Future<void>.delayed(kDelayStep * 10);
      expect(started2.isCompleted, false);

      // When phase is switched to run2, lock1 should be released soon and
      // lock2 should soon be able to proceed its protectedRun.
      phase = TestPhase.run2;
      await started2.future;
      await finished1;
      await finished2;
    },
    skip: credentialsJson == null,
  );

  test('GcsLock attempts to unlock again on a DetailedApiRequestError', () async {
    fakeAsync((FakeAsync fakeAsync) {
      final StorageApi mockStorageApi = MockStorageApi();
      final ObjectsResource mockObjectsResource = MockObjectsResource();
      final gcsLock = GcsLock(mockStorageApi, kTestBucketName);
      const lockFileName = 'test.lock';
      when(mockStorageApi.objects).thenReturn(mockObjectsResource);

      // Simulate a failure to delete a lock file.
      when(
        mockObjectsResource.delete(kTestBucketName, lockFileName),
      ).thenThrow(DetailedApiRequestError(504, ''));

      gcsLock.protectedRun(lockFileName, () async {});

      // Allow time to pass by to ensure deleting the lock file is retried multiple times.
      fakeAsync.elapse(const Duration(milliseconds: 30));
      verify(
        mockObjectsResource.delete(kTestBucketName, lockFileName),
      ).called(3);

      // Simulate a successful deletion of the lock file.
      when(
        mockObjectsResource.delete(kTestBucketName, lockFileName),
      ).thenAnswer(
        (_) => Future<void>(() {
          return;
        }),
      );

      // At this point, there should only be one more (successful) attempt to delete the lock file.
      fakeAsync.elapse(const Duration(minutes: 2));
      verify(
        mockObjectsResource.delete(kTestBucketName, lockFileName),
      ).called(1);
    });
  });
}