/*
 * 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 com.android.server.pm;

import static com.google.common.truth.Truth.assertThat;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.testng.Assert.assertThrows;

import android.apex.ApexInfo;
import android.apex.ApexSessionInfo;
import android.apex.ApexSessionParams;
import android.apex.IApexService;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.os.FileUtils;
import android.os.RemoteException;
import android.platform.test.annotations.Presubmit;

import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;

import com.android.frameworks.servicestests.R;

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

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;

@SmallTest
@Presubmit
@RunWith(AndroidJUnit4.class)
public class ApexManagerTest {
    private static final String TEST_APEX_PKG = "com.android.apex.test";
    private static final int TEST_SESSION_ID = 99999999;
    private static final int[] TEST_CHILD_SESSION_ID = {8888, 7777};
    private ApexManager mApexManager;
    private Context mContext;

    private IApexService mApexService = mock(IApexService.class);

    @Before
    public void setUp() throws RemoteException {
        mContext = InstrumentationRegistry.getInstrumentation().getContext();
        mApexManager = new ApexManager.ApexManagerImpl(mApexService);
    }

    @Test
    public void testGetPackageInfo_setFlagsMatchActivePackage() throws RemoteException {
        when(mApexService.getAllPackages()).thenReturn(createApexInfo(true, false));
        final PackageInfo activePkgPi = mApexManager.getPackageInfo(TEST_APEX_PKG,
                ApexManager.MATCH_ACTIVE_PACKAGE);

        assertThat(activePkgPi).isNotNull();
        assertThat(activePkgPi.packageName).contains(TEST_APEX_PKG);

        final PackageInfo factoryPkgPi = mApexManager.getPackageInfo(TEST_APEX_PKG,
                ApexManager.MATCH_FACTORY_PACKAGE);

        assertThat(factoryPkgPi).isNull();
    }

    @Test
    public void testGetPackageInfo_setFlagsMatchFactoryPackage() throws RemoteException {
        when(mApexService.getAllPackages()).thenReturn(createApexInfo(false, true));
        PackageInfo factoryPkgPi = mApexManager.getPackageInfo(TEST_APEX_PKG,
                ApexManager.MATCH_FACTORY_PACKAGE);

        assertThat(factoryPkgPi).isNotNull();
        assertThat(factoryPkgPi.packageName).contains(TEST_APEX_PKG);

        final PackageInfo activePkgPi = mApexManager.getPackageInfo(TEST_APEX_PKG,
                ApexManager.MATCH_ACTIVE_PACKAGE);

        assertThat(activePkgPi).isNull();
    }

    @Test
    public void testGetPackageInfo_setFlagsNone() throws RemoteException {
        when(mApexService.getAllPackages()).thenReturn(createApexInfo(false, true));

        assertThat(mApexManager.getPackageInfo(TEST_APEX_PKG, 0)).isNull();
    }

    @Test
    public void testGetActivePackages() throws RemoteException {
        when(mApexService.getAllPackages()).thenReturn(createApexInfo(true, true));

        assertThat(mApexManager.getActivePackages()).isNotEmpty();
    }

    @Test
    public void testGetActivePackages_noneActivePackages() throws RemoteException {
        when(mApexService.getAllPackages()).thenReturn(createApexInfo(false, true));

        assertThat(mApexManager.getActivePackages()).isEmpty();
    }

    @Test
    public void testGetFactoryPackages() throws RemoteException {
        when(mApexService.getAllPackages()).thenReturn(createApexInfo(false, true));

        assertThat(mApexManager.getFactoryPackages()).isNotEmpty();
    }

    @Test
    public void testGetFactoryPackages_noneFactoryPackages() throws RemoteException {
        when(mApexService.getAllPackages()).thenReturn(createApexInfo(true, false));

        assertThat(mApexManager.getFactoryPackages()).isEmpty();
    }

    @Test
    public void testGetInactivePackages() throws RemoteException {
        when(mApexService.getAllPackages()).thenReturn(createApexInfo(false, true));

        assertThat(mApexManager.getInactivePackages()).isNotEmpty();
    }

    @Test
    public void testGetInactivePackages_noneInactivePackages() throws RemoteException {
        when(mApexService.getAllPackages()).thenReturn(createApexInfo(true, false));

        assertThat(mApexManager.getInactivePackages()).isEmpty();
    }

    @Test
    public void testIsApexPackage() throws RemoteException {
        when(mApexService.getAllPackages()).thenReturn(createApexInfo(false, true));

        assertThat(mApexManager.isApexPackage(TEST_APEX_PKG)).isTrue();
    }

    @Test
    public void testIsApexSupported() {
        assertThat(mApexManager.isApexSupported()).isTrue();
    }

    @Test
    public void testGetStagedSessionInfo() throws RemoteException {
        when(mApexService.getStagedSessionInfo(anyInt())).thenReturn(
                getFakeStagedSessionInfo());

        mApexManager.getStagedSessionInfo(TEST_SESSION_ID);
        verify(mApexService, times(1)).getStagedSessionInfo(TEST_SESSION_ID);
    }

    @Test
    public void testGetStagedSessionInfo_unKnownStagedSessionId() throws RemoteException {
        when(mApexService.getStagedSessionInfo(anyInt())).thenReturn(
                getFakeUnknownSessionInfo());

        assertThat(mApexManager.getStagedSessionInfo(TEST_SESSION_ID)).isNull();
    }

    @Test
    public void testSubmitStagedSession_throwPackageManagerException() throws RemoteException {
        doAnswer(invocation -> {
            throw new Exception();
        }).when(mApexService).submitStagedSession(any(), any());

        assertThrows(PackageManagerException.class,
                () -> mApexManager.submitStagedSession(testParamsWithChildren()));
    }

    @Test
    public void testSubmitStagedSession_throwRunTimeException() throws RemoteException {
        doThrow(RemoteException.class).when(mApexService).submitStagedSession(any(), any());

        assertThrows(RuntimeException.class,
                () -> mApexManager.submitStagedSession(testParamsWithChildren()));
    }

    @Test
    public void testMarkStagedSessionReady_throwPackageManagerException() throws RemoteException {
        doAnswer(invocation -> {
            throw new Exception();
        }).when(mApexService).markStagedSessionReady(anyInt());

        assertThrows(PackageManagerException.class,
                () -> mApexManager.markStagedSessionReady(TEST_SESSION_ID));
    }

    @Test
    public void testMarkStagedSessionReady_throwRunTimeException() throws RemoteException {
        doThrow(RemoteException.class).when(mApexService).markStagedSessionReady(anyInt());

        assertThrows(RuntimeException.class,
                () -> mApexManager.markStagedSessionReady(TEST_SESSION_ID));
    }

    @Test
    public void testRevertActiveSessions_remoteException() throws RemoteException {
        doThrow(RemoteException.class).when(mApexService).revertActiveSessions();

        try {
            assertThat(mApexManager.revertActiveSessions()).isFalse();
        } catch (Exception e) {
            throw new AssertionError("ApexManager should not raise Exception");
        }
    }

    @Test
    public void testMarkStagedSessionSuccessful_throwRemoteException() throws RemoteException {
        doThrow(RemoteException.class).when(mApexService).markStagedSessionSuccessful(anyInt());

        assertThrows(RuntimeException.class,
                () -> mApexManager.markStagedSessionSuccessful(TEST_SESSION_ID));
    }

    @Test
    public void testUninstallApex_throwException_returnFalse() throws RemoteException {
        doAnswer(invocation -> {
            throw new Exception();
        }).when(mApexService).unstagePackages(any());

        assertThat(mApexManager.uninstallApex(TEST_APEX_PKG)).isFalse();
    }

    private ApexInfo[] createApexInfo(boolean isActive, boolean isFactory) {
        File apexFile = copyRawResourceToFile(TEST_APEX_PKG, R.raw.apex_test);
        ApexInfo apexInfo = new ApexInfo();
        apexInfo.isActive = isActive;
        apexInfo.isFactory = isFactory;
        apexInfo.moduleName = TEST_APEX_PKG;
        apexInfo.modulePath = apexFile.getPath();
        apexInfo.versionCode = 191000070;

        return new ApexInfo[]{apexInfo};
    }

    private ApexSessionInfo getFakeStagedSessionInfo() {
        ApexSessionInfo stagedSessionInfo = new ApexSessionInfo();
        stagedSessionInfo.sessionId = TEST_SESSION_ID;
        stagedSessionInfo.isStaged = true;

        return stagedSessionInfo;
    }

    private ApexSessionInfo getFakeUnknownSessionInfo() {
        ApexSessionInfo stagedSessionInfo = new ApexSessionInfo();
        stagedSessionInfo.sessionId = TEST_SESSION_ID;
        stagedSessionInfo.isUnknown = true;

        return stagedSessionInfo;
    }

    private static ApexSessionParams testParamsWithChildren() {
        ApexSessionParams params = new ApexSessionParams();
        params.sessionId = TEST_SESSION_ID;
        params.childSessionIds = TEST_CHILD_SESSION_ID;
        return params;
    }

    /**
     * Copies a specified {@code resourceId} to a temp file. Returns a non-null file if the copy
     * succeeded
     */
    File copyRawResourceToFile(String baseName, int resourceId) {
        File outFile;
        try {
            outFile = File.createTempFile(baseName, ".apex");
        } catch (IOException e) {
            throw new AssertionError("CreateTempFile IOException" + e);
        }

        try (InputStream is = mContext.getResources().openRawResource(resourceId);
             FileOutputStream os = new FileOutputStream(outFile)) {
            assertThat(FileUtils.copy(is, os)).isGreaterThan(0L);
        } catch (FileNotFoundException e) {
            throw new AssertionError("File not found exception " + e);
        } catch (IOException e) {
            throw new AssertionError("IOException" + e);
        }

        return outFile;
    }
}
