/*
 * 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.plugins;

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

import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
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.ContextWrapper;
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.os.HandlerThread;
import android.os.UserHandle;
import android.test.suitebuilder.annotation.SmallTest;

import androidx.test.annotation.UiThreadTest;
import androidx.test.runner.AndroidJUnit4;

import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.plugins.PluginInstanceManager.PluginInfo;
import com.android.systemui.plugins.VersionInfo.InvalidVersionException;
import com.android.systemui.plugins.annotations.Requires;

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

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

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

    // Static since the plugin needs to be generated by the PluginInstanceManager using newInstance.
    private static Plugin sMockPlugin;

    private HandlerThread mHandlerThread;
    private Context mContextWrapper;
    private PackageManager mMockPm;
    private PluginListener mMockListener;
    private PluginInstanceManager mPluginInstanceManager;
    private PluginManagerImpl mMockManager;
    private VersionInfo mMockVersionInfo;

    @Before
    public void setup() throws Exception {
        mHandlerThread = new HandlerThread("test_thread");
        mHandlerThread.start();
        mContextWrapper = new MyContextWrapper(getContext());
        mMockPm = mock(PackageManager.class);
        mMockListener = mock(PluginListener.class);
        mMockManager = mock(PluginManagerImpl.class);
        when(mMockManager.getClassLoader(any(), any()))
                .thenReturn(getClass().getClassLoader());
        mMockVersionInfo = mock(VersionInfo.class);
        mPluginInstanceManager = new PluginInstanceManager(mContextWrapper, mMockPm, "myAction",
                mMockListener, true, mHandlerThread.getLooper(), mMockVersionInfo,
                mMockManager, true);
        sMockPlugin = mock(Plugin.class);
        when(sMockPlugin.getVersion()).thenReturn(1);
    }

    @After
    public void tearDown() throws Exception {
        mHandlerThread.quit();
        sMockPlugin = null;
    }

    @UiThreadTest
    @Test
    public void testGetPlugin() throws Exception {
        setupFakePmQuery();
        PluginInfo p = mPluginInstanceManager.getPlugin();
        assertNotNull(p.mPlugin);
        verify(sMockPlugin).onCreate(any(), any());
    }

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

        waitForIdleSync(mPluginInstanceManager.mPluginHandler);
        waitForIdleSync(mPluginInstanceManager.mMainHandler);

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

    @Test
    public void testPluginCreate() throws Exception {
        createPlugin();

        // Verify startup lifecycle
        verify(sMockPlugin).onCreate(ArgumentCaptor.forClass(Context.class).capture(),
                ArgumentCaptor.forClass(Context.class).capture());
        verify(mMockListener).onPluginConnected(any(), any());
    }

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

        mPluginInstanceManager.destroy();

        waitForIdleSync(mPluginInstanceManager.mPluginHandler);
        waitForIdleSync(mPluginInstanceManager.mMainHandler);

        // Verify shutdown lifecycle
        verify(mMockListener).onPluginDisconnected(ArgumentCaptor.forClass(Plugin.class).capture());
        verify(sMockPlugin).onDestroy();
    }

    @Test
    public void testIncorrectVersion() throws Exception {
        NotificationManager nm = mock(NotificationManager.class);
        mContext.addMockSystemService(Context.NOTIFICATION_SERVICE, nm);
        setupFakePmQuery();
        doThrow(new InvalidVersionException("", false)).when(mMockVersionInfo).checkVersion(any());

        mPluginInstanceManager.loadAll();

        waitForIdleSync(mPluginInstanceManager.mPluginHandler);
        waitForIdleSync(mPluginInstanceManager.mMainHandler);

        // Plugin shouldn't be connected because it is the wrong version.
        verify(mMockListener, Mockito.never()).onPluginConnected(any(), any());
        verify(nm).notifyAsUser(eq(TestPlugin.class.getName()), eq(SystemMessage.NOTE_PLUGIN),
                any(), eq(UserHandle.ALL));
    }

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

        mPluginInstanceManager.onPackageChange("com.android.systemui");

        waitForIdleSync(mPluginInstanceManager.mPluginHandler);
        waitForIdleSync(mPluginInstanceManager.mMainHandler);

        // Verify the old one was destroyed.
        verify(mMockListener).onPluginDisconnected(ArgumentCaptor.forClass(Plugin.class).capture());
        verify(sMockPlugin).onDestroy();
        // Also verify we got a second onCreate.
        verify(sMockPlugin, Mockito.times(2)).onCreate(
                ArgumentCaptor.forClass(Context.class).capture(),
                ArgumentCaptor.forClass(Context.class).capture());
        verify(mMockListener, Mockito.times(2)).onPluginConnected(any(), any());
    }

    @Test
    public void testNonDebuggable() throws Exception {
        // Create a version that thinks the build is not debuggable.
        mPluginInstanceManager = new PluginInstanceManager(mContextWrapper, mMockPm, "myAction",
                mMockListener, true, mHandlerThread.getLooper(), mMockVersionInfo,
                mMockManager, false);
        setupFakePmQuery();

        mPluginInstanceManager.loadAll();

        waitForIdleSync(mPluginInstanceManager.mPluginHandler);
        waitForIdleSync(mPluginInstanceManager.mMainHandler);;

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

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

        // Start with an unrelated class.
        boolean result = mPluginInstanceManager.checkAndDisable(Activity.class.getName());
        assertFalse(result);
        verify(mMockPm, Mockito.never()).setComponentEnabledSetting(
                ArgumentCaptor.forClass(ComponentName.class).capture(),
                ArgumentCaptor.forClass(int.class).capture(),
                ArgumentCaptor.forClass(int.class).capture());

        // Now hand it a real class and make sure it disables the plugin.
        result = mPluginInstanceManager.checkAndDisable(TestPlugin.class.getName());
        assertTrue(result);
        verify(mMockPm).setComponentEnabledSetting(
                ArgumentCaptor.forClass(ComponentName.class).capture(),
                ArgumentCaptor.forClass(int.class).capture(),
                ArgumentCaptor.forClass(int.class).capture());
    }

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

        mPluginInstanceManager.disableAll();

        verify(mMockPm).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 = "com.android.systemui";
        info.serviceInfo.name = TestPlugin.class.getName();
        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);

        ApplicationInfo appInfo = getContext().getApplicationInfo();
        when(mMockPm.getApplicationInfo(Mockito.anyString(), Mockito.anyInt())).thenReturn(
                appInfo);
    }

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

        mPluginInstanceManager.loadAll();

        waitForIdleSync(mPluginInstanceManager.mPluginHandler);
        waitForIdleSync(mPluginInstanceManager.mMainHandler);
    }

    // Real context with no registering/unregistering of receivers.
    private static class MyContextWrapper extends ContextWrapper {
        public 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 sMockPlugin.getVersion();
        }

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

        @Override
        public void onDestroy() {
            sMockPlugin.onDestroy();
        }
    }
}
