/*
 * Copyright (C) 2016 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.systemui.shared.plugins;

import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;

import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.app.Activity;
import android.app.NotificationManager;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.test.suitebuilder.annotation.SmallTest;

import androidx.test.runner.AndroidJUnit4;

import com.android.systemui.SysuiTestCase;
import com.android.systemui.SysuiTestableContext;
import com.android.systemui.plugins.Plugin;
import com.android.systemui.plugins.PluginListener;
import com.android.systemui.plugins.annotations.Requires;
import com.android.systemui.util.concurrency.FakeExecutor;
import com.android.systemui.util.time.FakeSystemClock;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import org.mockito.stubbing.Answer;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

@SmallTest
@RunWith(AndroidJUnit4.class)
public class PluginActionManagerTest extends SysuiTestCase {

    private static final String PRIVILEGED_PACKAGE = "com.android.systemui.shared.plugins";
    private TestPlugin mMockPlugin;

    private PackageManager mMockPm;
    private PluginListener<TestPlugin> mMockListener;
    private PluginActionManager<TestPlugin> mPluginActionManager;
    private VersionInfo mMockVersionInfo;
    private PluginEnabler mMockEnabler;
    ComponentName mTestPluginComponentName =
            new ComponentName(PRIVILEGED_PACKAGE, TestPlugin.class.getName());
    private final FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock());
    NotificationManager mNotificationManager;
    private PluginInstance<TestPlugin> mPluginInstance;
    private PluginInstance.Factory mPluginInstanceFactory = new PluginInstance.Factory(
            this.getClass().getClassLoader(),
            new PluginInstance.InstanceFactory<>(), new PluginInstance.VersionCheckerImpl(),
            Collections.emptyList(), false) {
        @Override
        public <T extends Plugin> PluginInstance<T> create(Context context, ApplicationInfo appInfo,
                ComponentName componentName, Class<T> pluginClass, PluginListener<T> listener) {
            return (PluginInstance<T>) mPluginInstance;
        }
    };

    private PluginActionManager.Factory mActionManagerFactory;

    @Before
    public void setup() throws Exception {
        mContext = new MyContextWrapper(mContext);
        mMockPm = mock(PackageManager.class);
        mMockListener = mock(PluginListener.class);
        mMockEnabler = mock(PluginEnabler.class);
        mMockVersionInfo = mock(VersionInfo.class);
        mNotificationManager = mock(NotificationManager.class);
        mMockPlugin = mock(TestPlugin.class);
        mPluginInstance = mock(PluginInstance.class);
        when(mPluginInstance.getComponentName()).thenReturn(mTestPluginComponentName);
        when(mPluginInstance.getPackage()).thenReturn(mTestPluginComponentName.getPackageName());
        mActionManagerFactory = new PluginActionManager.Factory(getContext(), mMockPm,
                mFakeExecutor, mFakeExecutor, mNotificationManager, mMockEnabler,
                new ArrayList<>(), mPluginInstanceFactory);

        mPluginActionManager = mActionManagerFactory.create("myAction", mMockListener,
                TestPlugin.class, true, true);
        when(mMockPlugin.getVersion()).thenReturn(1);
    }

    @Test
    public void testNoPlugins() {
        when(mMockPm.queryIntentServices(any(), anyInt())).thenReturn(
                Collections.emptyList());
        mPluginActionManager.loadAll();

        mFakeExecutor.runAllReady();

        verify(mMockListener, never()).onPluginConnected(any(), any());
    }

    @Test
    public void testPluginCreate() throws Exception {
        //Debug.waitForDebugger();
        createPlugin();

        // Verify startup lifecycle
        verify(mPluginInstance).onCreate();
    }

    @Test
    public void testPluginDestroy() throws Exception {
        createPlugin(); // Get into valid created state.

        mPluginActionManager.destroy();

        mFakeExecutor.runAllReady();

        // Verify shutdown lifecycle
        verify(mPluginInstance).onDestroy();
    }

    @Test
    public void testReloadOnChange() throws Exception {
        createPlugin(); // Get into valid created state.

        mPluginActionManager.reloadPackage(PRIVILEGED_PACKAGE);

        mFakeExecutor.runAllReady();

        // Verify the old one was destroyed.
        verify(mPluginInstance).onDestroy();
        verify(mPluginInstance, Mockito.times(2))
                .onCreate();
    }

    @Test
    public void testNonDebuggable() throws Exception {
        // Create a version that thinks the build is not debuggable.
        mPluginActionManager = mActionManagerFactory.create("myAction", mMockListener,
                TestPlugin.class, true, false);
        setupFakePmQuery();

        mPluginActionManager.loadAll();

        mFakeExecutor.runAllReady();

        // Non-debuggable build should receive no plugins.
        verify(mMockListener, never()).onPluginConnected(any(), any());
    }

    @Test
    public void testNonDebuggable_privileged() throws Exception {
        // Create a version that thinks the build is not debuggable.
        PluginActionManager.Factory factory = new PluginActionManager.Factory(getContext(),
                mMockPm, mFakeExecutor, mFakeExecutor, mNotificationManager,
                mMockEnabler, Collections.singletonList(PRIVILEGED_PACKAGE),
                mPluginInstanceFactory);
        mPluginActionManager = factory.create("myAction", mMockListener,
                TestPlugin.class, true, false);
        setupFakePmQuery();

        mPluginActionManager.loadAll();

        mFakeExecutor.runAllReady();

        // Verify startup lifecycle
        verify(mPluginInstance).onCreate();
    }

    @Test
    public void testCheckAndDisable() throws Exception {
        createPlugin(); // Get into valid created state.

        // Start with an unrelated class.
        boolean result = mPluginActionManager.checkAndDisable(Activity.class.getName());
        assertFalse(result);
        verify(mMockEnabler, never()).setDisabled(any(ComponentName.class), anyInt());

        // Now hand it a real class and make sure it disables the plugin.
        result = mPluginActionManager.checkAndDisable(TestPlugin.class.getName());
        assertTrue(result);
        verify(mMockEnabler).setDisabled(
                mTestPluginComponentName, PluginEnabler.DISABLED_FROM_EXPLICIT_CRASH);
    }

    @Test
    public void testDisableAll() throws Exception {
        createPlugin(); // Get into valid created state.

        mPluginActionManager.disableAll();

        verify(mMockEnabler).setDisabled(
                mTestPluginComponentName, PluginEnabler.DISABLED_FROM_SYSTEM_CRASH);
    }

    @Test
    public void testDisablePrivileged() throws Exception {
        PluginActionManager.Factory factory = new PluginActionManager.Factory(getContext(),
                mMockPm, mFakeExecutor, mFakeExecutor, mNotificationManager,
                mMockEnabler, Collections.singletonList(PRIVILEGED_PACKAGE),
                mPluginInstanceFactory);
        mPluginActionManager = factory.create("myAction", mMockListener,
                TestPlugin.class, true, false);

        createPlugin(); // Get into valid created state.

        mPluginActionManager.disableAll();

        verify(mMockPm, never()).setComponentEnabledSetting(
                ArgumentCaptor.forClass(ComponentName.class).capture(),
                ArgumentCaptor.forClass(int.class).capture(),
                ArgumentCaptor.forClass(int.class).capture());
    }

    private void setupFakePmQuery() throws Exception {
        List<ResolveInfo> list = new ArrayList<>();
        ResolveInfo info = new ResolveInfo();
        info.serviceInfo = mock(ServiceInfo.class);
        info.serviceInfo.packageName = mTestPluginComponentName.getPackageName();
        info.serviceInfo.name = mTestPluginComponentName.getClassName();
        when(info.serviceInfo.loadLabel(any())).thenReturn("Test Plugin");
        list.add(info);
        when(mMockPm.queryIntentServices(any(), Mockito.anyInt())).thenReturn(list);
        when(mMockPm.getServiceInfo(any(), anyInt())).thenReturn(info.serviceInfo);

        when(mMockPm.checkPermission(Mockito.anyString(), Mockito.anyString())).thenReturn(
                PackageManager.PERMISSION_GRANTED);

        when(mMockPm.getApplicationInfo(Mockito.anyString(), anyInt())).thenAnswer(
                (Answer<ApplicationInfo>) invocation -> {
                    ApplicationInfo appInfo = getContext().getApplicationInfo();
                    appInfo.packageName = invocation.getArgument(0);
                    return appInfo;
                });
        when(mMockEnabler.isEnabled(mTestPluginComponentName)).thenReturn(true);
    }

    private void createPlugin() throws Exception {
        setupFakePmQuery();

        mPluginActionManager.loadAll();

        mFakeExecutor.runAllReady();
    }

    // Real context with no registering/unregistering of receivers.
    private static class MyContextWrapper extends SysuiTestableContext {
        MyContextWrapper(Context base) {
            super(base);
        }

        @Override
        public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
            return null;
        }

        @Override
        public void unregisterReceiver(BroadcastReceiver receiver) {
        }

        @Override
        public void sendBroadcast(Intent intent) {
            // Do nothing.
        }
    }

    // This target class doesn't matter, it just needs to have a Requires to hit the flow where
    // the mock version info is called.
    @Requires(target = PluginManagerTest.class, version = 1)
    public static class TestPlugin implements Plugin {
        @Override
        public int getVersion() {
            return 1;
        }

        @Override
        public void onCreate(Context sysuiContext, Context pluginContext) {
        }

        @Override
        public void onDestroy() {
        }
    }
}
