// 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/memory.dart';
import 'package:flutter_tools/src/android/android_device.dart';
import 'package:flutter_tools/src/android/android_sdk.dart';
import 'package:flutter_tools/src/android/application_package.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:test/fake.dart';

import '../../src/common.dart';
import '../../src/fake_process_manager.dart';

const kAdbVersionCommand = FakeCommand(
  command: <String>['adb', 'version'],
  stdout: 'Android Debug Bridge version 1.0.39',
);
const kAdbStartServerCommand = FakeCommand(command: <String>['adb', 'start-server']);
const kInstallCommand = FakeCommand(
  command: <String>['adb', '-s', '1234', 'install', '-t', '-r', '--user', '10', 'app-debug.apk'],
);
const kStoreShaCommand = FakeCommand(
  command: <String>[
    'adb',
    '-s',
    '1234',
    'shell',
    'echo',
    '-n',
    '',
    '>',
    '/data/local/tmp/sky.app.sha1',
  ],
);

void main() {
  late FileSystem fileSystem;
  late BufferLogger logger;

  setUp(() {
    fileSystem = MemoryFileSystem.test();
    logger = BufferLogger.test();
  });

  AndroidDevice setUpAndroidDevice({AndroidSdk? androidSdk, ProcessManager? processManager}) {
    androidSdk ??= FakeAndroidSdk();
    return AndroidDevice(
      '1234',
      modelID: 'TestModel',
      logger: logger,
      platform: FakePlatform(),
      androidSdk: androidSdk,
      fileSystem: fileSystem,
      processManager: processManager ?? FakeProcessManager.any(),
    );
  }

  testWithoutContext('Cannot install app on API level below 16', () async {
    final processManager = FakeProcessManager.list(<FakeCommand>[
      kAdbVersionCommand,
      kAdbStartServerCommand,
      const FakeCommand(
        command: <String>['adb', '-s', '1234', 'shell', 'getprop'],
        stdout: '[ro.build.version.sdk]: [11]',
      ),
    ]);
    final File apk = fileSystem.file('app-debug.apk')..createSync();
    final androidApk = AndroidApk(
      applicationPackage: apk,
      id: 'app',
      versionCode: 22,
      launchActivity: 'Main',
    );
    final AndroidDevice androidDevice = setUpAndroidDevice(processManager: processManager);

    expect(await androidDevice.installApp(androidApk), false);
    expect(processManager, hasNoRemainingExpectations);
  });

  testWithoutContext('Cannot install app if APK file is missing', () async {
    final File apk = fileSystem.file('app-debug.apk');
    final androidApk = AndroidApk(
      applicationPackage: apk,
      id: 'app',
      versionCode: 22,
      launchActivity: 'Main',
    );
    final AndroidDevice androidDevice = setUpAndroidDevice();

    expect(await androidDevice.installApp(androidApk), false);
  });

  testWithoutContext('Can install app on API level 16 or greater', () async {
    final processManager = FakeProcessManager.list(<FakeCommand>[
      kAdbVersionCommand,
      kAdbStartServerCommand,
      const FakeCommand(
        command: <String>['adb', '-s', '1234', 'shell', 'getprop'],
        stdout: '[ro.build.version.sdk]: [16]',
      ),
      kInstallCommand,
      kStoreShaCommand,
    ]);
    final File apk = fileSystem.file('app-debug.apk')..createSync();
    final androidApk = AndroidApk(
      applicationPackage: apk,
      id: 'app',
      versionCode: 22,
      launchActivity: 'Main',
    );
    final AndroidDevice androidDevice = setUpAndroidDevice(processManager: processManager);

    expect(await androidDevice.installApp(androidApk, userIdentifier: '10'), true);
    expect(processManager, hasNoRemainingExpectations);
  });

  testWithoutContext('Defaults to API level 16 if adb returns a null response', () async {
    final processManager = FakeProcessManager.list(<FakeCommand>[
      kAdbVersionCommand,
      kAdbStartServerCommand,
      const FakeCommand(command: <String>['adb', '-s', '1234', 'shell', 'getprop']),
      kInstallCommand,
      kStoreShaCommand,
    ]);
    final File apk = fileSystem.file('app-debug.apk')..createSync();
    final androidApk = AndroidApk(
      applicationPackage: apk,
      id: 'app',
      versionCode: 22,
      launchActivity: 'Main',
    );
    final AndroidDevice androidDevice = setUpAndroidDevice(processManager: processManager);

    expect(await androidDevice.installApp(androidApk, userIdentifier: '10'), true);
    expect(processManager, hasNoRemainingExpectations);
  });

  testWithoutContext('displays error if user not found', () async {
    final processManager = FakeProcessManager.list(<FakeCommand>[
      kAdbVersionCommand,
      kAdbStartServerCommand,
      const FakeCommand(command: <String>['adb', '-s', '1234', 'shell', 'getprop']),
      // This command is run before the user is checked and is allowed to fail.
      const FakeCommand(
        command: <String>[
          'adb',
          '-s',
          '1234',
          'install',
          '-t',
          '-r',
          '--user',
          'jane',
          'app-debug.apk',
        ],
        exitCode: 1,
        stderr:
            'Exception occurred while executing: '
            'java.lang.IllegalArgumentException: '
            'Bad user number: jane',
      ),
    ]);
    final File apk = fileSystem.file('app-debug.apk')..createSync();
    final androidApk = AndroidApk(
      applicationPackage: apk,
      id: 'app',
      versionCode: 22,
      launchActivity: 'Main',
    );
    final AndroidDevice androidDevice = setUpAndroidDevice(processManager: processManager);

    expect(await androidDevice.installApp(androidApk, userIdentifier: 'jane'), false);
    expect(
      logger.errorText,
      contains(
        'Error: User "jane" not found. Run "adb shell pm list users" '
        'to see list of available identifiers.',
      ),
    );
    expect(processManager, hasNoRemainingExpectations);
  });

  testWithoutContext('Will continue install if the correct version is up to date', () async {
    final processManager = FakeProcessManager.list(<FakeCommand>[
      kAdbVersionCommand,
      kAdbStartServerCommand,
      const FakeCommand(
        command: <String>['adb', '-s', '1234', 'shell', 'getprop'],
        stdout: '[ro.build.version.sdk]: [16]',
      ),
      kInstallCommand,
      const FakeCommand(
        command: <String>[
          'adb',
          '-s',
          '1234',
          'shell',
          'echo',
          '-n',
          'example_sha',
          '>',
          '/data/local/tmp/sky.app.sha1',
        ],
        stdout: 'example_sha',
      ),
    ]);
    final File apk = fileSystem.file('app-debug.apk')..createSync();
    fileSystem.file('app-debug.apk.sha1').writeAsStringSync('example_sha');
    final androidApk = AndroidApk(
      applicationPackage: apk,
      id: 'app',
      versionCode: 22,
      launchActivity: 'Main',
    );
    final AndroidDevice androidDevice = setUpAndroidDevice(processManager: processManager);

    expect(await androidDevice.installApp(androidApk, userIdentifier: '10'), true);
    expect(processManager, hasNoRemainingExpectations);
  });

  testWithoutContext(
    'Will uninstall if the correct version is not up to date and install fails',
    () async {
      final processManager = FakeProcessManager.list(<FakeCommand>[
        kAdbVersionCommand,
        kAdbStartServerCommand,
        const FakeCommand(
          command: <String>['adb', '-s', '1234', 'shell', 'getprop'],
          stdout: '[ro.build.version.sdk]: [16]',
        ),
        const FakeCommand(
          command: <String>[
            'adb',
            '-s',
            '1234',
            'install',
            '-t',
            '-r',
            '--user',
            '10',
            'app-debug.apk',
          ],
          exitCode: 1,
          stderr: '[INSTALL_FAILED_INSUFFICIENT_STORAGE]',
        ),
        const FakeCommand(
          command: <String>[
            'adb',
            '-s',
            '1234',
            'shell',
            'pm',
            'list',
            'packages',
            '--user',
            '10',
            'app',
          ],
          stdout: 'package:app\n',
        ),
        const FakeCommand(
          command: <String>['adb', '-s', '1234', 'uninstall', '--user', '10', 'app'],
        ),
        kInstallCommand,
        const FakeCommand(
          command: <String>[
            'adb',
            '-s',
            '1234',
            'shell',
            'echo',
            '-n',
            'example_sha',
            '>',
            '/data/local/tmp/sky.app.sha1',
          ],
        ),
      ]);
      final File apk = fileSystem.file('app-debug.apk')..createSync();
      fileSystem.file('app-debug.apk.sha1').writeAsStringSync('example_sha');
      final androidApk = AndroidApk(
        applicationPackage: apk,
        id: 'app',
        versionCode: 22,
        launchActivity: 'Main',
      );
      final AndroidDevice androidDevice = setUpAndroidDevice(processManager: processManager);

      expect(await androidDevice.installApp(androidApk, userIdentifier: '10'), true);
      expect(processManager, hasNoRemainingExpectations);
    },
  );

  testWithoutContext(
    'Will fail to install if the apk was never installed and it fails the first time',
    () async {
      final processManager = FakeProcessManager.list(<FakeCommand>[
        kAdbVersionCommand,
        kAdbStartServerCommand,
        const FakeCommand(
          command: <String>['adb', '-s', '1234', 'shell', 'getprop'],
          stdout: '[ro.build.version.sdk]: [16]',
        ),
        const FakeCommand(
          command: <String>[
            'adb',
            '-s',
            '1234',
            'install',
            '-t',
            '-r',
            '--user',
            '10',
            'app-debug.apk',
          ],
          exitCode: 1,
          stderr: '[INSTALL_FAILED_INSUFFICIENT_STORAGE]',
        ),
      ]);
      final File apk = fileSystem.file('app-debug.apk')..createSync();
      final androidApk = AndroidApk(
        applicationPackage: apk,
        id: 'app',
        versionCode: 22,
        launchActivity: 'Main',
      );
      final AndroidDevice androidDevice = setUpAndroidDevice(processManager: processManager);

      expect(await androidDevice.installApp(androidApk, userIdentifier: '10'), false);
      expect(processManager, hasNoRemainingExpectations);
    },
  );
}

class FakeAndroidSdk extends Fake implements AndroidSdk {
  @override
  String get adbPath => 'adb';
}
