/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.fastboot.tests;

import com.android.tradefed.build.IBuildInfo;
import com.android.tradefed.config.ConfigurationException;
import com.android.tradefed.config.Option;
import com.android.tradefed.config.OptionSetter;
import com.android.tradefed.device.DeviceDisconnectedException;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.IManagedTestDevice;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.device.ITestDevice.RecoveryMode;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
import com.android.tradefed.result.ITestInvocationListener;
import com.android.tradefed.result.TestDescription;
import com.android.tradefed.targetprep.BuildError;
import com.android.tradefed.targetprep.DeviceFlashPreparer;
import com.android.tradefed.targetprep.IDeviceFlasher.UserDataFlashOption;
import com.android.tradefed.targetprep.TargetSetupError;
import com.android.tradefed.testtype.IBuildReceiver;
import com.android.tradefed.testtype.IDeviceTest;
import com.android.tradefed.testtype.IRemoteTest;
import com.android.tradefed.util.FileUtil;
import com.android.tradefed.util.proto.TfMetricProtoUtil;

import java.io.File;
import java.util.HashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

/**
 * Flashes the device as part of the test, report device post-flash status as test results. TODO:
 * Add more fastboot test validation step.
 */
public class FastbootTest implements IRemoteTest, IDeviceTest, IBuildReceiver {

    private static final String FASTBOOT_TEST = FastbootTest.class.getName();
    /** the desired recentness of battery level * */
    private static final long BATTERY_FRESHNESS_MS = 30 * 1000;

    private static final long INVALID_TIME_DURATION = -1;
    private static final String INITIAL_BOOT_TIME = "initial-boot";
    private static final String ONLINE_TIME = "online";

    @Option(
            name = "device-boot-time",
            description = "Max time in ms to wait for" + " device to boot.",
            isTimeVal = true)
    private long mDeviceBootTimeMs = 5 * 60 * 1000;

    @Option(name = "userdata-flash", description = "Specify handling of userdata partition.")
    private UserDataFlashOption mUserDataFlashOption = UserDataFlashOption.WIPE;

    @Option(
            name = "concurrent-flasher-limit",
            description =
                    "The maximum number of concurrent"
                            + " flashers (may be useful to avoid memory constraints)")
    private Integer mConcurrentFlasherLimit = null;

    @Option(
            name = "skip-battery-check",
            description = "If true, the battery reading test will" + " be skipped.")
    private boolean mSkipBatteryCheck = false;

    @Option(
            name = "flasher-class",
            description =
                    "The Flasher class (implementing "
                            + "DeviceFlashPreparer) to be used for the fastboot test",
            mandatory = true)
    private String mFlasherClass;

    private IBuildInfo mBuildInfo;
    private ITestDevice mDevice;

    /** {@inheritDoc} */
    @Override
    public void setBuild(IBuildInfo buildInfo) {
        mBuildInfo = buildInfo;
    }

    /** {@inheritDoc} */
    @Override
    public void setDevice(ITestDevice device) {
        mDevice = device;
    }

    /** {@inheritDoc} */
    @Override
    public ITestDevice getDevice() {
        return mDevice;
    }

    /** {@inheritDoc} */
    @Override
    public void run(ITestInvocationListener listener) throws DeviceNotAvailableException {
        long start = System.currentTimeMillis();
        listener.testRunStarted(FASTBOOT_TEST, 1);
        String originalFastbootpath = ((IManagedTestDevice) mDevice).getFastbootPath();
        try {
            testFastboot(listener);
        } finally {
            // reset fastboot path
            ((IManagedTestDevice) mDevice).setFastbootPath(originalFastbootpath);
            listener.testRunEnded(
                    System.currentTimeMillis() - start, new HashMap<String, Metric>());
        }
    }

    /**
     * Flash the device and calculate the time to bring the device online and first boot.
     *
     * @param listener
     * @throws DeviceNotAvailableException
     */
    private void testFastboot(ITestInvocationListener listener) throws DeviceNotAvailableException {
        HashMap<String, Metric> result = new HashMap<>();
        TestDescription firstBootTestId =
                new TestDescription(
                        String.format("%s.%s", FASTBOOT_TEST, FASTBOOT_TEST), FASTBOOT_TEST);
        listener.testStarted(firstBootTestId);
        DeviceFlashPreparer flasher = loadFlashPreparerClass();
        long bootStart = INVALID_TIME_DURATION;
        long onlineTime = INVALID_TIME_DURATION;
        long bootTime = INVALID_TIME_DURATION;
        try {
            if (flasher == null) {
                throw new RuntimeException(
                        String.format("Could not find flasher %s", mFlasherClass));
            }
            try {
                OptionSetter setter = new OptionSetter(flasher);
                // duping and passing parameters down to flasher
                setter.setOptionValue("device-boot-time", Long.toString(mDeviceBootTimeMs));
                setter.setOptionValue("userdata-flash", mUserDataFlashOption.toString());
                if (mConcurrentFlasherLimit != null) {
                    setter.setOptionValue(
                            "concurrent-flasher-limit", mConcurrentFlasherLimit.toString());
                }
                // always to skip because the test needs to detect device online
                // and available state individually
                setter.setOptionValue("skip-post-flashing-setup", "true");
                setter.setOptionValue("force-system-flash", "true");
            } catch (ConfigurationException ce) {
                // this really shouldn't happen, but if it does, it'll indicate a setup problem
                // so this should be exposed, even at the expense of categorizing the build as
                // having a critical failure
                throw new RuntimeException("failed to set options for flasher", ce);
            }

            File fastboot = getFastbootFile(mBuildInfo);
            if (fastboot == null) {
                listener.testFailed(
                        firstBootTestId, "Couldn't find the fastboot binary in build info.");
                return;
            }
            // Set the fastboot path for device
            ((IManagedTestDevice) mDevice).setFastbootPath(fastboot.getAbsolutePath());

            // flash it!
            CLog.v("Flashing device %s", mDevice.getSerialNumber());
            try {
                flasher.setUp(mDevice, mBuildInfo);
                // we are skipping post boot setup so this is the start of boot process
                bootStart = System.currentTimeMillis();
            } catch (TargetSetupError | BuildError e) {
                // setUp() may throw DeviceNotAvailableException, TargetSetupError and BuildError.
                // DNAE is allowed to get thrown here so that a tool failure is triggered and build
                // maybe retried; the other 2x types are also rethrown as RuntimeException's for
                // the same purpose. In general, these exceptions reflect flashing or infra related
                // flakiness, so retrying is a reasonable mitigation.
                throw new RuntimeException("Exception during device flashing", e);
            }
            // check if device is online after flash, i.e. if adb is broken
            CLog.v("Waiting for device %s online", mDevice.getSerialNumber());
            mDevice.setRecoveryMode(RecoveryMode.ONLINE);
            try {
                mDevice.waitForDeviceOnline();
                onlineTime = System.currentTimeMillis() - bootStart;
            } catch (DeviceNotAvailableException dnae) {
                CLog.e("Device not online after flashing");
                CLog.e(dnae);
                listener.testRunFailed("Device not online after flashing");
                throw new DeviceDisconnectedException(
                        "Device not online after flashing", mDevice.getSerialNumber());
            }
            // check if device can be fully booted, i.e. if application framework won't boot
            CLog.v("Waiting for device %s boot complete", mDevice.getSerialNumber());
            mDevice.setRecoveryMode(RecoveryMode.AVAILABLE);
            try {
                mDevice.waitForDeviceAvailable(mDeviceBootTimeMs);
                bootTime = System.currentTimeMillis() - bootStart;
            } catch (DeviceNotAvailableException dnae) {
                CLog.e("Device %s not available after flashing", mDevice.getSerialNumber());
                CLog.e(dnae);
                // only report as test failure, not test run failure because we were able to run the
                // test until the end, despite the failure verdict, and the device is returned to
                // the pool in a useable state
                listener.testFailed(firstBootTestId, "Device not available after flashing");
                return;
            }
            CLog.v("Device %s boot complete", mDevice.getSerialNumber());
            if (mSkipBatteryCheck) {
                // If we skip the battery Check, we can return directly after boot complete.
                return;
            }
            // We check if battery level are readable as non root to ensure that device is usable.
            mDevice.disableAdbRoot();
            try {
                Future<Integer> batteryFuture =
                        mDevice.getIDevice()
                                .getBattery(BATTERY_FRESHNESS_MS, TimeUnit.MILLISECONDS);
                // get cached value or wait up to 500ms for battery level query
                Integer level = batteryFuture.get(500, TimeUnit.MILLISECONDS);
                CLog.d("Battery level value reading is: '%s'", level);
                if (level == null) {
                    listener.testFailed(firstBootTestId, "Reading of battery level is wrong.");
                    return;
                }
            } catch (InterruptedException
                    | ExecutionException
                    | java.util.concurrent.TimeoutException e) {
                CLog.e("Failed to query battery level for %s", mDevice.getSerialNumber());
                CLog.e(e);
                listener.testFailed(firstBootTestId, "Failed to query battery level.");
                return;
            } finally {
                mDevice.enableAdbRoot();
            }
        } finally {
            CLog.d("Device online time: %dms, initial boot time: %dms", onlineTime, bootTime);
            if (onlineTime != INVALID_TIME_DURATION) {
                result.put(
                        ONLINE_TIME, TfMetricProtoUtil.stringToMetric(Long.toString(onlineTime)));
            }
            if (bootTime != INVALID_TIME_DURATION) {
                result.put(
                        INITIAL_BOOT_TIME,
                        TfMetricProtoUtil.stringToMetric(Long.toString(bootTime)));
            }
            listener.testEnded(firstBootTestId, result);
        }
    }

    /**
     * Attempt to load the class implementing {@link DeviceFlashPreparer} based on the option
     * flasher-class, allows anybody to tests fastboot using their own implementation of it.
     */
    private DeviceFlashPreparer loadFlashPreparerClass() {
        try {
            Class<?> flasherClass = Class.forName(mFlasherClass);
            Object flasherObject = flasherClass.newInstance();
            if (flasherObject instanceof DeviceFlashPreparer) {
                return (DeviceFlashPreparer) flasherObject;
            } else {
                CLog.e(
                        "Loaded class '%s' is not an instance of DeviceFlashPreparer.",
                        flasherObject);
                return null;
            }
        } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
            CLog.e(e);
            return null;
        }
    }

    /** Helper to find the fastboot file as part of the buildinfo file list. */
    private File getFastbootFile(IBuildInfo buildInfo) {
        File fastboot = buildInfo.getFile("fastboot");
        if (fastboot == null) {
            return null;
        }
        FileUtil.chmodGroupRWX(fastboot);
        return fastboot;
    }
}
