// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for details.

import * as fs from "fs";
import * as path from "path";
import assert = require("assert");
import { AndroidPlatform } from "../../../src/extension/android/androidPlatform";
import { IAndroidRunOptions, PlatformType } from "../../../src/extension/launchArgs";
import { FileSystem } from "../../../src/common/node/fileSystem";
import { ReactNative065 } from "../../resources/reactNative065";
import * as adb from "../../../src/extension/android/adb";
import { RecordingsHelper } from "../../resources/recordingsHelper";
import { CommandExecutor } from "../../../src/common/commandExecutor";
import { ProjectVersionHelper } from "../../../src/common/projectVersionHelper";
import "should";
import * as sinon from "sinon";
import { SettingsHelper } from "../../../src/extension/settingsHelper";
import { rimrafAsync } from "../../common/utils";

suite("androidPlatform", function () {
    suite("extensionContext", function () {
        const projectRoot = path.join(
            __dirname,
            "..",
            "..",
            "resources",
            "projects",
            "SampleApplication_21",
        );
        const projectsFolder = path.join(projectRoot, "..");
        const androidProjectPath = path.join(projectRoot, "android");
        const applicationName = "SampleApplication";
        const androidPackageName = "com.sampleapplication";

        const nodeModulesRoot: string = projectRoot;
        const genericRunOptions: IAndroidRunOptions = {
            platform: PlatformType.Android,
            workspaceRoot: projectRoot,
            projectRoot,
            reactNativeVersions: {
                reactNativeVersion: "^0.19.0",
                reactNativeWindowsVersion: "",
                reactNativeMacOSVersion: "",
            },
            nodeModulesRoot,
        };

        const rnProjectContent = fs.readFileSync(ReactNative065.DEFAULT_PROJECT_FILE, "utf8");

        let fileSystem: FileSystem;
        let reactNative: ReactNative065;
        let androidPlatform: AndroidPlatform;

        let launchAppStub: Sinon.SinonStub;
        let getConnectedTargetsStub: Sinon.SinonStub;
        let getOnlineTargetsStub: Sinon.SinonStub;
        let apiVersionStub: Sinon.SinonStub;
        let reverseAdbStub: Sinon.SinonStub;
        let getReactNativeProjectRootStub: Sinon.SinonStub;
        let spawnReactCommandStub: Sinon.SinonStub;
        let getReactNativeVersionsStub: Sinon.SinonStub;
        let installAppInDeviceStub: Sinon.SinonStub;

        let devices: any;
        let adbHelper: adb.AdbHelper;

        function createAndroidPlatform(runOptions: IAndroidRunOptions): AndroidPlatform {
            return new AndroidPlatform(runOptions);
        }

        setup(async () => {
            // Configure all the dependencies we'll use in our tests
            fileSystem = new FileSystem();

            adbHelper = new adb.AdbHelper(genericRunOptions.projectRoot, nodeModulesRoot);
            launchAppStub = sinon.stub(
                adbHelper,
                "launchApp",
                async (projectRoot_: string, packageName: string, debugTarget?: string) => {
                    devices = devices.map((device: any) => {
                        if (!debugTarget) {
                            device.installedApplications[androidPackageName] = {
                                isInDebugMode: false,
                            };
                        }

                        if (debugTarget && debugTarget === device.id) {
                            device.installedApplications[androidPackageName] = {
                                isInDebugMode: false,
                            };
                        }

                        return device;
                    });
                },
            );
            getConnectedTargetsStub = sinon.stub(
                adbHelper,
                "getConnectedTargets",
                async function () {
                    return devices;
                },
            );
            getOnlineTargetsStub = sinon.stub(adbHelper, "getOnlineTargets", async function () {
                return devices.filter((device: any) => {
                    return device.isOnline;
                });
            });
            apiVersionStub = sinon.stub(adbHelper, "apiVersion", async function () {
                return adb.AndroidAPILevel.LOLLIPOP;
            });
            reverseAdbStub = sinon.stub(adbHelper, "reverseAdb", async function () {
                return;
            });

            reactNative = new ReactNative065(fileSystem, adbHelper);

            getReactNativeProjectRootStub = sinon.stub(
                SettingsHelper,
                "getReactNativeProjectRoot",
                () => projectRoot,
            );

            androidPlatform = createAndroidPlatform(genericRunOptions);

            spawnReactCommandStub = sinon.stub(
                CommandExecutor.prototype,
                "spawnReactCommand",
                function () {
                    return reactNative.runAndroid(genericRunOptions);
                },
            );

            getReactNativeVersionsStub = sinon.stub(
                ProjectVersionHelper,
                "getReactNativeVersions",
                async function () {
                    return {
                        reactNativeVersion: "0.0.1",
                        reactNativeWindowsVersion: "",
                    };
                },
            );

            (androidPlatform as any).adbHelper = adbHelper;

            installAppInDeviceStub = sinon.stub(
                reactNative,
                "installAppInDevice",
                async function (deviceId: string) {
                    devices = devices.map((device: any) => {
                        if (deviceId && deviceId === device.id) {
                            device.installedApplications[androidPackageName] = {};
                        }

                        return device;
                    });
                },
            );

            // Delete existing React Native project before creating
            await rimrafAsync(projectsFolder, {});
            // Create a React-Native project we'll use in our tests
            await reactNative
                .fromProjectFileContent(rnProjectContent)
                .createProject(projectRoot, applicationName);
        });

        teardown(async () => {
            // Delete existing React Native project after each test
            await rimrafAsync(projectsFolder, {});
            launchAppStub.restore();
            getConnectedTargetsStub.restore();
            getOnlineTargetsStub.restore();
            apiVersionStub.restore();
            reverseAdbStub.restore();
            getReactNativeProjectRootStub.restore();
            spawnReactCommandStub.restore();
            getReactNativeVersionsStub.restore();
            installAppInDeviceStub.restore();
            devices = [];
        });

        const testWithRecordings = new RecordingsHelper(() => reactNative).test;

        testWithRecordings(
            "runApp launches the app when a single emulator is connected",
            [
                "react-native/run-android/win10-rn0.21.0/succeedsWithOneVSEmulator",
                "react-native/run-android/win10-rn0.22.2/succeedsWithOneVSEmulator",
                "react-native/run-android/osx10.10-rn0.21.0/succeedsWithOneVSEmulator",
            ],
            async () => {
                devices = fillDevices(["Nexus_5"]);

                await androidPlatform.runApp();
                const isRunning =
                    devices[0].installedApplications[androidPackageName].isInDebugMode === false;
                isRunning.should.be.true();
            },
        );

        testWithRecordings(
            "runApp launches the app when two emulators are connected",
            ["react-native/run-android/win10-rn0.21.0/succeedsWithTwoVSEmulators"],
            async () => {
                devices = fillDevices(["Nexus_5", "Nexus_6"]);

                await androidPlatform.runApp();
                const [isRunningOnNexus5, isRunningOnNexus6] = [
                    devices[0].installedApplications[androidPackageName].isInDebugMode === false,
                    devices[1].installedApplications[androidPackageName].isInDebugMode === false,
                ];
                // It should be running in exactly one of these two devices
                isRunningOnNexus5.should.not.eql(isRunningOnNexus6);
            },
        );

        testWithRecordings(
            "runApp launches the app when three emulators are connected",
            ["react-native/run-android/win10-rn0.21.0/succeedsWithThreeVSEmulators"],
            async () => {
                devices = fillDevices(["Nexus_5", "Nexus_6", "Nexus_7"]);

                await androidPlatform.runApp();
                const isRunningList = [
                    devices[0].installedApplications[androidPackageName].isInDebugMode === false,
                    devices[1].installedApplications[androidPackageName].isInDebugMode === false,
                    devices[2].installedApplications[androidPackageName].isInDebugMode === false,
                ];
                // It should be running in exactly one of these three devices
                isRunningList.filter(v => v).should.eql([true]);
            },
        );

        testWithRecordings(
            "runApp fails if no devices are connected",
            ["react-native/run-android/win10-rn0.21.0/failsDueToNoDevicesConnected"],
            async () => {
                try {
                    await androidPlatform.runApp();
                    should.assert(false, "runApp should've exited with an error");
                } catch (error) {
                    (error as Error).message
                        .startsWith("There is no any Android debuggable online target")
                        .should.be.true();
                }
            },
        );

        testWithRecordings(
            "runApp launches the app in an online emulator only",
            ["react-native/run-android/win10-rn0.21.0/succeedsWithFiveVSEmulators"],
            async () => {
                devices = fillDevices(["Nexus_5", "Nexus_6", "Nexus_7", "Nexus_8", "Nexus_9"]);
                devices[4].isOnline = false;

                await androidPlatform.runApp();
                const isRunningOnOfflineDevice =
                    devices[4].installedApplications[androidPackageName].isInDebugMode === false;
                isRunningOnOfflineDevice.should.be.false();
            },
        );

        testWithRecordings(
            "runApp launches the app in the device specified as target",
            ["react-native/run-android/win10-rn0.21.0/succeedsWithFiveVSEmulators"],
            async () => {
                devices = fillDevices(["Nexus_5", "Nexus_6", "Nexus_10", "Nexus_11", "Nexus_12"]);

                const runOptions: any = {
                    platform: PlatformType.Android,
                    workspaceRoot: projectRoot,
                    projectRoot: projectRoot,
                    target: "Nexus_12",
                    reactNativeVersions: {
                        reactNativeVersion: "^0.19.0",
                        reactNativeWindowsVersion: "",
                    },
                    nodeModulesRoot,
                };
                const platform = createAndroidPlatform(runOptions);
                (platform as any).adbHelper = adbHelper;
                await platform.runApp();
                const isRunningOnNexus12 =
                    devices[4].installedApplications[androidPackageName].isInDebugMode === false;
                isRunningOnNexus12.should.be.true();
            },
        );

        testWithRecordings(
            "runApp launches the app in a random online device if the target is offline",
            ["react-native/run-android/win10-rn0.21.0/succeedsWithTenVSEmulators"],
            async () => {
                const onlineDevicesIds = [
                    "Nexus_11",
                    "Nexus_13",
                    "Nexus_14",
                    "Nexus_15",
                    "Nexus_16",
                    "Nexus_17",
                ];
                const offineDevicesIds = ["Nexus_5", "Nexus_6", "Nexus_10", "Nexus_12"];
                devices = fillDevices(offineDevicesIds.concat(onlineDevicesIds));
                devices[0].isOnline = false;
                devices[1].isOnline = false;
                devices[2].isOnline = false;
                devices[3].isOnline = false;

                const runOptions: any = {
                    platform: PlatformType.Android,
                    workspaceRoot: projectRoot,
                    projectRoot: projectRoot,
                    target: "Nexus_12",
                    reactNativeVersions: {
                        reactNativeVersion: "^0.19.0",
                        reactNativeWindowsVersion: "",
                    },
                    nodeModulesRoot,
                };
                const platform = createAndroidPlatform(runOptions);
                (platform as any).adbHelper = adbHelper;
                await platform.runApp();
                const devicesRunningAppId = devices.filter(
                    (device: any) =>
                        device.installedApplications[androidPackageName].isInDebugMode === false,
                );
                devicesRunningAppId.length.should.eql(1);
                onlineDevicesIds.should.containEql(devicesRunningAppId[0].id);
            },
        );

        testWithRecordings(
            "runApp doesn't fail even if the call to start the LogCat does fail",
            [
                "react-native/run-android/win10-rn0.21.0/succeedsWithOneVSEmulator",
                "react-native/run-android/win10-rn0.22.2/succeedsWithOneVSEmulator",
                "react-native/run-android/osx10.10-rn0.21.0/succeedsWithOneVSEmulator",
            ],
            async () => {
                devices = fillDevices(["Nexus_5"]);

                await androidPlatform.runApp();
                const isRunning =
                    devices[0].installedApplications[androidPackageName].isInDebugMode === false;
                isRunning.should.be.true();
            },
        );

        testWithRecordings(
            "runApp fails when the android project doesn't exist, and shows a nice error message",
            [
                "react-native/run-android/win10-rn0.21.0/failsDueToAndroidFolderMissing",
                "react-native/run-android/win10-rn0.22.2/failsDueToAndroidFolderMissing",
            ],
            async () => {
                devices = fillDevices(["Nexus_5"]);

                await fileSystem.rmdir(androidProjectPath);
                let isRunning: boolean;
                try {
                    await androidPlatform.runApp();
                    should.assert(false, "Expected runApp to end up with an error");
                    isRunning = false;
                } catch (error) {
                    (error as Error).message.should.eql(
                        "Android project not found. (error code 1203)",
                    );
                    isRunning = !!devices[0].installedApplications[androidPackageName];
                }
                isRunning.should.be.false();
            },
        );

        testWithRecordings(
            "runApp fails when the android emulator shell is unresponsive, and shows a nice error message",
            ["react-native/run-android/osx10.10-rn0.21.0/failsDueToAdbCommandTimeout"],
            async () => {
                devices = fillDevices(["Nexus_5"]);

                let isRunning: boolean;
                try {
                    await androidPlatform.runApp();
                    should.assert(false, "Expected runApp to end up with an error");
                    isRunning = false;
                } catch (error) {
                    "An Android shell command timed-out. Please retry the operation. (error code 1202)".should.eql(
                        (error as Error).message,
                    );
                    isRunning = !!devices[0].installedApplications[androidPackageName];
                }
                isRunning.should.be.false();
            },
        );

        test("getRunArguments should return correct target", function () {
            const runOptions: any = {
                platform: PlatformType.Android,
                workspaceRoot: projectRoot,
                projectRoot: projectRoot,
                target: "Nexus_12",
                nodeModulesRoot,
            };
            const platform = createAndroidPlatform(runOptions);
            const runArgs = platform.getRunArguments();

            runArgs.should.be.an.Array();
            runArgs.should.containDeepOrdered(["--deviceId", "Nexus_12"]);
        });

        test("getRunArguments should remove simulator target from args", function () {
            const runOptions: any = {
                platform: PlatformType.Android,
                workspaceRoot: projectRoot,
                projectRoot: projectRoot,
                target: "simulator",
                nodeModulesRoot,
            };
            const platform = createAndroidPlatform(runOptions);
            const runArgs = platform.getRunArguments();

            runArgs.should.be.an.Array();
            runArgs.should.be.empty();
        });

        test("getRunArguments should remove device target from args", function () {
            const runOptions: any = {
                platform: PlatformType.Android,
                workspaceRoot: projectRoot,
                projectRoot: projectRoot,
                target: "device",
                nodeModulesRoot,
            };
            const platform = createAndroidPlatform(runOptions);
            const runArgs = platform.getRunArguments();

            runArgs.should.be.an.Array();
            runArgs.should.be.empty();
        });

        test("getRunArguments should return correct args", function () {
            const args = ["--deviceId", "device_id"];
            const runOptions: any = {
                platform: PlatformType.Android,
                workspaceRoot: projectRoot,
                projectRoot: projectRoot,
                runArguments: args,
                target: "Nexus_12",
                nodeModulesRoot,
            };
            const platform = createAndroidPlatform(runOptions);
            const runArgs = platform.getRunArguments();

            runArgs.should.be.an.Array();
            runArgs.should.containDeepOrdered(args);
        });

        test("AdbHelper should correctly parse Android Sdk Location from local.properties file content", () => {
            const adbHelper = new adb.AdbHelper("", nodeModulesRoot);
            let getPlatformStub: Sinon.SinonStub;
            function testPaths(inputPath: string, expectedPath: string) {
                const resultPath1 = adbHelper.parseSdkLocation(`sdk.dir=${inputPath}`);
                const resultPath2 = adbHelper.parseSdkLocation(`sdk.dir   =${inputPath}`);
                const resultPath3 = adbHelper.parseSdkLocation(`sdk.dir = ${inputPath}`);
                assert.strictEqual(resultPath1, expectedPath);
                assert.strictEqual(resultPath2, expectedPath);
                assert.strictEqual(resultPath3, expectedPath);
            }

            const os = require("os");
            function mockPlatform(platform: NodeJS.Platform) {
                getPlatformStub?.restore();
                getPlatformStub = sinon.stub(os, "platform", function () {
                    return platform;
                });
            }

            mockPlatform("win32");
            testPaths(
                String.raw`C\:\\Users\\User1\\AndroidSdk`,
                String.raw`C:\Users\User1\AndroidSdk`,
            );
            testPaths(String.raw`\\\\Network\\Shared\\Folder`, String.raw`\\Network\Shared\Folder`);
            testPaths(
                String.raw`\\\\Network\\Shared\\Folder\\Android SDK`,
                String.raw`\\Network\Shared\Folder\Android SDK`,
            );
            testPaths(
                String.raw`C\:\\Users\\User1\\Android Sdk`,
                String.raw`C:\Users\User1\Android Sdk`,
            );

            mockPlatform("darwin");
            testPaths(String.raw`/var/lib/some/path`, String.raw`/var/lib/some/path`);
            testPaths(String.raw`~/Library`, String.raw`~/Library`);
            testPaths(String.raw`/Users/User1/home/path`, String.raw`/Users/User1/home/path`);
            testPaths(
                String.raw`/Users/User1/home/path/Android SDK`,
                String.raw`/Users/User1/home/path/Android SDK`,
            );
            testPaths(
                String.raw`/Volumes/Macintosh HD/Users/foo/Library/Android/sdk/platform-tools`,
                String.raw`/Volumes/Macintosh HD/Users/foo/Library/Android/sdk/platform-tools`,
            );

            teardown(() => {
                getPlatformStub?.restore();
            });
        });

        test("AdbHelper parseSdkLocation function should correctly parse Android Sdk Location from local.properties and wrap with quotes", () => {
            function testPaths(expectedPath: string, projectRoot: string) {
                const adbHelper = new adb.AdbHelper(projectRoot, nodeModulesRoot);
                const localPropertiesFilePath = path.join(
                    projectRoot,
                    "android",
                    "local.properties",
                );
                const fileContent = fs.readFileSync(localPropertiesFilePath).toString();
                const resultPath = adbHelper.parseSdkLocation(fileContent);
                assert.strictEqual(resultPath, expectedPath);
            }

            if (process.platform == "win32") {
                const mockProjectRoot = path.join(
                    __dirname,
                    "..",
                    "..",
                    "..",
                    "test",
                    "resources",
                    "auxiliaryFiles",
                    "templateProject",
                    "win",
                );
                testPaths(String.raw`C:\Android\android sdk`, mockProjectRoot);
            } else {
                const mockProjectRoot = path.join(
                    __dirname,
                    "..",
                    "..",
                    "..",
                    "test",
                    "resources",
                    "auxiliaryFiles",
                    "templateProject",
                    "others",
                );
                testPaths(
                    String.raw`/Volumes/Macintosh HD/Users/foo/Library/Android/sdk/`,
                    mockProjectRoot,
                );
            }
        });

        test("AdbHelper getAdbPath function should get sdk and adb path flexible from local.properties file and environment variable", () => {
            let mockProjectRoot;
            if (process.platform == "win32") {
                mockProjectRoot = path.join(
                    __dirname,
                    "..",
                    "..",
                    "..",
                    "test",
                    "resources",
                    "auxiliaryFiles",
                    "templateProject",
                    "win",
                );
            } else {
                mockProjectRoot = path.join(
                    __dirname,
                    "..",
                    "..",
                    "..",
                    "test",
                    "resources",
                    "auxiliaryFiles",
                    "templateProject",
                    "others",
                );
            }

            const adbHelper = new adb.AdbHelper(mockProjectRoot, nodeModulesRoot);
            const adbValue = adbHelper.getAdbPath(mockProjectRoot);
            assert.strictEqual(adbValue, "adb");
        });
    });
});

function fillDevices(ids: string[]): any[] {
    let devices: any[] = [];
    ids.forEach(id => {
        devices.push({
            isOnline: true,
            installedApplications: {},
            runningApplications: {},
            isVirtualTarget: true,
            id: id,
        });
    });

    return devices;
}
