// 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.

import 'dart:async';

import 'package:args/command_runner.dart';
import 'package:file/file.dart';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/commands/upgrade.dart';
import 'package:flutter_tools/src/version.dart';

import '../../src/context.dart';
import '../../src/fake_process_manager.dart';
import '../../src/fakes.dart' show FakeFlutterVersion;
import '../../src/test_flutter_command_runner.dart';

void main() {
  late FileSystem fileSystem;
  late BufferLogger logger;
  late FakeProcessManager processManager;
  UpgradeCommand command;
  late CommandRunner<void> runner;
  const String flutterRoot = '/path/to/flutter';

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

  setUp(() {
    fileSystem = MemoryFileSystem.test();
    logger = BufferLogger.test();
    processManager = FakeProcessManager.empty();
    command = UpgradeCommand(
      verboseHelp: false,
    );
    runner = createTestCommandRunner(command);
  });

  testUsingContext('can auto-migrate a user from dev to beta', () async {
    const String startingTag = '3.0.0-1.2.pre';
    const String latestUpstreamTag = '3.0.0-1.3.pre';
    const String upstreamHeadRevision = 'deadbeef';
    final Completer<void> reEntryCompleter = Completer<void>();

    Future<void> reEnterTool(List<String> command) async {
      await runner.run(<String>['upgrade', '--continue', '--no-version-check']);
      reEntryCompleter.complete();
    }

    processManager.addCommands(<FakeCommand>[
      const FakeCommand(
        command: <String>['git', 'tag', '--points-at', 'HEAD'],
        stdout: startingTag,
      ),
      // Ensure we have upstream tags present locally
      const FakeCommand(
        command: <String>['git', 'fetch', '--tags'],
      ),
      const FakeCommand(
        command: <String>['git', 'rev-parse', '--verify', '@{upstream}'],
        stdout: upstreamHeadRevision,
      ),
      const FakeCommand(
        command: <String>['git', 'tag', '--points-at', upstreamHeadRevision],
        stdout: latestUpstreamTag,
      ),
      // check for uncommitted changes; empty stdout means clean checkout
      const FakeCommand(
        command: <String>['git', 'status', '-s'],
      ),

      // here the tool is upgrading the branch from dev -> beta
      const FakeCommand(
        command: <String>['git', 'fetch'],
      ),
      // test if there already exists a local beta branch; 0 exit code means yes
      const FakeCommand(
        command: <String>['git', 'show-ref', '--verify', '--quiet', 'refs/heads/beta'],
      ),
      const FakeCommand(
        command: <String>['git', 'checkout', 'beta', '--'],
      ),

      // reset instead of pull since cherrypicks from one release branch will
      // not be present on a newer one
      const FakeCommand(
        command: <String>['git', 'reset', '--hard', upstreamHeadRevision],
      ),
      // re-enter flutter command with the newer version, so that `doctor`
      // checks will be up to date
      FakeCommand(
        command: const <String>['bin/flutter', 'upgrade', '--continue', '--no-version-check'],
        onRun: reEnterTool,
        completer: reEntryCompleter,
      ),

      // commands following this are from the re-entrant `flutter upgrade --continue` call

      const FakeCommand(
        command: <String>['git', 'tag', '--points-at', 'HEAD'],
        stdout: latestUpstreamTag,
      ),
      const FakeCommand(
        command: <String>['bin/flutter', '--no-color', '--no-version-check', 'precache'],
      ),
      const FakeCommand(
        command: <String>['bin/flutter', '--no-version-check', 'doctor'],
      ),
    ]);
    await runner.run(<String>['upgrade']);
    expect(processManager, hasNoRemainingExpectations);
    expect(logger.statusText, contains("Transitioning from 'dev' to 'beta'..."));
  }, overrides: <Type, Generator>{
    FileSystem: () => fileSystem,
    FlutterVersion: () => FakeFlutterVersion(branch: 'dev'),
    Logger: () => logger,
    ProcessManager: () => processManager,
  });

  const String startingTag = '3.0.0';
  const String latestUpstreamTag = '3.1.0';
  const String upstreamHeadRevision = '5765737420536964652053746f7279';

  testUsingContext('can push people from master to beta', () async {
    final Completer<void> reEntryCompleter = Completer<void>();

    Future<void> reEnterTool(List<String> args) async {
      await runner.run(<String>['upgrade', '--continue', '--no-version-check']);
      reEntryCompleter.complete();
    }

    processManager.addCommands(<FakeCommand>[
      const FakeCommand(
        command: <String>['git', 'tag', '--points-at', 'HEAD'],
        stdout: startingTag,
      ),
      const FakeCommand(
        command: <String>['git', 'fetch', '--tags'],
      ),
      const FakeCommand(
        command: <String>['git', 'rev-parse', '--verify', '@{upstream}'],
        stdout: upstreamHeadRevision,
      ),
      const FakeCommand(
        command: <String>['git', 'tag', '--points-at', upstreamHeadRevision],
        stdout: latestUpstreamTag,
      ),
      const FakeCommand(
        command: <String>['git', 'status', '-s'],
      ),
      const FakeCommand(
        command: <String>['git', 'reset', '--hard', upstreamHeadRevision],
      ),
      FakeCommand(
        command: const <String>['bin/flutter', 'upgrade', '--continue', '--no-version-check'],
        onRun: reEnterTool,
        completer: reEntryCompleter,
      ),

      // commands following this are from the re-entrant `flutter upgrade --continue` call

      const FakeCommand(
        command: <String>['git', 'tag', '--points-at', 'HEAD'],
        stdout: latestUpstreamTag,
      ),
      const FakeCommand(
        command: <String>['bin/flutter', '--no-color', '--no-version-check', 'precache'],
      ),
      const FakeCommand(
        command: <String>['bin/flutter', '--no-version-check', 'doctor'],
      ),
    ]);
    await runner.run(<String>['upgrade']);
    expect(processManager, hasNoRemainingExpectations);
    expect(logger.statusText,
      'Upgrading Flutter to 3.1.0 from 3.0.0 in ${Cache.flutterRoot}...\n'
      '\n'
      'Upgrading engine...\n'
      '\n'
      "Instance of 'FakeFlutterVersion'\n" // the real FlutterVersion has a better toString, heh
      '\n'
      'Running flutter doctor...\n'
      '\n'
      'This channel is intended for Flutter contributors. This channel is not as thoroughly '
      'tested as the "beta" and "stable" channels. We do not recommend using this channel '
      'for normal use as it more likely to contain serious regressions.\n'
      '\n'
      'For information on contributing to Flutter, see our contributing guide:\n'
      '    https://github.com/flutter/flutter/blob/master/CONTRIBUTING.md\n'
      '\n'
      'For the most up to date stable version of flutter, consider using the "beta" channel '
      'instead. The Flutter "beta" channel enjoys all the same automated testing as the '
      '"stable" channel, but is updated roughly once a month instead of once a quarter.\n'
      'To change channel, run the "flutter channel beta" command.\n'
    );
  }, overrides: <Type, Generator>{
    FileSystem: () => fileSystem,
    FlutterVersion: () => FakeFlutterVersion(frameworkVersion: startingTag, engineRevision: 'engine'),
    Logger: () => logger,
    ProcessManager: () => processManager,
  });

  testUsingContext('do not push people from beta to anything else', () async {
    final Completer<void> reEntryCompleter = Completer<void>();

    Future<void> reEnterTool(List<String> command) async {
      await runner.run(<String>['upgrade', '--continue', '--no-version-check']);
      reEntryCompleter.complete();
    }

    processManager.addCommands(<FakeCommand>[
      const FakeCommand(
        command: <String>['git', 'tag', '--points-at', 'HEAD'],
        stdout: startingTag,
        workingDirectory: flutterRoot,
      ),
      const FakeCommand(
        command: <String>['git', 'fetch', '--tags'],
        workingDirectory: flutterRoot,
      ),
      const FakeCommand(
        command: <String>['git', 'rev-parse', '--verify', '@{upstream}'],
        stdout: upstreamHeadRevision,
        workingDirectory: flutterRoot,
      ),
      const FakeCommand(
        command: <String>['git', 'tag', '--points-at', upstreamHeadRevision],
        stdout: latestUpstreamTag,
        workingDirectory: flutterRoot,
      ),
      const FakeCommand(
        command: <String>['git', 'status', '-s'],
        workingDirectory: flutterRoot,
      ),
      const FakeCommand(
        command: <String>['git', 'reset', '--hard', upstreamHeadRevision],
        workingDirectory: flutterRoot,
      ),
      FakeCommand(
        command: const <String>['bin/flutter', 'upgrade', '--continue', '--no-version-check'],
        onRun: reEnterTool,
        completer: reEntryCompleter,
        workingDirectory: flutterRoot,
      ),

      // commands following this are from the re-entrant `flutter upgrade --continue` call

      const FakeCommand(
        command: <String>['git', 'tag', '--points-at', 'HEAD'],
        stdout: latestUpstreamTag,
        workingDirectory: flutterRoot,
      ),
      const FakeCommand(
        command: <String>['bin/flutter', '--no-color', '--no-version-check', 'precache'],
        workingDirectory: flutterRoot,
      ),
      const FakeCommand(
        command: <String>['bin/flutter', '--no-version-check', 'doctor'],
        workingDirectory: flutterRoot,
      ),
    ]);
    await runner.run(<String>['upgrade']);
    expect(processManager, hasNoRemainingExpectations);
    expect(logger.statusText,
      'Upgrading Flutter to 3.1.0 from 3.0.0 in ${Cache.flutterRoot}...\n'
      '\n'
      'Upgrading engine...\n'
      '\n'
      "Instance of 'FakeFlutterVersion'\n" // the real FlutterVersion has a better toString, heh
      '\n'
      'Running flutter doctor...\n'
    );
  }, overrides: <Type, Generator>{
    FileSystem: () => fileSystem,
    FlutterVersion: () => FakeFlutterVersion(branch: 'beta', frameworkVersion: startingTag, engineRevision: 'engine'),
    Logger: () => logger,
    ProcessManager: () => processManager,
  });
}