package com.crossbowffs.remotepreferences.app;

import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.net.Uri;

import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;

import com.crossbowffs.remotepreferences.RemoteContract;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;

@RunWith(AndroidJUnit4.class)
public class RemotePreferenceProviderTest {
    private Context getLocalContext() {
        return InstrumentationRegistry.getInstrumentation().getContext();
    }

    private Context getRemoteContext() {
        return InstrumentationRegistry.getInstrumentation().getTargetContext();
    }

    private SharedPreferences getSharedPreferences() {
        Context context = getRemoteContext();
        return context.getSharedPreferences(Constants.PREF_FILE, Context.MODE_PRIVATE);
    }

    private Uri getQueryUri(String key) {
        String uri = "content://" + Constants.AUTHORITY + "/" + Constants.PREF_FILE;
        if (key != null) {
            uri += "/" + key;
        }
        return Uri.parse(uri);
    }

    @Before
    public void resetPreferences() {
        getSharedPreferences().edit().clear().commit();
    }

    @Test
    public void testQueryAllPrefs() {
        getSharedPreferences()
            .edit()
            .putString("string", "foobar")
            .putInt("int", 1337)
            .apply();

        ContentResolver resolver = getLocalContext().getContentResolver();
        Cursor q = resolver.query(getQueryUri(null), null, null, null, null);
        Assert.assertEquals(2, q.getCount());

        int key = q.getColumnIndex(RemoteContract.COLUMN_KEY);
        int type = q.getColumnIndex(RemoteContract.COLUMN_TYPE);
        int value = q.getColumnIndex(RemoteContract.COLUMN_VALUE);

        while (q.moveToNext()) {
            if (q.getString(key).equals("string")) {
                Assert.assertEquals(RemoteContract.TYPE_STRING, q.getInt(type));
                Assert.assertEquals("foobar", q.getString(value));
            } else if (q.getString(key).equals("int")) {
                Assert.assertEquals(RemoteContract.TYPE_INT, q.getInt(type));
                Assert.assertEquals(1337, q.getInt(value));
            } else {
                Assert.fail();
            }
        }
    }

    @Test
    public void testQuerySinglePref() {
        getSharedPreferences()
            .edit()
            .putString("string", "foobar")
            .putInt("int", 1337)
            .apply();

        ContentResolver resolver = getLocalContext().getContentResolver();
        Cursor q = resolver.query(getQueryUri("string"), null, null, null, null);
        Assert.assertEquals(1, q.getCount());

        int key = q.getColumnIndex(RemoteContract.COLUMN_KEY);
        int type = q.getColumnIndex(RemoteContract.COLUMN_TYPE);
        int value = q.getColumnIndex(RemoteContract.COLUMN_VALUE);

        q.moveToFirst();
        Assert.assertEquals("string", q.getString(key));
        Assert.assertEquals(RemoteContract.TYPE_STRING, q.getInt(type));
        Assert.assertEquals("foobar", q.getString(value));
    }

    @Test
    public void testQueryFailPermissionCheck() {
        getSharedPreferences()
            .edit()
            .putString(Constants.UNREADABLE_PREF_KEY, "foobar")
            .apply();
        ContentResolver resolver = getLocalContext().getContentResolver();
        try {
            resolver.query(getQueryUri(Constants.UNREADABLE_PREF_KEY), null, null, null, null);
            Assert.fail();
        } catch (SecurityException e) {
            // Expected
        }
    }

    @Test
    public void testInsertPref() {
        ContentValues values = new ContentValues();
        values.put(RemoteContract.COLUMN_KEY, "string");
        values.put(RemoteContract.COLUMN_TYPE, RemoteContract.TYPE_STRING);
        values.put(RemoteContract.COLUMN_VALUE, "foobar");

        ContentResolver resolver = getLocalContext().getContentResolver();
        Uri uri = resolver.insert(getQueryUri(null), values);
        Assert.assertEquals(getQueryUri("string"), uri);

        SharedPreferences prefs = getSharedPreferences();
        Assert.assertEquals("foobar", prefs.getString("string", null));
    }

    @Test
    public void testInsertOverridePref() {
        SharedPreferences prefs = getSharedPreferences();
        prefs
            .edit()
            .putString("string", "nyaa")
            .putInt("int", 1337)
            .apply();

        ContentValues values = new ContentValues();
        values.put(RemoteContract.COLUMN_KEY, "string");
        values.put(RemoteContract.COLUMN_TYPE, RemoteContract.TYPE_STRING);
        values.put(RemoteContract.COLUMN_VALUE, "foobar");

        ContentResolver resolver = getLocalContext().getContentResolver();
        Uri uri = resolver.insert(getQueryUri(null), values);
        Assert.assertEquals(getQueryUri("string"), uri);

        Assert.assertEquals("foobar", prefs.getString("string", null));
        Assert.assertEquals(1337, prefs.getInt("int", 0));
    }

    @Test
    public void testInsertPrefKeyInUri() {
        ContentValues values = new ContentValues();
        values.put(RemoteContract.COLUMN_TYPE, RemoteContract.TYPE_STRING);
        values.put(RemoteContract.COLUMN_VALUE, "foobar");

        ContentResolver resolver = getLocalContext().getContentResolver();
        Uri uri = resolver.insert(getQueryUri("string"), values);
        Assert.assertEquals(getQueryUri("string"), uri);

        SharedPreferences prefs = getSharedPreferences();
        Assert.assertEquals("foobar", prefs.getString("string", null));
    }

    @Test
    public void testInsertPrefKeyInUriAndValues() {
        ContentValues values = new ContentValues();
        values.put(RemoteContract.COLUMN_KEY, "string");
        values.put(RemoteContract.COLUMN_TYPE, RemoteContract.TYPE_STRING);
        values.put(RemoteContract.COLUMN_VALUE, "foobar");

        ContentResolver resolver = getLocalContext().getContentResolver();
        Uri uri = resolver.insert(getQueryUri("string"), values);
        Assert.assertEquals(getQueryUri("string"), uri);

        SharedPreferences prefs = getSharedPreferences();
        Assert.assertEquals("foobar", prefs.getString("string", null));
    }

    @Test
    public void testInsertPrefFailKeyInUriAndValuesMismatch() {
        ContentValues values = new ContentValues();
        values.put(RemoteContract.COLUMN_KEY, "string");
        values.put(RemoteContract.COLUMN_TYPE, RemoteContract.TYPE_STRING);
        values.put(RemoteContract.COLUMN_VALUE, "foobar");

        ContentResolver resolver = getLocalContext().getContentResolver();
        try {
            resolver.insert(getQueryUri("string2"), values);
            Assert.fail();
        } catch (IllegalArgumentException e) {
            // Expected
        }

        SharedPreferences prefs = getSharedPreferences();
        Assert.assertEquals("default", prefs.getString("string", "default"));
    }

    @Test
    public void testInsertMultiplePrefs() {
        ContentValues[] values = new ContentValues[2];
        values[0] = new ContentValues();
        values[0].put(RemoteContract.COLUMN_KEY, "string");
        values[0].put(RemoteContract.COLUMN_TYPE, RemoteContract.TYPE_STRING);
        values[0].put(RemoteContract.COLUMN_VALUE, "foobar");

        values[1] = new ContentValues();
        values[1].put(RemoteContract.COLUMN_KEY, "int");
        values[1].put(RemoteContract.COLUMN_TYPE, RemoteContract.TYPE_INT);
        values[1].put(RemoteContract.COLUMN_VALUE, 1337);

        ContentResolver resolver = getLocalContext().getContentResolver();
        int ret = resolver.bulkInsert(getQueryUri(null), values);
        Assert.assertEquals(2, ret);

        SharedPreferences prefs = getSharedPreferences();
        Assert.assertEquals("foobar", prefs.getString("string", null));
        Assert.assertEquals(1337, prefs.getInt("int", 0));
    }

    @Test
    public void testInsertFailPermissionCheck() {
        ContentValues[] values = new ContentValues[2];
        values[0] = new ContentValues();
        values[0].put(RemoteContract.COLUMN_KEY, "string");
        values[0].put(RemoteContract.COLUMN_TYPE, RemoteContract.TYPE_STRING);
        values[0].put(RemoteContract.COLUMN_VALUE, "foobar");

        values[1] = new ContentValues();
        values[1].put(RemoteContract.COLUMN_KEY, Constants.UNWRITABLE_PREF_KEY);
        values[1].put(RemoteContract.COLUMN_TYPE, RemoteContract.TYPE_INT);
        values[1].put(RemoteContract.COLUMN_VALUE, 1337);

        ContentResolver resolver = getLocalContext().getContentResolver();
        try {
            resolver.bulkInsert(getQueryUri(null), values);
            Assert.fail();
        } catch (SecurityException e) {
            // Expected
        }

        SharedPreferences prefs = getSharedPreferences();
        Assert.assertEquals("default", prefs.getString("string", "default"));
        Assert.assertEquals(0, prefs.getInt(Constants.UNWRITABLE_PREF_KEY, 0));
    }

    @Test
    public void testInsertMultipleFailUriContainingKey() {
        ContentValues[] values = new ContentValues[1];
        values[0] = new ContentValues();
        values[0].put(RemoteContract.COLUMN_KEY, "string");
        values[0].put(RemoteContract.COLUMN_TYPE, RemoteContract.TYPE_STRING);
        values[0].put(RemoteContract.COLUMN_VALUE, "foobar");

        ContentResolver resolver = getLocalContext().getContentResolver();
        try {
            resolver.bulkInsert(getQueryUri("key"), values);
            Assert.fail();
        } catch (IllegalArgumentException e) {
            // Expected
        }

        SharedPreferences prefs = getSharedPreferences();
        Assert.assertEquals("default", prefs.getString("string", "default"));
    }

    @Test
    public void testDeletePref() {
        SharedPreferences prefs = getSharedPreferences();
        prefs
            .edit()
            .putString("string", "nyaa")
            .apply();

        ContentResolver resolver = getLocalContext().getContentResolver();
        resolver.delete(getQueryUri("string"), null, null);

        Assert.assertEquals("default", prefs.getString("string", "default"));
    }

    @Test
    public void testDeleteUnwritablePref() {
        SharedPreferences prefs = getSharedPreferences();
        prefs
            .edit()
            .putString(Constants.UNWRITABLE_PREF_KEY, "nyaa")
            .apply();

        ContentResolver resolver = getLocalContext().getContentResolver();
        try {
            resolver.delete(getQueryUri(Constants.UNWRITABLE_PREF_KEY), null, null);
            Assert.fail();
        } catch (SecurityException e) {
            // Expected
        }

        Assert.assertEquals("nyaa", prefs.getString(Constants.UNWRITABLE_PREF_KEY, "default"));
    }

    @Test
    public void testReadBoolean() {
        getSharedPreferences()
            .edit()
            .putBoolean("true", true)
            .putBoolean("false", false)
            .apply();

        ContentResolver resolver = getLocalContext().getContentResolver();
        Cursor q = resolver.query(getQueryUri(null), null, null, null, null);
        Assert.assertEquals(2, q.getCount());

        int key = q.getColumnIndex(RemoteContract.COLUMN_KEY);
        int type = q.getColumnIndex(RemoteContract.COLUMN_TYPE);
        int value = q.getColumnIndex(RemoteContract.COLUMN_VALUE);

        while (q.moveToNext()) {
            if (q.getString(key).equals("true")) {
                Assert.assertEquals(RemoteContract.TYPE_BOOLEAN, q.getInt(type));
                Assert.assertEquals(1, q.getInt(value));
            } else if (q.getString(key).equals("false")) {
                Assert.assertEquals(RemoteContract.TYPE_BOOLEAN, q.getInt(type));
                Assert.assertEquals(0, q.getInt(value));
            } else {
                Assert.fail();
            }
        }
    }

    @Test
    public void testReadStringSet() {
        HashSet<String> set = new HashSet<>();
        set.add("foo");
        set.add("bar");

        getSharedPreferences()
            .edit()
            .putStringSet("pref", set)
            .apply();

        ContentResolver resolver = getLocalContext().getContentResolver();
        Cursor q = resolver.query(getQueryUri("pref"), null, null, null, null);
        Assert.assertEquals(1, q.getCount());

        int key = q.getColumnIndex(RemoteContract.COLUMN_KEY);
        int type = q.getColumnIndex(RemoteContract.COLUMN_TYPE);
        int value = q.getColumnIndex(RemoteContract.COLUMN_VALUE);

        while (q.moveToNext()) {
            if (q.getString(key).equals("pref")) {
                Assert.assertEquals(RemoteContract.TYPE_STRING_SET, q.getInt(type));

                // Horrible implementation detail that I now regret but can't change without
                // breaking backwards compatibility: Set<String> when serialized to a String
                // will always end with an extra delimiter. If we just split on the delimiter
                // we get an extraneous empty element at the end, so trim it off here.
                String serialized = q.getString(value);
                serialized = serialized.substring(0, serialized.length() - 1);
                Assert.assertEquals(set, new HashSet<>(Arrays.asList(serialized.split(";"))));
            } else {
                Assert.fail();
            }
        }
    }

    @Test
    public void testInsertStringSet() {
        ContentValues values = new ContentValues();
        values.put(RemoteContract.COLUMN_KEY, "pref");
        values.put(RemoteContract.COLUMN_TYPE, RemoteContract.TYPE_STRING_SET);
        values.put(RemoteContract.COLUMN_VALUE, "foo;bar\\;;baz;;");

        ContentResolver resolver = getLocalContext().getContentResolver();
        Uri uri = resolver.insert(getQueryUri(null), values);
        Assert.assertEquals(getQueryUri("pref"), uri);

        HashSet<String> set = new HashSet<>();
        set.add("foo");
        set.add("bar;");
        set.add("baz");
        set.add("");
        Assert.assertEquals(set, getSharedPreferences().getStringSet("pref", null));
    }
}