/*
 * Copyright (C) 2021 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.sdkext.extensions;

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

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeTrue;
import org.junit.Ignore;

import android.cts.install.lib.host.InstallUtilsHost;

import com.android.os.ext.testing.CurrentVersion;
import com.android.tests.rollback.host.AbandonSessionsRule;
import com.android.tradefed.device.ITestDevice.ApexInfo;
import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
import com.android.tradefed.util.CommandResult;

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

import java.io.File;
import java.lang.NumberFormatException;
import java.time.Duration;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@RunWith(DeviceJUnit4ClassRunner.class)
public class SdkExtensionsHostTest extends BaseHostJUnit4Test {

    private static final String APP_FILENAME = "sdkextensions_e2e_test_app.apk";
    private static final String APP_PACKAGE = "com.android.sdkext.extensions.apps";
    private static final String APP_R12_FILENAME = "sdkextensions_e2e_test_app_req_r12.apk";
    private static final String APP_R12_PACKAGE = "com.android.sdkext.extensions.apps.r12";
    private static final String APP_S12_FILENAME = "sdkextensions_e2e_test_app_req_s12.apk";
    private static final String APP_S12_PACKAGE = "com.android.sdkext.extensions.apps.s12";
    private static final String APP_R45_FILENAME = "sdkextensions_e2e_test_app_req_r45.apk";
    private static final String APP_R45_PACKAGE = "com.android.sdkext.extensions.apps.r45";
    private static final String APP_S45_FILENAME = "sdkextensions_e2e_test_app_req_s45.apk";
    private static final String APP_S45_PACKAGE = "com.android.sdkext.extensions.apps.s45";
    private static final String MEDIA_FILENAME = "test_com.android.media.apex";
    private static final String SDKEXTENSIONS_FILENAME = "test_com.android.sdkext.apex";

    private static final Duration BOOT_COMPLETE_TIMEOUT = Duration.ofMinutes(2);

    private final InstallUtilsHost mInstallUtils = new InstallUtilsHost(this);

    private Boolean mIsAtLeastS = null;
    private Boolean mIsAtLeastT = null;

    @Rule
    public AbandonSessionsRule mHostTestRule = new AbandonSessionsRule(this);

    @Before
    public void setUp() throws Exception {
        assumeTrue("Updating APEX is not supported", mInstallUtils.isApexUpdateSupported());
    }

    @Before
    public void installTestApp() throws Exception {
        File testAppFile = mInstallUtils.getTestFile(APP_FILENAME);
        String installResult = getDevice().installPackage(testAppFile, true);
        assertNull(installResult);
    }

    @Before // Generally not needed, but local test devices are sometimes in a "bad" start state.
    @After
    public void cleanup() throws Exception {
        getDevice().uninstallPackage(APP_PACKAGE);
        uninstallApexes(SDKEXTENSIONS_FILENAME, MEDIA_FILENAME);
    }

    @Test
    @Ignore("b/274764792")
    public void testDefault() throws Exception {
        assertVersionDefault();
    }

    @Test
    @Ignore("b/274764792")
    public void upgradeOneApexWithBump()  throws Exception {
        assertVersionDefault();
        mInstallUtils.installApexes(SDKEXTENSIONS_FILENAME);
        reboot();

        // Version 12 requires sdkext, which is fulfilled
        // Version 45 requires sdkext + media, which isn't fulfilled
        assertRVersionEquals(12);
        assertSVersionEquals(12);
        assertTestMethodsPresent(); // 45 APIs are available on 12 too.
    }

    @Test
    @Ignore("b/274764792")
    public void upgradeOneApex() throws Exception {
        // Version 45 requires updated sdkext and media, so updating just media changes nothing.
        assertVersionDefault();
        mInstallUtils.installApexes(MEDIA_FILENAME);
        reboot();
        assertVersionDefault();
    }

    @Test
    @Ignore("b/274764792")
    public void upgradeTwoApexes() throws Exception {
        // Updating sdkext and media bumps the version to 45.
        assertVersionDefault();
        mInstallUtils.installApexes(MEDIA_FILENAME, SDKEXTENSIONS_FILENAME);
        reboot();
        assertVersion45();
    }

    private boolean canInstallApp(String filename, String packageName) throws Exception {
        File appFile = mInstallUtils.getTestFile(filename);
        String installResult = getDevice().installPackage(appFile, true);
        if (installResult != null) {
            return false;
        }
        assertNull(getDevice().uninstallPackage(packageName));
        return true;
    }

    private String getExtensionVersionFromSysprop(String v) throws Exception {
        String command = "getprop build.version.extensions." + v;
        CommandResult res = getDevice().executeShellV2Command(command);
        assertEquals(0, (int) res.getExitCode());
        return res.getStdout().replace("\n", "");
    }

    private String broadcast(String action, String extra) throws Exception {
        String command = getBroadcastCommand(action, extra);
        CommandResult res = getDevice().executeShellV2Command(command);
        assertEquals(0, (int) res.getExitCode());
        Matcher matcher = Pattern.compile("data=\"([^\"]+)\"").matcher(res.getStdout());
        assertTrue("Unexpected output from am broadcast: " + res.getStdout(), matcher.find());
        return matcher.group(1);
    }

    private boolean broadcastForBoolean(String action, String extra) throws Exception {
        String result = broadcast(action, extra);
        if (result.equals("true") || result.equals("false")) {
            return result.equals("true");
        }
        throw getAppParsingError(result);
    }

    private int broadcastForInt(String action, String extra) throws Exception {
        String result = broadcast(action, extra);
        try {
            return Integer.parseInt(result);
        } catch (NumberFormatException e) {
            throw getAppParsingError(result);
        }
    }

    private Error getAppParsingError(String result) {
        String message = "App error! Full stack trace in logcat (grep for SdkExtensionsE2E): ";
        return new AssertionError(message + result);
    }

    private void assertVersionDefault() throws Exception {
        int expected = isAtLeastT() ? CurrentVersion.T_BASE_VERSION
            : isAtLeastS() ? CurrentVersion.S_BASE_VERSION
            : CurrentVersion.R_BASE_VERSION;
        assertRVersionEquals(expected);
        assertSVersionEquals(expected);
        assertTestMethodsNotPresent();
    }

    private void assertVersion45() throws Exception {
        assertRVersionEquals(45);
        assertSVersionEquals(45);
        assertTestMethodsPresent();
    }

    private void assertTestMethodsNotPresent() throws Exception {
        assertTrue(broadcastForBoolean("MAKE_CALLS_DEFAULT", null));
    }

    private void assertTestMethodsPresent() throws Exception {
        if (isAtLeastS()) {
            assertTrue(broadcastForBoolean("MAKE_CALLS_45", null));
        } else {
            // The APIs in the test apex are not currently getting installed correctly
            // on Android R devices because they rely on the dynamic classpath feature.
            // TODO(b/234361913): fix this
            assertTestMethodsNotPresent();
        }
    }

    private void assertRVersionEquals(int version) throws Exception {
        int appValue = broadcastForInt("GET_SDK_VERSION", "r");
        String syspropValue = getExtensionVersionFromSysprop("r");
        assertEquals(version, appValue);
        assertEquals(String.valueOf(version), syspropValue);
        assertEquals(version >= 12, canInstallApp(APP_R12_FILENAME, APP_R12_PACKAGE));
        assertEquals(version >= 45, canInstallApp(APP_R45_FILENAME, APP_R45_PACKAGE));
    }

    private void assertSVersionEquals(int version) throws Exception {
        int appValue = broadcastForInt("GET_SDK_VERSION", "s");
        String syspropValue = getExtensionVersionFromSysprop("s");
        if (isAtLeastS()) {
            assertEquals(version, appValue);
            assertEquals(String.valueOf(version), syspropValue);

            // These APKs require the same R version as they do S version.
            int minVersion = Math.min(version, broadcastForInt("GET_SDK_VERSION", "r"));
            assertEquals(minVersion >= 12, canInstallApp(APP_S12_FILENAME, APP_S12_PACKAGE));
            assertEquals(minVersion >= 45, canInstallApp(APP_S45_FILENAME, APP_S45_PACKAGE));
        } else {
            assertEquals(0, appValue);
            assertEquals("", syspropValue);
            assertFalse(canInstallApp(APP_S12_FILENAME, APP_S12_PACKAGE));
            assertFalse(canInstallApp(APP_S45_FILENAME, APP_S45_PACKAGE));
        }
    }

    private static String getBroadcastCommand(String action, String extra) {
        String cmd = "am broadcast";
        cmd += " -a com.android.sdkext.extensions.apps." + action;
        if (extra != null) {
            cmd += " -e extra " + extra;
        }
        cmd += " -n com.android.sdkext.extensions.apps/.Receiver";
        return cmd;
    }

    private boolean isAtLeastS() throws Exception {
        if (mIsAtLeastS == null) {
            mIsAtLeastS = broadcastForBoolean("IS_AT_LEAST", "s");
        }
        return mIsAtLeastS;
    }

    private boolean isAtLeastT() throws Exception {
        if (mIsAtLeastT == null) {
            mIsAtLeastT = broadcastForBoolean("IS_AT_LEAST", "t");
        }
        return mIsAtLeastT;
    }

    private boolean uninstallApexes(String... filenames) throws Exception {
        boolean reboot = false;
        for (String filename : filenames) {
            ApexInfo apex = mInstallUtils.getApexInfo(mInstallUtils.getTestFile(filename));
            String res = getDevice().uninstallPackage(apex.name);
            // res is null for successful uninstalls (non-null likely implesfactory version).
            reboot |= res == null;
        }
        if (reboot) {
            reboot();
            return true;
        }
        return false;
    }

    private void reboot() throws Exception {
        getDevice().reboot();
        boolean success = getDevice().waitForBootComplete(BOOT_COMPLETE_TIMEOUT.toMillis());
        assertWithMessage("Device didn't boot in %s", BOOT_COMPLETE_TIMEOUT).that(success).isTrue();
    }
}
