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

// @dart = 2.8

import 'dart:async';

import 'package:dds/src/dap/protocol_generated.dart';
import 'package:file/file.dart';
import 'package:flutter_tools/src/cache.dart';

import '../../src/common.dart';
import '../test_data/basic_project.dart';
import '../test_data/compile_error_project.dart';
import '../test_utils.dart';
import 'test_client.dart';
import 'test_support.dart';

void main() {
  Directory tempDir;
  /*late*/ DapTestSession dap;
  final String relativeMainPath = 'lib${fileSystem.path.separator}main.dart';

  setUpAll(() {
    Cache.flutterRoot = getFlutterRoot();
  });

  setUp(() async {
    tempDir = createResolvedTempDirectorySync('flutter_adapter_test.');
    dap = await DapTestSession.setUp();
  });

  tearDown(() async {
    await dap.tearDown();
    tryToDelete(tempDir);
  });

  testWithoutContext('can run and terminate a Flutter app in debug mode', () async {
    final BasicProject _project = BasicProject();
    await _project.setUpIn(tempDir);

    // Once the "topLevelFunction" output arrives, we can terminate the app.
    unawaited(
      dap.client.outputEvents
          .firstWhere((OutputEventBody output) => output.output.startsWith('topLevelFunction'))
          .whenComplete(() => dap.client.terminate()),
    );

    final List<OutputEventBody> outputEvents = await dap.client.collectAllOutput(
      launch: () => dap.client
          .launch(
            cwd: _project.dir.path,
            toolArgs: <String>['-d', 'flutter-tester'],
          ),
    );

    final String output = _uniqueOutputLines(outputEvents);

    expectLines(output, <Object>[
      'Launching $relativeMainPath on Flutter test device in debug mode...',
      startsWith('Connecting to VM Service at'),
      'topLevelFunction',
      '',
      startsWith('Exited'),
    ]);
  });

  testWithoutContext('can run and terminate a Flutter app in noDebug mode', () async {
    final BasicProject _project = BasicProject();
    await _project.setUpIn(tempDir);

    // Once the "topLevelFunction" output arrives, we can terminate the app.
    unawaited(
      dap.client.outputEvents
          .firstWhere((OutputEventBody output) => output.output.startsWith('topLevelFunction'))
          .whenComplete(() => dap.client.terminate()),
    );

    final List<OutputEventBody> outputEvents = await dap.client.collectAllOutput(
      launch: () => dap.client
          .launch(
            cwd: _project.dir.path,
            noDebug: true,
            toolArgs: <String>['-d', 'flutter-tester'],
          ),
    );

    final String output = _uniqueOutputLines(outputEvents);

    expectLines(output, <Object>[
      'Launching $relativeMainPath on Flutter test device in debug mode...',
      'topLevelFunction',
      '',
      startsWith('Exited'),
    ]);
  });

  testWithoutContext('correctly outputs launch errors and terminates', () async {
    final CompileErrorProject _project = CompileErrorProject();
    await _project.setUpIn(tempDir);

    final List<OutputEventBody> outputEvents = await dap.client.collectAllOutput(
      launch: () => dap.client
          .launch(
            cwd: _project.dir.path,
            toolArgs: <String>['-d', 'flutter-tester'],
          ),
    );

    final String output = _uniqueOutputLines(outputEvents);
    expect(output, contains('this code does not compile'));
    expect(output, contains('Exception: Failed to build'));
    expect(output, contains('Exited (1)'));
  });

  testWithoutContext('can hot reload', () async {
    final BasicProject _project = BasicProject();
    await _project.setUpIn(tempDir);

    // Launch the app and wait for it to print "topLevelFunction".
    await Future.wait(<Future<Object>>[
      dap.client.outputEvents.firstWhere((OutputEventBody output) => output.output.startsWith('topLevelFunction')),
      dap.client.start(
        launch: () => dap.client.launch(
          cwd: _project.dir.path,
          noDebug: true,
          toolArgs: <String>['-d', 'flutter-tester'],
        ),
      ),
    ], eagerError: true);

    // Capture the next two output events that we expect to be the Reload
    // notification and then topLevelFunction being printed again.
    final Future<List<String>> outputEventsFuture = dap.client.output
        // But skip any topLevelFunctions that come before the reload.
        .skipWhile((String output) => output.startsWith('topLevelFunction'))
        .take(2)
        .toList();

    await dap.client.hotReload();

    expectLines(
        (await outputEventsFuture).join(),
        <Object>[
          startsWith('Reloaded'),
          'topLevelFunction',
        ],
    );

    await dap.client.terminate();
  });

  testWithoutContext('can hot restart', () async {
    final BasicProject _project = BasicProject();
    await _project.setUpIn(tempDir);

    // Launch the app and wait for it to print "topLevelFunction".
    await Future.wait(<Future<Object>>[
      dap.client.outputEvents.firstWhere((OutputEventBody output) => output.output.startsWith('topLevelFunction')),
      dap.client.start(
        launch: () => dap.client.launch(
          cwd: _project.dir.path,
          noDebug: true,
          toolArgs: <String>['-d', 'flutter-tester'],
        ),
      ),
    ], eagerError: true);

    // Capture the next two output events that we expect to be the Restart
    // notification and then topLevelFunction being printed again.
    final Future<List<String>> outputEventsFuture = dap.client.output
        // But skip any topLevelFunctions that come before the restart.
        .skipWhile((String output) => output.startsWith('topLevelFunction'))
        .take(2)
        .toList();

    await dap.client.hotRestart();

    expectLines(
        (await outputEventsFuture).join(),
        <Object>[
          startsWith('Restarted application'),
          'topLevelFunction',
        ],
    );

    await dap.client.terminate();
  });
}

/// Extracts the output from a set of [OutputEventBody], removing any
/// adjacent duplicates and combining into a single string.
String _uniqueOutputLines(List<OutputEventBody> outputEvents) {
  String/*?*/ lastItem;
  return outputEvents
      .map((OutputEventBody e) => e.output)
      .where((String output) {
        // Skip the item if it's the same as the previous one.
        final bool isDupe = output == lastItem;
        lastItem = output;
        return !isDupe;
      })
      .join();
}