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

import static com.android.car.CarServiceUtils.toByteArray;
import static com.android.car.CarServiceUtils.toIntArray;

import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;

import android.car.hardware.property.VehicleHalStatusCode;
import android.car.test.util.TemporaryFile;
import android.car.vms.VmsAssociatedLayer;
import android.car.vms.VmsAvailableLayers;
import android.car.vms.VmsClient;
import android.car.vms.VmsClientManager.VmsClientCallback;
import android.car.vms.VmsLayer;
import android.car.vms.VmsLayerDependency;
import android.car.vms.VmsSubscriptionState;
import android.content.Context;
import android.content.res.Resources;
import android.hardware.automotive.vehicle.VehiclePropConfig;
import android.hardware.automotive.vehicle.VehicleProperty;
import android.hardware.automotive.vehicle.VehiclePropertyGroup;
import android.hardware.automotive.vehicle.VmsMessageType;
import android.os.Handler;
import android.os.ServiceSpecificException;

import com.android.car.R;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;

import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

@RunWith(MockitoJUnitRunner.class)
public class VmsHalServiceTest {
    private static final int LAYER_TYPE = 1;
    private static final int LAYER_SUBTYPE = 2;
    private static final int LAYER_VERSION = 3;
    private static final VmsLayer LAYER = new VmsLayer(LAYER_TYPE, LAYER_SUBTYPE, LAYER_VERSION);
    private static final int PUBLISHER_ID = 12345;
    private static final byte[] PAYLOAD = new byte[]{1, 2, 3, 4};
    private static final List<Byte> PAYLOAD_AS_LIST = Arrays.asList(new Byte[]{1, 2, 3, 4});
    private static final int CORE_ID = 54321;
    private static final int CLIENT_ID = 98765;

    private final HalPropValueBuilder mPropValueBuilder = new HalPropValueBuilder(/*isAidl=*/true);

    @Mock
    private Context mContext;
    @Mock
    private Resources mResources;
    @Mock
    private VehicleHal mVehicleHal;
    @Mock
    private VmsClient mVmsClient;

    private VmsHalService mHalService;
    private VmsClientCallback mEventCallback;
    private int mVmsInitCount;

    @Before
    public void setUp() throws Exception {
        initHalService(true);
    }

    private void initHalService(boolean propagatePropertyException) throws Exception {
        mVmsInitCount = 0;
        when(mContext.getResources()).thenReturn(mResources);
        when(mVehicleHal.getHalPropValueBuilder()).thenReturn(mPropValueBuilder);
        mHalService = new VmsHalService(mContext, mVehicleHal, () -> (long) CORE_ID,
                this::initVmsClient, propagatePropertyException);

        VehiclePropConfig propConfig = new VehiclePropConfig();
        propConfig.prop = VehicleProperty.VEHICLE_MAP_SERVICE;
        HalPropConfig halPropConfig = new AidlHalPropConfig(propConfig);
        mHalService.takeProperties(Collections.singleton(halPropConfig));

        when(mVmsClient.getAvailableLayers()).thenReturn(
                new VmsAvailableLayers(Collections.emptySet(), 0));
        mHalService.init();
        waitForHandlerCompletion();

        // Verify START_SESSION message was sent
        InOrder initOrder = Mockito.inOrder(mVehicleHal);
        initOrder.verify(mVehicleHal).subscribePropertySafe(mHalService,
                VehicleProperty.VEHICLE_MAP_SERVICE);
        initOrder.verify(mVehicleHal).set(createHalMessage(
                VmsMessageType.START_SESSION, // Message type
                CORE_ID,                      // Core ID
                -1));                          // Client ID (unknown)

        // Verify no more interections until handshake received
        initOrder.verifyNoMoreInteractions();

        // Send START_SESSION response from client
        sendHalMessage(createHalMessage(
                VmsMessageType.START_SESSION,  // Message type
                CORE_ID,                       // Core ID
                CLIENT_ID                      // Client ID
        ));
        waitForHandlerCompletion();

        initOrder.verify(mVehicleHal).set(createHalMessage(
                VmsMessageType.AVAILABILITY_CHANGE, // Message type
                0,                                  // Sequence number
                0));                                // # of associated layers


        waitForHandlerCompletion();
        initOrder.verifyNoMoreInteractions();
        assertEquals(1, mVmsInitCount);
        reset(mVmsClient, mVehicleHal);
    }

    private VmsClient initVmsClient(Handler handler, VmsClientCallback callback) {
        mEventCallback = callback;
        mVmsInitCount++;
        return mVmsClient;
    }

    @Test
    public void testCoreId_IntegerOverflow() throws Exception {
        when(mVehicleHal.getHalPropValueBuilder()).thenReturn(mPropValueBuilder);
        mHalService = new VmsHalService(mContext, mVehicleHal,
                () -> (long) Integer.MAX_VALUE + CORE_ID, this::initVmsClient, true);

        VehiclePropConfig propConfig = new VehiclePropConfig();
        propConfig.prop = VehicleProperty.VEHICLE_MAP_SERVICE;
        HalPropConfig halPropConfig = new AidlHalPropConfig(propConfig);
        mHalService.takeProperties(Collections.singleton(halPropConfig));

        when(mVmsClient.getAvailableLayers()).thenReturn(
                new VmsAvailableLayers(Collections.emptySet(), 0));
        mHalService.init();
        waitForHandlerCompletion();

        verify(mVehicleHal).set(createHalMessage(
                VmsMessageType.START_SESSION, // Message type
                CORE_ID,                      // Core ID
                -1));                          // Client ID (unknown)
    }

    @Test
    public void testTakeSupportedProperties() {
        VehiclePropConfig vmsPropConfig = new VehiclePropConfig();
        vmsPropConfig.prop = VehicleProperty.VEHICLE_MAP_SERVICE;
        HalPropConfig vmsHalPropConfig = new AidlHalPropConfig(vmsPropConfig);

        VehiclePropConfig otherPropConfig = new VehiclePropConfig();
        otherPropConfig.prop = VehicleProperty.CURRENT_GEAR;
        HalPropConfig otherHalPropConfig = new AidlHalPropConfig(otherPropConfig);

        mHalService.takeProperties(Arrays.asList(vmsHalPropConfig, otherHalPropConfig));
    }

    /**
     * DATA message format:
     * <ul>
     * <li>Message type
     * <li>Layer ID
     * <li>Layer subtype
     * <li>Layer version
     * <li>Publisher ID
     * <li>Payload
     * </ul>
     */
    @Test
    public void testHandleDataEvent() {
        HalPropValue message = createHalMessage(
                toByteArray(PAYLOAD_AS_LIST),
                VmsMessageType.DATA,                       // Message type
                LAYER_TYPE, LAYER_SUBTYPE, LAYER_VERSION,  // VmsLayer
                PUBLISHER_ID                               // PublisherId
        );

        sendHalMessage(message);
        verify(mVmsClient).publishPacket(PUBLISHER_ID, LAYER, PAYLOAD);
    }

    @Test
    public void testHandleInvalidEvent() {
        // Invalid message with no message type.
        HalPropValue message =  mPropValueBuilder.build(VehicleProperty.VEHICLE_MAP_SERVICE,
                /*areaId=*/0, "1234");

        sendHalMessage(message);
        verify(mVmsClient, Mockito.never()).publishPacket(
                Mockito.anyInt(), Mockito.any(), Mockito.any());

        // Incomplete VmsLayer.
        ArrayList<Integer> int32Values =
                new ArrayList<Integer>(
                        Arrays.asList(VmsMessageType.DATA, LAYER_TYPE, LAYER_SUBTYPE));
        message = mPropValueBuilder.build(VehicleProperty.VEHICLE_MAP_SERVICE,
                /*areaId=*/0, /*timestamp=*/0, /*status=*/0,
                /*int32Values=*/toIntArray(int32Values), /*floatValues=*/new float[0],
                /*int64Values=*/new long[0], "1234", /*byteValues=*/new byte[0]);

        sendHalMessage(message);
        verify(mVmsClient, Mockito.never()).publishPacket(
                Mockito.anyInt(), Mockito.any(), Mockito.any());

        // No publisher ID.
        int32Values.add(LAYER_VERSION);
        message = mPropValueBuilder.build(VehicleProperty.VEHICLE_MAP_SERVICE,
                /*areaId=*/0, /*timestamp=*/0, /*status=*/0,
                /*int32Values=*/toIntArray(int32Values), /*floatValues=*/new float[0],
                /*int64Values=*/new long[0], "1234", /*byteValues=*/new byte[0]);

        sendHalMessage(message);
        verify(mVmsClient, Mockito.never()).publishPacket(
                Mockito.anyInt(), Mockito.any(), Mockito.any());

        // No payload.
        int32Values.add(PUBLISHER_ID);
        message = mPropValueBuilder.build(VehicleProperty.VEHICLE_MAP_SERVICE,
                /*areaId=*/0, /*timestamp=*/0, /*status=*/0,
                /*int32Values=*/toIntArray(int32Values), /*floatValues=*/new float[0],
                /*int64Values=*/new long[0], "1234", /*byteValues=*/new byte[0]);

        sendHalMessage(message);
        verify(mVmsClient, Mockito.never()).publishPacket(
                Mockito.anyInt(), Mockito.any(), Mockito.any());

        // No payload for publisherID event.
        int32Values.set(0, VmsMessageType.PUBLISHER_ID_REQUEST);
        message = mPropValueBuilder.build(VehicleProperty.VEHICLE_MAP_SERVICE,
                /*areaId=*/0, /*timestamp=*/0, /*status=*/0,
                /*int32Values=*/toIntArray(int32Values), /*floatValues=*/new float[0],
                /*int64Values=*/new long[0], "1234", /*byteValues=*/new byte[0]);

        sendHalMessage(message);
        verify(mVmsClient, Mockito.never()).publishPacket(
                Mockito.anyInt(), Mockito.any(), Mockito.any());
    }

    @Test
    public void testOnPacketReceivedEvent() throws Exception {
        HalPropValue message = createHalMessage(
                toByteArray(PAYLOAD_AS_LIST),
                VmsMessageType.DATA,                       // Message type
                LAYER_TYPE, LAYER_SUBTYPE, LAYER_VERSION,  // VmsLayer
                PUBLISHER_ID                               // PublisherId
        );

        mEventCallback.onPacketReceived(PUBLISHER_ID, LAYER, PAYLOAD);
        verify(mVehicleHal).set(message);
    }

    /**
     * SUBSCRIBE message format:
     * <ul>
     * <li>Message type
     * <li>Layer ID
     * <li>Layer subtype
     * <li>Layer version
     * </ul>
     */
    @Test
    public void testHandleSubscribeEvent() {
        HalPropValue message = createHalMessage(
                VmsMessageType.SUBSCRIBE,                 // Message type
                LAYER_TYPE, LAYER_SUBTYPE, LAYER_VERSION  // VmsLayer
        );

        sendHalMessage(message);
        verify(mVmsClient).setSubscriptions(asSet(new VmsAssociatedLayer(LAYER, asSet())));
    }

    /**
     * SUBSCRIBE_TO_PUBLISHER message format:
     * <ul>
     * <li>Message type
     * <li>Layer ID
     * <li>Layer subtype
     * <li>Layer version
     * <li>Publisher ID
     * </ul>
     */
    @Test
    public void testHandleSubscribeToPublisherEvent() {
        HalPropValue message = createHalMessage(
                VmsMessageType.SUBSCRIBE_TO_PUBLISHER,     // Message type
                LAYER_TYPE, LAYER_SUBTYPE, LAYER_VERSION,  // VmsLayer
                PUBLISHER_ID                               // PublisherId
        );

        sendHalMessage(message);
        verify(mVmsClient).setSubscriptions(asSet(
                new VmsAssociatedLayer(LAYER, asSet(PUBLISHER_ID))));
    }

    /**
     * UNSUBSCRIBE message format:
     * <ul>
     * <li>Message type
     * <li>Layer ID
     * <li>Layer subtype
     * <li>Layer version
     * </ul>
     */
    @Test
    public void testHandleUnsubscribeEvent() {
        testHandleSubscribeEvent();

        HalPropValue message = createHalMessage(
                VmsMessageType.UNSUBSCRIBE,               // Message type
                LAYER_TYPE, LAYER_SUBTYPE, LAYER_VERSION  // VmsLayer
        );

        sendHalMessage(message);
        verify(mVmsClient).setSubscriptions(asSet());
    }

    /**
     * UNSUBSCRIBE_TO_PUBLISHER message format:
     * <ul>
     * <li>Message type
     * <li>Layer ID
     * <li>Layer subtype
     * <li>Layer version
     * <li>Publisher ID
     * </ul>
     */
    @Test
    public void testHandleUnsubscribeFromPublisherEvent() {
        testHandleSubscribeToPublisherEvent();

        HalPropValue message = createHalMessage(
                VmsMessageType.UNSUBSCRIBE_TO_PUBLISHER,   // Message type
                LAYER_TYPE, LAYER_SUBTYPE, LAYER_VERSION,  // VmsLayer
                PUBLISHER_ID                               // PublisherId
        );

        sendHalMessage(message);
        verify(mVmsClient).setSubscriptions(asSet());
    }

    /**
     * PUBLISHER_ID_REQUEST message format:
     * <ul>
     * <li>Message type
     * <li>Publisher info (bytes)
     * </ul>
     *
     * PUBLISHER_ID_RESPONSE message format:
     * <ul>
     * <li>Message type
     * <li>Publisher ID
     * </ul>
     */
    @Test
    public void testHandlePublisherIdRequestEvent() throws Exception {
        HalPropValue request = createHalMessage(
                toByteArray(PAYLOAD_AS_LIST),
                VmsMessageType.PUBLISHER_ID_REQUEST  // Message type
        );

        when(mVmsClient.registerProvider(PAYLOAD)).thenReturn(PUBLISHER_ID);

        HalPropValue response = createHalMessage(
                VmsMessageType.PUBLISHER_ID_RESPONSE,  // Message type
                PUBLISHER_ID                           // Publisher ID
        );

        sendHalMessage(request);
        verify(mVehicleHal).set(response);
    }

    /**
     * PUBLISHER_INFORMATION_REQUEST message format:
     * <ul>
     * <li>Message type
     * <li>Publisher ID
     * </ul>
     *
     * PUBLISHER_INFORMATION_RESPONSE message format:
     * <ul>
     * <li>Message type
     * <li>Publisher info (bytes)
     * </ul>
     */
    @Test
    public void testHandlePublisherInformationRequestEvent() throws Exception {
        HalPropValue request = createHalMessage(
                VmsMessageType.PUBLISHER_INFORMATION_REQUEST,  // Message type
                PUBLISHER_ID                                   // Publisher ID
        );

        when(mVmsClient.getProviderDescription(PUBLISHER_ID)).thenReturn(PAYLOAD);

        HalPropValue response = createHalMessage(
                toByteArray(PAYLOAD_AS_LIST),
                VmsMessageType.PUBLISHER_INFORMATION_RESPONSE  // Message type
        );

        sendHalMessage(request);
        verify(mVehicleHal).set(response);
    }

    /**
     * OFFERING message format:
     * <ul>
     * <li>Message type
     * <li>Publisher ID
     * <li>Number of offerings.
     * <li>Offerings (x number of offerings)
     * <ul>
     * <li>Layer ID
     * <li>Layer subtype
     * <li>Layer version
     * <li>Number of layer dependencies.
     * <li>Layer dependencies (x number of layer dependencies)
     * <ul>
     * <li>Layer ID
     * <li>Layer subtype
     * <li>Layer version
     * </ul>
     * </ul>
     * </ul>
     */
    @Test
    public void testHandleOfferingEvent_ZeroOfferings() {
        HalPropValue message = createHalMessage(
                VmsMessageType.OFFERING,  // Message type
                PUBLISHER_ID,             // PublisherId
                0                         // # of offerings
        );

        sendHalMessage(message);
        verify(mVmsClient).setProviderOfferings(PUBLISHER_ID, asSet());
    }

    @Test
    public void testHandleOfferingEvent_LayerOnly() {
        HalPropValue message = createHalMessage(
                VmsMessageType.OFFERING,                   // Message type
                PUBLISHER_ID,                              // PublisherId
                1,                                         // # of offerings
                // Offered layer
                LAYER_TYPE, LAYER_SUBTYPE, LAYER_VERSION,
                0                                          // # of dependencies
        );

        sendHalMessage(message);
        verify(mVmsClient).setProviderOfferings(
                PUBLISHER_ID,
                asSet(new VmsLayerDependency(LAYER)));
    }

    @Test
    public void testHandleOfferingEvent_LayerAndDependency() {
        HalPropValue message = createHalMessage(
                VmsMessageType.OFFERING,                   // Message type
                PUBLISHER_ID,                              // PublisherId
                1,                                         // # of offerings
                LAYER_TYPE, LAYER_SUBTYPE, LAYER_VERSION,  // Layer
                1,                                         // # of dependencies
                4, 5, 6                                    // Dependency layer
        );

        sendHalMessage(message);
        verify(mVmsClient).setProviderOfferings(
                PUBLISHER_ID,
                asSet(
                        new VmsLayerDependency(LAYER, Collections.singleton(
                                new VmsLayer(4, 5, 6)))));
    }

    @Test
    public void testHandleOfferingEvent_MultipleLayersAndDependencies() {
        HalPropValue message = createHalMessage(
                VmsMessageType.OFFERING,                   // Message type
                PUBLISHER_ID,                              // PublisherId
                3,                                         // # of offerings
                // Offered layer #1
                LAYER_TYPE, LAYER_SUBTYPE, LAYER_VERSION,  // Layer
                2,                                         // # of dependencies
                4, 5, 6,                                   // Dependency layer
                7, 8, 9,                                   // Dependency layer
                // Offered layer #2
                3, 2, 1,                                   // Layer
                0,                                         // # of dependencies
                // Offered layer #3
                6, 5, 4,                                   // Layer
                1,                                         // # of dependencies
                7, 8, 9                                    // Dependency layer
        );

        sendHalMessage(message);
        verify(mVmsClient).setProviderOfferings(
                PUBLISHER_ID,
                asSet(
                        new VmsLayerDependency(LAYER, new LinkedHashSet<>(Arrays.asList(
                                new VmsLayer(4, 5, 6),
                                new VmsLayer(7, 8, 9)
                        ))),
                        new VmsLayerDependency(new VmsLayer(3, 2, 1), Collections.emptySet()),
                        new VmsLayerDependency(new VmsLayer(6, 5, 4), Collections.singleton(
                                new VmsLayer(7, 8, 9)
                        ))));
    }

    /**
     * AVAILABILITY_REQUEST message format:
     * <ul>
     * <li>Message type
     * </ul>
     *
     * AVAILABILITY_RESPONSE message format:
     * <ul>
     * <li>Message type
     * <li>Sequence number.
     * <li>Number of associated layers.
     * <li>Associated layers (x number of associated layers)
     * <ul>
     * <li>Layer ID
     * <li>Layer subtype
     * <li>Layer version
     * <li>Number of publishers
     * <li>Publisher ID (x number of publishers)
     * </ul>
     * </ul>
     */
    @Test
    public void testHandleAvailabilityRequestEvent_ZeroLayers() throws Exception {
        HalPropValue request = createHalMessage(
                VmsMessageType.AVAILABILITY_REQUEST  // Message type
        );

        when(mVmsClient.getAvailableLayers()).thenReturn(
                new VmsAvailableLayers(Collections.emptySet(), 123));

        HalPropValue response = createHalMessage(
                VmsMessageType.AVAILABILITY_RESPONSE,  // Message type
                123,                                   // Sequence number
                0                                      // # of associated layers
        );

        sendHalMessage(request);
        verify(mVehicleHal).set(response);
    }

    @Test
    public void testHandleAvailabilityRequestEvent_OneLayer() throws Exception {
        HalPropValue request = createHalMessage(
                VmsMessageType.AVAILABILITY_REQUEST  // Message type
        );

        when(mVmsClient.getAvailableLayers()).thenReturn(
                new VmsAvailableLayers(Collections.singleton(
                        new VmsAssociatedLayer(LAYER, Collections.singleton(PUBLISHER_ID))), 123));

        HalPropValue response = createHalMessage(
                VmsMessageType.AVAILABILITY_RESPONSE,      // Message type
                123,                                       // Sequence number
                1,                                         // # of associated layers
                LAYER_TYPE, LAYER_SUBTYPE, LAYER_VERSION,  // Layer
                1,                                         // # of publisher IDs
                PUBLISHER_ID                               // Publisher ID
        );

        sendHalMessage(request);
        verify(mVehicleHal).set(response);
    }


    @Test
    public void testHandleAvailabilityRequestEvent_MultipleLayers() throws Exception {
        HalPropValue request = createHalMessage(
                VmsMessageType.AVAILABILITY_REQUEST  // Message type
        );

        when(mVmsClient.getAvailableLayers()).thenReturn(
                new VmsAvailableLayers(new LinkedHashSet<>(Arrays.asList(
                        new VmsAssociatedLayer(LAYER,
                                new LinkedHashSet<>(Arrays.asList(PUBLISHER_ID, 54321))),
                        new VmsAssociatedLayer(new VmsLayer(3, 2, 1),
                                Collections.emptySet()),
                        new VmsAssociatedLayer(new VmsLayer(6, 5, 4),
                                Collections.singleton(99999)))),
                        123));

        HalPropValue response = createHalMessage(
                VmsMessageType.AVAILABILITY_RESPONSE,      // Message type
                123,                                       // Sequence number
                3,                                         // # of associated layers
                // Associated layer #1
                LAYER_TYPE, LAYER_SUBTYPE, LAYER_VERSION,  // Layer
                2,                                         // # of publisher IDs
                PUBLISHER_ID,                              // Publisher ID
                54321,                                     // Publisher ID #2
                // Associated layer #2
                3, 2, 1,                                   // Layer
                0,                                         // # of publisher IDs
                // Associated layer #3
                6, 5, 4,                                   // Layer
                1,                                         // # of publisher IDs
                99999                                      // Publisher ID

        );

        sendHalMessage(request);
        verify(mVehicleHal).set(response);
    }

    /**
     * START_SESSION message format:
     * <ul>
     * <li>Message type
     * <li>Core ID
     * <li>Client ID
     * </ul>
     */
    @Test
    public void testHandleStartSessionEvent() throws Exception {
        when(mVmsClient.getAvailableLayers()).thenReturn(
                new VmsAvailableLayers(Collections.emptySet(), 5));

        HalPropValue request = createHalMessage(
                VmsMessageType.START_SESSION,  // Message type
                -1,                            // Core ID (unknown)
                CLIENT_ID                      // Client ID
        );

        HalPropValue response = createHalMessage(
                VmsMessageType.START_SESSION,  // Message type
                CORE_ID,                               // Core ID
                CLIENT_ID                              // Client ID
        );

        sendHalMessage(request);

        InOrder inOrder = Mockito.inOrder(mVehicleHal, mVmsClient);
        inOrder.verify(mVmsClient).unregister();
        inOrder.verify(mVehicleHal).set(response);
        assertEquals(2, mVmsInitCount);

        waitForHandlerCompletion();
        inOrder.verify(mVehicleHal).set(createHalMessage(
                VmsMessageType.AVAILABILITY_CHANGE, // Message type
                5,                                  // Sequence number
                0));                                // # of associated layers

    }

    /**
     * AVAILABILITY_CHANGE message format:
     * <ul>
     * <li>Message type
     * <li>Sequence number.
     * <li>Number of associated layers.
     * <li>Associated layers (x number of associated layers)
     * <ul>
     * <li>Layer ID
     * <li>Layer subtype
     * <li>Layer version
     * <li>Number of publishers
     * <li>Publisher ID (x number of publishers)
     * </ul>
     * </ul>
     */
    @Test
    public void testOnLayersAvailabilityChanged_ZeroLayers() throws Exception {
        mEventCallback.onLayerAvailabilityChanged(
                new VmsAvailableLayers(Collections.emptySet(), 123));

        HalPropValue message = createHalMessage(
                VmsMessageType.AVAILABILITY_CHANGE,    // Message type
                123,                                   // Sequence number
                0                                      // # of associated layers
        );

        waitForHandlerCompletion();
        verify(mVehicleHal).set(message);
    }

    @Test
    public void testOnLayersAvailabilityChanged_OneLayer() throws Exception {
        mEventCallback.onLayerAvailabilityChanged(
                new VmsAvailableLayers(Collections.singleton(
                        new VmsAssociatedLayer(LAYER, Collections.singleton(PUBLISHER_ID))), 123));

        HalPropValue message = createHalMessage(
                VmsMessageType.AVAILABILITY_CHANGE,        // Message type
                123,                                       // Sequence number
                1,                                         // # of associated layers
                LAYER_TYPE, LAYER_SUBTYPE, LAYER_VERSION,  // Layer
                1,                                         // # of publisher IDs
                PUBLISHER_ID                               // Publisher ID
        );

        waitForHandlerCompletion();
        verify(mVehicleHal).set(message);
    }


    @Test
    public void testOnLayersAvailabilityChanged_MultipleLayers() throws Exception {
        mEventCallback.onLayerAvailabilityChanged(
                new VmsAvailableLayers(new LinkedHashSet<>(Arrays.asList(
                        new VmsAssociatedLayer(LAYER,
                                new LinkedHashSet<>(Arrays.asList(PUBLISHER_ID, 54321))),
                        new VmsAssociatedLayer(new VmsLayer(3, 2, 1),
                                Collections.emptySet()),
                        new VmsAssociatedLayer(new VmsLayer(6, 5, 4),
                                Collections.singleton(99999)))),
                        123));

        HalPropValue message = createHalMessage(
                VmsMessageType.AVAILABILITY_CHANGE,      // Message type
                123,                                       // Sequence number
                3,                                         // # of associated layers
                // Associated layer #1
                LAYER_TYPE, LAYER_SUBTYPE, LAYER_VERSION,  // Layer
                2,                                         // # of publisher IDs
                PUBLISHER_ID,                              // Publisher ID
                54321,                                     // Publisher ID #2
                // Associated layer #2
                3, 2, 1,                                   // Layer
                0,                                         // # of publisher IDs
                // Associated layer #3
                6, 5, 4,                                   // Layer
                1,                                         // # of publisher IDs
                99999                                      // Publisher ID

        );

        waitForHandlerCompletion();
        verify(mVehicleHal).set(message);
    }

    /**
     * SUBSCRIPTION_REQUEST message format:
     * <ul>
     * <li>Message type
     * </ul>
     *
     * SUBSCRIPTION_RESPONSE message format:
     * <ul>
     * <li>Message type
     * <li>Sequence number
     * <li>Number of layers
     * <li>Number of associated layers
     * <li>Layers (x number of layers)
     * <ul>
     * <li>Layer ID
     * <li>Layer subtype
     * <li>Layer version
     * </ul>
     * <li>Associated layers (x number of associated layers)
     * <ul>
     * <li>Layer ID
     * <li>Layer subtype
     * <li>Layer version
     * <li>Number of publishers
     * <li>Publisher ID (x number of publishers)
     * </ul>
     * </ul>
     */
    @Test
    public void testHandleSubscriptionsRequestEvent_ZeroLayers() throws Exception {
        HalPropValue request = createHalMessage(
                VmsMessageType.SUBSCRIPTIONS_REQUEST  // Message type
        );

        when(mVmsClient.getSubscriptionState()).thenReturn(
                new VmsSubscriptionState(123, Collections.emptySet(), Collections.emptySet()));

        HalPropValue response = createHalMessage(
                VmsMessageType.SUBSCRIPTIONS_RESPONSE,  // Message type
                123,                                    // Sequence number
                0,                                      // # of layers
                0                                       // # of associated layers
        );

        sendHalMessage(request);
        verify(mVehicleHal).set(response);
    }

    @Test
    public void testHandleSubscriptionsRequestEvent_OneLayer_ZeroAssociatedLayers()
            throws Exception {
        HalPropValue request = createHalMessage(
                VmsMessageType.SUBSCRIPTIONS_REQUEST  // Message type
        );

        when(mVmsClient.getSubscriptionState()).thenReturn(
                new VmsSubscriptionState(123, Collections.singleton(LAYER),
                        Collections.emptySet()));

        HalPropValue response = createHalMessage(
                VmsMessageType.SUBSCRIPTIONS_RESPONSE,     // Message type
                123,                                       // Sequence number
                1,                                         // # of layers
                0,                                         // # of associated layers
                LAYER_TYPE, LAYER_SUBTYPE, LAYER_VERSION   // Layer
        );

        sendHalMessage(request);
        verify(mVehicleHal).set(response);
    }

    @Test
    public void testHandleSubscriptionsRequestEvent_ZeroLayers_OneAssociatedLayer()
            throws Exception {
        HalPropValue request = createHalMessage(
                VmsMessageType.SUBSCRIPTIONS_REQUEST  // Message type
        );

        when(mVmsClient.getSubscriptionState()).thenReturn(
                new VmsSubscriptionState(123, Collections.emptySet(), Collections.singleton(
                        new VmsAssociatedLayer(LAYER, Collections.singleton(PUBLISHER_ID)))));

        HalPropValue response = createHalMessage(
                VmsMessageType.SUBSCRIPTIONS_RESPONSE,     // Message type
                123,                                       // Sequence number
                0,                                         // # of layers
                1,                                         // # of associated layers
                LAYER_TYPE, LAYER_SUBTYPE, LAYER_VERSION,  // Layer
                1,                                         // # of publisher IDs
                PUBLISHER_ID                               // Publisher ID
        );

        sendHalMessage(request);
        verify(mVehicleHal).set(response);
    }

    @Test
    public void testHandleSubscriptionsRequestEvent_MultipleLayersAndAssociatedLayers()
            throws Exception {
        HalPropValue request = createHalMessage(
                VmsMessageType.SUBSCRIPTIONS_REQUEST  // Message type
        );

        when(mVmsClient.getSubscriptionState()).thenReturn(
                new VmsSubscriptionState(123,
                        new LinkedHashSet<>(Arrays.asList(
                                LAYER,
                                new VmsLayer(4, 5, 6),
                                new VmsLayer(7, 8, 9)
                        )),
                        new LinkedHashSet<>(Arrays.asList(
                                new VmsAssociatedLayer(LAYER, Collections.emptySet()),
                                new VmsAssociatedLayer(new VmsLayer(6, 5, 4),
                                        new LinkedHashSet<>(Arrays.asList(
                                                PUBLISHER_ID,
                                                54321))))))
        );

        HalPropValue response = createHalMessage(
                VmsMessageType.SUBSCRIPTIONS_RESPONSE,     // Message type
                123,                                       // Sequence number
                3,                                         // # of layers
                2,                                         // # of associated layers
                // Layer #1
                LAYER_TYPE, LAYER_SUBTYPE, LAYER_VERSION,  // Layer
                // Layer #2
                4, 5, 6,                                   // Layer
                // Layer #3
                7, 8, 9,                                   // Layer
                LAYER_TYPE, LAYER_SUBTYPE, LAYER_VERSION,  // Layer
                0,                                         // # of publisher IDs
                6, 5, 4,                                   // Layer
                2,                                         // # of publisher IDs
                PUBLISHER_ID,                              // Publisher ID
                54321                                      // Publisher ID #2
        );

        sendHalMessage(request);
        verify(mVehicleHal).set(response);
    }

    /**
     * SUBSCRIPTIONS_CHANGE message format:
     * <ul>
     * <li>Message type
     * <li>Sequence number
     * <li>Number of layers
     * <li>Number of associated layers
     * <li>Layers (x number of layers)
     * <ul>
     * <li>Layer ID
     * <li>Layer subtype
     * <li>Layer version
     * </ul>
     * <li>Associated layers (x number of associated layers)
     * <ul>
     * <li>Layer ID
     * <li>Layer subtype
     * <li>Layer version
     * <li>Number of publishers
     * <li>Publisher ID (x number of publishers)
     * </ul>
     * </ul>
     */
    @Test
    public void testOnVmsSubscriptionChange_ZeroLayers() throws Exception {
        mEventCallback.onSubscriptionStateChanged(
                new VmsSubscriptionState(123, Collections.emptySet(), Collections.emptySet()));

        HalPropValue response = createHalMessage(
                VmsMessageType.SUBSCRIPTIONS_CHANGE,    // Message type
                123,                                    // Sequence number
                0,                                      // # of layers
                0                                       // # of associated layers
        );

        waitForHandlerCompletion();
        verify(mVehicleHal).set(response);
    }

    @Test
    public void testOnVmsSubscriptionChange_OneLayer_ZeroAssociatedLayers()
            throws Exception {
        mEventCallback.onSubscriptionStateChanged(
                new VmsSubscriptionState(123, Collections.singleton(LAYER),
                        Collections.emptySet()));

        HalPropValue response = createHalMessage(
                VmsMessageType.SUBSCRIPTIONS_CHANGE,       // Message type
                123,                                       // Sequence number
                1,                                         // # of layers
                0,                                         // # of associated layers
                LAYER_TYPE, LAYER_SUBTYPE, LAYER_VERSION   // Layer
        );

        waitForHandlerCompletion();
        verify(mVehicleHal).set(response);
    }

    @Test
    public void testOnVmsSubscriptionChange_ZeroLayers_OneAssociatedLayer()
            throws Exception {
        mEventCallback.onSubscriptionStateChanged(
                new VmsSubscriptionState(123, Collections.emptySet(), Collections.singleton(
                        new VmsAssociatedLayer(LAYER, Collections.singleton(PUBLISHER_ID)))));

        HalPropValue response = createHalMessage(
                VmsMessageType.SUBSCRIPTIONS_CHANGE,       // Message type
                123,                                       // Sequence number
                0,                                         // # of layers
                1,                                         // # of associated layers
                LAYER_TYPE, LAYER_SUBTYPE, LAYER_VERSION,  // Layer
                1,                                         // # of publisher IDs
                PUBLISHER_ID                               // Publisher ID
        );

        waitForHandlerCompletion();
        verify(mVehicleHal).set(response);
    }

    @Test
    public void testOnVmsSubscriptionChange_MultipleLayersAndAssociatedLayers()
            throws Exception {
        mEventCallback.onSubscriptionStateChanged(
                new VmsSubscriptionState(123,
                        new LinkedHashSet<>(Arrays.asList(
                                LAYER,
                                new VmsLayer(4, 5, 6),
                                new VmsLayer(7, 8, 9)
                        )),
                        new LinkedHashSet<>(Arrays.asList(
                                new VmsAssociatedLayer(LAYER, Collections.emptySet()),
                                new VmsAssociatedLayer(new VmsLayer(6, 5, 4),
                                        new LinkedHashSet<>(Arrays.asList(
                                                PUBLISHER_ID,
                                                54321))))))
        );

        HalPropValue response = createHalMessage(
                VmsMessageType.SUBSCRIPTIONS_CHANGE,       // Message type
                123,                                       // Sequence number
                3,                                         // # of layers
                2,                                         // # of associated layers
                // Layer #1
                LAYER_TYPE, LAYER_SUBTYPE, LAYER_VERSION,  // Layer
                // Layer #2
                4, 5, 6,                                   // Layer
                // Layer #3
                7, 8, 9,                                   // Layer
                LAYER_TYPE, LAYER_SUBTYPE, LAYER_VERSION,  // Layer
                0,                                         // # of publisher IDs
                6, 5, 4,                                   // Layer
                2,                                         // # of publisher IDs
                PUBLISHER_ID,                              // Publisher ID
                54321                                      // Publisher ID #2
        );

        waitForHandlerCompletion();
        verify(mVehicleHal).set(response);
    }

    @Test
    public void testPropertySetExceptionNotPropagated_CoreStartSession() throws Exception {
        doThrow(new RuntimeException()).when(mVehicleHal).set(any());
        initHalService(false);

        waitForHandlerCompletion();
    }

    @Test
    public void testPropertySetExceptionNotPropagated_ClientStartSession() throws Exception {
        initHalService(false);

        when(mVmsClient.getAvailableLayers()).thenReturn(
                new VmsAvailableLayers(Collections.emptySet(), 0));
        doThrow(new RuntimeException()).when(mVehicleHal).set(any());

        HalPropValue request = createHalMessage(
                VmsMessageType.START_SESSION,  // Message type
                -1,                            // Core ID (unknown)
                CLIENT_ID                      // Client ID
        );

        sendHalMessage(request);
        waitForHandlerCompletion();
    }

    @Test
    public void testDumpMetrics_DefaultConfig() {
        mHalService.dumpMetrics(new FileDescriptor());
        verifyZeroInteractions(mVehicleHal);
    }

    @Test
    public void testDumpMetrics_NonVendorProperty() throws Exception {
        HalPropValue vehicleProp = mPropValueBuilder.build(/* prop= */ 0, /* areaId= */ 0,
                toByteArray(PAYLOAD_AS_LIST));
        when(mVehicleHal.get(anyInt())).thenReturn(vehicleProp);

        when(mResources.getInteger(
                R.integer.vmsHalClientMetricsProperty)).thenReturn(
                VehicleProperty.VEHICLE_MAP_SERVICE);
        setUp();

        mHalService.dumpMetrics(new FileDescriptor());
        verifyZeroInteractions(mVehicleHal);
    }

    @Test
    public void testDumpMetrics_VendorProperty() throws Exception {
        int metricsPropertyId = VehiclePropertyGroup.VENDOR | 1;
        when(mResources.getInteger(
                R.integer.vmsHalClientMetricsProperty)).thenReturn(
                metricsPropertyId);
        setUp();

        HalPropValue metricsProperty = mPropValueBuilder.build(/* prop= */ 0, /* areaId= */ 0,
                toByteArray(PAYLOAD_AS_LIST));
        when(mVehicleHal.get(metricsPropertyId)).thenReturn(metricsProperty);

        try (TemporaryFile dumpsysFile = new TemporaryFile("VmsHalServiceTest")) {
            try (FileOutputStream outputStream = new FileOutputStream(dumpsysFile.getFile())) {
                mHalService.dumpMetrics(outputStream.getFD());
                verify(mVehicleHal).get(metricsPropertyId);
            }

            try (FileInputStream inputStream = new FileInputStream(dumpsysFile.getFile())) {
                byte[] dumpsysOutput = new byte[PAYLOAD.length];
                assertEquals(PAYLOAD.length, inputStream.read(dumpsysOutput));
                assertArrayEquals(PAYLOAD, dumpsysOutput);
            }
        }
    }

    @Test
    public void testDumpMetrics_VendorProperty_Timeout() throws Exception {
        int metricsPropertyId = VehiclePropertyGroup.VENDOR | 1;
        when(mResources.getInteger(
                R.integer.vmsHalClientMetricsProperty)).thenReturn(
                metricsPropertyId);
        setUp();

        when(mVehicleHal.get(metricsPropertyId))
                .thenThrow(new ServiceSpecificException(VehicleHalStatusCode.STATUS_TRY_AGAIN,
                        "propertyId: 0x" + Integer.toHexString(metricsPropertyId)));

        mHalService.dumpMetrics(new FileDescriptor());
        verify(mVehicleHal).get(metricsPropertyId);
    }

    @Test
    public void testDumpMetrics_VendorProperty_Unavailable() throws Exception {
        int metricsPropertyId = VehiclePropertyGroup.VENDOR | 1;
        when(mResources.getInteger(
                R.integer.vmsHalClientMetricsProperty)).thenReturn(
                metricsPropertyId);
        setUp();

        when(mVehicleHal.get(metricsPropertyId)).thenReturn(null);

        mHalService.dumpMetrics(new FileDescriptor());
        verify(mVehicleHal).get(metricsPropertyId);
    }

    private HalPropValue createHalMessage(Integer... message) {
        HalPropValue result = mPropValueBuilder.build(VehicleProperty.VEHICLE_MAP_SERVICE,
                /*areaId=*/0, toIntArray(Arrays.asList(message)));
        return result;
    }

    private HalPropValue createHalMessage(byte[] payload, Integer... message) {
        HalPropValue result = mPropValueBuilder.build(
                VehicleProperty.VEHICLE_MAP_SERVICE,
                /*areaId=*/0, /*timestamp=*/0, /*status=*/0,
                /*int32Values=*/toIntArray(Arrays.asList(message)), /*floatValues=*/new float[0],
                /*int64Values=*/new long[0], /*stringValue=*/new String(), /*byteValues=*/payload);
        return result;
    }

    private void sendHalMessage(HalPropValue message) {
        mHalService.onHalEvents(Collections.singletonList(message));
    }

    private void waitForHandlerCompletion() throws Exception {
        final CountDownLatch latch = new CountDownLatch(1);
        mHalService.getHandler().post(latch::countDown);
        latch.await(5, TimeUnit.SECONDS);
    }

    @SafeVarargs
    private static <T> Set<T> asSet(T... values) {
        return new HashSet<>(Arrays.asList(values));
    }
}
