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

import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS;
import static android.view.WindowManagerPolicyConstants.FLAG_PASS_TO_USER;

import static com.android.server.accessibility.AccessibilityInputFilter.FLAG_FEATURE_AUTOCLICK;
import static com.android.server.accessibility.AccessibilityInputFilter.FLAG_FEATURE_CONTROL_SCREEN_MAGNIFIER;
import static com.android.server.accessibility.AccessibilityInputFilter.FLAG_FEATURE_FILTER_KEY_EVENTS;
import static com.android.server.accessibility.AccessibilityInputFilter.FLAG_FEATURE_INJECT_MOTION_EVENTS;
import static com.android.server.accessibility.AccessibilityInputFilter.FLAG_FEATURE_TOUCH_EXPLORATION;
import static com.android.server.accessibility.AccessibilityInputFilter.FLAG_FEATURE_TRIGGERED_SCREEN_MAGNIFIER;

import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;

import android.content.Context;
import android.hardware.display.DisplayManagerGlobal;
import android.os.Looper;
import android.os.SystemClock;
import android.util.SparseArray;
import android.view.Display;
import android.view.DisplayInfo;
import android.view.InputDevice;
import android.view.InputEvent;
import android.view.KeyEvent;
import android.view.MotionEvent;

import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.MockitoAnnotations;

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

/**
 * Tests for AccessibilityInputFilterTest
 */
@RunWith(AndroidJUnit4.class)
public class AccessibilityInputFilterTest {
    private static int sNextDisplayId = DEFAULT_DISPLAY;
    private static final int SECOND_DISPLAY = DEFAULT_DISPLAY + 1;
    private static final float DEFAULT_X = 100f;
    private static final float DEFAULT_Y = 100f;

    private final SparseArray<EventStreamTransformation> mEventHandler = new SparseArray<>(0);
    private final ArrayList<Display> mDisplayList = new ArrayList<>();
    private final int mFeatures = FLAG_FEATURE_AUTOCLICK
            | FLAG_FEATURE_TOUCH_EXPLORATION
            | FLAG_FEATURE_CONTROL_SCREEN_MAGNIFIER
            | FLAG_FEATURE_TRIGGERED_SCREEN_MAGNIFIER
            | FLAG_FEATURE_INJECT_MOTION_EVENTS
            | FLAG_FEATURE_FILTER_KEY_EVENTS;

    // The expected order of EventStreamTransformations.
    private final Class[] mExpectedEventHandlerTypes =
            {KeyboardInterceptor.class, MotionEventInjector.class,
                    MagnificationGestureHandler.class, TouchExplorer.class,
                    AutoclickController.class, AccessibilityInputFilter.class};

    private MagnificationController mMockMagnificationController;
    private AccessibilityManagerService mAms;
    private AccessibilityInputFilter mA11yInputFilter;
    private EventCaptor mCaptor1;
    private EventCaptor mCaptor2;
    private long mLastDownTime = Integer.MIN_VALUE;

    private class EventCaptor implements EventStreamTransformation {
        List<InputEvent> mEvents = new ArrayList<>();

        @Override
        public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
            mEvents.add(event.copy());
        }

        @Override
        public void onKeyEvent(KeyEvent event, int policyFlags) {
            mEvents.add(event.copy());
        }

        @Override
        public void setNext(EventStreamTransformation next) {
        }

        @Override
        public EventStreamTransformation getNext() {
            return null;
        }

        @Override
        public void clearEvents(int inputSource) {
            clear();
        }

        private void clear() {
            mEvents.clear();
        }
    }

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        Context context = InstrumentationRegistry.getContext();

        setDisplayCount(1);
        mAms = spy(new AccessibilityManagerService(context));
        mMockMagnificationController = mock(MagnificationController.class);
        mA11yInputFilter = new AccessibilityInputFilter(context, mAms, mEventHandler);
        mA11yInputFilter.onInstalled();

        when(mAms.getValidDisplayList()).thenReturn(mDisplayList);
        when(mAms.getMagnificationController()).thenReturn(mMockMagnificationController);
    }

    @After
    public void tearDown() {
        mA11yInputFilter.onUninstalled();
    }

    @Test
    public void testEventHandler_shouldChangeAfterSetUserAndEnabledFeatures() {
        prepareLooper();

        // Check if there is no mEventHandler when no feature is set.
        assertEquals(0, mEventHandler.size());

        // Check if mEventHandler is added/removed after setting a11y features.
        mA11yInputFilter.setUserAndEnabledFeatures(0, mFeatures);
        assertEquals(1, mEventHandler.size());

        mA11yInputFilter.setUserAndEnabledFeatures(0, 0);
        assertEquals(0, mEventHandler.size());
    }

    @Test
    public void testEventHandler_shouldChangeAfterOnDisplayChanged() {
        prepareLooper();

        // Check if there is only one mEventHandler when there is one default display.
        mA11yInputFilter.setUserAndEnabledFeatures(0, mFeatures);
        assertEquals(1, mEventHandler.size());

        // Check if it has correct numbers of mEventHandler for corresponding displays.
        setDisplayCount(4);
        mA11yInputFilter.onDisplayChanged();
        assertEquals(4, mEventHandler.size());

        setDisplayCount(2);
        mA11yInputFilter.onDisplayChanged();
        assertEquals(2, mEventHandler.size());
    }

    @Test
    public void testEventHandler_shouldHaveCorrectOrderForEventStreamTransformation() {
        prepareLooper();

        setDisplayCount(2);
        mA11yInputFilter.setUserAndEnabledFeatures(0, mFeatures);
        assertEquals(2, mEventHandler.size());

        // Check if mEventHandler for each display has correct order of the
        // EventStreamTransformations.
        EventStreamTransformation next = mEventHandler.get(DEFAULT_DISPLAY);
        for (int i = 0; next != null; i++) {
            assertEquals(next.getClass(), mExpectedEventHandlerTypes[i]);
            next = next.getNext();
        }

        next = mEventHandler.get(SECOND_DISPLAY);
        // Start from index 1 because KeyboardInterceptor only exists in EventHandler for
        // DEFAULT_DISPLAY.
        for (int i = 1; next != null; i++) {
            assertEquals(next.getClass(), mExpectedEventHandlerTypes[i]);
            next = next.getNext();
        }
    }

    @Test
    public void testInputEvent_shouldDispatchToCorrespondingEventHandlers() {
        prepareLooper();

        setDisplayCount(2);
        mA11yInputFilter.setUserAndEnabledFeatures(0, mFeatures);
        assertEquals(2, mEventHandler.size());

        mCaptor1 = new EventCaptor();
        mCaptor2 = new EventCaptor();
        mEventHandler.put(DEFAULT_DISPLAY, mCaptor1);
        mEventHandler.put(SECOND_DISPLAY, mCaptor2);

        // InputEvent with different displayId should be dispatched to corresponding EventHandler.
        send(downEvent(DEFAULT_DISPLAY, InputDevice.SOURCE_TOUCHSCREEN));
        send(downEvent(SECOND_DISPLAY, InputDevice.SOURCE_TOUCHSCREEN));

        assertEquals(1, mCaptor1.mEvents.size());
        assertEquals(1, mCaptor2.mEvents.size());
    }

    @Test
    public void testInputEvent_shouldClearEventsForAllEventHandlers() {
        prepareLooper();

        mA11yInputFilter.setUserAndEnabledFeatures(0, mFeatures);
        assertEquals(1, mEventHandler.size());

        mCaptor1 = new EventCaptor();
        mEventHandler.put(DEFAULT_DISPLAY, mCaptor1);

        send(downEvent(DEFAULT_DISPLAY, InputDevice.SOURCE_TOUCHSCREEN));
        send(downEvent(DEFAULT_DISPLAY, InputDevice.SOURCE_TOUCHSCREEN));
        assertEquals(2, mCaptor1.mEvents.size());

        // InputEvent with different input source should trigger clearEvents() for each
        // EventStreamTransformation in EventHandler.
        send(downEvent(DEFAULT_DISPLAY, InputDevice.SOURCE_MOUSE));
        assertEquals(1, mCaptor1.mEvents.size());
    }

    private static void prepareLooper() {
        if (Looper.myLooper() == null) {
            Looper.prepare();
        }
    }

    private Display createStubDisplay(DisplayInfo displayInfo) {
        final int displayId = sNextDisplayId++;
        final Display display = new Display(DisplayManagerGlobal.getInstance(), displayId,
                displayInfo, DEFAULT_DISPLAY_ADJUSTMENTS);
        return display;
    }

    private void setDisplayCount(int count) {
        sNextDisplayId = DEFAULT_DISPLAY;
        mDisplayList.clear();
        for (int i = 0; i < count; i++) {
            mDisplayList.add(createStubDisplay(new DisplayInfo()));
        }
    }

    private void send(InputEvent event) {
        mA11yInputFilter.onInputEvent(event, /* policyFlags */ FLAG_PASS_TO_USER);
    }

    private MotionEvent downEvent(int displayId, int source) {
        mLastDownTime = SystemClock.uptimeMillis();
        final MotionEvent ev = MotionEvent.obtain(mLastDownTime, mLastDownTime,
                MotionEvent.ACTION_DOWN, DEFAULT_X, DEFAULT_Y, 0);
        ev.setDisplayId(displayId);
        ev.setSource(source);
        return ev;
    }
}
