/*
 * Copyright (C) 2024 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 android.provider;

import static android.provider.VerificationLogsHelper.createIsNotNullLog;
import static android.provider.VerificationLogsHelper.createIsNotValidLog;
import static android.provider.VerificationLogsHelper.createIsNullLog;
import static android.provider.VerificationLogsHelper.logVerifications;
import static android.provider.VerificationLogsHelper.verifyCursorNotNullAndMediaCollectionIdPresent;
import static android.provider.VerificationLogsHelper.verifyMediaCollectionId;
import static android.provider.VerificationLogsHelper.verifyProjectionForCursor;
import static android.provider.VerificationLogsHelper.verifyTotalTimeForExecution;

import android.annotation.StringDef;
import android.content.Intent;
import android.content.res.AssetFileDescriptor;
import android.database.Cursor;
import android.graphics.BitmapFactory;
import android.graphics.Point;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.os.SystemProperties;
import android.util.Log;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

/**
 * Provides helper methods that help verify that the received results from cloud provider
 * implementations are staying true to contract by returning non null outputs and setting required
 * extras/states in the result.
 *
 * Note: logs for local provider and not printed.
 */
final class CmpApiVerifier {
    private static final String LOCAL_PROVIDER_AUTHORITY =
            "com.android.providers.media.photopicker";

    private static boolean isCloudMediaProviderLoggingEnabled() {
        return (SystemProperties.getInt("ro.debuggable", 0) == 1) && Log.isLoggable(
                "CloudMediaProvider", Log.VERBOSE);
    }

    /**
     * Verifies and logs results received by CloudMediaProvider Apis.
     *
     * <p><b>Note:</b> It only logs the errors and does not throw any exceptions.
     */
    static void verifyApiResult(CmpApiResult result, long totalTimeTakenForExecution,
            String authority) {
        // Do not perform any operation if the authority is of the local provider or when the
        // logging is not enabled.
        if (!LOCAL_PROVIDER_AUTHORITY.equals(authority)
                && isCloudMediaProviderLoggingEnabled()) {
            try {
                ArrayList<String> verificationResult = new ArrayList<>();
                ArrayList<String> errors = new ArrayList<>();
                verifyTotalTimeForExecution(totalTimeTakenForExecution,
                        CMP_API_TO_THRESHOLD_MAP.get(result.getApi()), errors);

                switch (result.getApi()) {
                    case CloudMediaProviderApis.OnGetMediaCollectionInfo: {
                        verifyOnGetMediaCollectionInfo(result.getBundle(), verificationResult,
                                errors);
                        break;
                    }
                    case CloudMediaProviderApis.OnQueryMedia: {
                        verifyOnQueryMedia(result.getCursor(), verificationResult, errors);
                        break;
                    }
                    case CloudMediaProviderApis.OnQueryDeletedMedia: {
                        verifyOnQueryDeletedMedia(result.getCursor(), verificationResult, errors);
                        break;
                    }
                    case CloudMediaProviderApis.OnQueryAlbums: {
                        verifyOnQueryAlbums(result.getCursor(), verificationResult, errors);
                        break;
                    }
                    case CloudMediaProviderApis.OnOpenPreview: {
                        verifyOnOpenPreview(result.getAssetFileDescriptor(), result.getDimensions(),
                                verificationResult, errors);
                        break;
                    }
                    case CloudMediaProviderApis.OnOpenMedia: {
                        verifyOnOpenMedia(result.getParcelFileDescriptor(), verificationResult,
                                errors);
                        break;
                    }
                    default:
                        throw new UnsupportedOperationException(
                                "The verification for requested API is not supported.");
                }
                logVerifications(authority, result.getApi(), totalTimeTakenForExecution,
                        verificationResult, errors);
            } catch (Exception e) {
                VerificationLogsHelper.logException(e.getMessage());
            }
        }
    }

    /**
     * Verifies OnGetMediaCollectionInfo API by performing and logging the following checks:
     *
     * <ul>
     * <li>Received Bundle is not null.</li>
     * <li>Bundle contains media collection ID:
     * {@link CloudMediaProviderContract.MediaCollectionInfo#MEDIA_COLLECTION_ID}</li>
     * <li>Bundle contains last sync generation:
     * {@link CloudMediaProviderContract.MediaCollectionInfo#LAST_MEDIA_SYNC_GENERATION}</li>
     * <li>Bundle contains account name:
     * {@link CloudMediaProviderContract.MediaCollectionInfo#ACCOUNT_NAME}</li>
     * <li>Bundle contains account configuration intent:
     * {@link CloudMediaProviderContract.MediaCollectionInfo#ACCOUNT_CONFIGURATION_INTENT}</li>
     * </ul>
     */
    static void verifyOnGetMediaCollectionInfo(
            Bundle outputBundle, List<String> verificationResult, List<String> errors
    ) {
        if (outputBundle != null) {
            verificationResult.add(createIsNotNullLog("Received bundle"));

            String mediaCollectionId = outputBundle.getString(
                    CloudMediaProviderContract.MediaCollectionInfo.MEDIA_COLLECTION_ID
            );
            // verifies media collection id.
            verifyMediaCollectionId(
                    mediaCollectionId,
                    verificationResult,
                    errors
            );

            long syncGeneration = outputBundle.getLong(
                    CloudMediaProviderContract.MediaCollectionInfo.LAST_MEDIA_SYNC_GENERATION,
                    -1L
            );

            // verified last sync generation.
            if (syncGeneration != -1L) {
                if (syncGeneration >= 0) {
                    verificationResult.add(
                            CloudMediaProviderContract.MediaCollectionInfo
                                    .LAST_MEDIA_SYNC_GENERATION + " : " + syncGeneration
                    );
                } else {
                    errors.add(
                            CloudMediaProviderContract.MediaCollectionInfo
                                    .LAST_MEDIA_SYNC_GENERATION + " is < 0"
                    );
                }
            } else {
                errors.add(
                        createIsNotValidLog(
                                CloudMediaProviderContract.MediaCollectionInfo
                                        .LAST_MEDIA_SYNC_GENERATION
                        )
                );
            }

            String accountName = outputBundle.getString(
                    CloudMediaProviderContract.MediaCollectionInfo.ACCOUNT_NAME
            );

            // verifies account name.
            if (accountName != null) {
                if (!accountName.isEmpty()) {
                    // In future if the cloud media provider is extended to have multiple
                    // accounts then logging account name itself might be a useful
                    // information to log but for now only logging its presence.
                    verificationResult.add(
                            CloudMediaProviderContract.MediaCollectionInfo.ACCOUNT_NAME
                                    + " is present "
                    );
                } else {
                    errors.add(
                            CloudMediaProviderContract.MediaCollectionInfo.ACCOUNT_NAME
                                    + " is empty"
                    );
                }
            } else {
                errors.add(createIsNullLog(
                                CloudMediaProviderContract.MediaCollectionInfo.ACCOUNT_NAME
                        )
                );
            }

            Intent intent = outputBundle.getParcelable(
                    CloudMediaProviderContract.MediaCollectionInfo.ACCOUNT_CONFIGURATION_INTENT
            );
            // verified the presence of account configuration intent.
            if (intent != null) {
                verificationResult.add(
                        CloudMediaProviderContract.MediaCollectionInfo
                                .ACCOUNT_CONFIGURATION_INTENT
                                + " is present."
                );
            } else {
                errors.add(createIsNullLog(
                                CloudMediaProviderContract.MediaCollectionInfo
                                        .ACCOUNT_CONFIGURATION_INTENT
                        )
                );
            }

        } else {
            errors.add(createIsNullLog("Received output bundle"));
        }
    }

    /**
     * Verifies OnQueryMedia API by performing and logging the following checks:
     *
     * <ul>
     * <li>Received Cursor is not null.</li>
     * <li>Cursor contains non empty media collection ID:
     * {@link CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID}</li>
     * <li>Projection for cursor is as expected:
     * {@link CloudMediaProviderContract.MediaColumns#ALL_PROJECTION}</li>
     * <li>Logs count of rows in the cursor, if cursor is non null.</li>
     * </ul>
     */
    static void verifyOnQueryMedia(
            Cursor c, List<String> verificationResult, List<String> errors
    ) {
        if (c != null) {
            verifyCursorNotNullAndMediaCollectionIdPresent(
                    c,
                    verificationResult,
                    errors
            );
            // verify that all columns are present per CloudMediaProviderContract.AlbumColumns
            verifyProjectionForCursor(
                    c,
                    Arrays.asList(CloudMediaProviderContract.MediaColumns.ALL_PROJECTION),
                    errors
            );
        } else {
            errors.add(createIsNullLog("Received cursor"));
        }
    }

    /**
     * Verifies OnQueryDeletedMedia API by performing and logging the following checks:
     *
     * <ul>
     * <li>Received Cursor is not null.</li>
     * <li>Cursor contains non empty media collection ID:
     * {@link CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID}</li>
     * <li>Logs count of rows in the cursor, if cursor is non null.</li>
     * </ul>
     */
    static void verifyOnQueryDeletedMedia(
            Cursor c, List<String> verificationResult, List<String> errors
    ) {
        verifyCursorNotNullAndMediaCollectionIdPresent(c, verificationResult, errors);
    }

    /**
     * Verifies OnQueryAlbums API by performing and logging the following checks:
     *
     * <ul>
     * <li>Received Cursor is not null.</li>
     * <li>Cursor contains non empty media collection ID:
     * {@link CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID}</li>
     * <li>Projection for cursor is as expected:
     * {@link CloudMediaProviderContract.AlbumColumns#ALL_PROJECTION}</li>
     * <li>Logs count of rows in the cursor and the album names, if cursor is non null.</li>
     * </ul>
     */
    static void verifyOnQueryAlbums(
            Cursor c, List<String> verificationResult, List<String> errors
    ) {
        if (c != null) {
            verifyCursorNotNullAndMediaCollectionIdPresent(c, verificationResult, errors);

            // verify that all columns are present per CloudMediaProviderContract.AlbumColumns
            verifyProjectionForCursor(
                    c,
                    Arrays.asList(CloudMediaProviderContract.AlbumColumns.ALL_PROJECTION),
                    errors
            );
            if (c.getCount() > 0) {
                // Only log album data if projection and other checks have returned positive
                // results.
                StringBuilder strBuilder = new StringBuilder("Albums present and their count: ");
                int columnIndexForId = c.getColumnIndex(CloudMediaProviderContract.AlbumColumns.ID);
                int columnIndexForItemCount = c.getColumnIndex(
                        CloudMediaProviderContract.AlbumColumns.MEDIA_COUNT);
                c.moveToPosition(-1);
                while (c.moveToNext()) {
                    strBuilder.append("\n\t\t\t" + c.getString(columnIndexForId) + ", " + c.getLong(
                            columnIndexForItemCount));
                }
                c.moveToPosition(-1);
                verificationResult.add(strBuilder.toString());
            }
        }
    }


    /**
     * Verifies OnOpenPreview API by performing and logging the following checks:
     *
     * <ul>
     * <li>Received AssetFileDescriptor is not null.</li>
     * <li>Logs size of the thumbnail.</li>
     * </ul>
     */
    static void verifyOnOpenPreview(
            AssetFileDescriptor assetFileDescriptor,
            Point expectedSize, List<String> verificationResult, List<String> errors
    ) {
        if (assetFileDescriptor == null) {
            errors.add(createIsNullLog("Received AssetFileDescriptor"));
        } else {
            verificationResult.add(createIsNotNullLog("Received AssetFileDescriptor"));
            BitmapFactory.Options options = new BitmapFactory.Options();
            options.inJustDecodeBounds = true; // Only decode the bounds
            BitmapFactory.decodeFileDescriptor(assetFileDescriptor.getFileDescriptor(), null,
                    options);

            int width = options.outWidth;
            int height = options.outHeight;

            verificationResult.add("Dimensions of file received: "
                    + "Width: " + width + ", Height: " + height + ", expected: " + expectedSize.x
                    + ", " + expectedSize.y);
        }
    }

    /**
     * Verifies OnOpenMedia API by performing and logging the following checks:
     *
     * <ul>
     * <li>Received ParcelFileDescriptor is not null.</li>
     * </ul>
     */
    static void verifyOnOpenMedia(
            ParcelFileDescriptor fd,
            List<String> verificationResult, List<String> errors
    ) {
        if (fd == null) {
            errors.add(createIsNullLog("Received FileDescriptor"));
        } else {
            verificationResult.add(createIsNotNullLog("Received FileDescriptor"));
        }
    }

    @StringDef({
            CloudMediaProviderApis.OnGetMediaCollectionInfo,
            CloudMediaProviderApis.OnQueryMedia,
            CloudMediaProviderApis.OnQueryDeletedMedia,
            CloudMediaProviderApis.OnQueryAlbums,
            CloudMediaProviderApis.OnOpenPreview,
            CloudMediaProviderApis.OnOpenMedia
    })
    @Retention(RetentionPolicy.SOURCE)
    @interface CloudMediaProviderApis {
        String OnGetMediaCollectionInfo = "onGetMediaCollectionInfo";
        String OnQueryMedia = "onQueryMedia";
        String OnQueryDeletedMedia = "onQueryDeletedMedia";
        String OnQueryAlbums = "onQueryAlbums";
        String OnOpenPreview = "onOpenPreview";
        String OnOpenMedia = "onOpenMedia";
    }

    private static final Map<String, Long> CMP_API_TO_THRESHOLD_MAP = Map.of(
            CloudMediaProviderApis.OnGetMediaCollectionInfo, 200L,
            CloudMediaProviderApis.OnQueryMedia, 500L,
            CloudMediaProviderApis.OnQueryDeletedMedia, 500L,
            CloudMediaProviderApis.OnQueryAlbums, 500L,
            CloudMediaProviderApis.OnOpenPreview, 1000L,
            CloudMediaProviderApis.OnOpenMedia, 1000L
    );
}
