/*
 * 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 com.android.car;

import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;

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

import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyFloat;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import static java.lang.Integer.toHexString;

import android.car.hardware.property.ICarPropertyEventListener;
import android.hardware.automotive.vehicle.VehicleAreaWheel;
import android.hardware.automotive.vehicle.VehicleGear;
import android.hardware.automotive.vehicle.VehiclePropValue;
import android.hardware.automotive.vehicle.VehicleProperty;
import android.hardware.automotive.vehicle.VehiclePropertyAccess;
import android.hardware.automotive.vehicle.VehiclePropertyChangeMode;
import android.os.IBinder;
import android.os.ServiceSpecificException;
import android.os.SystemClock;
import android.util.Log;

import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.MediumTest;

import com.android.car.hal.test.AidlMockedVehicleHal.VehicleHalPropertyHandler;
import com.android.car.hal.test.AidlVehiclePropValueBuilder;
import com.android.car.internal.property.CarSubscription;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;

import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Test for {@link com.android.car.CarPropertyService}
 */
@RunWith(AndroidJUnit4.class)
@MediumTest
public class CarPropertyServiceTest extends MockedCarTestBase {
    private static final String TAG = CarPropertyServiceTest.class.getSimpleName();

    private final Map<Integer, VehiclePropValue> mDefaultPropValues = new HashMap<>();

    private CarPropertyService mService;

    @Mock
    private VehicleHalPropertyHandler mMockPropertyHandler;

    // This is a zoned continuous property with two areas used by subscription testing.
    private static final int TEST_SUBSCRIBE_PROP = VehicleProperty.TIRE_PRESSURE;

    public CarPropertyServiceTest() {
        // Unusual default values for the vehicle properties registered to listen via
        // CarPropertyService.registerListener. Unusual default values like the car is in motion,
        // night mode is on, or the car is low on fuel.
        mDefaultPropValues.put(VehicleProperty.GEAR_SELECTION,
                AidlVehiclePropValueBuilder.newBuilder(VehicleProperty.GEAR_SELECTION)
                .addIntValues(VehicleGear.GEAR_DRIVE)
                .setTimestamp(SystemClock.elapsedRealtimeNanos()).build());
        mDefaultPropValues.put(VehicleProperty.PARKING_BRAKE_ON,
                AidlVehiclePropValueBuilder.newBuilder(VehicleProperty.PARKING_BRAKE_ON)
                .setBooleanValue(false)
                .setTimestamp(SystemClock.elapsedRealtimeNanos()).build());
        mDefaultPropValues.put(VehicleProperty.PERF_VEHICLE_SPEED,
                AidlVehiclePropValueBuilder.newBuilder(VehicleProperty.PERF_VEHICLE_SPEED)
                .addFloatValues(30.0f)
                .setTimestamp(SystemClock.elapsedRealtimeNanos()).build());
        mDefaultPropValues.put(VehicleProperty.NIGHT_MODE,
                AidlVehiclePropValueBuilder.newBuilder(VehicleProperty.NIGHT_MODE)
                .setBooleanValue(true)
                .setTimestamp(SystemClock.elapsedRealtimeNanos()).build());
    }

    @Override
    protected void configureMockedHal() {
        PropertyHandler handler = new PropertyHandler();
        for (VehiclePropValue value : mDefaultPropValues.values()) {
            handler.onPropertySet(value);
            addAidlProperty(value.prop, handler);
        }
        addAidlProperty(TEST_SUBSCRIBE_PROP, mMockPropertyHandler)
                .setChangeMode(VehiclePropertyChangeMode.CONTINUOUS)
                .setAccess(VehiclePropertyAccess.READ)
                .addAreaConfig(VehicleAreaWheel.LEFT_FRONT)
                .addAreaConfig(VehicleAreaWheel.RIGHT_FRONT)
                .setMinSampleRate(0f)
                .setMaxSampleRate(100f);
    }

    @Override
    protected void spyOnBeforeCarImplInit(ICarImpl carImpl) {
        mService = CarLocalServices.getService(CarPropertyService.class);
        assertThat(mService).isNotNull();
        spyOn(mService);
    }

    @Test
    public void testMatchesDefaultPropertyValues() {
        Set<Integer> expectedPropIds = mDefaultPropValues.keySet();
        ArgumentCaptor<Integer> propIdCaptor = ArgumentCaptor.forClass(Integer.class);
        verify(mService, atLeast(expectedPropIds.size())).registerListener(
                propIdCaptor.capture(), anyFloat(), any());

        Set<Integer> actualPropIds = new HashSet<Integer>(propIdCaptor.getAllValues());
        assertWithMessage("Should assign default values for missing property IDs")
                .that(expectedPropIds).containsAtLeastElementsIn(actualPropIds.toArray());
        assertWithMessage("Missing registerListener for property IDs")
                .that(actualPropIds).containsAtLeastElementsIn(expectedPropIds.toArray());
    }

    @Test
    public void testregisterListener() {
        CarSubscription options = new CarSubscription();
        options.propertyId = TEST_SUBSCRIBE_PROP;
        options.areaIds = new int[]{
                android.car.VehicleAreaWheel.WHEEL_LEFT_FRONT,
                android.car.VehicleAreaWheel.WHEEL_RIGHT_FRONT
        };
        options.updateRateHz = 10f;
        ICarPropertyEventListener mockHandler = mock(ICarPropertyEventListener.class);
        IBinder mockBinder = mock(IBinder.class);
        when(mockHandler.asBinder()).thenReturn(mockBinder);
        // This is for initial get value requests.
        when(mMockPropertyHandler.onPropertyGet(any())).thenReturn(
                AidlVehiclePropValueBuilder.newBuilder(TEST_SUBSCRIBE_PROP)
                    .addFloatValues(1.23f)
                    .setTimestamp(SystemClock.elapsedRealtimeNanos()).build());

        mService.registerListener(List.of(options), mockHandler);

        ArgumentCaptor<int[]> areaIdsCaptor = ArgumentCaptor.forClass(int[].class);

        verify(mMockPropertyHandler).onPropertySubscribe(eq(TEST_SUBSCRIBE_PROP),
                areaIdsCaptor.capture(), eq(10f));
        assertWithMessage("Received expected areaIds for subscription")
                .that(areaIdsCaptor.getValue()).asList().containsExactly(
                        VehicleAreaWheel.LEFT_FRONT, VehicleAreaWheel.RIGHT_FRONT);
    }

    @Test
    public void testregisterListener_exceptionAndRetry() {
        CarSubscription options = new CarSubscription();
        options.propertyId = TEST_SUBSCRIBE_PROP;
        options.areaIds = new int[]{
                android.car.VehicleAreaWheel.WHEEL_LEFT_FRONT,
                android.car.VehicleAreaWheel.WHEEL_RIGHT_FRONT
        };
        options.updateRateHz = 10f;
        ICarPropertyEventListener mockHandler = mock(ICarPropertyEventListener.class);
        IBinder mockBinder = mock(IBinder.class);
        when(mockHandler.asBinder()).thenReturn(mockBinder);
        // This is for initial get value requests.
        when(mMockPropertyHandler.onPropertyGet(any())).thenReturn(
                AidlVehiclePropValueBuilder.newBuilder(TEST_SUBSCRIBE_PROP)
                    .addFloatValues(1.23f)
                    .setTimestamp(SystemClock.elapsedRealtimeNanos()).build());
        doThrow(new ServiceSpecificException(0)).when(mMockPropertyHandler).onPropertySubscribe(
                anyInt(), any(), anyFloat());

        assertThrows(ServiceSpecificException.class, () ->
                mService.registerListener(List.of(options), mockHandler));

        // Simulate the error goes away.
        doNothing().when(mMockPropertyHandler).onPropertySubscribe(anyInt(), any(), anyFloat());

        ArgumentCaptor<int[]> areaIdsCaptor = ArgumentCaptor.forClass(int[].class);

        // Retry.
        mService.registerListener(List.of(options), mockHandler);

        // The retry must reach VHAL.
        verify(mMockPropertyHandler, times(2)).onPropertySubscribe(eq(TEST_SUBSCRIBE_PROP),
                areaIdsCaptor.capture(), eq(10f));
        assertWithMessage("Received expected areaIds for subscription")
                .that(areaIdsCaptor.getValue()).asList().containsExactly(
                        VehicleAreaWheel.LEFT_FRONT, VehicleAreaWheel.RIGHT_FRONT);
    }

    @Test
    public void testUnregisterListener() {
        CarSubscription options = new CarSubscription();
        options.propertyId = TEST_SUBSCRIBE_PROP;
        options.areaIds = new int[]{
                android.car.VehicleAreaWheel.WHEEL_LEFT_FRONT,
                android.car.VehicleAreaWheel.WHEEL_RIGHT_FRONT
        };
        options.updateRateHz = 10f;
        ICarPropertyEventListener mockHandler = mock(ICarPropertyEventListener.class);
        IBinder mockBinder = mock(IBinder.class);
        when(mockHandler.asBinder()).thenReturn(mockBinder);
        // This is for initial get value requests.
        when(mMockPropertyHandler.onPropertyGet(any())).thenReturn(
                AidlVehiclePropValueBuilder.newBuilder(TEST_SUBSCRIBE_PROP)
                    .addFloatValues(1.23f)
                    .setTimestamp(SystemClock.elapsedRealtimeNanos()).build());

        mService.registerListener(List.of(options), mockHandler);

        verify(mMockPropertyHandler).onPropertySubscribe(eq(TEST_SUBSCRIBE_PROP), any(), eq(10f));

        mService.unregisterListener(TEST_SUBSCRIBE_PROP, mockHandler);

        verify(mMockPropertyHandler).onPropertyUnsubscribe(TEST_SUBSCRIBE_PROP);
    }

    @Test
    public void testUnregisterListener_exceptionAndRetry() {
        CarSubscription options = new CarSubscription();
        options.propertyId = TEST_SUBSCRIBE_PROP;
        options.areaIds = new int[]{
                android.car.VehicleAreaWheel.WHEEL_LEFT_FRONT,
                android.car.VehicleAreaWheel.WHEEL_RIGHT_FRONT
        };
        options.updateRateHz = 10f;
        ICarPropertyEventListener mockHandler = mock(ICarPropertyEventListener.class);
        IBinder mockBinder = mock(IBinder.class);
        when(mockHandler.asBinder()).thenReturn(mockBinder);
        // This is for initial get value requests.
        when(mMockPropertyHandler.onPropertyGet(any())).thenReturn(
                AidlVehiclePropValueBuilder.newBuilder(TEST_SUBSCRIBE_PROP)
                    .addFloatValues(1.23f)
                    .setTimestamp(SystemClock.elapsedRealtimeNanos()).build());
        // The first unsubscribe will throw exception, then the error goes away.
        doThrow(new ServiceSpecificException(0)).doNothing()
                .when(mMockPropertyHandler).onPropertyUnsubscribe(anyInt());

        mService.registerListener(List.of(options), mockHandler);

        verify(mMockPropertyHandler).onPropertySubscribe(eq(TEST_SUBSCRIBE_PROP), any(), eq(10f));

        assertThrows(ServiceSpecificException.class, () ->
                mService.unregisterListener(TEST_SUBSCRIBE_PROP, mockHandler));

        // Retry.
        mService.unregisterListener(TEST_SUBSCRIBE_PROP, mockHandler);

        // The retry must reach VHAL.
        verify(mMockPropertyHandler, times(2)).onPropertyUnsubscribe(TEST_SUBSCRIBE_PROP);
    }

    private static final class PropertyHandler implements VehicleHalPropertyHandler {
        private final Map<Integer, VehiclePropValue> mMap = new HashMap<>();

        @Override
        public synchronized void onPropertySet(VehiclePropValue value) {
            mMap.put(value.prop, value);
        }

        @Override
        public synchronized VehiclePropValue onPropertyGet(VehiclePropValue value) {
            assertWithMessage("onPropertyGet missing property: %s", toHexString(value.prop))
                    .that(mMap).containsKey(value.prop);
            VehiclePropValue currentValue = mMap.get(value.prop);
            return currentValue != null ? currentValue : value;
        }

        @Override
        public synchronized void onPropertySubscribe(int property, float sampleRate) {
            assertWithMessage("onPropertySubscribe missing property: %s", toHexString(property))
                    .that(mMap).containsKey(property);
            Log.d(TAG, "onPropertySubscribe property "
                    + property + " sampleRate " + sampleRate);
        }

        @Override
        public synchronized void onPropertyUnsubscribe(int property) {
            assertWithMessage("onPropertyUnsubscribe missing property: %s", toHexString(property))
                    .that(mMap).containsKey(property);
            Log.d(TAG, "onPropertyUnSubscribe property " + property);
        }
    }
}