/*
 * 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 libcore.libcore.content.type;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import libcore.content.type.MimeMap;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

/**
 * Tests {@link MimeMap} and {@link MimeMap.Builder}.
 */
public class MimeMapTest {

    private MimeMap mimeMap;
    private MimeMap emptyMap;

    @Before public void setUp() {
        mimeMap = MimeMap.getDefault();
        emptyMap = MimeMap.builder().build();
    }

    @After public void tearDown() {
        mimeMap = null;
    }

    @Test public void lookup_invalidExtension() {
        assertNull(mimeMap.guessMimeTypeFromExtension(null));
        assertNull(mimeMap.guessMimeTypeFromExtension(""));
        assertFalse(mimeMap.hasExtension(null));
        assertFalse(mimeMap.hasExtension(""));
    }

    @Test public void lookup_invalidMimeType() {
        assertNull(mimeMap.guessExtensionFromMimeType(null));
        assertNull(mimeMap.guessExtensionFromMimeType(""));
        assertFalse(mimeMap.hasMimeType(null));
        assertFalse(mimeMap.hasMimeType(""));
    }

    @Test public void caseNormalization_key() {
        mimeMap = MimeMap.builder()
                .addMimeMapping("application/msWord", Arrays.asList("Doc"))
                .build();
        assertEquals("application/msword", mimeMap.guessMimeTypeFromExtension("dOc"));
        assertEquals("doc", mimeMap.guessExtensionFromMimeType("appliCATion/mSWOrd"));
        assertTrue(mimeMap.hasMimeType("application/msword"));
        assertTrue(mimeMap.hasMimeType("Application/mSWord"));

        assertTrue(mimeMap.hasExtension("doc"));
        assertTrue(mimeMap.hasExtension("DOC"));
        assertTrue(mimeMap.hasExtension("dOc"));
    }

    @Test public void caseNormalization_value() {
        // Default map
        for (String extension : mimeMap.extensions()) {
            assertLowerCase(mimeMap.guessMimeTypeFromExtension(extension));
        }
        for (String mimeType : mimeMap.mimeTypes()) {
            assertLowerCase(mimeMap.guessExtensionFromMimeType(mimeType));
        }

        // Known keys for a custom map
        mimeMap = MimeMap.builder()
                .addMimeMapping("application/msWord", Arrays.asList("Doc"))
                .build();
        assertEquals("doc", mimeMap.guessExtensionFromMimeType("Application/mSWord"));
        assertEquals("application/msword", mimeMap.guessMimeTypeFromExtension("DoC"));
    }

    private static void assertLowerCase(String s) {
        assertEquals(s.toLowerCase(Locale.ROOT), s);
    }

    @Test public void unmapped() {
        mimeMap = MimeMap.builder()
                .addMimeMapping("mime/test", Arrays.asList("test", "tst"))
                .build();
        assertNull(mimeMap.guessExtensionFromMimeType("mime/unknown"));
        assertFalse(mimeMap.hasMimeType("mime/unknown"));

        assertNull(mimeMap.guessMimeTypeFromExtension("absent"));
        assertFalse(mimeMap.hasExtension("absent"));
    }

    @Test public void getDefault_returnsSameInstance() {
        assertSame(MimeMap.getDefault(), MimeMap.getDefault());
    }

    @Test public void getDefault_afterSetDefaultSupplier() {
        MimeMap originalDefault = MimeMap.getDefault();
        try {
            // Constructs a new instance every time it is called
            MimeMap.setDefaultSupplier(() -> MimeMap.builder().addMimeMapping("mime/sup", "sup").build());
            // Same instance is returned both times
            assertSame(MimeMap.getDefault(), MimeMap.getDefault());
            // Check that the supplier is in effect
            assertTrue(originalDefault != MimeMap.getDefault());
            assertEquals("mime/sup", MimeMap.getDefault().guessMimeTypeFromExtension("sup"));
        } finally {
            MimeMap.setDefaultSupplier(() -> originalDefault);
        }
        assertSame(originalDefault, MimeMap.getDefault());
    }

    @Test public void setDefaultSupplier_returningNull() {
        MimeMap originalDefault = MimeMap.getDefault();
        try {
            // A Supplier that returns null is invalid, but we only notice during getDefault().
            MimeMap.setDefaultSupplier(() -> null);
            try {
                MimeMap.getDefault();
                fail();
            } catch (NullPointerException expected) {
            }
        } finally {
            MimeMap.setDefaultSupplier(() -> originalDefault);
        }
    }

    @Test public void buildUpon() {
        mimeMap = MimeMap.builder()
                .build();
        assertMap(
                makeMap(),
                makeMap(),
                mimeMap);

        mimeMap = mimeMap.buildUpon()
                .build();
        assertMap(
                makeMap(),
                makeMap(),
                mimeMap);

        mimeMap = mimeMap.buildUpon()
                .addMimeMapping("text/plain", "txt")
                .build();
        assertMap(
                makeMap("text/plain", "txt"),
                makeMap("txt", "text/plain"),
                mimeMap);

        mimeMap = mimeMap.buildUpon()
                .addMimeMapping("audio/mpeg", Arrays.asList("mp2", "mp3"))
                .build();
        assertMap(
                makeMap("audio/mpeg", "mp2",
                        "text/plain", "txt"),
                makeMap("mp2", "audio/mpeg",
                        "mp3", "audio/mpeg",
                        "txt", "text/plain"),
                mimeMap);

        mimeMap = mimeMap.buildUpon()
                .addMimeMapping("text/plain", "text")
                .build();
        assertMap(
                makeMap("audio/mpeg", "mp2",
                        "text/plain", "text"),
                makeMap("mp2", "audio/mpeg",
                        "mp3", "audio/mpeg",
                        "text", "text/plain",
                        "txt", "text/plain"),
                mimeMap);
    }

    @Test public void put() {
        MimeMap a = MimeMap.builder()
                .addMimeMapping("text/plain", Arrays.asList("txt", "text"))
                .addMimeMapping("application/msword", "doc")
                .build();
        MimeMap b = MimeMap.builder()
                .addMimeMapping("text/plain", Arrays.asList("txt", "text"))
                .addMimeMapping("application/msword", "doc")
                .build();
        assertEqualsButNotSame(a, b);
        assertEqualsButNotSame(a, a.buildUpon().build());
        assertMap(
                makeMap(
                        "text/plain", "txt",
                        "application/msword", "doc"),
                makeMap("txt", "text/plain",
                        "text", "text/plain",
                        "doc", "application/msword"),
                a);
    }

    @Test public void put_noExtensions() {
        checkPut_noExtensions(emptyMap);
        checkPut_noExtensions(MimeMap.builder().addMimeMapping("text/plain", "txt").build());
        checkPut_noExtensions(mimeMap);
    }

    /**
     * Checks that put(String, emptyList()) doesn't change or add any mappings.
     */
    private static void checkPut_noExtensions(MimeMap baseMap) {
        MimeMap mimeMap = baseMap.buildUpon()
                .addMimeMapping("mime/type", Collections.emptyList())
                .build();
        assertEquals(baseMap, mimeMap);
    }

    @Test public void put_String_List_nullOrEmpty() {
        // We still check mimeType for validity even if no extensions are specified
        assertPutThrowsNpe(null);
        assertPutThrowsIae("");

        // null or "" are not allowed for either MIME type or extension
        assertPutThrowsNpe(null, "ext");
        assertPutThrowsIae("", "ext");
        assertPutThrowsNpe("mime/type", (String) null);
        assertPutThrowsIae("mime/type", "");

        assertPutThrowsNpe("mime/type", "ext", null);
        assertPutThrowsIae("mime/type", "ext", "");
    }

    @Test public void put_String_String_nullOrEmpty() {
        assertThrowsNpe(() -> MimeMap.builder().addMimeMapping(null, "ext"));
        assertThrowsIae(() -> MimeMap.builder().addMimeMapping("", "ext"));

        assertThrowsNpe(() -> MimeMap.builder().addMimeMapping("mime/type", (String) null));
        assertThrowsIae(() -> MimeMap.builder().addMimeMapping("mime/type", ""));
    }

    /**
     * Tests put() arguments that have a prefix {@code "?"} which leads to putIfAbsent semantics.
     */
    @Test public void putIfAbsent() {
        // Starting from an empty mapping, add a bunch more, some with and some without '?'.
        mimeMap = MimeMap.builder()
                .addMimeMapping("?text/plain", "?txt")
                .addMimeMapping("audio/mpeg", Arrays.asList("mpga", "mpega", "?mp2", "mp3"))
                .build();
        assertEquals("txt", mimeMap.guessExtensionFromMimeType("text/plain"));
        assertEquals("text/plain", mimeMap.guessMimeTypeFromExtension("txt"));
        assertEquals("mpga", mimeMap.guessExtensionFromMimeType("audio/mpeg"));
        assertEquals("audio/mpeg", mimeMap.guessMimeTypeFromExtension("mp2"));
        assertEquals("audio/mpeg", mimeMap.guessMimeTypeFromExtension("mp3"));

        // Override a ext -> MIME mapping without overriding the MIME -> ext mapping.
        mimeMap = mimeMap.buildUpon()
                .addMimeMapping("?audio/mpeg", "m4a")
                .build();
        assertEquals("mpga", mimeMap.guessExtensionFromMimeType("audio/mpeg"));
        assertEquals("audio/mpeg", mimeMap.guessMimeTypeFromExtension("m4a"));

        // Override a MIME -> ext mapping without overriding the ext -> MIME mapping.
        mimeMap = mimeMap.buildUpon()
                .addMimeMapping("audio/mpeg", "?txt")
                .build();
        assertEquals("txt", mimeMap.guessExtensionFromMimeType("audio/mpeg"));
        assertEquals("text/plain", mimeMap.guessMimeTypeFromExtension("txt"));


        // Check final state
        assertMap(
                makeMap(
                        "text/plain", "txt",
                        "audio/mpeg", "txt"
                        ),
                makeMap(
                        "txt", "text/plain",
                        "m4a", "audio/mpeg",
                        "mp2", "audio/mpeg",
                        "mp3", "audio/mpeg",
                        "mpega", "audio/mpeg",
                        "mpga", "audio/mpeg"
                ),
                mimeMap
        );
    }

    @Test public void extensions() {
        assertEquals(Collections.emptySet(), emptyMap.extensions());
        mimeMap = MimeMap.builder()
                .addMimeMapping("text/plain", Arrays.asList("txt", "text"))
                .addMimeMapping("audi/mpeg", "m4a")
                .addMimeMapping("application/msword", "doc")
                .addMimeMapping("text/plain", "tx")
                .build();
        Set<String> extensions = new HashSet<>(Arrays.asList(
                "txt", "text", "m4a", "doc", "tx"));
        assertEquals(extensions, mimeMap.extensions());
        // Check that the extensions() view is unmodifiable
        try {
            mimeMap.extensions().add("ext");
            fail();
        } catch (UnsupportedOperationException expected) {
        }
    }

    @Test public void mimeTypes() {
        assertEquals(Collections.emptySet(), emptyMap.mimeTypes());
        mimeMap = MimeMap.builder()
                .addMimeMapping("text/plain", Arrays.asList("txt", "text"))
                .addMimeMapping("audio/mpeg", "m4a")
                .addMimeMapping("application/msword", "doc")
                .addMimeMapping("text/plain", "tx")
                .build();
        Set<String> mimeTypes = new HashSet<>(Arrays.asList(
                "text/plain",
                "audio/mpeg",
                "application/msword"));
        assertEquals(mimeTypes, mimeMap.mimeTypes());
        // Check that the mimeTypes() view is unmodifiable
        try {
            mimeMap.mimeTypes().add("foo/bar");
            fail();
        } catch (UnsupportedOperationException expected) {
        }
    }

    /**
     * Tests invalid put() invocations that have '?' in additional/invalid places.
     */
    @Test public void put_invalid_additionalQuestionMarks() {
        // Potentially we could tolerate additional ? as a prefix in future, but right now we don't.
        assertPutThrowsIae("??text/plain", "txt");
        assertPutThrowsIae("text/p?lain", "txt");
        assertPutThrowsIae("text/plain", "txt", "t?ext");
        assertPutThrowsIae("text/plain", "??txt");
        assertPutThrowsIae("text/plain", "t?xt");
    }

    /** Checks that MIME types must have a '/', while extensions must not. */
    @Test public void put_invalid_slash() {
        assertPutThrowsIae("mime/type", "invalid/ext");
        assertPutThrowsIae("invalidmime", "ext");

        // During lookups, wrong arguments return null rather than throwing.
        mimeMap = MimeMap.builder().addMimeMapping("mime/type", "ext").build();
        assertNull(mimeMap.guessExtensionFromMimeType("ext")); // ext is no mime type
        assertNull(mimeMap.guessMimeTypeFromExtension("mime/type")); // mime/type is no extension
    }

    private static void assertPutThrowsNpe(String mime, String... exts) {
        assertThrowsNpe(() -> MimeMap.builder().addMimeMapping(mime, Arrays.asList(exts)));
    }

    private static void assertPutThrowsIae(final String mime, final String... exts) {
        assertThrowsIae(() -> MimeMap.builder().addMimeMapping(mime, Arrays.asList(exts)));
    }

    private static void assertThrowsNpe(Runnable runnable) {
        try {
            runnable.run();
            fail();
        } catch (NullPointerException expected) {
        }
    }

    private static void assertThrowsIae(Runnable runnable) {
        try {
            runnable.run();
            fail();
        } catch (IllegalArgumentException expected) {
        }
    }

    @Test public void hashCodeValue() {
        assertEquals(0, emptyMap.hashCode());
        MimeMap a = MimeMap.builder().addMimeMapping("mime/test", "test").build();
        MimeMap b = a.buildUpon().addMimeMapping("foo/bar", "baz").build();
        assertTrue(0 != a.hashCode());
        assertTrue((a.hashCode() != b.hashCode()));
    }

    @Test public void empty_copies() {
        assertEqualsButNotSame(emptyMap, MimeMap.builder().build());
        assertEqualsButNotSame(emptyMap, emptyMap.buildUpon().build());
    }

    /** Creates a map from alternating key/value arguments, useful for test assertions. */
    private static Map<String, String> makeMap(String... keysAndValues) {
        if (keysAndValues.length % 2 != 0) {
            throw new IllegalArgumentException(
                    "Invalid length " + keysAndValues.length + ": " + keysAndValues);
        }
        Map<String, String> result = new HashMap<>();
        for (int i = 0; i < keysAndValues.length; i += 2) {
            String key = keysAndValues[i];
            String value = keysAndValues[i + 1];
            result.put(key, value);
        }
        return result;

    }

    /**
     * Asserts that the given {@code MimeMap} has exactly the given mime -> ext and ext -> mime
     * mappings, but no others.
     */
    private static<T> void assertMap(
            Map<String, String> expectedMimeToExt,
            Map<String, String> expectedExtToMime,
            MimeMap mimeMap)
    {
        MimeMap.Builder expectedBuilder = MimeMap.builder();
        for (Map.Entry<String, String> entry : expectedExtToMime.entrySet()) {
            String ext = entry.getKey();
            String mime = entry.getValue();
            assertEquals(ext + ": " + mimeMap, mime, mimeMap.guessMimeTypeFromExtension(ext));
            expectedBuilder.addMimeMapping("?" + mime, ext);
        }
        for (Map.Entry<String, String> entry : expectedMimeToExt.entrySet()) {
            String mime = entry.getKey();
            String ext = entry.getValue();
            assertEquals(mime + ": "  + mimeMap, ext, mimeMap.guessExtensionFromMimeType(mime));
            expectedBuilder.addMimeMapping(mime, "?" + ext);
        }
        // Check that there are no unexpected additional mappings.
        assertEqualsButNotSame(expectedBuilder.build(), mimeMap);
    }

    private static<T> void assertEqualsButNotSame(T a, T b) {
        assertEquals(a, b);
        assertEquals(b, a);
        assertNotSame(a, b);
        assertEquals(a.hashCode(), b.hashCode());
    }

}
