// Copyright 2013-2019 Michel Kraemer
//
// 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 de.undercouch.gradle.tasks.download;

import groovy.json.JsonOutput;
import groovy.json.JsonSlurper;
import org.apache.commons.io.FileUtils;
import org.junit.Test;

import java.io.File;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;

import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.absent;
import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
import static com.github.tomakehurst.wiremock.client.WireMock.get;
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;

/**
 * Tests if the plugin uses the ETag header correctly
 * @author Michel Kraemer
 */
public class ETagTest extends TestBaseWithMockServer {
    /**
     * Tests if the plugin can handle a missing ETag header
     * @throws Exception if anything goes wrong
     */
    @Test
    public void missingETag() throws Exception {
        wireMockRule.stubFor(get(urlEqualTo("/" + TEST_FILE_NAME))
                .withHeader("If-None-Match", absent())
                .willReturn(aResponse()
                        .withBody(CONTENTS)));

        Download t = makeProjectAndTask();
        t.src(wireMockRule.url(TEST_FILE_NAME));
        File dst = folder.newFile();
        assertTrue(dst.delete());
        assertFalse(dst.exists());
        t.dest(dst);
        t.onlyIfModified(true);
        t.useETag(true);
        t.compress(false);
        t.execute();

        String dstContents = FileUtils.readFileToString(dst);
        assertEquals(CONTENTS, dstContents);
        assertFalse(t.getCachedETagsFile().exists());
    }
    
    /**
     * Tests if the plugin can handle an incorrect (unquoted) ETag header and
     * still downloads the file
     * @throws Exception if anything goes wrong
     */
    @Test
    public void incorrectETag() throws Exception {
        wireMockRule.stubFor(get(urlEqualTo("/" + TEST_FILE_NAME))
                .withHeader("If-None-Match", absent())
                .willReturn(aResponse()
                        .withHeader("ETag", "abcd")
                        .withBody(CONTENTS)));

        Download t = makeProjectAndTask();
        t.src(wireMockRule.url(TEST_FILE_NAME));
        File dst = folder.newFile();
        assertTrue(dst.delete());
        assertFalse(dst.exists());
        t.dest(dst);
        t.onlyIfModified(true);
        t.useETag(true);
        t.compress(false);
        t.execute();

        String dstContents = FileUtils.readFileToString(dst);
        assertEquals(CONTENTS, dstContents);
    }

    /**
     * Tests if the plugin downloads two files and stores their etags correctly
     * to the default cached etags file
     * @throws Exception if anything goes wrong
     */
    @Test
    public void storeMultipleETags() throws Exception {
        String etag1 = "\"foobar1\"";
        String etag2 = "\"foobar2\"";

        wireMockRule.stubFor(get(urlEqualTo("/file1"))
                .withHeader("If-None-Match", absent())
                .willReturn(aResponse()
                        .withHeader("ETag", etag1)
                        .withBody(CONTENTS + "1")));
        wireMockRule.stubFor(get(urlEqualTo("/file2"))
                .withHeader("If-None-Match", absent())
                .willReturn(aResponse()
                        .withHeader("ETag", etag2)
                        .withBody(CONTENTS + "2")));

        // download first file
        Download t = makeProjectAndTask();
        t.src(wireMockRule.url("file1"));
        File dst1 = folder.newFile();
        assertTrue(dst1.delete());
        assertFalse(dst1.exists());
        t.dest(dst1);
        t.onlyIfModified(true);
        t.useETag(true);
        t.compress(false);
        t.execute();

        // download second file
        t = makeProjectAndTask();
        t.src(wireMockRule.url("file2"));
        File dst2 = folder.newFile();
        assertTrue(dst2.delete());
        assertFalse(dst2.exists());
        t.dest(dst2);
        t.onlyIfModified(true);
        t.useETag(true);
        t.compress(false);
        t.execute();

        // check server responses
        String dst1Contents = FileUtils.readFileToString(dst1);
        assertEquals(CONTENTS + "1", dst1Contents);
        String dst2Contents = FileUtils.readFileToString(dst2);
        assertEquals(CONTENTS + "2", dst2Contents);

        // read cached etags file
        JsonSlurper slurper = new JsonSlurper();
        @SuppressWarnings("unchecked")
        Map<String, Object> cachedETags = (Map<String, Object>)slurper.parse(
                t.getCachedETagsFile(), "UTF-8");

        // check cached etags
        Map<String, Object> expectedETag1 = new LinkedHashMap<>();
        expectedETag1.put("ETag", etag1);
        Map<String, Object> expectedETag2 = new LinkedHashMap<>();
        expectedETag2.put("ETag", etag2);

        Map<String, Object> expectedHost = new LinkedHashMap<>();
        expectedHost.put("/file1", expectedETag1);
        expectedHost.put("/file2", expectedETag2);

        Map<String, Object> expectedCachedETags = new LinkedHashMap<>();
        expectedCachedETags.put(wireMockRule.baseUrl(), expectedHost);

        assertEquals(expectedCachedETags, cachedETags);
    }

    /**
     * Tests if the plugin downloads a file and stores the etag correctly to
     * the default cached etags file
     * @throws Exception if anything goes wrong
     */
    @Test
    public void storeETag() throws Exception {
        String etag = "\"foobar\"";

        wireMockRule.stubFor(get(urlEqualTo("/" + TEST_FILE_NAME))
                .withHeader("If-None-Match", absent())
                .willReturn(aResponse()
                        .withHeader("ETag", etag)
                        .withBody(CONTENTS)));

        Download t = makeProjectAndTask();
        t.src(wireMockRule.url(TEST_FILE_NAME));
        File dst = folder.newFile();
        assertTrue(dst.delete());
        assertFalse(dst.exists());
        t.dest(dst);
        t.onlyIfModified(true);
        t.useETag(true);
        assertTrue((Boolean)t.getUseETag());
        t.compress(false);
        t.execute();

        String dstContents = FileUtils.readFileToString(dst);
        assertEquals(CONTENTS, dstContents);

        File buildDir = new File(projectDir, "build");
        File downloadTaskDir = new File(buildDir, "download-task");
        assertEquals(downloadTaskDir.getCanonicalFile(), t.getDownloadTaskDir());
        File cachedETagsFile = new File(downloadTaskDir, "etags.json");
        assertEquals(cachedETagsFile.getCanonicalFile(), t.getCachedETagsFile());

        JsonSlurper slurper = new JsonSlurper();
        @SuppressWarnings("unchecked")
        Map<String, Object> cachedETags = (Map<String, Object>)slurper.parse(
                cachedETagsFile, "UTF-8");

        Map<String, Object> expectedETag = new LinkedHashMap<>();
        expectedETag.put("ETag", etag);

        Map<String, Object> expectedHost = new LinkedHashMap<>();
        expectedHost.put("/" + TEST_FILE_NAME, expectedETag);

        Map<String, Object> expectedCachedETags = new LinkedHashMap<>();
        expectedCachedETags.put(wireMockRule.baseUrl(), expectedHost);

        assertEquals(expectedCachedETags, cachedETags);
    }

    /**
     * Tests if the downloadTaskDir can be configured
     * @throws Exception if anything goes wrong
     */
    @Test
    public void configureDownloadTaskDir() throws Exception {
        String etag = "\"foobar\"";

        wireMockRule.stubFor(get(urlEqualTo("/" + TEST_FILE_NAME))
                .withHeader("If-None-Match", absent())
                .willReturn(aResponse()
                        .withHeader("ETag", etag)
                        .withBody(CONTENTS)));

        Download t = makeProjectAndTask();
        File newDownloadTaskDir = folder.newFolder();
        t.downloadTaskDir(newDownloadTaskDir);
        t.src(wireMockRule.url(TEST_FILE_NAME));
        File dst = folder.newFile();
        assertTrue(dst.delete());
        t.dest(dst);
        t.onlyIfModified(true);
        t.useETag(true);
        t.compress(false);
        t.execute();

        String dstContents = FileUtils.readFileToString(dst);
        assertEquals(CONTENTS, dstContents);

        assertEquals(newDownloadTaskDir, t.getDownloadTaskDir());
        File cachedETagsFile = new File(newDownloadTaskDir, "etags.json");
        assertTrue(cachedETagsFile.exists());
        assertEquals(cachedETagsFile, t.getCachedETagsFile());
    }

    /**
     * Tests if the cachedETagsFile can be configured
     * @throws Exception if anything goes wrong
     */
    @Test
    public void configureCachedETagsFile() throws Exception {
        String etag = "\"foobar\"";

        wireMockRule.stubFor(get(urlEqualTo("/" + TEST_FILE_NAME))
                .withHeader("If-None-Match", absent())
                .willReturn(aResponse()
                        .withHeader("ETag", etag)
                        .withBody(CONTENTS)));

        Download t = makeProjectAndTask();
        File newCachedETagsFile = folder.newFile();
        assertTrue(newCachedETagsFile.delete());
        t.cachedETagsFile(newCachedETagsFile);
        t.src(wireMockRule.url(TEST_FILE_NAME));
        File dst = folder.newFile();
        assertTrue(dst.delete());
        t.dest(dst);
        t.onlyIfModified(true);
        t.useETag(true);
        t.compress(false);
        t.execute();

        String dstContents = FileUtils.readFileToString(dst);
        assertEquals(CONTENTS, dstContents);

        assertEquals(newCachedETagsFile, t.getCachedETagsFile());
        assertTrue(newCachedETagsFile.exists());
    }

    /**
     * Create a cached ETags file for the given etag
     * @param cachedETagsFile the file to create
     * @throws IOException if the file could not be created
     */
    private void prepareCachedETagsFile(File cachedETagsFile, String etag) throws IOException {
        Map<String, Object> etagMap = new LinkedHashMap<>();
        etagMap.put("ETag", etag);
        Map<String, Object> hostMap = new LinkedHashMap<>();
        hostMap.put("/" + TEST_FILE_NAME, etagMap);
        Map<String, Object> cachedETags = new LinkedHashMap<>();
        cachedETags.put(wireMockRule.baseUrl(), hostMap);
        String cachedETagsContents = JsonOutput.toJson(cachedETags);
        FileUtils.writeStringToFile(cachedETagsFile, cachedETagsContents);
    }

    /**
     * Tests if the plugin doesn't download a file if the etag equals
     * the cached one
     * @throws Exception if anything goes wrong
     */
    @Test
    public void dontDownloadIfEqual() throws Exception {
        String etag = "\"foobar\"";

        wireMockRule.stubFor(get(urlEqualTo("/" + TEST_FILE_NAME))
                .withHeader("If-None-Match", equalTo(etag))
                .willReturn(aResponse()
                        .withStatus(304)));

        Download t = makeProjectAndTask();
        t.src(wireMockRule.url(TEST_FILE_NAME));
        File dst = folder.newFile();
        FileUtils.writeStringToFile(dst, "Hello");
        t.dest(dst);
        t.onlyIfModified(true);
        t.useETag(true);

        prepareCachedETagsFile(t.getCachedETagsFile(), etag);

        t.compress(false);
        t.execute();

        String dstContents = FileUtils.readFileToString(dst);
        assertEquals("Hello", dstContents);
        assertNotEquals(CONTENTS, dstContents);
    }

    /**
     * Tests if the plugin still downloads a file if the cached ETag is correct
     * but the destination file does not exist.
     * @throws Exception if anything goes wrong
     */
    @Test
    public void forceDownloadIfDestNotExists() throws Exception {
        String etag = "\"foobar\"";

        wireMockRule.stubFor(get(urlEqualTo("/" + TEST_FILE_NAME))
                .withHeader("If-None-Match", absent())
                .willReturn(aResponse()
                        .withHeader("ETag", etag)
                        .withBody(CONTENTS)));

        Download t = makeProjectAndTask();
        t.src(wireMockRule.url(TEST_FILE_NAME));
        File dst = folder.newFile();
        assertTrue(dst.delete());
        t.dest(dst);
        t.onlyIfModified(true);
        t.useETag(true);

        prepareCachedETagsFile(t.getCachedETagsFile(), etag);

        t.compress(false);
        t.execute();

        String dstContents = FileUtils.readFileToString(dst);
        assertEquals(CONTENTS, dstContents);
    }

    /**
     * Tests if the plugin downloads a file if its ETag does not match the
     * cached one
     * @throws Exception if anything goes wrong
     */
    @Test
    public void modifiedDownload() throws Exception {
        String wrongEtag = "\"barfoo\"";
        String etag = "\"foobar\"";

        wireMockRule.stubFor(get(urlEqualTo("/" + TEST_FILE_NAME))
                .withHeader("If-None-Match", equalTo(wrongEtag))
                .willReturn(aResponse()
                        .withHeader("ETag", etag)
                        .withBody(CONTENTS)));

        Download t = makeProjectAndTask();
        t.src(wireMockRule.url(TEST_FILE_NAME));
        File dst = folder.newFile();
        FileUtils.writeStringToFile(dst, "Hello");
        t.dest(dst);
        t.onlyIfModified(true);
        t.useETag(true);

        prepareCachedETagsFile(t.getCachedETagsFile(), wrongEtag);

        t.compress(false);
        t.execute();

        String dstContents = FileUtils.readFileToString(dst);
        assertEquals(CONTENTS, dstContents);
    }

    /**
     * Tests if the plugin supports weak etags
     * @throws Exception if anything goes wrong
     */
    @Test
    public void storeWeakETagIssueWarning() throws Exception {
        String etag = "W/\"foobar1\"";

        wireMockRule.stubFor(get(urlEqualTo("/" + TEST_FILE_NAME))
                .withHeader("If-None-Match", absent())
                .willReturn(aResponse()
                        .withHeader("ETag", etag)
                        .withBody(CONTENTS)));

        Download t = makeProjectAndTask();
        t.src(wireMockRule.url(TEST_FILE_NAME));
        File dst = folder.newFile();
        assertTrue(dst.delete());
        assertFalse(dst.exists());
        t.dest(dst);
        t.onlyIfModified(true);
        t.useETag(true);
        assertEquals(Boolean.TRUE, t.getUseETag());
        t.compress(false);
        t.execute();

        // check server response
        String dstContents = FileUtils.readFileToString(dst);
        assertEquals(CONTENTS, dstContents);

        // read cached etags file
        JsonSlurper slurper = new JsonSlurper();
        @SuppressWarnings("unchecked")
        Map<String, Object> cachedETags = (Map<String, Object>)slurper.parse(
                t.getCachedETagsFile(), "UTF-8");

        // check cached etags
        Map<String, Object> expectedETag = new LinkedHashMap<>();
        expectedETag.put("ETag", etag);
        Map<String, Object> expectedHost = new LinkedHashMap<>();
        expectedHost.put("/" + TEST_FILE_NAME, expectedETag);
        Map<String, Object> expectedCachedETags = new LinkedHashMap<>();
        expectedCachedETags.put(wireMockRule.baseUrl(), expectedHost);
        assertEquals(expectedCachedETags, cachedETags);
    }

    /**
     * Tests if the plugin supports weak etags
     * @throws Exception if anything goes wrong
     */
    @Test
    public void storeAllETags() throws Exception {
        String etag = "W/\"foobar1\"";

        wireMockRule.stubFor(get(urlEqualTo("/" + TEST_FILE_NAME))
                .withHeader("If-None-Match", absent())
                .willReturn(aResponse()
                        .withHeader("ETag", etag)
                        .withBody(CONTENTS)));

        Download t = makeProjectAndTask();
        t.src(wireMockRule.url(TEST_FILE_NAME));
        File dst = folder.newFile();
        assertTrue(dst.delete());
        assertFalse(dst.exists());
        t.dest(dst);
        t.onlyIfModified(true);
        t.useETag("all");
        assertEquals("all", t.getUseETag());
        t.compress(false);
        t.execute();

        // check server response
        String dstContents = FileUtils.readFileToString(dst);
        assertEquals(CONTENTS, dstContents);

        // read cached etags file
        JsonSlurper slurper = new JsonSlurper();
        @SuppressWarnings("unchecked")
        Map<String, Object> cachedETags = (Map<String, Object>)slurper.parse(
                t.getCachedETagsFile(), "UTF-8");

        // check cached etags
        Map<String, Object> expectedETag = new LinkedHashMap<>();
        expectedETag.put("ETag", etag);
        Map<String, Object> expectedHost = new LinkedHashMap<>();
        expectedHost.put("/" + TEST_FILE_NAME, expectedETag);
        Map<String, Object> expectedCachedETags = new LinkedHashMap<>();
        expectedCachedETags.put(wireMockRule.baseUrl(), expectedHost);
        assertEquals(expectedCachedETags, cachedETags);
    }

    /**
     * Tests if the plugin can ignore weak ETags
     * @throws Exception if anything goes wrong
     */
    @Test
    public void storeStrongOnly() throws Exception {
        String etag1 = "W/\"foobar1\"";
        String etag2 = "\"foobar2\"";

        wireMockRule.stubFor(get(urlEqualTo("/file1"))
                .withHeader("If-None-Match", absent())
                .willReturn(aResponse()
                        .withHeader("ETag", etag1)
                        .withBody(CONTENTS + "1")));
        wireMockRule.stubFor(get(urlEqualTo("/file2"))
                .withHeader("If-None-Match", absent())
                .willReturn(aResponse()
                        .withHeader("ETag", etag2)
                        .withBody(CONTENTS + "2")));

        // download first file
        Download t = makeProjectAndTask();
        t.src(wireMockRule.url("file1"));
        File dst1 = folder.newFile();
        assertTrue(dst1.delete());
        assertFalse(dst1.exists());
        t.dest(dst1);
        t.onlyIfModified(true);
        t.useETag("strongOnly");
        assertEquals("strongOnly", t.getUseETag());
        t.compress(false);
        t.execute();

        // download second file
        t = makeProjectAndTask();
        t.src(wireMockRule.url("file2"));
        File dst2 = folder.newFile();
        assertTrue(dst2.delete());
        assertFalse(dst2.exists());
        t.dest(dst2);
        t.onlyIfModified(true);
        t.useETag("strongOnly");
        assertEquals("strongOnly", t.getUseETag());
        t.compress(false);
        t.execute();

        // check server responses
        String dst1Contents = FileUtils.readFileToString(dst1);
        assertEquals(CONTENTS + "1", dst1Contents);
        String dst2Contents = FileUtils.readFileToString(dst2);
        assertEquals(CONTENTS + "2", dst2Contents);

        // read cached etags file
        JsonSlurper slurper = new JsonSlurper();
        @SuppressWarnings("unchecked")
        Map<String, Object> cachedETags = (Map<String, Object>)slurper.parse(
                t.getCachedETagsFile(), "UTF-8");

        // check cached etags (there should be no entry for etag1)
        Map<String, Object> expectedETag2 = new LinkedHashMap<>();
        expectedETag2.put("ETag", etag2);

        Map<String, Object> expectedHost = new LinkedHashMap<>();
        expectedHost.put("/file2", expectedETag2);

        Map<String, Object> expectedCachedETags = new LinkedHashMap<>();
        expectedCachedETags.put(wireMockRule.baseUrl(), expectedHost);

        assertEquals(expectedCachedETags, cachedETags);
    }

    /**
     * Make sure we cannot assign an invalid value to the "useETag" flag
     */
    @Test(expected = IllegalArgumentException.class)
    public void invalidUseETagFlag() {
        Download t = makeProjectAndTask();
        t.useETag("foobar");
    }
}