// 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 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/commands/upgrade.dart';
import 'package:flutter_tools/src/convert.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:flutter_tools/src/persistent_tool_state.dart';
import 'package:flutter_tools/src/runner/flutter_command.dart';
import 'package:flutter_tools/src/version.dart';

import '../../src/common.dart';
import '../../src/context.dart';
import '../../src/fake_process_manager.dart';
import '../../src/fakes.dart';
import '../../src/test_flutter_command_runner.dart';

void main() {
  group('UpgradeCommandRunner', () {
    late FakeUpgradeCommandRunner fakeCommandRunner;
    late UpgradeCommandRunner realCommandRunner;
    late FakeProcessManager processManager;
    late FakePlatform fakePlatform;
    const GitTagVersion gitTagVersion = GitTagVersion(
      x: 1,
      y: 2,
      z: 3,
      hotfix: 4,
      commits: 5,
      hash: 'asd',
    );

    setUp(() {
      fakeCommandRunner = FakeUpgradeCommandRunner();
      realCommandRunner = UpgradeCommandRunner();
      processManager = FakeProcessManager.empty();
      fakeCommandRunner.willHaveUncommittedChanges = false;
      fakePlatform = FakePlatform()..environment = Map<String, String>.unmodifiable(<String, String>{
        'ENV1': 'irrelevant',
        'ENV2': 'irrelevant',
      });
    });

    testUsingContext('throws on unknown tag, official branch,  noforce', () async {
      final FakeFlutterVersion flutterVersion = FakeFlutterVersion(channel: 'beta');
      const String upstreamRevision = '';
      final FakeFlutterVersion latestVersion = FakeFlutterVersion(frameworkRevision: upstreamRevision);
      fakeCommandRunner.remoteVersion = latestVersion;

      final Future<FlutterCommandResult> result = fakeCommandRunner.runCommand(
        force: false,
        continueFlow: false,
        testFlow: false,
        gitTagVersion: const GitTagVersion.unknown(),
        flutterVersion: flutterVersion,
        verifyOnly: false,
      );
      expect(result, throwsToolExit());
      expect(processManager, hasNoRemainingExpectations);
    }, overrides: <Type, Generator>{
      Platform: () => fakePlatform,
    });

    testUsingContext('throws tool exit with uncommitted changes', () async {
      final FakeFlutterVersion flutterVersion = FakeFlutterVersion(channel: 'beta');
      const String upstreamRevision = '';
      final FakeFlutterVersion latestVersion = FakeFlutterVersion(frameworkRevision: upstreamRevision);
      fakeCommandRunner.remoteVersion = latestVersion;
      fakeCommandRunner.willHaveUncommittedChanges = true;

      final Future<FlutterCommandResult> result = fakeCommandRunner.runCommand(
        force: false,
        continueFlow: false,
        testFlow: false,
        gitTagVersion: gitTagVersion,
        flutterVersion: flutterVersion,
        verifyOnly: false,
      );
      expect(result, throwsToolExit());
      expect(processManager, hasNoRemainingExpectations);
    }, overrides: <Type, Generator>{
      Platform: () => fakePlatform,
    });

    testUsingContext("Doesn't continue on known tag, beta branch, no force, already up-to-date", () async {
      const String revision = 'abc123';
      final FakeFlutterVersion latestVersion = FakeFlutterVersion(frameworkRevision: revision);
      final FakeFlutterVersion flutterVersion = FakeFlutterVersion(channel: 'beta', frameworkRevision: revision);
      fakeCommandRunner.alreadyUpToDate = true;
      fakeCommandRunner.remoteVersion = latestVersion;

      final Future<FlutterCommandResult> result = fakeCommandRunner.runCommand(
        force: false,
        continueFlow: false,
        testFlow: false,
        gitTagVersion: gitTagVersion,
        flutterVersion: flutterVersion,
        verifyOnly: false,
      );
      expect(await result, FlutterCommandResult.success());
      expect(testLogger.statusText, contains('Flutter is already up to date'));
      expect(processManager, hasNoRemainingExpectations);
    }, overrides: <Type, Generator>{
      ProcessManager: () => processManager,
      Platform: () => fakePlatform,
    });

    testUsingContext('correctly provides upgrade version on verify only', () async {
      const String revision = 'abc123';
      const String upstreamRevision = 'def456';
      const String version = '1.2.3';
      const String upstreamVersion = '4.5.6';

      final FakeFlutterVersion flutterVersion = FakeFlutterVersion(
        channel: 'beta',
        frameworkRevision: revision,
        frameworkRevisionShort: revision,
        frameworkVersion: version,
      );

      final FakeFlutterVersion latestVersion = FakeFlutterVersion(
        frameworkRevision: upstreamRevision,
        frameworkRevisionShort: upstreamRevision,
        frameworkVersion: upstreamVersion,
      );

      fakeCommandRunner.alreadyUpToDate = false;
      fakeCommandRunner.remoteVersion = latestVersion;

      final Future<FlutterCommandResult> result = fakeCommandRunner.runCommand(
        force: false,
        continueFlow: false,
        testFlow: false,
        gitTagVersion: gitTagVersion,
        flutterVersion: flutterVersion,
        verifyOnly: true,
      );
      expect(await result, FlutterCommandResult.success());
      expect(testLogger.statusText, contains('A new version of Flutter is available'));
      expect(testLogger.statusText, contains('The latest version: 4.5.6 (revision def456)'));
      expect(testLogger.statusText, contains('Your current version: 1.2.3 (revision abc123)'));
      expect(processManager, hasNoRemainingExpectations);
    }, overrides: <Type, Generator>{
      ProcessManager: () => processManager,
      Platform: () => fakePlatform,
    });

    testUsingContext('fetchLatestVersion returns version if git succeeds', () async {
      const String revision = 'abc123';
      const String version = '1.2.3';

      processManager.addCommands(<FakeCommand>[
        const FakeCommand(command: <String>[
          'git', 'fetch', '--tags',
        ]),
        const FakeCommand(command: <String>[
          'git', 'rev-parse', '--verify', '@{upstream}',
        ],
        stdout: revision),
        const FakeCommand(command: <String>[
          'git', 'tag', '--points-at', revision,
        ]),
        const FakeCommand(command: <String>[
          'git', 'describe', '--match', '*.*.*', '--long', '--tags', revision,
        ],
        stdout: version),
      ]);

      final FlutterVersion updateVersion = await realCommandRunner.fetchLatestVersion(localVersion: FakeFlutterVersion());

      expect(updateVersion.frameworkVersion, version);
      expect(updateVersion.frameworkRevision, revision);
      expect(processManager, hasNoRemainingExpectations);
    }, overrides: <Type, Generator>{
      ProcessManager: () => processManager,
      Platform: () => fakePlatform,
    });

    testUsingContext('fetchLatestVersion throws toolExit if HEAD is detached', () async {
      processManager.addCommands(const <FakeCommand>[
        FakeCommand(command: <String>[
          'git', 'fetch', '--tags',
        ]),
        FakeCommand(
          command: <String>['git', 'rev-parse', '--verify', '@{upstream}'],
          exception: ProcessException(
            'git',
            <String>['rev-parse', '--verify', '@{upstream}'],
            'fatal: HEAD does not point to a branch',
          ),
        ),
      ]);

      await expectLater(
        () async => realCommandRunner.fetchLatestVersion(localVersion: FakeFlutterVersion()),
        throwsToolExit(message: 'Unable to upgrade Flutter: Your Flutter checkout '
          'is currently not on a release branch.\n'
          'Use "flutter channel" to switch to an official channel, and retry. '
          'Alternatively, re-install Flutter by going to https://flutter.dev/docs/get-started/install.'
        ),
      );
      expect(processManager, hasNoRemainingExpectations);
    }, overrides: <Type, Generator>{
      ProcessManager: () => processManager,
      Platform: () => fakePlatform,
    });

    testUsingContext('fetchLatestVersion throws toolExit if no upstream configured', () async {
      processManager.addCommands(const <FakeCommand>[
        FakeCommand(command: <String>[
          'git', 'fetch', '--tags',
        ]),
        FakeCommand(
          command: <String>['git', 'rev-parse', '--verify', '@{upstream}'],
          exception: ProcessException(
            'git',
            <String>['rev-parse', '--verify', '@{upstream}'],
            'fatal: no upstream configured for branch',
          ),
        ),
      ]);

      await expectLater(
        () async => realCommandRunner.fetchLatestVersion(localVersion: FakeFlutterVersion()),
        throwsToolExit(message: 'Unable to upgrade Flutter: The current Flutter '
          'branch/channel is not tracking any remote repository.\n'
          'Re-install Flutter by going to https://flutter.dev/docs/get-started/install.'
        ),
      );
      expect(processManager, hasNoRemainingExpectations);
    }, overrides: <Type, Generator>{
      ProcessManager: () => processManager,
      Platform: () => fakePlatform,
    });

    testUsingContext('git exception during attemptReset throwsToolExit', () async {
      const String revision = 'abc123';
      const String errorMessage = 'fatal: Could not parse object ´$revision´';
      processManager.addCommand(
        const FakeCommand(
          command: <String>['git', 'reset', '--hard', revision],
          exception: ProcessException(
            'git',
            <String>['reset', '--hard', revision],
            errorMessage,
          ),
        ),
      );

      await expectLater(
            () async => realCommandRunner.attemptReset(revision),
        throwsToolExit(message: errorMessage),
      );
      expect(processManager, hasNoRemainingExpectations);
    }, overrides: <Type, Generator>{
      ProcessManager: () => processManager,
      Platform: () => fakePlatform,
    });

    testUsingContext('flutterUpgradeContinue passes env variables to child process', () async {
      processManager.addCommand(
        FakeCommand(
          command: <String>[
            globals.fs.path.join('bin', 'flutter'),
            'upgrade',
            '--continue',
            '--no-version-check',
          ],
          environment: <String, String>{'FLUTTER_ALREADY_LOCKED': 'true', ...fakePlatform.environment}
        ),
      );
      await realCommandRunner.flutterUpgradeContinue();
      expect(processManager, hasNoRemainingExpectations);
    }, overrides: <Type, Generator>{
      ProcessManager: () => processManager,
      Platform: () => fakePlatform,
    });

    testUsingContext('Show current version to the upgrade message.', () async {
      const String revision = 'abc123';
      const String upstreamRevision = 'def456';
      const String version = '1.2.3';
      const String upstreamVersion = '4.5.6';

      final FakeFlutterVersion flutterVersion = FakeFlutterVersion(
        channel: 'beta',
        frameworkRevision: revision,
        frameworkVersion: version,
      );
      final FakeFlutterVersion latestVersion = FakeFlutterVersion(
        frameworkRevision: upstreamRevision,
        frameworkVersion: upstreamVersion,
      );

      fakeCommandRunner.alreadyUpToDate = false;
      fakeCommandRunner.remoteVersion = latestVersion;
      fakeCommandRunner.workingDirectory = 'workingDirectory/aaa/bbb';

      final Future<FlutterCommandResult> result = fakeCommandRunner.runCommand(
        force: true,
        continueFlow: false,
        testFlow: true,
        gitTagVersion: gitTagVersion,
        flutterVersion: flutterVersion,
        verifyOnly: false,
      );
      expect(await result, FlutterCommandResult.success());
      expect(testLogger.statusText, contains('Upgrading Flutter to 4.5.6 from 1.2.3 in workingDirectory/aaa/bbb...'));
    }, overrides: <Type, Generator>{
      ProcessManager: () => processManager,
      Platform: () => fakePlatform,
    });

    testUsingContext('precacheArtifacts passes env variables to child process', () async {
      processManager.addCommand(
        FakeCommand(
          command: <String>[
            globals.fs.path.join('bin', 'flutter'),
            '--no-color',
            '--no-version-check',
            'precache',
          ],
          environment: <String, String>{'FLUTTER_ALREADY_LOCKED': 'true', ...fakePlatform.environment}
        ),
      );
      await realCommandRunner.precacheArtifacts();
      expect(processManager, hasNoRemainingExpectations);
    }, overrides: <Type, Generator>{
      ProcessManager: () => processManager,
      Platform: () => fakePlatform,
    });

    group('runs upgrade', () {
      setUp(() {
        processManager.addCommand(
          FakeCommand(command: <String>[
            globals.fs.path.join('bin', 'flutter'),
            'upgrade',
            '--continue',
            '--no-version-check',
          ]),
        );
      });

      testUsingContext('does not throw on unknown tag, official branch, force', () async {
        fakeCommandRunner.remoteVersion = FakeFlutterVersion(frameworkRevision: '1234');
        final FakeFlutterVersion flutterVersion = FakeFlutterVersion(channel: 'beta');

        final Future<FlutterCommandResult> result = fakeCommandRunner.runCommand(
          force: true,
          continueFlow: false,
          testFlow: false,
          gitTagVersion: const GitTagVersion.unknown(),
          flutterVersion: flutterVersion,
          verifyOnly: false,
        );
        expect(await result, FlutterCommandResult.success());
        expect(processManager, hasNoRemainingExpectations);
      }, overrides: <Type, Generator>{
        ProcessManager: () => processManager,
        Platform: () => fakePlatform,
      });

      testUsingContext('does not throw tool exit with uncommitted changes and force', () async {
        final FakeFlutterVersion flutterVersion = FakeFlutterVersion(channel: 'beta');
        fakeCommandRunner.remoteVersion = FakeFlutterVersion(frameworkRevision: '1234');
        fakeCommandRunner.willHaveUncommittedChanges = true;

        final Future<FlutterCommandResult> result = fakeCommandRunner.runCommand(
          force: true,
          continueFlow: false,
          testFlow: false,
          gitTagVersion: gitTagVersion,
          flutterVersion: flutterVersion,
          verifyOnly: false,
        );
        expect(await result, FlutterCommandResult.success());
        expect(processManager, hasNoRemainingExpectations);
      }, overrides: <Type, Generator>{
        ProcessManager: () => processManager,
        Platform: () => fakePlatform,
      });

      testUsingContext("Doesn't throw on known tag, beta branch, no force", () async {
        final FakeFlutterVersion flutterVersion = FakeFlutterVersion(channel: 'beta');
        fakeCommandRunner.remoteVersion = FakeFlutterVersion(frameworkRevision: '1234');

        final Future<FlutterCommandResult> result = fakeCommandRunner.runCommand(
          force: false,
          continueFlow: false,
          testFlow: false,
          gitTagVersion: gitTagVersion,
          flutterVersion: flutterVersion,
          verifyOnly: false,
        );
        expect(await result, FlutterCommandResult.success());
        expect(processManager, hasNoRemainingExpectations);
      }, overrides: <Type, Generator>{
        ProcessManager: () => processManager,
        Platform: () => fakePlatform,
      });

      group('full command', () {
        late FakeProcessManager fakeProcessManager;
        late Directory tempDir;
        late File flutterToolState;

        setUp(() {
          Cache.disableLocking();
          fakeProcessManager = FakeProcessManager.list(<FakeCommand>[
            const FakeCommand(
              command: <String>[
                'git', 'tag', '--points-at', 'HEAD',
              ],
            ),
            const FakeCommand(
              command: <String>[
                'git', 'describe', '--match', '*.*.*', '--long', '--tags', 'HEAD',
              ],
              stdout: 'v1.12.16-19-gb45b676af',
            ),
          ]);
          tempDir = globals.fs.systemTempDirectory.createTempSync('flutter_upgrade_test.');
          flutterToolState = tempDir.childFile('.flutter_tool_state');
        });

        tearDown(() {
          Cache.enableLocking();
          tryToDelete(tempDir);
        });

        testUsingContext('upgrade continue prints welcome message', () async {
          final UpgradeCommand upgradeCommand = UpgradeCommand(
            verboseHelp: false,
            commandRunner: fakeCommandRunner,
          );

          await createTestCommandRunner(upgradeCommand).run(
            <String>[
              'upgrade',
              '--continue',
            ],
          );

          expect(
            json.decode(flutterToolState.readAsStringSync()),
            containsPair('redisplay-welcome-message', true),
          );
        }, overrides: <Type, Generator>{
          FlutterVersion: () => FakeFlutterVersion(),
          ProcessManager: () => fakeProcessManager,
          PersistentToolState: () => PersistentToolState.test(
            directory: tempDir,
            logger: testLogger,
          ),
        });
      });
    });

  });
}

class FakeUpgradeCommandRunner extends UpgradeCommandRunner {
  bool willHaveUncommittedChanges = false;
  bool alreadyUpToDate = false;

  late FlutterVersion remoteVersion;

  @override
  Future<FlutterVersion> fetchLatestVersion({FlutterVersion? localVersion}) async => remoteVersion;

  @override
  Future<bool> hasUncommittedChanges() async => willHaveUncommittedChanges;

  @override
  Future<void> attemptReset(String newRevision) async {}

  @override
  Future<void> precacheArtifacts() async {}

  @override
  Future<void> updatePackages(FlutterVersion flutterVersion) async {}

  @override
  Future<void> runDoctor() async {}
}