/*
 * Copyright (C) 2019 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.tradefed.cluster;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

import com.android.tradefed.config.ConfigurationException;
import com.android.tradefed.config.OptionSetter;
import com.android.tradefed.invoker.InvocationContext;
import com.android.tradefed.util.FileUtil;

import com.android.tradefed.cluster.ClusterLogSaver.FilePickingStrategy;

import org.json.JSONException;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.Mockito;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

/** Unit tests for {@link ClusterLogSaverTest}. */
@RunWith(JUnit4.class)
public class ClusterLogSaverTest {

    private static final String REQUEST_ID = "request_id";
    private static final String COMMAND_ID = "command_id";

    private File mWorkDir;
    private TestOutputUploader mMockTestOutputUploader;
    private IClusterClient mMockClusterClient;
    private ClusterLogSaver mClusterLogSaver;
    private OptionSetter mOptionSetter;

    @Before
    public void setUp() throws Exception {
        mWorkDir = FileUtil.createTempDir(this.getClass().getName());
        mMockTestOutputUploader = Mockito.mock(TestOutputUploader.class);
        mMockClusterClient = Mockito.mock(IClusterClient.class);
        mClusterLogSaver = Mockito.spy(new ClusterLogSaver());
        Mockito.doReturn(mMockTestOutputUploader).when(mClusterLogSaver).getTestOutputUploader();
        Mockito.doReturn(mMockClusterClient).when(mClusterLogSaver).getClusterClient();
        mOptionSetter = new OptionSetter(mClusterLogSaver);
        mOptionSetter.setOptionValue("cluster:root-dir", mWorkDir.getAbsolutePath());
        mOptionSetter.setOptionValue("cluster:request-id", REQUEST_ID);
        mOptionSetter.setOptionValue("cluster:command-id", COMMAND_ID);
    }

    @After
    public void tearDown() {
        FileUtil.recursiveDelete(mWorkDir);
    }

    /** Create an empty file in the test's temporary directory. */
    private File createMockFile(String path, String name) throws IOException {
        final File dir = new File(mWorkDir, path);
        dir.mkdirs();
        final File file = new File(dir, name);
        file.createNewFile();
        return file;
    }

    /** Create an empty zip file in the test's temporary directory. */
    private File createMockZipFile(String path, String name) throws IOException {
        File file = createMockFile(path, name);
        new ZipOutputStream(new FileOutputStream(file)).close();
        return file;
    }

    /** Get the names of all entries in a zip file. */
    private Set<String> getZipEntries(File file) throws IOException {
        try (ZipInputStream zip = new ZipInputStream(new FileInputStream(file))) {
            Set<String> names = new LinkedHashSet<>();
            for (ZipEntry entry = zip.getNextEntry(); entry != null; entry = zip.getNextEntry()) {
                names.add(entry.getName());
            }
            return names;
        }
    }

    @Test
    public void testFindTestContextFile() throws IOException {
        final String pattern = "android-cts/results/(?<SESSION>[^/]+)/test_result\\.xml";
        final String sessionId = "1970.01.01_00.05.10";
        final File file =
                createMockFile(
                        String.format("android-cts/results/%s", sessionId), "test_result.xml");
        Map<String, String> envVars = new TreeMap<>();

        File contextFile =
                mClusterLogSaver.findTestContextFile(
                        mWorkDir, pattern, FilePickingStrategy.PICK_LAST, envVars);

        assertNotNull(contextFile);
        assertEquals(file.getAbsolutePath(), contextFile.getAbsolutePath());
        assertEquals(1, envVars.size());
        assertEquals(sessionId, envVars.get("SESSION"));
    }

    @Test
    public void testFindTestContextFile_multipleMatches_pickFirst() throws IOException {
        final String pattern = "android-cts/results/(?<SESSION>[^/]+)/test_result\\.xml";
        final String[] sessionIds = new String[] {"1970.01.01_00.05.10", "2018.01.01_00.05.10"};
        final File[] files =
                new File[] {
                    createMockFile(
                            String.format("android-cts/results/%s", sessionIds[0]),
                            "test_result.xml"),
                    createMockFile(
                            String.format("android-cts/results/%s", sessionIds[1]),
                            "test_result.xml")
                };
        Map<String, String> envVars = new TreeMap<>();

        File contextFile =
                mClusterLogSaver.findTestContextFile(
                        mWorkDir, pattern, FilePickingStrategy.PICK_FIRST, envVars);

        assertNotNull(contextFile);
        assertEquals(files[0].getAbsolutePath(), contextFile.getAbsolutePath());
        assertEquals(1, envVars.size());
        assertEquals(sessionIds[0], envVars.get("SESSION"));
    }

    @Test
    public void testFindTestContextFile_multipleMatches_pickLast() throws IOException {
        final String pattern = "android-cts/results/(?<SESSION>[^/]+)/test_result\\.xml";
        final String[] sessionIds = new String[] {"1970.01.01_00.05.10", "2018.01.01_00.05.10"};
        final File[] files =
                new File[] {
                    createMockFile(
                            String.format("android-cts/results/%s", sessionIds[0]),
                            "test_result.xml"),
                    createMockFile(
                            String.format("android-cts/results/%s", sessionIds[1]),
                            "test_result.xml")
                };
        Map<String, String> envVars = new TreeMap<>();

        File contextFile =
                mClusterLogSaver.findTestContextFile(
                        mWorkDir, pattern, FilePickingStrategy.PICK_LAST, envVars);

        assertNotNull(contextFile);
        assertEquals(files[1].getAbsolutePath(), contextFile.getAbsolutePath());
        assertEquals(1, envVars.size());
        assertEquals(sessionIds[1], envVars.get("SESSION"));
    }

    @Test
    public void testFindTestContextFile_noMatch() throws IOException {
        final String pattern = "android-cts/results/(?<SESSION>[^/]+)/test_result\\.xml";
        final String sessionId = "1970.01.01_00.05.10";
        createMockFile(String.format("android-cts/results/%s", sessionId), "test_result2.xml");
        Map<String, String> envVars = new TreeMap<>();

        File contextFile =
                mClusterLogSaver.findTestContextFile(
                        mWorkDir, pattern, FilePickingStrategy.PICK_LAST, envVars);

        assertNull(contextFile);
        assertEquals(0, envVars.size());
    }

    @Test
    public void testFindTestContextFile_existingEnvVar() throws IOException {
        final String pattern = "android-cts/results/(?<SESSION>[^/]+)/test_result\\.xml";
        final String sessionId = "1970.01.01_00.05.10";
        final File file =
                createMockFile(
                        String.format("android-cts/results/%s", sessionId), "test_result.xml");
        Map<String, String> envVars = new TreeMap<>();
        envVars.put("SESSION", "foo");

        File contextFile =
                mClusterLogSaver.findTestContextFile(
                        mWorkDir, pattern, FilePickingStrategy.PICK_LAST, envVars);

        assertNotNull(contextFile);
        assertEquals(file.getAbsolutePath(), contextFile.getAbsolutePath());
        assertEquals(1, envVars.size());
        assertEquals(sessionId, envVars.get("SESSION"));
    }

    @Test
    public void testInvocationEnded() throws IOException, ConfigurationException, JSONException {
        final InvocationContext invocationContext = new InvocationContext();
        final String outputFileUploadUrl = "output_file_upload_url";
        final String retryCommandLine = "retry_command_line";
        // create output files (host_log_\d+ will be skipped)
        File fooTxtOutputFile = createMockFile("logs", "foo.txt");
        File fooHtmlOutputFile = createMockFile("android-cts/results/timestamp", "foo.html");
        File barHtmlOutputFile =
                createMockFile("android-cts/results/timestamp/module_reports", "bar.html");
        File fooCtxOutputFile = createMockFile("context", "foo.ctx");
        createMockFile("logs", "host_log_123456.txt");
        mOptionSetter.setOptionValue("cluster:output-file-upload-url", outputFileUploadUrl);
        mOptionSetter.setOptionValue("cluster:output-file-pattern", ".*\\.html");
        mOptionSetter.setOptionValue("cluster:context-file-pattern", ".*\\.ctx");
        mOptionSetter.setOptionValue("cluster:retry-command-line", retryCommandLine);
        final String testOutputUrl = "test_output_url";
        Mockito.doReturn(testOutputUrl)
                .when(mMockTestOutputUploader)
                .uploadFile(Mockito.any(), Mockito.any());

        mClusterLogSaver.invocationStarted(invocationContext);
        mClusterLogSaver.invocationEnded(0);

        // verify that file names are properly stored including any path prefix
        final File fileNamesFile = new File(mWorkDir, ClusterLogSaver.FILE_NAMES_FILE_NAME);
        String expectedFileNames =
                Stream.of("tool-logs/foo.txt", "foo.html", "module_reports/bar.html", "foo.ctx")
                        .sorted()
                        .collect(Collectors.joining("\n"));
        assertEquals(expectedFileNames, FileUtil.readStringFromFile(fileNamesFile));

        Mockito.verify(mMockTestOutputUploader).setUploadUrl(outputFileUploadUrl);
        Mockito.verify(mMockTestOutputUploader).uploadFile(fileNamesFile, null);
        Mockito.verify(mMockTestOutputUploader)
                .uploadFile(fooTxtOutputFile, ClusterLogSaver.TOOL_LOG_PATH);
        Mockito.verify(mMockTestOutputUploader).uploadFile(fooHtmlOutputFile, "");
        Mockito.verify(mMockTestOutputUploader).uploadFile(barHtmlOutputFile, "module_reports");
        Mockito.verify(mMockTestOutputUploader).uploadFile(fooCtxOutputFile, null);
        TestContext expTextContext = new TestContext();
        expTextContext.addTestResource(new TestResource("context/foo.ctx", testOutputUrl));
        expTextContext.setCommandLine(retryCommandLine);
        Mockito.verify(mMockClusterClient)
                .updateTestContext(REQUEST_ID, COMMAND_ID, expTextContext);
    }

    @Test
    public void testInvocationEnded_uploadError() throws IOException, ConfigurationException {
        final InvocationContext invocationContext = new InvocationContext();
        final String outputFileUploadUrl = "output_file_upload_url";
        createMockFile("", ClusterLogSaver.FILE_NAMES_FILE_NAME);
        File fooTxtOutputFile = createMockFile("logs", "foo.txt");
        File barTxtOutputFile = createMockFile("logs", "bar.txt");
        File fooCtxOutputFile = createMockFile("context", "foo.ctx");
        mOptionSetter.setOptionValue("cluster:context-file-pattern", ".*\\.ctx");
        mOptionSetter.setOptionValue("cluster:output-file-upload-url", outputFileUploadUrl);
        final String testOutputUrl = "test_output_url";
        Mockito.doReturn(testOutputUrl)
                .doThrow(RuntimeException.class)
                .doReturn(testOutputUrl)
                .when(mMockTestOutputUploader)
                .uploadFile(Mockito.any(), Mockito.any());

        mClusterLogSaver.invocationStarted(invocationContext);
        mClusterLogSaver.invocationEnded(0);

        // verify that file names are properly stored including any path prefix
        final File fileNamesFile = new File(mWorkDir, ClusterLogSaver.FILE_NAMES_FILE_NAME);
        String expectedFileNames =
                Stream.of("tool-logs/foo.txt", "tool-logs/bar.txt", "foo.ctx")
                        .sorted()
                        .collect(Collectors.joining("\n"));
        assertEquals(expectedFileNames, FileUtil.readStringFromFile(fileNamesFile));

        Mockito.verify(mMockTestOutputUploader).setUploadUrl(outputFileUploadUrl);
        Mockito.verify(mMockTestOutputUploader).uploadFile(fileNamesFile, null);
        Mockito.verify(mMockTestOutputUploader)
                .uploadFile(fooTxtOutputFile, ClusterLogSaver.TOOL_LOG_PATH);
        Mockito.verify(mMockTestOutputUploader)
                .uploadFile(barTxtOutputFile, ClusterLogSaver.TOOL_LOG_PATH);
        Mockito.verify(mMockTestOutputUploader).uploadFile(fooCtxOutputFile, null);
    }

    @Test
    public void testInvocationEnded_duplicateUpload() throws IOException, ConfigurationException {
        String outputFileUploadUrl = "output_file_upload_url";
        mOptionSetter.setOptionValue("cluster:output-file-upload-url", outputFileUploadUrl);

        // create two files with same destination, only the second should get picked
        mOptionSetter.setOptionValue("cluster:output-file-pattern", ".*\\.txt");
        mOptionSetter.setOptionValue("cluster:file-picking-strategy", "PICK_LAST");
        File first = createMockFile("android-gts/results/first", "foo.txt");
        File second = createMockFile("android-gts/results/second", "foo.txt");

        InvocationContext invocationContext = new InvocationContext();
        mClusterLogSaver.invocationStarted(invocationContext);
        mClusterLogSaver.invocationEnded(0);

        // verify that only one file is recorded in the filename file
        File fileNamesFile = new File(mWorkDir, ClusterLogSaver.FILE_NAMES_FILE_NAME);
        assertEquals("foo.txt", FileUtil.readStringFromFile(fileNamesFile));

        // verify that only the second file is uploaded (and filename file)
        Mockito.verify(mMockTestOutputUploader).setUploadUrl(outputFileUploadUrl);
        Mockito.verify(mMockTestOutputUploader).uploadFile(fileNamesFile, null);
        Mockito.verify(mMockTestOutputUploader).uploadFile(second, "");
        Mockito.verifyNoMoreInteractions(mMockTestOutputUploader);
    }

    @Test
    public void testAppendFilesToContext() throws IOException {
        // create context file
        File contextFile = createMockZipFile("context", "context.zip");

        // create files to append
        File first = createMockFile("extra", "1.txt");
        File second = createMockFile("extra", "2.txt");

        // append files using absolute, relative, and unknown paths
        mClusterLogSaver.appendFilesToContext(
                contextFile, Arrays.asList(first.getAbsolutePath(), "extra/2.txt", "unknown.txt"));

        // verify that context file contains the additional files
        Set<String> entries = getZipEntries(contextFile);
        assertTrue(entries.contains("1.txt"));
        assertTrue(entries.contains("2.txt"));
        assertFalse(entries.contains("unknown.txt")); // unknown file ignored
    }
}
