/*
 * 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.media.tests;

import com.android.tradefed.config.OptionClass;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.IFileEntry;
import com.android.tradefed.invoker.TestInformation;
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.util.FileUtil;
import com.android.tradefed.util.proto.TfMetricProtoUtil;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Camera2 framework stress test
 * This is a test invocation that runs stress tests against Camera2 framework (API & HAL) to
 * isolate stability issues from Camera application.
 * This invocation uses a Camera2InstrumentationTestRunner to run a set of
 * Camera framework stress tests.
 */
@OptionClass(alias = "camera2-framework-stress")
public class Camera2FrameworkStressTest extends CameraTestBase {

    // Keys in instrumentation test metrics
    private static final String RESULT_DIR =
            "/sdcard/Android/data/com.android.mediaframeworktest/files/camera-out/";
    private static final String RESULT_FILE_FORMAT = RESULT_DIR + "fwk-stress_camera_%s.txt";
    private static final Pattern RESULT_FILE_REGEX = Pattern.compile(
            "^fwk-stress_camera_(?<id>.+).txt");
    private static final String KEY_NUM_ATTEMPTS = "numAttempts";
    private static final String KEY_ITERATION = "iteration";

    public Camera2FrameworkStressTest() {
        // Note that default value in constructor will be overridden by the passing option from
        // a command line.
        setTestPackage("com.android.mediaframeworktest");
        setTestRunner("com.android.mediaframeworktest.Camera2InstrumentationTestRunner");
        setRuKey("CameraFrameworkStress");
        setTestTimeoutMs(2 * 60 * 60 * 1000);   // 2 hours
        setLogcatOnFailure(true);
    }

    /** {@inheritDoc} */
    @Override
    public void run(TestInformation testInfo, ITestInvocationListener listener)
            throws DeviceNotAvailableException {
        runInstrumentationTest(testInfo, listener, new CollectingListener(listener));
    }

    /**
     * A listener to collect the output from test run and fatal errors
     */
    public class CollectingListener extends CameraTestMetricsCollectionListener.DefaultCollectingListener {

        public CollectingListener(ITestInvocationListener listener) {
            super(listener);
        }

        @Override
        public void handleMetricsOnTestEnded(TestDescription test, Map<String, String> testMetrics) {
            if (testMetrics == null) {
                return; // No-op if there is nothing to post.
            }
            for (Map.Entry<String, String> metric : testMetrics.entrySet()) {
                getAggregatedMetrics().put(metric.getKey(), metric.getValue());
            }
        }

        @Override
        public void testEnded(
                TestDescription test, long endTime, HashMap<String, Metric> testMetrics) {
            if (hasTestRunFatalError()) {
                CLog.v(
                        "The instrumentation result not found. Fall back to get the metrics from a "
                                + "log file. errorMsg: %s",
                        getErrorMessage());
            }

            // For stress test, parse the metrics from a log file.
            testMetrics = TfMetricProtoUtil.upgradeConvert(parseLog(test.getTestName()));
            super.testEnded(test, endTime, testMetrics);
        }

        // Return null if failed to parse the result file or the test didn't even start.
        private Map<String, String> parseLog(String testName) {
            Map<String, String> postMetrics = new HashMap<String, String>();
            String resultFilePath;
            try {
                for (String cameraId : getCameraIdList(RESULT_DIR)) {
                    File outputFile = FileUtil.createTempFile("fwk-stress", ".txt");
                    resultFilePath = getResultFilePath(cameraId);
                    getDevice().pullFile(resultFilePath, outputFile);
                    if (outputFile == null) {
                        throw new DeviceNotAvailableException(String.format("Failed to pull the "
                                + "result file: %s", resultFilePath),
                                getDevice().getSerialNumber());
                    }

                    BufferedReader reader = new BufferedReader(new FileReader(outputFile));
                    String line;
                    Map<String, String> resultMap = new HashMap<String, String>();

                    // Parse results from log file that contain the key-value pairs.
                    // eg. "numAttempts=10|iteration=9[|cameraId=0]"
                    try {
                        while ((line = reader.readLine()) != null) {
                            String[] pairs = line.split("\\|");
                            for (String pair : pairs) {
                                String[] keyValue = pair.split("=");
                                // Each should be a pair of key and value.
                                String key = keyValue[0].trim();
                                String value = keyValue[1].trim();
                                resultMap.put(key, value);
                            }
                        }
                    } finally {
                        reader.close();
                    }

                    // Fail if a stress test doesn't start.
                    if (0 == Integer.parseInt(resultMap.get(KEY_NUM_ATTEMPTS))) {
                        CLog.w("Failed to start stress tests. test setup configured incorrectly?");
                        return null;
                    }
                    // Post the number of iterations only with the key name associated with
                    // test name and camera ID.
                    String keyName = testName + "_" + cameraId;
                    postMetrics.put(keyName, resultMap.get(KEY_ITERATION));
                }
            } catch (IOException e) {
                CLog.w("Couldn't parse the output log file");
                CLog.e(e);
            } catch (DeviceNotAvailableException e) {
                CLog.w("Could not pull file: %s, error:", RESULT_DIR);
                CLog.e(e);
            } catch (NumberFormatException e) {
                CLog.w("Could not find the key in file: %s, error:", KEY_NUM_ATTEMPTS);
                CLog.e(e);
            }
            return postMetrics;
        }
    }

    private ArrayList<String> getCameraIdList(String resultDir) throws DeviceNotAvailableException {
        // The result files are created per each camera ID
        ArrayList<String> cameraIds = new ArrayList<>();
        IFileEntry dirEntry = getDevice().getFileEntry(resultDir);
        if (dirEntry != null) {
            for (IFileEntry file : dirEntry.getChildren(false)) {
                String fileName = file.getName();
                Matcher matcher = RESULT_FILE_REGEX.matcher(fileName);
                if (matcher.matches()) {
                    cameraIds.add(matcher.group("id"));
                }
            }
        }

        if (cameraIds.isEmpty()) {
            CLog.w("No camera ID is found in %s. The resultToFile instrumentation argument is set"
                    + " to false?", resultDir);
        }
        return cameraIds;
    }

    private String getResultFilePath(String cameraId) {
        return String.format(RESULT_FILE_FORMAT, cameraId);
    }
}
