/*
 * Copyright (C) 2019 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 android.car;

import static android.car.CarProjectionManager.ProjectionAccessPointCallback.ERROR_GENERIC;

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

import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.robolectric.Shadows.shadowOf;

import android.car.CarProjectionManager.ProjectionAccessPointCallback;
import android.car.testapi.CarProjectionController;
import android.car.testapi.FakeCar;
import android.content.Context;
import android.content.Intent;
import android.net.MacAddress;
import android.net.wifi.SoftApConfiguration;
import android.net.wifi.WifiConfiguration;
import android.os.Looper;
import android.util.ArraySet;

import androidx.test.core.app.ApplicationProvider;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InOrder;
import org.mockito.Spy;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.internal.DoNotInstrument;

import java.util.Arrays;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;

@RunWith(RobolectricTestRunner.class)
@DoNotInstrument
public class CarProjectionManagerTest {
    @Rule
    public MockitoRule rule = MockitoJUnit.rule();

    @Captor
    private ArgumentCaptor<Intent> mIntentArgumentCaptor;

    @Spy
    private final Context mContext = ApplicationProvider.getApplicationContext();

    private static final int DEFAULT_TIMEOUT_MS = 1000;

    /** An {@link Executor} that immediately runs its callbacks synchronously. */
    private static final Executor DIRECT_EXECUTOR = Runnable::run;

    private CarProjectionManager mProjectionManager;
    private CarProjectionController mController;
    private ApCallback mApCallback;

    @Before
    public void setUp() {
        FakeCar fakeCar = FakeCar.createFakeCar(mContext);
        mController = fakeCar.getCarProjectionController();
        mProjectionManager =
                (CarProjectionManager) fakeCar.getCar().getCarManager(Car.PROJECTION_SERVICE);
        assertThat(mProjectionManager).isNotNull();

        mApCallback = new ApCallback();
    }

    @Test
    public void startAp_fail() throws InterruptedException {
        mController.setSoftApConfiguration(null);

        mProjectionManager.startProjectionAccessPoint(mApCallback);
        shadowOf(Looper.getMainLooper()).idle();
        mApCallback.mFailed.await(DEFAULT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
        assertThat(mApCallback.mFailureReason).isEqualTo(ERROR_GENERIC);
    }

    @Test
    public void startAp_success() throws InterruptedException {
        SoftApConfiguration config = new SoftApConfiguration.Builder()
                .setSsid("Hello")
                .setBssid(MacAddress.fromString("AA:BB:CC:CC:DD:EE"))
                .setPassphrase("password", SoftApConfiguration.SECURITY_TYPE_WPA2_PSK)
                .setMacRandomizationSetting(SoftApConfiguration.RANDOMIZATION_NONE)
                .build();
        mController.setSoftApConfiguration(config);

        mProjectionManager.startProjectionAccessPoint(mApCallback);
        shadowOf(Looper.getMainLooper()).idle();
        mApCallback.mStarted.await(DEFAULT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
        assertThat(mApCallback.mSoftApConfiguration).isEqualTo(config);
    }

    @Test
    public void startAp_success_setWifiConfiguration() throws InterruptedException {
        SoftApConfiguration config = new SoftApConfiguration.Builder()
                .setSsid("Hello")
                .setBssid(MacAddress.fromString("AA:BB:CC:CC:DD:EE"))
                .setPassphrase("password", SoftApConfiguration.SECURITY_TYPE_WPA2_PSK)
                .setMacRandomizationSetting(SoftApConfiguration.RANDOMIZATION_NONE)
                .build();
        WifiConfiguration wifiConfig = config.toWifiConfiguration();
        mController.setWifiConfiguration(wifiConfig);

        mProjectionManager.startProjectionAccessPoint(mApCallback);
        shadowOf(Looper.getMainLooper()).idle();
        mApCallback.mStarted.await(DEFAULT_TIMEOUT_MS, TimeUnit.MILLISECONDS);

        assertThat(mApCallback.mSoftApConfiguration).isNull();
        assertThat(mApCallback.mWifiConfiguration).isEqualTo(wifiConfig);
    }

    @Test
    public void registerProjectionRunner() throws CarNotConnectedException {
        Intent intent = new Intent("my_action");
        intent.setPackage("my.package");
        mProjectionManager.registerProjectionRunner(intent);

        verify(mContext).bindService(mIntentArgumentCaptor.capture(), any(),
                eq(Context.BIND_AUTO_CREATE));
        assertThat(mIntentArgumentCaptor.getValue()).isEqualTo(intent);
    }

    @Test
    public void keyEventListener_registerMultipleEventListeners() {
        CarProjectionManager.ProjectionKeyEventHandler eventHandler1 =
                mock(CarProjectionManager.ProjectionKeyEventHandler.class);
        CarProjectionManager.ProjectionKeyEventHandler eventHandler2 =
                mock(CarProjectionManager.ProjectionKeyEventHandler.class);

        mProjectionManager.addKeyEventHandler(
                Collections.singleton(CarProjectionManager.KEY_EVENT_CALL_SHORT_PRESS_KEY_UP),
                DIRECT_EXECUTOR,
                eventHandler1);
        mProjectionManager.addKeyEventHandler(
                new ArraySet<>(
                        Arrays.asList(
                                CarProjectionManager.KEY_EVENT_CALL_SHORT_PRESS_KEY_UP,
                                CarProjectionManager.KEY_EVENT_CALL_LONG_PRESS_KEY_DOWN)),
                DIRECT_EXECUTOR,
                eventHandler2);

        mController.fireKeyEvent(CarProjectionManager.KEY_EVENT_CALL_SHORT_PRESS_KEY_UP);
        verify(eventHandler1).onKeyEvent(CarProjectionManager.KEY_EVENT_CALL_SHORT_PRESS_KEY_UP);
        verify(eventHandler2).onKeyEvent(CarProjectionManager.KEY_EVENT_CALL_SHORT_PRESS_KEY_UP);

        mController.fireKeyEvent(CarProjectionManager.KEY_EVENT_CALL_LONG_PRESS_KEY_DOWN);
        verify(eventHandler1, never())
                .onKeyEvent(CarProjectionManager.KEY_EVENT_CALL_LONG_PRESS_KEY_DOWN);
        verify(eventHandler2).onKeyEvent(CarProjectionManager.KEY_EVENT_CALL_LONG_PRESS_KEY_DOWN);

        mController.fireKeyEvent(CarProjectionManager.KEY_EVENT_CALL_KEY_DOWN);
        verify(eventHandler1, never()).onKeyEvent(CarProjectionManager.KEY_EVENT_CALL_KEY_DOWN);
        verify(eventHandler2, never()).onKeyEvent(CarProjectionManager.KEY_EVENT_CALL_KEY_DOWN);
    }

    @Test
    public void keyEventHandler_canRegisterAllEvents() {
        CarProjectionManager.ProjectionKeyEventHandler eventHandler =
                mock(CarProjectionManager.ProjectionKeyEventHandler.class);

        Set<Integer> events = new ArraySet<>(CarProjectionManager.NUM_KEY_EVENTS);
        for (int evt = 0; evt < CarProjectionManager.NUM_KEY_EVENTS; evt++) {
            events.add(evt);
        }

        mProjectionManager.addKeyEventHandler(events, DIRECT_EXECUTOR, eventHandler);

        for (int evt : events) {
            mController.fireKeyEvent(evt);
            verify(eventHandler).onKeyEvent(evt);
        }
    }

    @Test
    public void keyEventHandler_eventsOutOfRange_throw() {
        CarProjectionManager.ProjectionKeyEventHandler eventHandler =
                mock(CarProjectionManager.ProjectionKeyEventHandler.class);

        try {
            mProjectionManager.addKeyEventHandler(Collections.singleton(-1), eventHandler);
            fail();
        } catch (IllegalArgumentException expected) { }

        try {
            mProjectionManager.addKeyEventHandler(
                    Collections.singleton(CarProjectionManager.NUM_KEY_EVENTS), eventHandler);
            fail();
        } catch (IllegalArgumentException expected) { }
    }

    @Test
    public void keyEventHandler_whenRegisteredAgain_replacesEventList() {
        CarProjectionManager.ProjectionKeyEventHandler eventHandler =
                mock(CarProjectionManager.ProjectionKeyEventHandler.class);
        InOrder inOrder = inOrder(eventHandler);

        mProjectionManager.addKeyEventHandler(
                Collections.singleton(CarProjectionManager.KEY_EVENT_CALL_SHORT_PRESS_KEY_UP),
                DIRECT_EXECUTOR,
                eventHandler);
        mController.fireKeyEvent(CarProjectionManager.KEY_EVENT_CALL_SHORT_PRESS_KEY_UP);
        inOrder.verify(eventHandler)
                .onKeyEvent(CarProjectionManager.KEY_EVENT_CALL_SHORT_PRESS_KEY_UP);

        mProjectionManager.addKeyEventHandler(
                Collections.singleton(CarProjectionManager.KEY_EVENT_CALL_LONG_PRESS_KEY_DOWN),
                DIRECT_EXECUTOR,
                eventHandler);
        mController.fireKeyEvent(CarProjectionManager.KEY_EVENT_CALL_SHORT_PRESS_KEY_UP);
        inOrder.verify(eventHandler, never())
                .onKeyEvent(CarProjectionManager.KEY_EVENT_CALL_SHORT_PRESS_KEY_UP);
    }

    @Test
    public void keyEventHandler_removed_noLongerFires() {
        CarProjectionManager.ProjectionKeyEventHandler eventHandler =
                mock(CarProjectionManager.ProjectionKeyEventHandler.class);

        mProjectionManager.addKeyEventHandler(
                Collections.singleton(CarProjectionManager.KEY_EVENT_CALL_SHORT_PRESS_KEY_UP),
                DIRECT_EXECUTOR,
                eventHandler);
        mProjectionManager.removeKeyEventHandler(eventHandler);

        mController.fireKeyEvent(CarProjectionManager.KEY_EVENT_CALL_SHORT_PRESS_KEY_UP);
        verify(eventHandler, never())
                .onKeyEvent(CarProjectionManager.KEY_EVENT_CALL_SHORT_PRESS_KEY_UP);
    }

    @Test
    public void keyEventHandler_withAlternateExecutor_usesExecutor() {
        CarProjectionManager.ProjectionKeyEventHandler eventHandler =
                mock(CarProjectionManager.ProjectionKeyEventHandler.class);
        Executor executor = mock(Executor.class);
        ArgumentCaptor<Runnable> runnableCaptor = ArgumentCaptor.forClass(Runnable.class);

        mProjectionManager.addKeyEventHandler(
                Collections.singleton(
                        CarProjectionManager.KEY_EVENT_VOICE_SEARCH_SHORT_PRESS_KEY_UP),
                executor,
                eventHandler);

        mController.fireKeyEvent(CarProjectionManager.KEY_EVENT_VOICE_SEARCH_SHORT_PRESS_KEY_UP);
        verify(eventHandler, never()).onKeyEvent(anyInt());
        verify(executor).execute(runnableCaptor.capture());

        runnableCaptor.getValue().run();
        verify(eventHandler)
                .onKeyEvent(CarProjectionManager.KEY_EVENT_VOICE_SEARCH_SHORT_PRESS_KEY_UP);
    }

    private static class ApCallback extends ProjectionAccessPointCallback {
        CountDownLatch mStarted = new CountDownLatch(1);
        CountDownLatch mFailed = new CountDownLatch(1);
        int mFailureReason = -1;
        SoftApConfiguration mSoftApConfiguration;
        WifiConfiguration mWifiConfiguration;

        @Override
        public void onStarted(WifiConfiguration wifiConfiguration) {
            mWifiConfiguration = wifiConfiguration;
            mStarted.countDown();
        }

        @Override
        public void onStarted(SoftApConfiguration softApConfiguration) {
            mSoftApConfiguration = softApConfiguration;
            mStarted.countDown();
        }

        @Override
        public void onStopped() {
        }

        @Override
        public void onFailed(int reason) {
            mFailureReason = reason;
            mFailed.countDown();
        }
    }
}
