// Copyright 2013 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:args/command_runner.dart';
import 'package:file/file.dart';
import 'package:file/memory.dart';
import 'package:flutter_plugin_tools/src/analyze_command.dart';
import 'package:flutter_plugin_tools/src/common/core.dart';
import 'package:test/test.dart';

import 'mocks.dart';
import 'util.dart';

void main() {
  late FileSystem fileSystem;
  late MockPlatform mockPlatform;
  late Directory packagesDir;
  late RecordingProcessRunner processRunner;
  late CommandRunner<void> runner;

  setUp(() {
    fileSystem = MemoryFileSystem();
    mockPlatform = MockPlatform();
    packagesDir = createPackagesDirectory(fileSystem: fileSystem);
    processRunner = RecordingProcessRunner();
    final AnalyzeCommand analyzeCommand = AnalyzeCommand(
      packagesDir,
      processRunner: processRunner,
      platform: mockPlatform,
    );

    runner = CommandRunner<void>('analyze_command', 'Test for analyze_command');
    runner.addCommand(analyzeCommand);
  });

  test('analyzes all packages', () async {
    final RepositoryPackage package1 = createFakePackage('a', packagesDir);
    final RepositoryPackage plugin2 = createFakePlugin('b', packagesDir);

    await runCapturingPrint(runner, <String>['analyze']);

    expect(
        processRunner.recordedCalls,
        orderedEquals(<ProcessCall>[
          ProcessCall('flutter', const <String>['pub', 'get'], package1.path),
          ProcessCall('dart', const <String>['analyze', '--fatal-infos'],
              package1.path),
          ProcessCall('flutter', const <String>['pub', 'get'], plugin2.path),
          ProcessCall(
              'dart', const <String>['analyze', '--fatal-infos'], plugin2.path),
        ]));
  });

  test('skips flutter pub get for examples', () async {
    final RepositoryPackage plugin1 = createFakePlugin('a', packagesDir);

    await runCapturingPrint(runner, <String>['analyze']);

    expect(
        processRunner.recordedCalls,
        orderedEquals(<ProcessCall>[
          ProcessCall('flutter', const <String>['pub', 'get'], plugin1.path),
          ProcessCall(
              'dart', const <String>['analyze', '--fatal-infos'], plugin1.path),
        ]));
  });

  test('runs flutter pub get for non-example subpackages', () async {
    final RepositoryPackage mainPackage = createFakePackage('a', packagesDir);
    final Directory otherPackagesDir =
        mainPackage.directory.childDirectory('other_packages');
    final RepositoryPackage subpackage1 =
        createFakePackage('subpackage1', otherPackagesDir);
    final RepositoryPackage subpackage2 =
        createFakePackage('subpackage2', otherPackagesDir);

    await runCapturingPrint(runner, <String>['analyze']);

    expect(
        processRunner.recordedCalls,
        orderedEquals(<ProcessCall>[
          ProcessCall(
              'flutter', const <String>['pub', 'get'], mainPackage.path),
          ProcessCall(
              'flutter', const <String>['pub', 'get'], subpackage1.path),
          ProcessCall(
              'flutter', const <String>['pub', 'get'], subpackage2.path),
          ProcessCall('dart', const <String>['analyze', '--fatal-infos'],
              mainPackage.path),
        ]));
  });

  test('passes lib/ directory with --lib-only', () async {
    final RepositoryPackage package =
        createFakePackage('a_package', packagesDir);

    await runCapturingPrint(runner, <String>['analyze', '--lib-only']);

    expect(
        processRunner.recordedCalls,
        orderedEquals(<ProcessCall>[
          ProcessCall('flutter', const <String>['pub', 'get'], package.path),
          ProcessCall('dart', const <String>['analyze', '--fatal-infos', 'lib'],
              package.path),
        ]));
  });

  test('skips when missing lib/ directory with --lib-only', () async {
    final RepositoryPackage package =
        createFakePackage('a_package', packagesDir);
    package.libDirectory.deleteSync();

    final List<String> output =
        await runCapturingPrint(runner, <String>['analyze', '--lib-only']);

    expect(processRunner.recordedCalls, isEmpty);
    expect(
      output,
      containsAllInOrder(<Matcher>[
        contains('SKIPPING: No lib/ directory'),
      ]),
    );
  });

  test(
      'does not run flutter pub get for non-example subpackages with --lib-only',
      () async {
    final RepositoryPackage mainPackage = createFakePackage('a', packagesDir);
    final Directory otherPackagesDir =
        mainPackage.directory.childDirectory('other_packages');
    createFakePackage('subpackage1', otherPackagesDir);
    createFakePackage('subpackage2', otherPackagesDir);

    await runCapturingPrint(runner, <String>['analyze', '--lib-only']);

    expect(
        processRunner.recordedCalls,
        orderedEquals(<ProcessCall>[
          ProcessCall(
              'flutter', const <String>['pub', 'get'], mainPackage.path),
          ProcessCall('dart', const <String>['analyze', '--fatal-infos', 'lib'],
              mainPackage.path),
        ]));
  });

  test("don't elide a non-contained example package", () async {
    final RepositoryPackage plugin1 = createFakePlugin('a', packagesDir);
    final RepositoryPackage plugin2 = createFakePlugin('example', packagesDir);

    await runCapturingPrint(runner, <String>['analyze']);

    expect(
        processRunner.recordedCalls,
        orderedEquals(<ProcessCall>[
          ProcessCall('flutter', const <String>['pub', 'get'], plugin1.path),
          ProcessCall(
              'dart', const <String>['analyze', '--fatal-infos'], plugin1.path),
          ProcessCall('flutter', const <String>['pub', 'get'], plugin2.path),
          ProcessCall(
              'dart', const <String>['analyze', '--fatal-infos'], plugin2.path),
        ]));
  });

  test('uses a separate analysis sdk', () async {
    final RepositoryPackage plugin = createFakePlugin('a', packagesDir);

    await runCapturingPrint(
        runner, <String>['analyze', '--analysis-sdk', 'foo/bar/baz']);

    expect(
      processRunner.recordedCalls,
      orderedEquals(<ProcessCall>[
        ProcessCall(
          'flutter',
          const <String>['pub', 'get'],
          plugin.path,
        ),
        ProcessCall(
          'foo/bar/baz/bin/dart',
          const <String>['analyze', '--fatal-infos'],
          plugin.path,
        ),
      ]),
    );
  });

  test('downgrades first when requested', () async {
    final RepositoryPackage plugin = createFakePlugin('a', packagesDir);

    await runCapturingPrint(runner, <String>['analyze', '--downgrade']);

    expect(
      processRunner.recordedCalls,
      orderedEquals(<ProcessCall>[
        ProcessCall(
          'flutter',
          const <String>['pub', 'downgrade'],
          plugin.path,
        ),
        ProcessCall(
          'flutter',
          const <String>['pub', 'get'],
          plugin.path,
        ),
        ProcessCall(
          'dart',
          const <String>['analyze', '--fatal-infos'],
          plugin.path,
        ),
      ]),
    );
  });

  group('verifies analysis settings', () {
    test('fails analysis_options.yaml', () async {
      createFakePlugin('foo', packagesDir,
          extraFiles: <String>['analysis_options.yaml']);

      Error? commandError;
      final List<String> output = await runCapturingPrint(
          runner, <String>['analyze'], errorHandler: (Error e) {
        commandError = e;
      });

      expect(commandError, isA<ToolExit>());
      expect(
        output,
        containsAllInOrder(<Matcher>[
          contains(
              'Found an extra analysis_options.yaml at /packages/foo/analysis_options.yaml'),
          contains('  foo:\n'
              '    Unexpected local analysis options'),
        ]),
      );
    });

    test('fails .analysis_options', () async {
      createFakePlugin('foo', packagesDir,
          extraFiles: <String>['.analysis_options']);

      Error? commandError;
      final List<String> output = await runCapturingPrint(
          runner, <String>['analyze'], errorHandler: (Error e) {
        commandError = e;
      });

      expect(commandError, isA<ToolExit>());
      expect(
        output,
        containsAllInOrder(<Matcher>[
          contains(
              'Found an extra analysis_options.yaml at /packages/foo/.analysis_options'),
          contains('  foo:\n'
              '    Unexpected local analysis options'),
        ]),
      );
    });

    test('takes an allow list', () async {
      final RepositoryPackage plugin = createFakePlugin('foo', packagesDir,
          extraFiles: <String>['analysis_options.yaml']);

      await runCapturingPrint(
          runner, <String>['analyze', '--custom-analysis', 'foo']);

      expect(
          processRunner.recordedCalls,
          orderedEquals(<ProcessCall>[
            ProcessCall('flutter', const <String>['pub', 'get'], plugin.path),
            ProcessCall('dart', const <String>['analyze', '--fatal-infos'],
                plugin.path),
          ]));
    });

    test('takes an allow config file', () async {
      final RepositoryPackage plugin = createFakePlugin('foo', packagesDir,
          extraFiles: <String>['analysis_options.yaml']);
      final File allowFile = packagesDir.childFile('custom.yaml');
      allowFile.writeAsStringSync('- foo');

      await runCapturingPrint(
          runner, <String>['analyze', '--custom-analysis', allowFile.path]);

      expect(
          processRunner.recordedCalls,
          orderedEquals(<ProcessCall>[
            ProcessCall('flutter', const <String>['pub', 'get'], plugin.path),
            ProcessCall('dart', const <String>['analyze', '--fatal-infos'],
                plugin.path),
          ]));
    });

    test('allows an empty config file', () async {
      createFakePlugin('foo', packagesDir,
          extraFiles: <String>['analysis_options.yaml']);
      final File allowFile = packagesDir.childFile('custom.yaml');
      allowFile.createSync();

      await expectLater(
          () => runCapturingPrint(
              runner, <String>['analyze', '--custom-analysis', allowFile.path]),
          throwsA(isA<ToolExit>()));
    });

    // See: https://github.com/flutter/flutter/issues/78994
    test('takes an empty allow list', () async {
      createFakePlugin('foo', packagesDir,
          extraFiles: <String>['analysis_options.yaml']);

      await expectLater(
          () => runCapturingPrint(
              runner, <String>['analyze', '--custom-analysis', '']),
          throwsA(isA<ToolExit>()));
    });
  });

  test('skips if requested if "pub get" fails in the resolver', () async {
    final RepositoryPackage plugin = createFakePlugin('foo', packagesDir);

    final FakeProcessInfo failingPubGet = FakeProcessInfo(
        MockProcess(
            exitCode: 1,
            stderr: 'So, because foo depends on both thing_one ^1.0.0 and '
                'thing_two from path, version solving failed.'),
        <String>['pub', 'get']);
    processRunner.mockProcessesForExecutable['flutter'] = <FakeProcessInfo>[
      failingPubGet,
      // The command re-runs failures when --skip-if-resolver-fails is passed
      // to check the output, so provide the same failing outcome.
      failingPubGet,
    ];

    final List<String> output = await runCapturingPrint(
        runner, <String>['analyze', '--skip-if-resolving-fails']);

    expect(
      output,
      containsAllInOrder(<Matcher>[
        contains('Skipping package due to pub resolution failure.'),
      ]),
    );
    expect(
        processRunner.recordedCalls,
        orderedEquals(<ProcessCall>[
          ProcessCall('flutter', const <String>['pub', 'get'], plugin.path),
          ProcessCall('flutter', const <String>['pub', 'get'], plugin.path),
        ]));
  });

  test('fails if "pub get" fails', () async {
    createFakePlugin('foo', packagesDir);

    processRunner.mockProcessesForExecutable['flutter'] = <FakeProcessInfo>[
      FakeProcessInfo(MockProcess(exitCode: 1), <String>['pub', 'get'])
    ];

    Error? commandError;
    final List<String> output = await runCapturingPrint(
        runner, <String>['analyze'], errorHandler: (Error e) {
      commandError = e;
    });

    expect(commandError, isA<ToolExit>());
    expect(
      output,
      containsAllInOrder(<Matcher>[
        contains('Unable to get dependencies'),
      ]),
    );
  });

  test('fails if "pub downgrade" fails', () async {
    createFakePlugin('foo', packagesDir);

    processRunner.mockProcessesForExecutable['flutter'] = <FakeProcessInfo>[
      FakeProcessInfo(MockProcess(exitCode: 1), <String>['pub', 'downgrade'])
    ];

    Error? commandError;
    final List<String> output = await runCapturingPrint(
        runner, <String>['analyze', '--downgrade'], errorHandler: (Error e) {
      commandError = e;
    });

    expect(commandError, isA<ToolExit>());
    expect(
      output,
      containsAllInOrder(<Matcher>[
        contains('Unable to downgrade dependencies'),
      ]),
    );
  });

  test('fails if "analyze" fails', () async {
    createFakePlugin('foo', packagesDir);

    processRunner.mockProcessesForExecutable['dart'] = <FakeProcessInfo>[
      FakeProcessInfo(MockProcess(exitCode: 1), <String>['analyze'])
    ];

    Error? commandError;
    final List<String> output = await runCapturingPrint(
        runner, <String>['analyze'], errorHandler: (Error e) {
      commandError = e;
    });

    expect(commandError, isA<ToolExit>());
    expect(
      output,
      containsAllInOrder(<Matcher>[
        contains('The following packages had errors:'),
        contains('  foo'),
      ]),
    );
  });

  // Ensure that the command used to analyze flutter/plugins in the Dart repo:
  // https://github.com/dart-lang/sdk/blob/main/tools/bots/flutter/analyze_flutter_plugins.sh
  // continues to work.
  //
  // DO NOT remove or modify this test without a coordination plan in place to
  // modify the script above, as it is run from source, but out-of-repo.
  // Contact stuartmorgan or devoncarew for assistance.
  test('Dart repo analyze command works', () async {
    final RepositoryPackage plugin = createFakePlugin('foo', packagesDir,
        extraFiles: <String>['analysis_options.yaml']);
    final File allowFile = packagesDir.childFile('custom.yaml');
    allowFile.writeAsStringSync('- foo');

    await runCapturingPrint(runner, <String>[
      // DO NOT change this call; see comment above.
      'analyze',
      '--analysis-sdk',
      'foo/bar/baz',
      '--custom-analysis',
      allowFile.path
    ]);

    expect(
      processRunner.recordedCalls,
      orderedEquals(<ProcessCall>[
        ProcessCall(
          'flutter',
          const <String>['pub', 'get'],
          plugin.path,
        ),
        ProcessCall(
          'foo/bar/baz/bin/dart',
          const <String>['analyze', '--fatal-infos'],
          plugin.path,
        ),
      ]),
    );
  });
}