// 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:convert' show jsonDecode;

import 'package:args/command_runner.dart';
import 'package:conductor_core/src/proto/conductor_state.pb.dart' as pb;
import 'package:conductor_core/src/proto/conductor_state.pbenum.dart';
import 'package:conductor_core/src/repository.dart';
import 'package:conductor_core/src/start.dart';
import 'package:conductor_core/src/state.dart';
import 'package:file/file.dart';
import 'package:file/memory.dart';
import 'package:platform/platform.dart';

import './common.dart';

void main() {
  group('start command', () {
    const String branchPointRevision =
        '5131a6e5e0c50b8b7b2906cd58dab8746d6450be';
    const String flutterRoot = '/flutter';
    const String checkoutsParentDirectory = '$flutterRoot/dev/tools/';
    const String githubUsername = 'user';
    const String frameworkMirror =
        'git@github.com:$githubUsername/flutter.git';
    const String engineMirror = 'git@github.com:$githubUsername/engine.git';
    const String candidateBranch = 'flutter-1.2-candidate.3';
    const String releaseChannel = 'beta';
    const String revision = 'abcd1234';
    const String conductorVersion = 'deadbeef';
    late Checkouts checkouts;
    late MemoryFileSystem fileSystem;
    late FakePlatform platform;
    late TestStdio stdio;
    late FakeProcessManager processManager;

    setUp(() {
      stdio = TestStdio();
      fileSystem = MemoryFileSystem.test();
    });

    CommandRunner<void> createRunner({
      Map<String, String>? environment,
      String? operatingSystem,
      List<FakeCommand>? commands,
    }) {
      operatingSystem ??= const LocalPlatform().operatingSystem;
      final String pathSeparator = operatingSystem == 'windows' ? r'\' : '/';
      environment ??= <String, String>{
        'HOME': '/path/to/user/home',
      };
      final Directory homeDir = fileSystem.directory(
        environment['HOME'],
      );
      // Tool assumes this exists
      homeDir.createSync(recursive: true);
      platform = FakePlatform(
        environment: environment,
        operatingSystem: operatingSystem,
        pathSeparator: pathSeparator,
      );
      processManager = FakeProcessManager.list(commands ?? <FakeCommand>[]);
      checkouts = Checkouts(
        fileSystem: fileSystem,
        parentDirectory: fileSystem.directory(checkoutsParentDirectory),
        platform: platform,
        processManager: processManager,
        stdio: stdio,
      );
      final StartCommand command = StartCommand(
        checkouts: checkouts,
        conductorVersion: conductorVersion,
      );
      return CommandRunner<void>('codesign-test', '')..addCommand(command);
    }

    test('throws exception if run from Windows', () async {
      final CommandRunner<void> runner = createRunner(
        commands: <FakeCommand>[
          const FakeCommand(
            command: <String>['git', 'rev-parse', 'HEAD'],
            stdout: revision,
          ),
        ],
        operatingSystem: 'windows',
      );
      await expectLater(
        () async => runner.run(<String>[
          'start',
          '--$kCandidateOption',
          candidateBranch,
          '--$kReleaseOption',
          'beta',
          '--$kStateOption',
          '/path/to/statefile.json',
        ]),
        throwsExceptionWith(
          'Error! This tool is only supported on macOS and Linux',
        ),
      );
    });

    test('throws if provided an invalid --$kVersionOverrideOption', () async {
      final CommandRunner<void> runner = createRunner();

      final String stateFilePath = fileSystem.path.join(
        platform.environment['HOME']!,
        kStateFileName,
      );

      await expectLater(
        () async => runner.run(<String>[
          'start',
          '--$kCandidateOption',
          candidateBranch,
          '--$kReleaseOption',
          releaseChannel,
          '--$kStateOption',
          stateFilePath,
          '--$kVersionOverrideOption',
          'an invalid version string',
          '--$kGithubUsernameOption',
          githubUsername,
        ]),
        throwsExceptionWith('an invalid version string cannot be parsed'),
      );
    });

    test('creates state file if provided correct inputs', () async {
      stdio.stdin.add('y'); // accept prompt from ensureBranchPointTagged()
      const String revision2 = 'def789';
      const String revision3 = '123abc';
      const String previousDartRevision =
          '171876a4e6cf56ee6da1f97d203926bd7afda7ef';
      const String nextDartRevision =
          'f6c91128be6b77aef8351e1e3a9d07c85bc2e46e';
      const String previousVersion = '1.2.0-1.0.pre';
      // This is what this release will be
      const String nextVersion = '1.2.0-1.1.pre';
      const String candidateBranch = 'flutter-1.2-candidate.1';

      final Directory engine = fileSystem
          .directory(checkoutsParentDirectory)
          .childDirectory('flutter_conductor_checkouts')
          .childDirectory('engine');

      final File depsFile = engine.childFile('DEPS');

      final List<FakeCommand> engineCommands = <FakeCommand>[
        FakeCommand(
            command: <String>[
              'git',
              'clone',
              '--origin',
              'upstream',
              '--',
              EngineRepository.defaultUpstream,
              engine.path,
            ],
            onRun: () {
              // Create the DEPS file which the tool will update
              engine.createSync(recursive: true);
              depsFile
                  .writeAsStringSync(generateMockDeps(previousDartRevision));
            }),
        const FakeCommand(
          command: <String>['git', 'remote', 'add', 'mirror', engineMirror],
        ),
        const FakeCommand(
          command: <String>['git', 'fetch', 'mirror'],
        ),
        const FakeCommand(
          command: <String>['git', 'checkout', 'upstream/$candidateBranch'],
        ),
        const FakeCommand(
          command: <String>['git', 'rev-parse', 'HEAD'],
          stdout: revision2,
        ),
        const FakeCommand(
          command: <String>[
            'git',
            'checkout',
            '-b',
            'cherrypicks-$candidateBranch',
          ],
        ),
        const FakeCommand(
          command: <String>['git', 'status', '--porcelain'],
          stdout: 'MM path/to/DEPS',
        ),
        const FakeCommand(
          command: <String>['git', 'add', '--all'],
        ),
        const FakeCommand(
          command: <String>[
            'git',
            'commit',
            '--message',
            'Update Dart SDK to $nextDartRevision',
          ],
        ),
        const FakeCommand(
          command: <String>['git', 'rev-parse', 'HEAD'],
          stdout: revision2,
        ),
        const FakeCommand(
          command: <String>['git', 'rev-parse', 'HEAD'],
          stdout: revision2,
        ),
      ];

      final List<FakeCommand> frameworkCommands = <FakeCommand>[
        FakeCommand(
          command: <String>[
            'git',
            'clone',
            '--origin',
            'upstream',
            '--',
            FrameworkRepository.defaultUpstream,
            fileSystem.path.join(
              checkoutsParentDirectory,
              'flutter_conductor_checkouts',
              'framework',
            ),
          ],
        ),
        const FakeCommand(
          command: <String>['git', 'remote', 'add', 'mirror', frameworkMirror],
        ),
        const FakeCommand(
          command: <String>['git', 'fetch', 'mirror'],
        ),
        const FakeCommand(
          command: <String>['git', 'checkout', 'upstream/$candidateBranch'],
        ),
        const FakeCommand(
          command: <String>['git', 'rev-parse', 'HEAD'],
          stdout: revision3,
        ),
        const FakeCommand(
          command: <String>[
            'git',
            'checkout',
            '-b',
            'cherrypicks-$candidateBranch',
          ],
        ),
        const FakeCommand(
          command: <String>[
            'git',
            'describe',
            '--match',
            '*.*.*',
            '--tags',
            'refs/remotes/upstream/$candidateBranch',
          ],
          stdout: '$previousVersion-42-gabc123',
        ),
        const FakeCommand(
          command: <String>['git', 'rev-parse', 'HEAD'],
          stdout: revision3,
        ),
        const FakeCommand(
          command: <String>[
            'git',
            'merge-base',
            'upstream/$candidateBranch',
            'upstream/master',
          ],
          stdout: branchPointRevision,
        ),
        // check if commit is tagged, zero exit code means it is tagged
        const FakeCommand(
          command: <String>[
            'git',
            'describe',
            '--exact-match',
            '--tags',
            branchPointRevision,
          ],
        ),
      ];

      final CommandRunner<void> runner = createRunner(
        commands: <FakeCommand>[
          ...engineCommands,
          ...frameworkCommands,
        ],
      );

      final String stateFilePath = fileSystem.path.join(
        platform.environment['HOME']!,
        kStateFileName,
      );

      await runner.run(<String>[
        'start',
        '--$kCandidateOption',
        candidateBranch,
        '--$kReleaseOption',
        releaseChannel,
        '--$kStateOption',
        stateFilePath,
        '--$kDartRevisionOption',
        nextDartRevision,
        '--$kGithubUsernameOption',
        githubUsername,
      ]);

      final File stateFile = fileSystem.file(stateFilePath);

      final pb.ConductorState state = pb.ConductorState();
      state.mergeFromProto3Json(
        jsonDecode(stateFile.readAsStringSync()),
      );

      expect(state.releaseType, ReleaseType.BETA_HOTFIX);
      expect(
          stdio.error,
          isNot(contains(
              'Tried to tag the branch point, however the target version')));
      expect(processManager, hasNoRemainingExpectations);
      expect(state.isInitialized(), true);
      expect(state.releaseChannel, releaseChannel);
      expect(state.releaseVersion, nextVersion);
      expect(state.engine.candidateBranch, candidateBranch);
      expect(state.engine.startingGitHead, revision2);
      expect(state.engine.dartRevision, nextDartRevision);
      expect(state.engine.upstream.url, 'git@github.com:flutter/engine.git');
      expect(state.framework.candidateBranch, candidateBranch);
      expect(state.framework.startingGitHead, revision3);
      expect(
          state.framework.upstream.url, 'git@github.com:flutter/flutter.git');
      expect(state.currentPhase, ReleasePhase.APPLY_ENGINE_CHERRYPICKS);
      expect(state.conductorVersion, conductorVersion);
    });

    test('uses --$kVersionOverrideOption', () async {
      stdio.stdin.add('y'); // accept prompt from ensureBranchPointTagged()
      const String revision2 = 'def789';
      const String revision3 = '123abc';
      const String previousDartRevision =
          '171876a4e6cf56ee6da1f97d203926bd7afda7ef';
      const String nextDartRevision =
          'f6c91128be6b77aef8351e1e3a9d07c85bc2e46e';
      const String previousVersion = '1.2.0-1.0.pre';
      const String candidateBranch = 'flutter-1.2-candidate.1';
      const String versionOverride = '42.0.0-42.0.pre';

      final Directory engine = fileSystem
          .directory(checkoutsParentDirectory)
          .childDirectory('flutter_conductor_checkouts')
          .childDirectory('engine');

      final File depsFile = engine.childFile('DEPS');

      final List<FakeCommand> engineCommands = <FakeCommand>[
        FakeCommand(
            command: <String>[
              'git',
              'clone',
              '--origin',
              'upstream',
              '--',
              EngineRepository.defaultUpstream,
              engine.path,
            ],
            onRun: () {
              // Create the DEPS file which the tool will update
              engine.createSync(recursive: true);
              depsFile
                  .writeAsStringSync(generateMockDeps(previousDartRevision));
            }),
        const FakeCommand(
          command: <String>['git', 'remote', 'add', 'mirror', engineMirror],
        ),
        const FakeCommand(
          command: <String>['git', 'fetch', 'mirror'],
        ),
        const FakeCommand(
          command: <String>['git', 'checkout', 'upstream/$candidateBranch'],
        ),
        const FakeCommand(
          command: <String>['git', 'rev-parse', 'HEAD'],
          stdout: revision2,
        ),
        const FakeCommand(
          command: <String>[
            'git',
            'checkout',
            '-b',
            'cherrypicks-$candidateBranch',
          ],
        ),
        const FakeCommand(
          command: <String>['git', 'status', '--porcelain'],
          stdout: 'MM path/to/DEPS',
        ),
        const FakeCommand(
          command: <String>['git', 'add', '--all'],
        ),
        const FakeCommand(
          command: <String>[
            'git',
            'commit',
            '--message',
            'Update Dart SDK to $nextDartRevision'
          ],
        ),
        const FakeCommand(
          command: <String>['git', 'rev-parse', 'HEAD'],
          stdout: revision2,
        ),
        const FakeCommand(
          command: <String>['git', 'rev-parse', 'HEAD'],
          stdout: revision2,
        ),
      ];

      final List<FakeCommand> frameworkCommands = <FakeCommand>[
        FakeCommand(
          command: <String>[
            'git',
            'clone',
            '--origin',
            'upstream',
            '--',
            FrameworkRepository.defaultUpstream,
            fileSystem.path.join(
              checkoutsParentDirectory,
              'flutter_conductor_checkouts',
              'framework',
            ),
          ],
        ),
        const FakeCommand(
          command: <String>['git', 'remote', 'add', 'mirror', frameworkMirror],
        ),
        const FakeCommand(
          command: <String>['git', 'fetch', 'mirror'],
        ),
        const FakeCommand(
          command: <String>['git', 'checkout', 'upstream/$candidateBranch'],
        ),
        const FakeCommand(
          command: <String>['git', 'rev-parse', 'HEAD'],
          stdout: revision3,
        ),
        const FakeCommand(
          command: <String>[
            'git',
            'checkout',
            '-b',
            'cherrypicks-$candidateBranch',
          ],
        ),
        const FakeCommand(
          command: <String>[
            'git',
            'describe',
            '--match',
            '*.*.*',
            '--tags',
            'refs/remotes/upstream/$candidateBranch',
          ],
          stdout: '$previousVersion-42-gabc123',
        ),
        const FakeCommand(
          command: <String>['git', 'rev-parse', 'HEAD'],
          stdout: revision3,
        ),
        const FakeCommand(
          command: <String>[
            'git',
            'merge-base',
            'upstream/$candidateBranch',
            'upstream/master'
          ],
          stdout: branchPointRevision,
        ),
      ];

      final CommandRunner<void> runner = createRunner(
        commands: <FakeCommand>[
          ...engineCommands,
          ...frameworkCommands,
        ],
      );

      final String stateFilePath = fileSystem.path.join(
        platform.environment['HOME']!,
        kStateFileName,
      );

      await runner.run(<String>[
        'start',
        '--$kCandidateOption',
        candidateBranch,
        '--$kReleaseOption',
        releaseChannel,
        '--$kStateOption',
        stateFilePath,
        '--$kDartRevisionOption',
        nextDartRevision,
        '--$kVersionOverrideOption',
        versionOverride,
        '--$kGithubUsernameOption',
        githubUsername,
      ]);

      final File stateFile = fileSystem.file(stateFilePath);

      final pb.ConductorState state = pb.ConductorState();
      state.mergeFromProto3Json(
        jsonDecode(stateFile.readAsStringSync()),
      );

      expect(processManager, hasNoRemainingExpectations);
      expect(state.releaseVersion, versionOverride);
    });

    test('logs to STDERR but does not fail on an unexpected candidate branch',
        () async {
      stdio.stdin.add('y'); // accept prompt from ensureBranchPointTagged()
      const String revision2 = 'def789';
      const String revision3 = '123abc';
      const String previousDartRevision =
          '171876a4e6cf56ee6da1f97d203926bd7afda7ef';
      const String nextDartRevision =
          'f6c91128be6b77aef8351e1e3a9d07c85bc2e46e';
      // note that this significantly behind the candidate branch name
      const String previousVersion = '0.9.0-1.0.pre';
      // This is what this release will be
      const String nextVersion = '0.9.0-1.1.pre';

      final Directory engine = fileSystem
          .directory(checkoutsParentDirectory)
          .childDirectory('flutter_conductor_checkouts')
          .childDirectory('engine');

      final File depsFile = engine.childFile('DEPS');

      final List<FakeCommand> engineCommands = <FakeCommand>[
        FakeCommand(
            command: <String>[
              'git',
              'clone',
              '--origin',
              'upstream',
              '--',
              EngineRepository.defaultUpstream,
              engine.path,
            ],
            onRun: () {
              // Create the DEPS file which the tool will update
              engine.createSync(recursive: true);
              depsFile
                  .writeAsStringSync(generateMockDeps(previousDartRevision));
            }),
        const FakeCommand(
          command: <String>['git', 'remote', 'add', 'mirror', engineMirror],
        ),
        const FakeCommand(
          command: <String>['git', 'fetch', 'mirror'],
        ),
        const FakeCommand(
          command: <String>['git', 'checkout', 'upstream/$candidateBranch'],
        ),
        const FakeCommand(
          command: <String>['git', 'rev-parse', 'HEAD'],
          stdout: revision2,
        ),
        const FakeCommand(
          command: <String>[
            'git',
            'checkout',
            '-b',
            'cherrypicks-$candidateBranch',
          ],
        ),
        const FakeCommand(
          command: <String>['git', 'status', '--porcelain'],
          stdout: 'MM path/to/DEPS',
        ),
        const FakeCommand(
          command: <String>['git', 'add', '--all'],
        ),
        const FakeCommand(
          command: <String>[
            'git',
            'commit',
            '--message',
            'Update Dart SDK to $nextDartRevision',
          ],
        ),
        const FakeCommand(
          command: <String>['git', 'rev-parse', 'HEAD'],
          stdout: revision2,
        ),
        const FakeCommand(
          command: <String>['git', 'rev-parse', 'HEAD'],
          stdout: revision2,
        ),
      ];

      final List<FakeCommand> frameworkCommands = <FakeCommand>[
        FakeCommand(
          command: <String>[
            'git',
            'clone',
            '--origin',
            'upstream',
            '--',
            FrameworkRepository.defaultUpstream,
            fileSystem.path.join(
              checkoutsParentDirectory,
              'flutter_conductor_checkouts',
              'framework',
            ),
          ],
        ),
        const FakeCommand(
          command: <String>['git', 'remote', 'add', 'mirror', frameworkMirror],
        ),
        const FakeCommand(
          command: <String>['git', 'fetch', 'mirror'],
        ),
        const FakeCommand(
          command: <String>['git', 'checkout', 'upstream/$candidateBranch'],
        ),
        const FakeCommand(
          command: <String>['git', 'rev-parse', 'HEAD'],
          stdout: revision3,
        ),
        const FakeCommand(
          command: <String>[
            'git',
            'checkout',
            '-b',
            'cherrypicks-$candidateBranch',
          ],
        ),
        const FakeCommand(
          command: <String>[
            'git',
            'describe',
            '--match',
            '*.*.*',
            '--tags',
            'refs/remotes/upstream/$candidateBranch',
          ],
          stdout: '$previousVersion-42-gabc123',
        ),
        const FakeCommand(
          command: <String>['git', 'rev-parse', 'HEAD'],
          stdout: revision3,
        ),
        const FakeCommand(
          command: <String>[
            'git',
            'merge-base',
            'upstream/$candidateBranch',
            'upstream/master',
          ],
          stdout: branchPointRevision,
        ),
        // check if commit is tagged, 0 exit code means it is tagged
        const FakeCommand(
          command: <String>[
            'git',
            'describe',
            '--exact-match',
            '--tags',
            branchPointRevision,
          ],
        ),
      ];

      final CommandRunner<void> runner = createRunner(
        commands: <FakeCommand>[
          ...engineCommands,
          ...frameworkCommands,
        ],
      );

      final String stateFilePath = fileSystem.path.join(
        platform.environment['HOME']!,
        kStateFileName,
      );

      await runner.run(<String>[
        'start',
        '--$kCandidateOption',
        candidateBranch,
        '--$kReleaseOption',
        releaseChannel,
        '--$kStateOption',
        stateFilePath,
        '--$kDartRevisionOption',
        nextDartRevision,
        '--$kGithubUsernameOption',
        githubUsername,
      ]);

      final File stateFile = fileSystem.file(stateFilePath);

      final pb.ConductorState state = pb.ConductorState();
      state.mergeFromProto3Json(
        jsonDecode(stateFile.readAsStringSync()),
      );

      expect(stdio.error,
          isNot(contains('Tried to tag the branch point, however')));
      expect(processManager, hasNoRemainingExpectations);
      expect(state.isInitialized(), true);
      expect(state.releaseChannel, releaseChannel);
      expect(state.releaseVersion, nextVersion);
      expect(state.engine.candidateBranch, candidateBranch);
      expect(state.engine.startingGitHead, revision2);
      expect(state.engine.dartRevision, nextDartRevision);
      expect(state.engine.upstream.url, 'git@github.com:flutter/engine.git');
      expect(state.framework.candidateBranch, candidateBranch);
      expect(state.framework.startingGitHead, revision3);
      expect(
          state.framework.upstream.url, 'git@github.com:flutter/flutter.git');
      expect(state.currentPhase, ReleasePhase.APPLY_ENGINE_CHERRYPICKS);
      expect(state.conductorVersion, conductorVersion);
      expect(state.releaseType, ReleaseType.BETA_HOTFIX);
      expect(
          stdio.error,
          contains(
              'Parsed version $previousVersion.42 has a different x value than candidate branch $candidateBranch'));
    });

    test('can convert from dev style version to stable version', () async {
      const String revision2 = 'def789';
      const String revision3 = '123abc';
      const String previousDartRevision =
          '171876a4e6cf56ee6da1f97d203926bd7afda7ef';
      const String nextDartRevision =
          'f6c91128be6b77aef8351e1e3a9d07c85bc2e46e';
      const String previousVersion = '1.2.0-3.0.pre';
      const String nextVersion = '1.2.0';

      final Directory engine = fileSystem
          .directory(checkoutsParentDirectory)
          .childDirectory('flutter_conductor_checkouts')
          .childDirectory('engine');

      final File depsFile = engine.childFile('DEPS');

      final List<FakeCommand> engineCommands = <FakeCommand>[
        FakeCommand(
            command: <String>[
              'git',
              'clone',
              '--origin',
              'upstream',
              '--',
              EngineRepository.defaultUpstream,
              engine.path,
            ],
            onRun: () {
              // Create the DEPS file which the tool will update
              engine.createSync(recursive: true);
              depsFile
                  .writeAsStringSync(generateMockDeps(previousDartRevision));
            }),
        const FakeCommand(
          command: <String>['git', 'remote', 'add', 'mirror', engineMirror],
        ),
        const FakeCommand(
          command: <String>['git', 'fetch', 'mirror'],
        ),
        const FakeCommand(
          command: <String>['git', 'checkout', 'upstream/$candidateBranch'],
        ),
        const FakeCommand(
          command: <String>['git', 'rev-parse', 'HEAD'],
          stdout: revision2,
        ),
        const FakeCommand(
          command: <String>[
            'git',
            'checkout',
            '-b',
            'cherrypicks-$candidateBranch',
          ],
        ),
        const FakeCommand(
          command: <String>['git', 'status', '--porcelain'],
          stdout: 'MM path/to/DEPS',
        ),
        const FakeCommand(
          command: <String>['git', 'add', '--all'],
        ),
        const FakeCommand(
          command: <String>[
            'git',
            'commit',
            '--message',
            'Update Dart SDK to $nextDartRevision',
          ],
        ),
        const FakeCommand(
          command: <String>['git', 'rev-parse', 'HEAD'],
          stdout: revision2,
        ),
        const FakeCommand(
          command: <String>['git', 'rev-parse', 'HEAD'],
          stdout: revision2,
        ),
      ];

      final List<FakeCommand> frameworkCommands = <FakeCommand>[
        FakeCommand(
          command: <String>[
            'git',
            'clone',
            '--origin',
            'upstream',
            '--',
            FrameworkRepository.defaultUpstream,
            fileSystem.path.join(
              checkoutsParentDirectory,
              'flutter_conductor_checkouts',
              'framework',
            ),
          ],
        ),
        const FakeCommand(
          command: <String>['git', 'remote', 'add', 'mirror', frameworkMirror],
        ),
        const FakeCommand(
          command: <String>['git', 'fetch', 'mirror'],
        ),
        const FakeCommand(
          command: <String>['git', 'checkout', 'upstream/$candidateBranch'],
        ),
        const FakeCommand(
          command: <String>['git', 'rev-parse', 'HEAD'],
          stdout: revision3,
        ),
        const FakeCommand(
          command: <String>[
            'git',
            'checkout',
            '-b',
            'cherrypicks-$candidateBranch',
          ],
        ),
        const FakeCommand(
          command: <String>[
            'git',
            'describe',
            '--match',
            '*.*.*',
            '--tags',
            'refs/remotes/upstream/$candidateBranch',
          ],
          stdout: '$previousVersion-42-gabc123',
        ),
        const FakeCommand(
          command: <String>['git', 'rev-parse', 'HEAD'],
          stdout: revision3,
        ),
        const FakeCommand(
          command: <String>[
            'git',
            'merge-base',
            'upstream/$candidateBranch',
            'upstream/master'
          ],
          stdout: branchPointRevision,
        ),
        // check if commit is tagged, 0 exit code thus it is tagged
        const FakeCommand(
          command: <String>[
            'git',
            'describe',
            '--exact-match',
            '--tags',
            branchPointRevision,
          ],
        ),
      ];

      final CommandRunner<void> runner = createRunner(
        commands: <FakeCommand>[
          ...engineCommands,
          ...frameworkCommands,
        ],
      );

      final String stateFilePath = fileSystem.path.join(
        platform.environment['HOME']!,
        kStateFileName,
      );

      await runner.run(<String>[
        'start',
        '--$kCandidateOption',
        candidateBranch,
        '--$kReleaseOption',
        'stable',
        '--$kStateOption',
        stateFilePath,
        '--$kDartRevisionOption',
        nextDartRevision,
        '--$kGithubUsernameOption',
        githubUsername,
      ]);

      final File stateFile = fileSystem.file(stateFilePath);

      final pb.ConductorState state = pb.ConductorState();
      state.mergeFromProto3Json(
        jsonDecode(stateFile.readAsStringSync()),
      );

      expect(processManager.hasRemainingExpectations, false);
      expect(state.isInitialized(), true);
      expect(state.releaseChannel, 'stable');
      expect(state.releaseVersion, nextVersion);
      expect(state.engine.candidateBranch, candidateBranch);
      expect(state.engine.startingGitHead, revision2);
      expect(state.engine.dartRevision, nextDartRevision);
      expect(state.framework.candidateBranch, candidateBranch);
      expect(state.framework.startingGitHead, revision3);
      expect(state.currentPhase, ReleasePhase.APPLY_ENGINE_CHERRYPICKS);
      expect(state.conductorVersion, conductorVersion);
      expect(state.releaseType, ReleaseType.STABLE_INITIAL);
    });
    test(
        'StartContext gets engine and framework checkout directories after run',
        () async {
      stdio.stdin.add('y');
      const String revision2 = 'def789';
      const String revision3 = '123abc';
      const String branchPointRevision = 'deadbeef';
      const String previousDartRevision =
          '171876a4e6cf56ee6da1f97d203926bd7afda7ef';
      const String nextDartRevision =
          'f6c91128be6b77aef8351e1e3a9d07c85bc2e46e';
      const String previousVersion = '1.2.0-1.0.pre';
      // This is a git tag applied to the branch point, not an actual release
      const String branchPointTag = '1.2.0-3.0.pre';

      final Directory engine = fileSystem
          .directory(checkoutsParentDirectory)
          .childDirectory('flutter_conductor_checkouts')
          .childDirectory('engine');

      final Directory framework = fileSystem
          .directory(checkoutsParentDirectory)
          .childDirectory('flutter_conductor_checkouts')
          .childDirectory('framework');

      final File depsFile = engine.childFile('DEPS');

      final List<FakeCommand> engineCommands = <FakeCommand>[
        FakeCommand(
            command: <String>[
              'git',
              'clone',
              '--origin',
              'upstream',
              '--',
              EngineRepository.defaultUpstream,
              engine.path,
            ],
            onRun: () {
              // Create the DEPS file which the tool will update
              engine.createSync(recursive: true);
              depsFile
                  .writeAsStringSync(generateMockDeps(previousDartRevision));
            }),
        const FakeCommand(
          command: <String>['git', 'remote', 'add', 'mirror', engineMirror],
        ),
        const FakeCommand(
          command: <String>['git', 'fetch', 'mirror'],
        ),
        const FakeCommand(
          command: <String>['git', 'checkout', 'upstream/$candidateBranch'],
        ),
        const FakeCommand(
          command: <String>['git', 'rev-parse', 'HEAD'],
          stdout: revision2,
        ),
        const FakeCommand(
          command: <String>[
            'git',
            'checkout',
            '-b',
            'cherrypicks-$candidateBranch',
          ],
        ),
        const FakeCommand(
          command: <String>['git', 'status', '--porcelain'],
          stdout: 'MM path/to/DEPS',
        ),
        const FakeCommand(
          command: <String>['git', 'add', '--all'],
        ),
        const FakeCommand(
          command: <String>[
            'git',
            'commit',
            '--message',
            'Update Dart SDK to $nextDartRevision'
          ],
        ),
        const FakeCommand(
          command: <String>['git', 'rev-parse', 'HEAD'],
          stdout: revision2,
        ),
        const FakeCommand(
          command: <String>['git', 'rev-parse', 'HEAD'],
          stdout: revision2,
        ),
      ];

      final List<FakeCommand> frameworkCommands = <FakeCommand>[
        FakeCommand(
          command: <String>[
            'git',
            'clone',
            '--origin',
            'upstream',
            '--',
            FrameworkRepository.defaultUpstream,
            framework.path,
          ],
        ),
        const FakeCommand(
          command: <String>['git', 'remote', 'add', 'mirror', frameworkMirror],
        ),
        const FakeCommand(
          command: <String>['git', 'fetch', 'mirror'],
        ),
        const FakeCommand(
          command: <String>['git', 'checkout', 'upstream/$candidateBranch'],
        ),
        const FakeCommand(
          command: <String>['git', 'rev-parse', 'HEAD'],
          stdout: revision3,
        ),
        const FakeCommand(
          command: <String>[
            'git',
            'checkout',
            '-b',
            'cherrypicks-$candidateBranch',
          ],
        ),
        const FakeCommand(
          command: <String>[
            'git',
            'describe',
            '--match',
            '*.*.*',
            '--tags',
            'refs/remotes/upstream/$candidateBranch',
          ],
          stdout: '$previousVersion-42-gabc123',
        ),
        // HEAD and branch point are same
        const FakeCommand(
          command: <String>['git', 'rev-parse', 'HEAD'],
          stdout: branchPointRevision,
        ),
        const FakeCommand(
          command: <String>[
            'git',
            'merge-base',
            'upstream/$candidateBranch',
            'upstream/master'
          ],
          stdout: branchPointRevision,
        ),
        // check if commit is tagged
        const FakeCommand(
          command: <String>[
            'git',
            'describe',
            '--exact-match',
            '--tags',
            branchPointRevision
          ],
          // non-zero exit code means branch point is NOT tagged
          exitCode: 128,
        ),
        const FakeCommand(
          command: <String>['git', 'tag', branchPointTag, branchPointRevision],
        ),
        const FakeCommand(
          command: <String>[
            'git',
            'push',
            FrameworkRepository.defaultUpstream,
            branchPointTag
          ],
        ),
      ];

      final String operatingSystem = const LocalPlatform().operatingSystem;
      final Map<String, String> environment = <String, String>{
        'HOME': '/path/to/user/home',
      };
      final Directory homeDir = fileSystem.directory(
        environment['HOME'],
      );
      // Tool assumes this exists
      homeDir.createSync(recursive: true);
      platform = FakePlatform(
        environment: environment,
        operatingSystem: operatingSystem,
      );

      final String stateFilePath = fileSystem.path.join(
        platform.environment['HOME']!,
        kStateFileName,
      );
      final File stateFile = fileSystem.file(stateFilePath);

      processManager = FakeProcessManager.list(<FakeCommand>[
        ...engineCommands,
        ...frameworkCommands,
      ]);
      checkouts = Checkouts(
        fileSystem: fileSystem,
        parentDirectory: fileSystem.directory(checkoutsParentDirectory),
        platform: platform,
        processManager: processManager,
        stdio: stdio,
      );

      final StartContext startContext = StartContext(
        candidateBranch: candidateBranch,
        checkouts: checkouts,
        dartRevision: nextDartRevision,
        engineCherrypickRevisions: <String>[],
        engineMirror: engineMirror,
        engineUpstream: EngineRepository.defaultUpstream,
        frameworkCherrypickRevisions: <String>[],
        frameworkMirror: frameworkMirror,
        frameworkUpstream: FrameworkRepository.defaultUpstream,
        releaseChannel: releaseChannel,
        processManager: processManager,
        conductorVersion: conductorVersion,
        githubUsername: githubUsername,
        stateFile: stateFile,
      );

      await startContext.run();

      final pb.ConductorState state = pb.ConductorState();
      state.mergeFromProto3Json(
        jsonDecode(stateFile.readAsStringSync()),
      );

      expect((await startContext.engine.checkoutDirectory).path,
          equals(engine.path));
      expect((await startContext.framework.checkoutDirectory).path,
          equals(framework.path));
      expect(state.releaseType, ReleaseType.BETA_INITIAL);
      expect(processManager, hasNoRemainingExpectations);
    });
  }, onPlatform: <String, dynamic>{
    'windows': const Skip('Flutter Conductor only supported on macos/linux'),
  });
}

String generateMockDeps(String dartRevision) {
  return '''
vars = {
  'chromium_git': 'https://chromium.googlesource.com',
  'swiftshader_git': 'https://swiftshader.googlesource.com',
  'dart_git': 'https://dart.googlesource.com',
  'flutter_git': 'https://flutter.googlesource.com',
  'fuchsia_git': 'https://fuchsia.googlesource.com',
  'github_git': 'https://github.com',
  'skia_git': 'https://skia.googlesource.com',
  'ocmock_git': 'https://github.com/erikdoe/ocmock.git',
  'skia_revision': '4e9d5e2bdf04c58bc0bff57be7171e469e5d7175',

  'dart_revision': '$dartRevision',
  'dart_boringssl_gen_rev': '7322fc15cc065d8d2957fccce6b62a509dc4d641',
}''';
}
