/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.gecko.tests;

import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.util.Log;

import org.mozilla.gecko.GeckoProfile;
import org.mozilla.gecko.GeckoProfileDirectories;
import org.mozilla.gecko.db.*;
import org.mozilla.gecko.reader.SavedReaderViewHelper;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;

import static org.mozilla.gecko.db.BrowserContract.*;
import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertEquals;
import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertNotNull;
import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertTrue;

// TODO: Move to junit 3 tests, once those run in automation. There is no ui testing to do so it's a better fit.

/**
 * This test runs the 30 to 31 database upgrade, which moves reading-list INPUT_FILES from a separate
 * reading-list folder into mobile bookmarks.
 *
 * It is based on testBrowserDatabaseHelperUpgrades. We load a v30 db containing two reading list
 * INPUT_FILES, and test that these have successfully been converted into bookmarks.
 */
public class testReadingListToBookmarksMigration extends UITest {
    private ArrayList<File> tempFiles;

    // These names are generated by hashing the URLs, see INPUT_URLS below, and
    // BrowserDatabaseHelper.getReaderCacheFileNameForURL()
    private static final ArrayList<String> INPUT_FILES = new ArrayList<String>() {{
        add("DWUP3U4ERC6TKJVSYXKJLHHEFY.json");
        add("KWNV7PXD3JFOJBQJVFXI3CQKNE.json");
    }};

    // same ordering as in INPUT_FILES, although we don't rely on ordering in this test
    private static final ArrayList<String> INPUT_URLS = new ArrayList<String>() {{
        add("http://www.bbc.com/news/election-us-2016-35962179");
        add("http://www.bbc.com/news/world-europe-35962670");
    }};

    @Override
    public void setUp() throws Exception {
        super.setUp();
        // TODO: We already install & remove the profile directory each run so it'd be safer for clean-up to store
        // this there. That being said, temporary files are still stored in the application directory so these temporary
        // files will get cleaned up when the application is uninstalled or when data is cleared.
        tempFiles = new ArrayList<>();
    }

    @Override
    public void tearDown() throws Exception {
        super.tearDown();
        for (final File file : tempFiles) {
            file.delete();
        }
    }

    private void walkRLPreMigration(SQLiteDatabase db) {
        Set<String> urls = new HashSet<>(INPUT_URLS);

        final Cursor c = db.rawQuery("SELECT * FROM " + ReadingListItems.TABLE_NAME, null);

        fAssertNotNull("Cursor cannot be null", c);
        try {
            final boolean movedToFirst = c.moveToFirst();
            fAssertTrue("Cursor must have data", movedToFirst);

            int urlIndex = c.getColumnIndexOrThrow(ReadingListItems.URL);
            do {
                final String url = c.getString(urlIndex);

                boolean removed = urls.remove(url);
                fAssertTrue("Unexpected reading-list URL in database", removed);
            } while (c.moveToNext());
        } finally {
            c.close();
        }

        fAssertTrue("All urls should have been removed from set", urls.isEmpty());
    }

    private void walkRLPostMigration(SQLiteDatabase db) {
        Set<String> urls = new HashSet<>(INPUT_URLS);

        final Cursor c = db.rawQuery("SELECT * FROM " +
                                     Bookmarks.VIEW_WITH_ANNOTATIONS
                                     + " WHERE " + BrowserContract.UrlAnnotations.KEY + " = ?",
                                     new String[] {
                                             BrowserContract.UrlAnnotations.Key.READER_VIEW.getDbValue()
                                     });

        fAssertNotNull("Cursor cannot be null", c);
        try {
            final boolean movedToFirst = c.moveToFirst();
            fAssertTrue("Cursor must have data", movedToFirst);

            int urlIndex = c.getColumnIndexOrThrow(Bookmarks.URL);
            do {
                final String url = c.getString(urlIndex);

                boolean removed = urls.remove(url);
                fAssertTrue("Unexpected reading-list URL in database", removed);
            } while (c.moveToNext());
        } finally {
            c.close();
        }

        fAssertTrue("All urls should have been removed from set", urls.isEmpty());
    }

    /**
     * @throws IOException if the database or input files cannot be copied.
     */
    public void testReadingListToBookmarksMigration() throws IOException {
        final String tempDbPath = copyAssets();
        final SQLiteDatabase db = SQLiteDatabase.openDatabase(tempDbPath, null, 0);

        try {
            // This initialises the helper, but does not open the DB.
            BrowserDatabaseHelper dbHelper = new BrowserDatabaseHelper(getActivity(), tempDbPath);

            walkRLPreMigration(db);

            // Run just one upgrade - we don't know what future upgrades might do, whereas with one
            // upgrade we can guarantee a given DB state.
            dbHelper.onUpgrade(db, 30, 31);

            // SavedReaderViewHelper writes annotations directly to the GeckoProfile DB (as opposed
            // to our local DB copy). We aren't able to read this here (and the data isn't written
            // to our own db), hence we can't test the DB content yet.
//            walkRLPostMigration(db);

            SavedReaderViewHelper rvh = SavedReaderViewHelper.getSavedReaderViewHelper(getActivity());

            fAssertEquals("All input files should have been migrated", INPUT_FILES.size(), rvh.size());
            for (String url : INPUT_URLS) {
                boolean isCached = rvh.isURLCached(url);
                fAssertTrue("URL no longer in cache after migration", isCached);
            }
        } finally {
            db.close();
        }
    }

    private void copyAssetToFile(String inputPath, File destination) throws IOException {
        final InputStream inputStream = openFileFromAssets(inputPath);
        final OutputStream os = new BufferedOutputStream(new FileOutputStream(destination));
        try {
            final byte[] buffer = new byte[1024];
            int len;
            while ((len = inputStream.read(buffer)) > 0) {
                os.write(buffer, 0, len);
            }
            os.flush();
        } finally {
            os.close();
            inputStream.close();
        }
    }

    /**
     * Copies assets into the desired locations. We need to copy our DB into a temporary file,
     * and readercache items into the profile directory.
     *
     * @throws IOException if reading the existing files or writing the temporary files fails
     */
    private String copyAssets() throws IOException {
        final File profileDir = GeckoProfile.get(getActivity()).getDir();
        final File cacheDir = new File(profileDir, "readercache");
        cacheDir.mkdir();

        for (String name : INPUT_FILES) {
            final String path = "readercache" + File.separator + name;
            final File destination = new File(cacheDir, name);
            tempFiles.add(destination);

            Log.d(LOGTAG, "Moving readerview cache file to " + destination.getPath());
            copyAssetToFile(path, destination);
        }

        final File dbDestination = File.createTempFile("temporaryDB_", "db");
        tempFiles.add(dbDestination);

        Log.d(LOGTAG, "Moving DB from assets to " + dbDestination.getPath());
        copyAssetToFile("browser.db", dbDestination);

        return dbDestination.getPath();
    }

    private InputStream openFileFromAssets(final String name) throws IOException {
        final String assetPath = String.format("reading_list_bookmarks_migration" + File.separator + name);
        Log.d(LOGTAG, "Opening file from assets: " + assetPath);
        try {
            return new BufferedInputStream(getInstrumentation().getContext().getAssets().open(assetPath));
        } catch (final FileNotFoundException e) {
            throw new IllegalStateException("Declared input files must be provided", e);
        }
    }
}
