// 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:file/file.dart';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/terminal.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/commands/downgrade.dart';
import 'package:flutter_tools/src/persistent_tool_state.dart';
import 'package:test/fake.dart';

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

void main() {
  late FileSystem fileSystem;
  late BufferLogger bufferLogger;
  late FakeTerminal terminal;
  late ProcessManager processManager;
  late FakeStdio stdio;

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

  tearDownAll(() {
    Cache.enableLocking();
  });

  setUp(() {
    stdio = FakeStdio();
    processManager = FakeProcessManager.any();
    terminal = FakeTerminal();
    fileSystem = MemoryFileSystem.test();
    bufferLogger = BufferLogger.test(terminal: terminal);
  });

  testUsingContext('Downgrade exits on unknown channel', () async {
    final FakeFlutterVersion fakeFlutterVersion = FakeFlutterVersion();
    fileSystem.currentDirectory.childFile('.flutter_tool_state')
      .writeAsStringSync('{"last-active-master-version":"invalid"}');
    final DowngradeCommand command = DowngradeCommand(
      persistentToolState: PersistentToolState.test(directory: fileSystem.currentDirectory, logger: bufferLogger),
      processManager: processManager,
      terminal: terminal,
      stdio: stdio,
      flutterVersion: fakeFlutterVersion,
      logger: bufferLogger,
    );

    expect(createTestCommandRunner(command).run(const <String>['downgrade']),
      throwsToolExit(message: 'Flutter is not currently on a known channel.'));
  });

  testUsingContext('Downgrade exits on no recorded version', () async {
    final FakeFlutterVersion fakeFlutterVersion = FakeFlutterVersion(channel: 'beta');
    fileSystem.currentDirectory.childFile('.flutter_tool_state')
      .writeAsStringSync('{"last-active-master-version":"abcd"}');
    final DowngradeCommand command = DowngradeCommand(
      persistentToolState: PersistentToolState.test(directory: fileSystem.currentDirectory, logger: bufferLogger),
      processManager: FakeProcessManager.list(<FakeCommand>[
        const FakeCommand(
          command: <String>[
            'git', 'describe', '--tags', 'abcd',
          ],
          stdout: 'v1.2.3',
        ),
      ]),
      terminal: terminal,
      stdio: stdio,
      flutterVersion: fakeFlutterVersion,
      logger: bufferLogger,
    );

    expect(createTestCommandRunner(command).run(const <String>['downgrade']),
      throwsToolExit(message:
        'There is no previously recorded version for channel "beta".\n'
        'Channel "master" was previously on: v1.2.3.'
      ),
    );
  });

  testUsingContext('Downgrade exits on unknown recorded version', () async {
    final FakeFlutterVersion fakeFlutterVersion = FakeFlutterVersion(channel: 'master');
    fileSystem.currentDirectory.childFile('.flutter_tool_state')
      .writeAsStringSync('{"last-active-master-version":"invalid"}');
    final DowngradeCommand command = DowngradeCommand(
      persistentToolState: PersistentToolState.test(directory: fileSystem.currentDirectory, logger: bufferLogger),
      processManager: FakeProcessManager.list(<FakeCommand>[
        const FakeCommand(
          command: <String>[
            'git', 'describe', '--tags', 'invalid',
          ],
          exitCode: 1,
        ),
      ]),
      terminal: terminal,
      stdio: stdio,
      flutterVersion: fakeFlutterVersion,
      logger: bufferLogger,
    );

    expect(createTestCommandRunner(command).run(const <String>['downgrade']),
      throwsToolExit(message: 'Failed to parse version for downgrade'));
  });

   testUsingContext('Downgrade prompts for user input when terminal is attached - y', () async {
    final FakeFlutterVersion fakeFlutterVersion = FakeFlutterVersion(channel: 'master');
    stdio.hasTerminal = true;
    fileSystem.currentDirectory.childFile('.flutter_tool_state')
      .writeAsStringSync('{"last-active-master-version":"g6b00b5e88"}');
    final DowngradeCommand command = DowngradeCommand(
      persistentToolState: PersistentToolState.test(directory: fileSystem.currentDirectory, logger: bufferLogger),
      processManager: processManager,
      terminal: terminal,
      stdio: stdio,
      flutterVersion: fakeFlutterVersion,
      logger: bufferLogger,
    );

    terminal.addPrompt(const <String>['y', 'n'], 'y');

    await createTestCommandRunner(command).run(const <String>['downgrade']);

    expect(bufferLogger.statusText, contains('Success'));
  });

   testUsingContext('Downgrade prompts for user input when terminal is attached - n', () async {
    final FakeFlutterVersion fakeFlutterVersion = FakeFlutterVersion(channel: 'master');
    stdio.hasTerminal = true;
    fileSystem.currentDirectory.childFile('.flutter_tool_state')
      .writeAsStringSync('{"last-active-master-version":"g6b00b5e88"}');
    final DowngradeCommand command = DowngradeCommand(
      persistentToolState: PersistentToolState.test(directory: fileSystem.currentDirectory, logger: bufferLogger),
      processManager: processManager,
      terminal: terminal,
      stdio: stdio,
      flutterVersion: fakeFlutterVersion,
      logger: bufferLogger,
    );

    terminal.addPrompt(const <String>['y', 'n'], 'n');

    await createTestCommandRunner(command).run(const <String>['downgrade']);

    expect(bufferLogger.statusText, isNot(contains('Success')));
  });

  testUsingContext('Downgrade does not prompt when there is no terminal', () async {
    final FakeFlutterVersion fakeFlutterVersion = FakeFlutterVersion(channel: 'master');
    stdio.hasTerminal = false;
    fileSystem.currentDirectory.childFile('.flutter_tool_state')
      .writeAsStringSync('{"last-active-master-version":"g6b00b5e88"}');
    final DowngradeCommand command = DowngradeCommand(
      persistentToolState: PersistentToolState.test(
        directory: fileSystem.currentDirectory,
        logger: bufferLogger,
      ),
      processManager: processManager,
      terminal: terminal,
      stdio: stdio,
      flutterVersion: fakeFlutterVersion,
      logger: bufferLogger,
    );

    await createTestCommandRunner(command).run(const <String>['downgrade']);

    expect(bufferLogger.statusText, contains('Success'));
  });

  testUsingContext('Downgrade performs correct git commands', () async {
    final FakeFlutterVersion fakeFlutterVersion = FakeFlutterVersion(channel: 'master');
    stdio.hasTerminal = false;
    fileSystem.currentDirectory.childFile('.flutter_tool_state')
      .writeAsStringSync('{"last-active-master-version":"g6b00b5e88"}');
    final DowngradeCommand command = DowngradeCommand(
      persistentToolState: PersistentToolState.test(
        directory: fileSystem.currentDirectory,
        logger: bufferLogger,
      ),
      processManager: FakeProcessManager.list(<FakeCommand>[
        const FakeCommand(
          command: <String>[
            'git', 'describe', '--tags', 'g6b00b5e88',
          ],
          stdout: 'v1.2.3',
        ),
        const FakeCommand(
          command: <String>[
            'git', 'reset', '--hard', 'g6b00b5e88',
          ],
        ),
        const FakeCommand(
          command: <String>[
            'git', 'checkout', 'master', '--',
          ],
        ),
      ]),
      terminal: terminal,
      stdio: stdio,
      flutterVersion: fakeFlutterVersion,
      logger: bufferLogger,
    );

    await createTestCommandRunner(command).run(const <String>['downgrade']);

    expect(bufferLogger.statusText, contains('Success'));
  });
}

class FakeTerminal extends Fake implements Terminal {
  @override
  bool usesTerminalUi = false;

  void addPrompt(List<String> characters, String selected) {
    _characters = characters;
    _selected = selected;
  }

  List<String>? _characters;
  late String _selected;

  @override
  Future<String> promptForCharInput(List<String> acceptedCharacters, {Logger? logger, String? prompt, int? defaultChoiceIndex, bool displayAcceptedCharacters = true}) async {
    expect(acceptedCharacters, _characters);
    return _selected;
  }
}

class FakeStdio extends Fake implements Stdio {
  @override
  bool hasTerminal = true;
}