// 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:args/command_runner.dart';
import 'package:conductor_core/src/codesign.dart';
import 'package:conductor_core/src/repository.dart';
import 'package:file/file.dart';
import 'package:file/memory.dart';
import 'package:platform/platform.dart';

import './common.dart';

void main() {
  group('codesign command', () {
    const String flutterRoot = '/flutter';
    const String checkoutsParentDirectory = '$flutterRoot/dev/conductor/';
    const String flutterCache =
        '${checkoutsParentDirectory}flutter_conductor_checkouts/framework/bin/cache';
    const String flutterBin =
        '${checkoutsParentDirectory}flutter_conductor_checkouts/framework/bin/flutter';
    const String revision = 'abcd1234';
    late CommandRunner<void> runner;
    late Checkouts checkouts;
    late MemoryFileSystem fileSystem;
    late FakePlatform platform;
    late TestStdio stdio;
    late FakeProcessManager processManager;
    const List<String> binariesWithEntitlements = <String>[
      '$flutterCache/dart-sdk/bin/dart',
      '$flutterCache/dart-sdk/bin/dartaotruntime',
    ];
    const List<String> binariesWithoutEntitlements = <String>[
      '$flutterCache/engine/darwin-x64/font-subset',
    ];
    const List<String> allBinaries = <String>[
      ...binariesWithEntitlements,
      ...binariesWithoutEntitlements,
    ];

    void createRunner({
      String operatingSystem = 'macos',
      List<FakeCommand>? commands,
    }) {
      stdio = TestStdio();
      fileSystem = MemoryFileSystem.test();
      platform = FakePlatform(operatingSystem: operatingSystem);
      processManager = FakeProcessManager.list(commands ?? <FakeCommand>[]);
      checkouts = Checkouts(
        fileSystem: fileSystem,
        parentDirectory: fileSystem.directory(checkoutsParentDirectory),
        platform: platform,
        processManager: processManager,
        stdio: stdio,
      );
      final FakeCodesignCommand command = FakeCodesignCommand(
        checkouts: checkouts,
        binariesWithEntitlements: Future<List<String>>.value(binariesWithEntitlements),
        binariesWithoutEntitlements: Future<List<String>>.value(binariesWithoutEntitlements),
        flutterRoot: fileSystem.directory(flutterRoot),
      );
      runner = CommandRunner<void>('codesign-test', '')
        ..addCommand(command);
    }

    test('throws exception if not run from macos', () async {
      createRunner(operatingSystem: 'linux');
      expect(
        () async => runner.run(<String>['codesign']),
        throwsExceptionWith('Error! Expected operating system "macos"'),
      );
    });

    test('throws exception if verify flag is not provided', () async {
      createRunner();
      expect(
        () async => runner.run(<String>['codesign']),
        throwsExceptionWith(
            'Sorry, but codesigning is not implemented yet. Please pass the --$kVerify flag to verify signatures'),
      );
    });

    test('does not fail if --revision flag not provided', () async {
      final List<FakeCommand> codesignCheckCommands = <FakeCommand>[];
      for (final String bin in binariesWithEntitlements) {
        codesignCheckCommands.add(
          FakeCommand(
            command: <String>['codesign', '-vvv', bin],
          ),
        );
        codesignCheckCommands.add(
          FakeCommand(
            command: <String>['codesign', '--display', '--entitlements', ':-', bin],
            stdout: expectedEntitlements.join('\n'),
          ),
        );
      }
      for (final String bin in binariesWithoutEntitlements) {
        codesignCheckCommands.add(
          FakeCommand(
            command: <String>['codesign', '-vvv', bin],
          ),
        );
      }
      createRunner(commands: <FakeCommand>[
        const FakeCommand(command: <String>[
          'git',
          'clone',
          '--origin',
          'upstream',
          '--',
          'file://$flutterRoot/',
          '${checkoutsParentDirectory}flutter_conductor_checkouts/framework',
        ]),
        const FakeCommand(command: <String>[
          'git',
          'rev-parse',
          'HEAD',
        ], stdout: revision),
        const FakeCommand(command: <String>[
          'git',
          'rev-parse',
          'HEAD',
        ], stdout: revision),
        const FakeCommand(command: <String>[
          'git',
          'checkout',
          revision,
        ]),
        const FakeCommand(command: <String>[
          flutterBin,
          'help',
        ]),
        const FakeCommand(command: <String>[
          flutterBin,
          'help',
        ]),
        const FakeCommand(command: <String>[
          flutterBin,
          'precache',
          '--android',
          '--ios',
          '--macos',
        ]),
        FakeCommand(
          command: const <String>[
            'find',
            '${checkoutsParentDirectory}flutter_conductor_checkouts/framework/bin/cache',
            '-type',
            'f',
          ],
          stdout: allBinaries.join('\n'),
        ),
        for (String bin in allBinaries)
          FakeCommand(
            command: <String>['file', '--mime-type', '-b', bin],
            stdout: 'application/x-mach-binary',
          ),
        ...codesignCheckCommands,
      ]);
      await runner.run(<String>['codesign', '--$kVerify']);
      expect(processManager.hasRemainingExpectations, false);
      expect(stdio.stdout, contains('Verified that binaries are codesigned and have expected entitlements'));
    });


    test('succeeds if every binary is codesigned and has correct entitlements', () async {
      final List<FakeCommand> codesignCheckCommands = <FakeCommand>[];
      for (final String bin in binariesWithEntitlements) {
        codesignCheckCommands.add(
          FakeCommand(
            command: <String>['codesign', '-vvv', bin],
          ),
        );
        codesignCheckCommands.add(
          FakeCommand(
            command: <String>['codesign', '--display', '--entitlements', ':-', bin],
            stdout: expectedEntitlements.join('\n'),
          ),
        );
      }
      for (final String bin in binariesWithoutEntitlements) {
        codesignCheckCommands.add(
          FakeCommand(
            command: <String>['codesign', '-vvv', bin],
          ),
        );
      }
      createRunner(commands: <FakeCommand>[
        const FakeCommand(command: <String>[
          'git',
          'clone',
          '--origin',
          'upstream',
          '--',
          'file://$flutterRoot/',
          '${checkoutsParentDirectory}flutter_conductor_checkouts/framework',
        ]),
        const FakeCommand(command: <String>[
          'git',
          'rev-parse',
          'HEAD',
        ], stdout: revision),
        const FakeCommand(command: <String>[
          'git',
          'checkout',
          revision,
        ]),
        const FakeCommand(command: <String>[
          flutterBin,
          'help',
        ]),
        const FakeCommand(command: <String>[
          flutterBin,
          'help',
        ]),
        const FakeCommand(command: <String>[
          flutterBin,
          'precache',
          '--android',
          '--ios',
          '--macos',
        ]),
        FakeCommand(
          command: const <String>[
            'find',
            '${checkoutsParentDirectory}flutter_conductor_checkouts/framework/bin/cache',
            '-type',
            'f',
          ],
          stdout: allBinaries.join('\n'),
        ),
        for (String bin in allBinaries)
          FakeCommand(
            command: <String>['file', '--mime-type', '-b', bin],
            stdout: 'application/x-mach-binary',
          ),
        ...codesignCheckCommands,
      ]);
      await runner.run(<String>['codesign', '--$kVerify', '--$kRevision', revision]);
      expect(processManager.hasRemainingExpectations, false);
      expect(stdio.stdout, contains('Verified that binaries for commit $revision are codesigned and have expected entitlements'));
    });

    test('fails if a single binary is not codesigned', () async {
      final List<FakeCommand> codesignCheckCommands = <FakeCommand>[];
      codesignCheckCommands.add(
        const FakeCommand(
          command: <String>['codesign', '-vvv', '$flutterCache/dart-sdk/bin/dart'],
        ),
      );
      codesignCheckCommands.add(
        FakeCommand(
          command: const <String>[
            'codesign',
            '--display',
            '--entitlements',
            ':-',
            '$flutterCache/dart-sdk/bin/dart',
          ],
          stdout: expectedEntitlements.join('\n'),
        )
      );
      // Not signed
      codesignCheckCommands.add(
        const FakeCommand(
          command: <String>['codesign', '-vvv', '$flutterCache/dart-sdk/bin/dartaotruntime'],
          exitCode: 1,
        ),
      );
      codesignCheckCommands.add(
        const FakeCommand(
          command: <String>['codesign', '-vvv', '$flutterCache/engine/darwin-x64/font-subset'],
        ),
      );

      createRunner(commands: <FakeCommand>[
        const FakeCommand(command: <String>[
          'git',
          'clone',
          '--origin',
          'upstream',
          '--',
          'file://$flutterRoot/',
          '${checkoutsParentDirectory}flutter_conductor_checkouts/framework',
        ]),
        const FakeCommand(command: <String>[
          'git',
          'rev-parse',
          'HEAD',
        ], stdout: revision),
        const FakeCommand(command: <String>[
          'git',
          'checkout',
          revision,
        ]),
        const FakeCommand(command: <String>[
          flutterBin,
          'help',
        ]),
        const FakeCommand(command: <String>[
          flutterBin,
          'help',
        ]),
        const FakeCommand(command: <String>[
          flutterBin,
          'precache',
          '--android',
          '--ios',
          '--macos',
        ]),
        FakeCommand(
          command: const <String>[
            'find',
            '${checkoutsParentDirectory}flutter_conductor_checkouts/framework/bin/cache',
            '-type',
            'f',
          ],
          stdout: allBinaries.join('\n'),
        ),
        for (String bin in allBinaries)
          FakeCommand(
            command: <String>['file', '--mime-type', '-b', bin],
            stdout: 'application/x-mach-binary',
          ),
        ...codesignCheckCommands,
      ]);
      await expectLater(
        () => runner.run(<String>['codesign', '--$kVerify', '--$kRevision', revision]),
        throwsExceptionWith('Test failed because unsigned binaries detected.'),
      );
      expect(processManager.hasRemainingExpectations, false);
    });

    test('fails if a single binary has the wrong entitlements', () async {
      final List<FakeCommand> codesignCheckCommands = <FakeCommand>[];
      codesignCheckCommands.add(
        const FakeCommand(
          command: <String>['codesign', '-vvv', '$flutterCache/dart-sdk/bin/dart'],
        ),
      );
      codesignCheckCommands.add(
        FakeCommand(
          command: const <String>['codesign', '--display', '--entitlements', ':-', '$flutterCache/dart-sdk/bin/dart'],
          stdout: expectedEntitlements.join('\n'),
        )
      );
      codesignCheckCommands.add(
        const FakeCommand(
          command: <String>['codesign', '-vvv', '$flutterCache/dart-sdk/bin/dartaotruntime'],
        ),
      );
      // No entitlements
      codesignCheckCommands.add(
        const FakeCommand(
          command: <String>['codesign', '--display', '--entitlements', ':-', '$flutterCache/dart-sdk/bin/dartaotruntime'],
        )
      );
      codesignCheckCommands.add(
        const FakeCommand(
          command: <String>['codesign', '-vvv', '$flutterCache/engine/darwin-x64/font-subset'],
        ),
      );
      createRunner(commands: <FakeCommand>[
        const FakeCommand(command: <String>[
          'git',
          'clone',
          '--origin',
          'upstream',
          '--',
          'file://$flutterRoot/',
          '${checkoutsParentDirectory}flutter_conductor_checkouts/framework',
        ]),
        const FakeCommand(command: <String>[
          'git',
          'rev-parse',
          'HEAD',
        ], stdout: revision),
        const FakeCommand(command: <String>[
          'git',
          'checkout',
          revision,
        ]),
        const FakeCommand(command: <String>[
          flutterBin,
          'help',
        ]),
        const FakeCommand(command: <String>[
          flutterBin,
          'help',
        ]),
        const FakeCommand(command: <String>[
          flutterBin,
          'precache',
          '--android',
          '--ios',
          '--macos',
        ]),
        FakeCommand(
          command: const <String>[
            'find',
            '${checkoutsParentDirectory}flutter_conductor_checkouts/framework/bin/cache',
            '-type',
            'f',
          ],
          stdout: allBinaries.join('\n'),
        ),
        for (String bin in allBinaries)
          FakeCommand(
            command: <String>['file', '--mime-type', '-b', bin],
            stdout: 'application/x-mach-binary',
          ),
        ...codesignCheckCommands,
      ]);
      await expectLater(
        () => runner.run(<String>['codesign', '--$kVerify', '--$kRevision', revision]),
        throwsExceptionWith('Test failed because files found with the wrong entitlements'),
      );
      expect(processManager.hasRemainingExpectations, false);
    });

    test('does not check signatures or entitlements if --no-$kSignatures specified', () async {
      createRunner(commands: <FakeCommand>[
        const FakeCommand(command: <String>[
          'git',
          'clone',
          '--origin',
          'upstream',
          '--',
          'file://$flutterRoot/',
          '${checkoutsParentDirectory}flutter_conductor_checkouts/framework',
        ]),
        const FakeCommand(command: <String>[
          'git',
          'rev-parse',
          'HEAD',
        ], stdout: revision),
        const FakeCommand(command: <String>[
          'git',
          'checkout',
          revision,
        ]),
        const FakeCommand(command: <String>[
          flutterBin,
          'help',
        ]),
        const FakeCommand(command: <String>[
          flutterBin,
          'help',
        ]),
        const FakeCommand(command: <String>[
          flutterBin,
          'precache',
          '--android',
          '--ios',
          '--macos',
        ]),
        FakeCommand(
          command: const <String>[
            'find',
            '${checkoutsParentDirectory}flutter_conductor_checkouts/framework/bin/cache',
            '-type',
            'f',
          ],
          stdout: allBinaries.join('\n'),
        ),
        for (String bin in allBinaries)
          FakeCommand(
            command: <String>['file', '--mime-type', '-b', bin],
            stdout: 'application/x-mach-binary',
          ),
      ]);
      await runner.run(<String>[
        'codesign',
        '--$kVerify',
        '--no-$kSignatures',
        '--$kRevision',
        revision,
      ]);
      expect(
        processManager.hasRemainingExpectations,
        false,
      );
    });
  });
}

class FakeCodesignCommand extends CodesignCommand {
  FakeCodesignCommand({
    required Checkouts checkouts,
    required this.binariesWithEntitlements,
    required this.binariesWithoutEntitlements,
    required Directory flutterRoot,
  }) : super(checkouts: checkouts, flutterRoot: flutterRoot);

  @override
  final Future<List<String>> binariesWithEntitlements;

  @override
  final Future<List<String>> binariesWithoutEntitlements;
}