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

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

import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import android.car.experimental.DriverAwarenessEvent;
import android.car.occupantawareness.GazeDetection;
import android.car.occupantawareness.OccupantAwarenessDetection;
import android.content.Context;
import android.content.Intent;
import android.os.IBinder;

import androidx.test.core.app.ApplicationProvider;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.rule.ServiceTestRule;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;

@RunWith(MockitoJUnitRunner.class)
public class GazeDriverAwarenessSupplierTest {

    private static final long START_TIME_MILLIS = 1234L;
    private static final long FRAME_TIME_MILLIS = 1000L;

    private Context mSpyContext;
    private GazeDriverAwarenessSupplier mGazeSupplier;
    private float mInitialValue;
    private float mGrowthRate;
    private float mDecayRate;
    private FakeTimeSource mTimeSource;

    @Rule
    public final ServiceTestRule serviceRule = new ServiceTestRule();

    @Before
    public void setUp() throws Exception {
        mSpyContext = spy(InstrumentationRegistry.getInstrumentation().getTargetContext());

        mInitialValue =
                mSpyContext
                        .getResources()
                        .getFloat(R.fraction.driverAwarenessGazeModelInitialValue);
        mGrowthRate =
                mSpyContext.getResources().getFloat(R.fraction.driverAwarenessGazeModelGrowthRate);
        mDecayRate =
                mSpyContext.getResources().getFloat(R.fraction.driverAwarenessGazeModelDecayRate);

        mTimeSource = new FakeTimeSource(START_TIME_MILLIS);
        mGazeSupplier = spy(new GazeDriverAwarenessSupplier(mSpyContext, mTimeSource));
    }

    @Test
    public void testWithBoundService() throws Exception {
        Intent serviceIntent =
                new Intent(ApplicationProvider.getApplicationContext(),
                        GazeDriverAwarenessSupplier.class);

        // Bind the service and grab a reference to the binder.
        IBinder binder = serviceRule.bindService(serviceIntent);
        assertThat(binder instanceof GazeDriverAwarenessSupplier.SupplierBinder).isTrue();
    }

    @Test
    public void testonReady_initialCallbackIsGenerated() throws Exception {
        // Supplier should return an initial callback after onReady().
        mGazeSupplier.onReady();

        verify(mGazeSupplier)
                .emitAwarenessEvent(new DriverAwarenessEvent(START_TIME_MILLIS, mInitialValue));
    }

    @Test
    public void testprocessDetectionEvent_noGazeDataProvided() throws Exception {
        // If detection events happen with *no gaze*, no further events should be generated by the
        // attention supplier.
        mGazeSupplier.onReady();

        mGazeSupplier.processDetectionEvent(buildEmptyDetection(START_TIME_MILLIS));

        // Should have exactly one call from the initial onReady(), but no further events.
        verify(mGazeSupplier, times(1))
                .emitAwarenessEvent(new DriverAwarenessEvent(START_TIME_MILLIS, mInitialValue));
    }

    @Test
    public void testprocessDetectionEvent_neverExceedsOne() throws Exception {
        // Attention value should never exceed '1' no matter how long the driver looks on-road.
        mGazeSupplier.onReady();

        // Should have initial callback from onReady().
        verify(mGazeSupplier)
                .emitAwarenessEvent(new DriverAwarenessEvent(START_TIME_MILLIS, mInitialValue));

        long timestamp = START_TIME_MILLIS + FRAME_TIME_MILLIS;
        float attention = mInitialValue;

        for (int i = 0; i < 100; i++) {
            OccupantAwarenessDetection detection =
                    buildGazeDetection(timestamp, GazeDetection.VEHICLE_REGION_FORWARD_ROADWAY);
            mGazeSupplier.processDetectionEvent(detection);

            verify(mGazeSupplier)
                    .emitAwarenessEvent(new DriverAwarenessEvent(timestamp, attention));

            // Increase attention, but not past 1.
            attention = Math.min(attention + mGrowthRate, 1.0f);
            timestamp += FRAME_TIME_MILLIS;
        }
    }

    @Test
    public void testprocessDetectionEvent_neverFallsBelowZero() throws Exception {
        // Attention value should never fall below '0' no matter how long the driver looks off-road.
        mGazeSupplier.onReady();

        // Should have initial callback from onReady().
        verify(mGazeSupplier)
                .emitAwarenessEvent(new DriverAwarenessEvent(START_TIME_MILLIS, mInitialValue));

        long timestamp = START_TIME_MILLIS + FRAME_TIME_MILLIS;
        float attention = mInitialValue;

        for (int i = 0; i < 100; i++) {
            OccupantAwarenessDetection detection =
                    buildGazeDetection(timestamp, GazeDetection.VEHICLE_REGION_HEAD_UNIT_DISPLAY);
            mGazeSupplier.processDetectionEvent(detection);

            verify(mGazeSupplier)
                    .emitAwarenessEvent(new DriverAwarenessEvent(timestamp, attention));

            // Decrement the attention, but not past 0.
            attention = Math.max(attention - mDecayRate, 0);
            timestamp += FRAME_TIME_MILLIS;
        }
    }

    /** Builds a {link OccupantAwarenessDetection} with the specified target for testing. */
    private OccupantAwarenessDetection buildGazeDetection(
            long timestamp, @GazeDetection.VehicleRegion int gazeTarget) {
        GazeDetection gaze =
                new GazeDetection(
                        OccupantAwarenessDetection.CONFIDENCE_LEVEL_HIGH,
                        null /*leftEyePosition*/,
                        null /*rightEyePosition*/,
                        null /*headAngleUnitVector*/,
                        null /*gazeAngleUnitVector*/,
                        gazeTarget,
                        FRAME_TIME_MILLIS);

        return new OccupantAwarenessDetection(
                OccupantAwarenessDetection.VEHICLE_OCCUPANT_DRIVER, timestamp, true, gaze, null);
    }

    /** Builds a {link OccupantAwarenessDetection} with the specified target for testing. */
    private OccupantAwarenessDetection buildEmptyDetection(long timestamp) {
        return new OccupantAwarenessDetection(
                OccupantAwarenessDetection.VEHICLE_OCCUPANT_DRIVER, timestamp, true, null, null);
    }
}
