/*
 * Copyright (C) 2010 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.cts.verifier;

import static com.android.cts.verifier.TestListActivity.sCurrentDisplayMode;
import static com.android.cts.verifier.TestListAdapter.setTestNameSuffix;

import android.app.backup.BackupManager;
import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.os.ParcelFileDescriptor;

import androidx.annotation.NonNull;

import com.android.compatibility.common.util.ReportLog;
import com.android.compatibility.common.util.TestScreenshotsMetadata;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.Arrays;
import java.util.Comparator;

/**
 * {@link ContentProvider} that provides read and write access to the test results.
 */
public class TestResultsProvider extends ContentProvider {

    static final String _ID = "_id";
    /** String name of the test like "com.android.cts.verifier.foo.FooTestActivity" */
    static final String COLUMN_TEST_NAME = "testname";
    /** Integer test result corresponding to constants in {@link TestResult}. */
    static final String COLUMN_TEST_RESULT = "testresult";
    /** Boolean indicating whether the test info has been seen. */
    static final String COLUMN_TEST_INFO_SEEN = "testinfoseen";
    /** String containing the test's details. */
    static final String COLUMN_TEST_DETAILS = "testdetails";
    /** ReportLog containing the test result metrics. */
    static final String COLUMN_TEST_METRICS = "testmetrics";
    /** TestResultHistory containing the test run histories. */
    static final String COLUMN_TEST_RESULT_HISTORY = "testresulthistory";
    /** TestScreenshotsMetadata containing the test screenshot metadata. */
    static final String COLUMN_TEST_SCREENSHOTS_METADATA = "testscreenshotsmetadata";

    /**
     * Report saved location
     */
    private static final String REPORTS_PATH = "reports";
    private static final String RESULTS_PATH = "results";
    private static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
    private static final int RESULTS_ALL = 1;
    private static final int RESULTS_ID = 2;
    private static final int RESULTS_TEST_NAME = 3;
    private static final int REPORT = 4;
    private static final int REPORT_ROW = 5;
    private static final int REPORT_FILE_NAME = 6;
    private static final int REPORT_LATEST = 7;
    private static final String TABLE_NAME = "results";
    private SQLiteOpenHelper mOpenHelper;
    private BackupManager mBackupManager;

    /**
     * Get the URI from the result content.
     *
     * @param context
     * @return Uri
     */
    public static Uri getResultContentUri(Context context) {
        final String packageName = context.getPackageName();
        final Uri contentUri = Uri.parse("content://" + packageName + ".testresultsprovider");
        return Uri.withAppendedPath(contentUri, RESULTS_PATH);
    }

    /**
     * Get the URI from the test name.
     *
     * @param context
     * @param testName
     * @return Uri
     */
    public static Uri getTestNameUri(Context context) {
        String name = context.getClass().getName();
        name = setTestNameSuffix(sCurrentDisplayMode, name);
        return getTestNameUri(context, name);
    }

    /**
     * Gets the URI from the context and test name.
     * @param context current context
     * @param testName name of the test which needs to get the URI
     * @return the URI for the test result
     */
    public static Uri getTestNameUri(Context context, String testName) {
        return Uri.withAppendedPath(getResultContentUri(context), testName);
    }

    static void setTestResult(Context context, String testName, int testResult,
            String testDetails, ReportLog reportLog, TestResultHistoryCollection historyCollection,
            TestScreenshotsMetadata screenshotsMetadata) {
        ContentValues values = new ContentValues(2);
        values.put(TestResultsProvider.COLUMN_TEST_RESULT, testResult);
        values.put(TestResultsProvider.COLUMN_TEST_NAME, testName);
        values.put(TestResultsProvider.COLUMN_TEST_DETAILS, testDetails);
        values.put(TestResultsProvider.COLUMN_TEST_METRICS, serialize(reportLog));
        values.put(TestResultsProvider.COLUMN_TEST_RESULT_HISTORY, serialize(historyCollection));
        values.put(
                TestResultsProvider.COLUMN_TEST_SCREENSHOTS_METADATA,
                serialize(screenshotsMetadata));

        final Uri uri = getResultContentUri(context);
        ContentResolver resolver = context.getContentResolver();
        int numUpdated = resolver.update(uri, values,
                TestResultsProvider.COLUMN_TEST_NAME + " = ?",
                new String[]{testName});

        if (numUpdated == 0) {
            resolver.insert(uri, values);
        }
    }

    /**
     * Called by screenshot consumers to provide extra metadata that allows to understand
     * screenshots better.
     *
     * @param context application context
     * @param testName corresponding test name
     * @param screenshotsMetadata A {@link TestScreenshotsMetadata} set that contains metadata
     */
    public static void updateColumnTestScreenshotsMetadata(
            Context context, String testName, TestScreenshotsMetadata screenshotsMetadata) {
        ContentValues values = new ContentValues(2);
        values.put(TestResultsProvider.COLUMN_TEST_NAME, testName);
        values.put(
                TestResultsProvider.COLUMN_TEST_SCREENSHOTS_METADATA,
                serialize(screenshotsMetadata));
        final Uri uri = getResultContentUri(context);
        ContentResolver resolver = context.getContentResolver();
        int numUpdated = resolver.update(
                uri, values, TestResultsProvider.COLUMN_TEST_NAME + " = ?",
                new String[]{ testName });
        if (numUpdated == 0) {
            resolver.insert(uri, values);
        }

    }

    private static byte[] serialize(Object o) {
        ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutput = null;
        try {
            objectOutput = new ObjectOutputStream(byteStream);
            objectOutput.writeObject(o);
            return byteStream.toByteArray();
        } catch (IOException e) {
            return null;
        } finally {
            try {
                if (objectOutput != null) {
                    objectOutput.close();
                }
                byteStream.close();
            } catch (IOException e) {
                // Ignore close exception.
            }
        }
    }

    @Override
    public boolean onCreate() {
        final String authority = getContext().getPackageName() + ".testresultsprovider";

        URI_MATCHER.addURI(authority, RESULTS_PATH, RESULTS_ALL);
        URI_MATCHER.addURI(authority, RESULTS_PATH + "/#", RESULTS_ID);
        URI_MATCHER.addURI(authority, RESULTS_PATH + "/*", RESULTS_TEST_NAME);
        URI_MATCHER.addURI(authority, REPORTS_PATH, REPORT);
        URI_MATCHER.addURI(authority, REPORTS_PATH + "/latest", REPORT_LATEST);
        URI_MATCHER.addURI(authority, REPORTS_PATH + "/#", REPORT_ROW);
        URI_MATCHER.addURI(authority, REPORTS_PATH + "/*", REPORT_FILE_NAME);

        mOpenHelper = new TestResultsOpenHelper(getContext());
        mBackupManager = new BackupManager(getContext());
        return false;
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
                        String sortOrder) {
        SQLiteQueryBuilder query = new SQLiteQueryBuilder();
        query.setTables(TABLE_NAME);

        int match = URI_MATCHER.match(uri);
        switch (match) {
            case RESULTS_ALL:
                break;

            case RESULTS_ID:
                query.appendWhere(_ID);
                query.appendWhere("=");
                query.appendWhere(uri.getPathSegments().get(1));
                break;

            case RESULTS_TEST_NAME:
                query.appendWhere(COLUMN_TEST_NAME);
                query.appendWhere("=");
                query.appendWhere("\"" + uri.getPathSegments().get(1) + "\"");
                break;

            case REPORT:
                final MatrixCursor cursor = new MatrixCursor(new String[]{"filename"});
                for (String filename : getFileList()) {
                    cursor.addRow(new Object[]{filename});
                }
                return cursor;

            case REPORT_FILE_NAME:
            case REPORT_ROW:
            case REPORT_LATEST:
                throw new IllegalArgumentException(
                        "Report query not supported. Use content read.");

            default:
                throw new IllegalArgumentException("Unknown URI: " + uri);
        }

        SQLiteDatabase db = mOpenHelper.getReadableDatabase();
        return query.query(db, projection, selection, selectionArgs, null, null, sortOrder);
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        int match = URI_MATCHER.match(uri);
        switch (match) {
            case REPORT:
                throw new IllegalArgumentException(
                        "Report insert not supported. Use content query.");
            case REPORT_FILE_NAME:
            case REPORT_ROW:
            case REPORT_LATEST:
                throw new IllegalArgumentException(
                        "Report insert not supported. Use content read.");
            default:
                break;
        }
        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
        long id = db.insert(TABLE_NAME, null, values);
        getContext().getContentResolver().notifyChange(uri, null);
        mBackupManager.dataChanged();
        return Uri.withAppendedPath(getResultContentUri(getContext()), "" + id);
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        int match = URI_MATCHER.match(uri);
        switch (match) {
            case RESULTS_ALL:
                break;

            case RESULTS_ID:
                String idSelection = _ID + "=" + uri.getPathSegments().get(1);
                if (selection != null && selection.length() > 0) {
                    selection = idSelection + " AND " + selection;
                } else {
                    selection = idSelection;
                }
                break;

            case RESULTS_TEST_NAME:
                String testNameSelection = COLUMN_TEST_NAME + "=\""
                        + uri.getPathSegments().get(1) + "\"";
                if (selection != null && selection.length() > 0) {
                    selection = testNameSelection + " AND " + selection;
                } else {
                    selection = testNameSelection;
                }
                break;
            case REPORT:
                throw new IllegalArgumentException(
                        "Report update not supported. Use content query.");
            case REPORT_FILE_NAME:
            case REPORT_ROW:
            case REPORT_LATEST:
                throw new IllegalArgumentException(
                        "Report update not supported. Use content read.");
            default:
                throw new IllegalArgumentException("Unknown URI: " + uri);
        }

        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
        int numUpdated = db.update(TABLE_NAME, values, selection, selectionArgs);
        if (numUpdated > 0) {
            getContext().getContentResolver().notifyChange(uri, null);
            mBackupManager.dataChanged();
        }
        return numUpdated;
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        int match = URI_MATCHER.match(uri);
        switch (match) {
            case REPORT:
                throw new IllegalArgumentException(
                        "Report delete not supported. Use content query.");
            case REPORT_FILE_NAME:
            case REPORT_ROW:
            case REPORT_LATEST:
                throw new IllegalArgumentException(
                        "Report delete not supported. Use content read.");
            default:
                break;
        }

        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
        int numDeleted = db.delete(TABLE_NAME, selection, selectionArgs);
        if (numDeleted > 0) {
            getContext().getContentResolver().notifyChange(uri, null);
            mBackupManager.dataChanged();
        }
        return numDeleted;
    }

    @Override
    public String getType(Uri uri) {
        return null;
    }

    @Override
    public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode)
            throws FileNotFoundException {
        String fileName;
        String[] fileList;
        File file;
        int match = URI_MATCHER.match(uri);
        switch (match) {
            case REPORT_ROW:
                int rowId = Integer.parseInt(uri.getPathSegments().get(1));
                file = getFileByIndex(rowId);
                break;

            case REPORT_FILE_NAME:
                fileName = uri.getPathSegments().get(1);
                file = getFileByName(fileName);
                break;

            case REPORT_LATEST:
                file = getLatestFile();
                break;

            case REPORT:
                throw new IllegalArgumentException("Read not supported. Use content query.");

            case RESULTS_ALL:
            case RESULTS_ID:
            case RESULTS_TEST_NAME:
                throw new IllegalArgumentException("Read not supported for URI: " + uri);

            default:
                throw new IllegalArgumentException("Unknown URI: " + uri);
        }
        try {
            FileInputStream fis = new FileInputStream(file);
            return ParcelFileDescriptor.dup(fis.getFD());
        } catch (IOException e) {
            throw new IllegalArgumentException("Cannot open file.");
        }
    }


    private File getFileByIndex(int index) {
        File[] files = getFiles();
        if (files.length == 0) {
            throw new IllegalArgumentException("No report saved at " + index + ".");
        }
        return files[index];
    }

    private File getFileByName(String fileName) {
        File[] files = getFiles();
        if (files.length == 0) {
            throw new IllegalArgumentException("No reports saved.");
        }
        for (File file : files) {
            if (fileName.equals(file.getName())) {
                return file;
            }
        }
        throw new IllegalArgumentException(fileName + " not found.");
    }

    private File getLatestFile() {
        File[] files = getFiles();
        if (files.length == 0) {
            throw new IllegalArgumentException("No reports saved.");
        }
        return files[files.length - 1];
    }

    private String[] getFileList() {
        return Arrays.stream(getFiles()).map(File::getName).toArray(String[]::new);
    }

    private File[] getFiles() {
        File dir = getContext().getDir(ReportExporter.REPORT_DIRECTORY, Context.MODE_PRIVATE);
        File[] files = dir.listFiles();
        Arrays.sort(files, Comparator.comparingLong(File::lastModified));
        return files;
    }

    private static class TestResultsOpenHelper extends SQLiteOpenHelper {

        private static final String DATABASE_NAME = "results.db";

        private static final int DATABASE_VERSION = 6;

        TestResultsOpenHelper(Context context) {
            super(context, DATABASE_NAME, null, DATABASE_VERSION);
        }

        @Override
        public void onCreate(SQLiteDatabase db) {
            db.execSQL("CREATE TABLE " + TABLE_NAME + " ("
                    + _ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
                    + COLUMN_TEST_NAME + " TEXT, "
                    + COLUMN_TEST_RESULT + " INTEGER,"
                    + COLUMN_TEST_INFO_SEEN + " INTEGER DEFAULT 0,"
                    + COLUMN_TEST_DETAILS + " TEXT,"
                    + COLUMN_TEST_METRICS + " BLOB,"
                    + COLUMN_TEST_RESULT_HISTORY + " BLOB,"
                    + COLUMN_TEST_SCREENSHOTS_METADATA + " BLOB);");
        }

        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
            db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
            onCreate(db);
        }
    }
}
