// Copyright 2019 The Chromium Authors. All rights reserved.
// 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:file/memory.dart';
import 'package:flutter_tools/runner.dart' as runner;
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart' as io;
import 'package:flutter_tools/src/base/common.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/reporting/reporting.dart';
import 'package:flutter_tools/src/runner/flutter_command.dart';
import 'package:platform/platform.dart';

import '../../src/common.dart';
import '../../src/context.dart';

void main() {
  group('runner', () {
    setUp(() {
      runner.crashFileSystem = MemoryFileSystem();
      // Instead of exiting with dart:io exit(), this causes an exception to
      // be thrown, which we catch with the onError callback in the zone below.
      io.setExitFunctionForTests((int _) { throw 'test exit';});
      Cache.disableLocking();
    });

    tearDown(() {
      runner.crashFileSystem = const LocalFileSystem();
      io.restoreExitFunction();
      Cache.enableLocking();
    });

    testUsingContext('error handling', () async {
      final Completer<void> completer = Completer<void>();
      // runner.run() asynchronously calls the exit function set above, so we
      // catch it in a zone.
      unawaited(runZoned<Future<void>>(
        () {
          unawaited(runner.run(
            <String>['test'],
            <FlutterCommand>[
              CrashingFlutterCommand(),
            ],
            // This flutterVersion disables crash reporting.
            flutterVersion: '[user-branch]/',
            reportCrashes: true,
          ));
          return null;
        },
        onError: (Object error) {
          expect(error, 'test exit');
          completer.complete();
        },
      ));
      await completer.future;

      // This is the main check of this test.
      //
      // We are checking that, even though crash reporting failed with an
      // exception on the first attempt, the second attempt tries to report the
      // *original* crash, and not the crash from the first crash report
      // attempt.
      final CrashingUsage crashingUsage = flutterUsage;
      expect(crashingUsage.sentException, 'runCommand');
    }, overrides: <Type, Generator>{
      Platform: () => FakePlatform(environment: <String, String>{
        'FLUTTER_ANALYTICS_LOG_FILE': 'test',
        'FLUTTER_ROOT': '/',
      }),
      FileSystem: () => MemoryFileSystem(),
      ProcessManager: () => FakeProcessManager.any(),
      Usage: () => CrashingUsage(),
    });
  });
}

class CrashingFlutterCommand extends FlutterCommand {
  @override
  String get description => null;

  @override
  String get name => 'test';

  @override
  Future<FlutterCommandResult> runCommand() async {
    throw 'runCommand';
  }
}

class CrashingUsage implements Usage {
  CrashingUsage() : _impl = Usage(versionOverride: '[user-branch]');

  final Usage _impl;

  dynamic get sentException => _sentException;
  dynamic _sentException;

  bool _firstAttempt = true;

  // Crash while crashing.
  @override
  void sendException(dynamic exception) {
    if (_firstAttempt) {
      _firstAttempt = false;
      throw 'sendException';
    }
    _sentException = exception;
  }

  @override
  bool get isFirstRun => _impl.isFirstRun;

  @override
  bool get suppressAnalytics => _impl.suppressAnalytics;

  @override
  set suppressAnalytics(bool value) {
    _impl.suppressAnalytics = value;
  }

  @override
  bool get enabled => _impl.enabled;

  @override
  set enabled(bool value) {
    _impl.enabled = value;
  }

  @override
  String get clientId => _impl.clientId;

  @override
  void sendCommand(String command, {Map<String, String> parameters}) =>
      _impl.sendCommand(command, parameters: parameters);

  @override
  void sendEvent(
    String category,
    String parameter, {
    String label,
    int value,
    Map<String, String> parameters,
  }) => _impl.sendEvent(
    category,
    parameter,
    label: label,
    value: value,
    parameters: parameters,
  );

  @override
  void sendTiming(
    String category,
    String variableName,
    Duration duration, {
    String label,
  }) => _impl.sendTiming(category, variableName, duration, label: label);

  @override
  Stream<Map<String, dynamic>> get onSend => _impl.onSend;

  @override
  Future<void> ensureAnalyticsSent() => _impl.ensureAnalyticsSent();

  @override
  void printWelcome() => _impl.printWelcome();
}
