/*
 * Copyright (C) 2022 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.art;

import static com.android.server.art.DexUseManagerLocal.DexLoader;
import static com.android.server.art.DexUseManagerLocal.SecondaryDexInfo;
import static com.android.server.art.model.DexoptStatus.DexContainerFileDexoptStatus;

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

import static org.mockito.Mockito.any;
import static org.mockito.Mockito.argThat;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;

import android.annotation.NonNull;
import android.os.SystemProperties;

import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;

import com.android.server.art.model.DexoptStatus;
import com.android.server.art.testing.StaticMockitoRule;
import com.android.server.pm.PackageManagerLocal;
import com.android.server.pm.pkg.AndroidPackage;
import com.android.server.pm.pkg.PackageState;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

@SmallTest
@RunWith(AndroidJUnit4.class)
public class DumpHelperTest {
    private static final String PKG_NAME_FOO = "com.example1.foo";
    private static final String PKG_NAME_BAR = "com.example2.bar";

    @Rule
    public StaticMockitoRule mockitoRule =
            new StaticMockitoRule(SystemProperties.class, Constants.class);

    @Mock private DumpHelper.Injector mInjector;
    @Mock private ArtManagerLocal mArtManagerLocal;
    @Mock private DexUseManagerLocal mDexUseManagerLocal;
    @Mock private PackageManagerLocal.FilteredSnapshot mSnapshot;
    @Mock private IArtd mArtd;

    private DumpHelper mDumpHelper;

    @Before
    public void setUp() throws Exception {
        lenient().when(Constants.getPreferredAbi()).thenReturn("arm64-v8a");
        lenient().when(Constants.getNative64BitAbi()).thenReturn("arm64-v8a");
        lenient().when(Constants.getNative32BitAbi()).thenReturn("armeabi-v7a");

        // No ISA translation.
        lenient()
                .when(SystemProperties.get(argThat(arg -> arg.startsWith("ro.dalvik.vm.isa."))))
                .thenReturn("");

        lenient().when(mInjector.getArtManagerLocal()).thenReturn(mArtManagerLocal);
        lenient().when(mInjector.getDexUseManager()).thenReturn(mDexUseManagerLocal);
        lenient().when(mInjector.getArtd()).thenReturn(mArtd);

        Map<String, PackageState> pkgStates = createPackageStates();
        lenient().when(mSnapshot.getPackageStates()).thenReturn(pkgStates);
        for (var entry : pkgStates.entrySet()) {
            lenient().when(mSnapshot.getPackageState(entry.getKey())).thenReturn(entry.getValue());
        }

        setUpForFoo();
        setUpForBar();

        mDumpHelper = new DumpHelper(mInjector);
    }

    @Test
    public void testDump() throws Exception {
        String expected = "[com.example1.foo]\n"
                + "  path: /data/app/foo/base.apk\n"
                + "    arm64: [status=speed-profile] [reason=bg-dexopt] [primary-abi]\n"
                + "      [location is /data/app/foo/oat/arm64/base.odex]\n"
                + "    arm: [status=verify] [reason=install]\n"
                + "      [location is /data/app/foo/oat/arm/base.odex]\n"
                + "  path: /data/app/foo/split_0.apk\n"
                + "    arm64: [status=verify] [reason=vdex] [primary-abi]\n"
                + "      [location is primary.vdex in /data/app/foo/split_0.dm]\n"
                + "    arm: [status=verify] [reason=vdex]\n"
                + "      [location is primary.vdex in /data/app/foo/split_0.dm]\n"
                + "    used by other apps: [com.example2.bar (isa=arm)]\n"
                + "  known secondary dex files:\n"
                + "    /data/user_de/0/foo/1.apk (removed)\n"
                + "      arm: [status=run-from-apk] [reason=unknown]\n"
                + "        [location is unknown]\n"
                + "      class loader context: =VaryingClassLoaderContexts=\n"
                + "        com.example1.foo (isolated): CLC1\n"
                + "        com.example3.baz: CLC2\n"
                + "      used by other apps: [com.example1.foo (isolated) (isa=arm64), com.example3.baz (removed)]\n"
                + "    /data/user_de/0/foo/2.apk (public)\n"
                + "      arm64: [status=speed-profile] [reason=bg-dexopt] [primary-abi]\n"
                + "        [location is /data/user_de/0/foo/oat/arm64/2.odex]\n"
                + "      arm: [status=verify] [reason=vdex]\n"
                + "        [location is /data/user_de/0/foo/oat/arm/2.vdex]\n"
                + "      class loader context: PCL[]\n"
                + "[com.example2.bar]\n"
                + "  path: /data/app/bar/base.apk\n"
                + "    arm: [status=verify] [reason=install] [primary-abi]\n"
                + "      [location is /data/app/bar/oat/arm/base.odex]\n"
                + "    arm64: [status=verify] [reason=install]\n"
                + "      [location is /data/app/bar/oat/arm64/base.odex]\n";

        var stringWriter = new StringWriter();
        mDumpHelper.dump(new PrintWriter(stringWriter), mSnapshot);
        assertThat(stringWriter.toString()).isEqualTo(expected);
    }

    private PackageState createPackageState(@NonNull String packageName, int appId,
            boolean hasPackage, @NonNull String primaryAbi, @NonNull String secondaryAbi) {
        var pkgState = mock(PackageState.class);
        lenient().when(pkgState.getPackageName()).thenReturn(packageName);
        lenient().when(pkgState.getAppId()).thenReturn(appId);
        lenient()
                .when(pkgState.getAndroidPackage())
                .thenReturn(hasPackage ? mock(AndroidPackage.class) : null);
        lenient().when(pkgState.getPrimaryCpuAbi()).thenReturn(primaryAbi);
        lenient().when(pkgState.getSecondaryCpuAbi()).thenReturn(secondaryAbi);
        return pkgState;
    }

    private Map<String, PackageState> createPackageStates() {
        var pkgStates = new HashMap<String, PackageState>();
        pkgStates.put(PKG_NAME_FOO,
                createPackageState(PKG_NAME_FOO, 10001 /* appId */, true /* hasPackage */,
                        "arm64-v8a", "armeabi-v7a"));
        pkgStates.put(PKG_NAME_BAR,
                createPackageState(PKG_NAME_BAR, 10003 /* appId */, true /* hasPackage */,
                        "armeabi-v7a", "arm64-v8a"));
        // This should not be included in the output because it has a negative app id.
        pkgStates.put("com.android.art",
                createPackageState("com.android.art", -1 /* appId */, true /* hasPackage */,
                        "arm64-v8a", "armeabi-v7a"));
        // This should not be included in the output because it does't have AndroidPackage.
        pkgStates.put("com.example.null",
                createPackageState("com.example.null", 10010 /* appId */, false /* hasPackage */,
                        "arm64-v8a", "armeabi-v7a"));
        return pkgStates;
    }

    private void setUpForFoo() throws Exception {
        // The order of the primary dex files and the ABIs should be kept in the output. Secondary
        // dex files should be reordered in lexicographical order.
        var status = DexoptStatus.create(List.of(
                DexContainerFileDexoptStatus.create("/data/app/foo/base.apk",
                        true /* isPrimaryDex */, true /* isPrimaryAbi */, "arm64-v8a",
                        "speed-profile", "bg-dexopt", "/data/app/foo/oat/arm64/base.odex"),
                DexContainerFileDexoptStatus.create("/data/app/foo/base.apk",
                        true /* isPrimaryDex */, false /* isPrimaryAbi */, "armeabi-v7a", "verify",
                        "install", "/data/app/foo/oat/arm/base.odex"),
                DexContainerFileDexoptStatus.create("/data/app/foo/split_0.apk",
                        true /* isPrimaryDex */, true /* isPrimaryAbi */, "arm64-v8a", "verify",
                        "vdex", "primary.vdex in /data/app/foo/split_0.dm"),
                DexContainerFileDexoptStatus.create("/data/app/foo/split_0.apk",
                        true /* isPrimaryDex */, false /* isPrimaryAbi */, "armeabi-v7a", "verify",
                        "vdex", "primary.vdex in /data/app/foo/split_0.dm"),
                DexContainerFileDexoptStatus.create("/data/user_de/0/foo/2.apk",
                        false /* isPrimaryDex */, true /* isPrimaryAbi */, "arm64-v8a",
                        "speed-profile", "bg-dexopt", "/data/user_de/0/foo/oat/arm64/2.odex"),
                DexContainerFileDexoptStatus.create("/data/user_de/0/foo/2.apk",
                        false /* isPrimaryDex */, false /* isPrimaryAbi */, "armeabi-v7a", "verify",
                        "vdex", "/data/user_de/0/foo/oat/arm/2.vdex"),
                DexContainerFileDexoptStatus.create("/data/user_de/0/foo/1.apk",
                        false /* isPrimaryDex */, false /* isPrimaryAbi */, "armeabi-v7a",
                        "run-from-apk", "unknown", "unknown")));

        lenient()
                .when(mArtManagerLocal.getDexoptStatus(any(), eq(PKG_NAME_FOO)))
                .thenReturn(status);

        // The output should not show "used by other apps:".
        lenient()
                .when(mDexUseManagerLocal.getPrimaryDexLoaders(
                        PKG_NAME_FOO, "/data/app/foo/base.apk"))
                .thenReturn(Set.of());

        // The output should not show "foo" in "used by other apps:".
        lenient()
                .when(mDexUseManagerLocal.getPrimaryDexLoaders(
                        PKG_NAME_FOO, "/data/app/foo/split_0.apk"))
                .thenReturn(Set.of(DexLoader.create(PKG_NAME_FOO, false /* isolatedProcess */),
                        DexLoader.create(PKG_NAME_BAR, false /* isolatedProcess */)));

        var info1 = mock(SecondaryDexInfo.class);
        lenient().when(info1.dexPath()).thenReturn("/data/user_de/0/foo/1.apk");
        lenient()
                .when(info1.displayClassLoaderContext())
                .thenReturn(SecondaryDexInfo.VARYING_CLASS_LOADER_CONTEXTS);

        lenient()
                .when(mDexUseManagerLocal.getSecondaryClassLoaderContext(PKG_NAME_FOO,
                        "/data/user_de/0/foo/1.apk",
                        DexLoader.create(PKG_NAME_FOO, true /* isolatedProcess */)))
                .thenReturn("CLC1");
        lenient()
                .when(mDexUseManagerLocal.getSecondaryClassLoaderContext(PKG_NAME_FOO,
                        "/data/user_de/0/foo/1.apk",
                        DexLoader.create("com.example3.baz", false /* isolatedProcess */)))
                .thenReturn("CLC2");

        var loaders = new HashSet<DexLoader>();
        // The output should show "foo" with "(isolated)" in "used by other apps:".
        loaders.add(DexLoader.create(PKG_NAME_FOO, true /* isolatedProcess */));
        // The output should show "baz" with "(removed)" in "used by other apps:".
        loaders.add(DexLoader.create("com.example3.baz", false /* isolatedProcess */));
        lenient().when(info1.loaders()).thenReturn(loaders);

        // The output should show the dex path with "(removed)".
        lenient()
                .when(mArtd.getDexFileVisibility("/data/user_de/0/foo/1.apk"))
                .thenReturn(FileVisibility.NOT_FOUND);

        var info2 = mock(SecondaryDexInfo.class);
        lenient().when(info2.dexPath()).thenReturn("/data/user_de/0/foo/2.apk");
        lenient().when(info2.displayClassLoaderContext()).thenReturn("PCL[]");
        lenient()
                .when(mDexUseManagerLocal.getSecondaryClassLoaderContext(PKG_NAME_FOO,
                        "/data/user_de/0/foo/2.apk",
                        DexLoader.create(PKG_NAME_FOO, false /* isolatedProcess */)))
                .thenReturn("PCL[]");
        // The output should not show "used by other apps:".
        lenient()
                .when(info2.loaders())
                .thenReturn(Set.of(DexLoader.create(PKG_NAME_FOO, false /* isolatedProcess */)));
        lenient()
                .when(mArtd.getDexFileVisibility("/data/user_de/0/foo/2.apk"))
                .thenReturn(FileVisibility.OTHER_READABLE);

        lenient()
                .doReturn(List.of(info1, info2))
                .when(mDexUseManagerLocal)
                .getSecondaryDexInfo(PKG_NAME_FOO);
    }

    private void setUpForBar() {
        // The order of the ABI should be kept in the output, despite that it's different from the
        // order for package "foo".
        // The output should not show "known secondary dex files:".
        var status = DexoptStatus.create(
                List.of(DexContainerFileDexoptStatus.create("/data/app/bar/base.apk",
                                true /* isPrimaryDex */, true /* isPrimaryAbi */, "armeabi-v7a",
                                "verify", "install", "/data/app/bar/oat/arm/base.odex"),
                        DexContainerFileDexoptStatus.create("/data/app/bar/base.apk",
                                true /* isPrimaryDex */, false /* isPrimaryAbi */, "arm64-v8a",
                                "verify", "install", "/data/app/bar/oat/arm64/base.odex")));

        lenient()
                .when(mArtManagerLocal.getDexoptStatus(any(), eq(PKG_NAME_BAR)))
                .thenReturn(status);

        lenient()
                .when(mDexUseManagerLocal.getPrimaryDexLoaders(
                        PKG_NAME_BAR, "/data/app/bar/base.apk"))
                .thenReturn(Set.of());
    }
}
