package com.android.compatibility.common.util;

import static junit.framework.TestCase.fail;

import com.google.common.base.Joiner;
import com.google.common.io.Closeables;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * Inherit this class and implement {@link #executeShellCommand(String)} to be able to assert that
 * logcat contains what you want.
 */
public abstract class LogcatInspector {
    private static final int SMALL_LOGCAT_DELAY = 1000;

    /**
     * Should execute adb shell {@param command} and return an {@link InputStream} with the result.
     */
    protected abstract InputStream executeShellCommand(String command) throws IOException;

    /**
     * Logs an unique string using tag {@param tag} and wait until it appears to continue execution.
     *
     * @return a unique separator string.
     * @throws IOException if error while executing command.
     */
    public String mark(String tag) throws IOException {
        String uniqueString = ":::" + UUID.randomUUID().toString();
        Closeables.closeQuietly(executeShellCommand("log -t " + tag + " " + uniqueString));
        // This is to guarantee that we only return after the string has been logged, otherwise
        // in practice the case where calling Log.?(<message1>) right after clearAndMark() resulted
        // in <message1> appearing before the unique identifier. It's not guaranteed per the docs
        // that log command will have written when returning, so better be safe. 5s should be fine.
        assertLogcatContainsInOrder(tag + ":* *:S", 5, uniqueString);
        return uniqueString;
    }

    /**
     * Wait for up to {@param maxTimeoutInSeconds} for the given {@param logcatStrings} strings to
     * appear in logcat in the given order. By passing the separator returned by {@link
     * #mark(String)} as the first string you can ensure that only logs emitted after that
     * call to mark() are found. Repeated strings are not supported.
     *
     * @throws AssertionError if the strings are not found in the given time.
     * @throws IOException if error while reading.
     */
    public void assertLogcatContainsInOrder(
            String filterSpec, int maxTimeoutInSeconds, String... logcatStrings)
            throws AssertionError, IOException {
        try {
            int nextStringIndex =
                    numberOfLogcatStringsFound(filterSpec, maxTimeoutInSeconds, logcatStrings);
            if (nextStringIndex < logcatStrings.length) {
                fail(
                        "Couldn't find "
                                + logcatStrings[nextStringIndex]
                                + (nextStringIndex > 0
                                        ? " after " + logcatStrings[nextStringIndex - 1]
                                        : "")
                                + " within "
                                + maxTimeoutInSeconds
                                + " seconds ");
            }
        } catch (InterruptedException e) {
            fail("Thread interrupted unexpectedly: " + e.getMessage());
        }
    }

    /**
     * Wait for up to {@param timeInSeconds}, if all the strings {@param logcatStrings} are found in
     * order then the assertion fails, otherwise it succeeds.
     *
     * @throws AssertionError if all the strings are found in order in the given time.
     * @throws IOException if error while reading.
     */
    public void assertLogcatDoesNotContainInOrder(int timeInSeconds, String... logcatStrings)
            throws IOException {
        try {
            int stringsFound = numberOfLogcatStringsFound("", timeInSeconds, logcatStrings);
            if (stringsFound == logcatStrings.length) {
                fail("Found " + Joiner.on(", ").join(logcatStrings) + " that weren't expected");
            }
        } catch (InterruptedException e) {
            fail("Thread interrupted unexpectedly: " + e.getMessage());
        }
    }

    private int numberOfLogcatStringsFound(
            String filterSpec, int timeInSeconds, String... logcatStrings)
            throws InterruptedException, IOException {
        long timeout = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(timeInSeconds);
        int stringIndex = 0;
        long lastEpochMicroseconds = 0;
        while (timeout >= System.currentTimeMillis()) {
            // '-v epoch' -> Displays time as seconds since Jan 1 1970.
            // '-v usec' -> Displays time down the microsecond precision.
            InputStream logcatStream =
                    executeShellCommand("logcat -v epoch -v usec -d " + filterSpec);
            BufferedReader logcat = new BufferedReader(new InputStreamReader(logcatStream));
            String line;
            while ((line = logcat.readLine()) != null) {
                if (line.contains(logcatStrings[stringIndex])) {
                    // Now we need to get the timestamp of this log line to ensure that
                    // this log is after the previously matched log.

                    // Strip the leading spaces and split the line by spaces
                    String[] splitLine = line.stripLeading().split(" ");

                    // The first one is epoch time in seconds, with microsecond precision.
                    // It is of the format <epoch time in seconds>.xxxxxx
                    String epochMicrosecondsStr = splitLine[0].replace(".", "");
                    long epochMicroseconds = Long.parseLong(epochMicrosecondsStr);

                    // Check that this log time is after previously matched log
                    if (epochMicroseconds >= lastEpochMicroseconds) {
                        stringIndex++;
                        if (stringIndex >= logcatStrings.length) {
                            StreamUtil.drainAndClose(logcat);
                            return stringIndex;
                        }
                    }
                }
            }
            Closeables.closeQuietly(logcat);
            // In case the key has not been found, wait for the log to update before
            // performing the next search.
            Thread.sleep(SMALL_LOGCAT_DELAY);
        }
        return stringIndex;
    }
}
