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

import static android.car.oem.CarAudioFeaturesInfo.AUDIO_FEATURE_NO_FEATURE;
import static android.media.AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE;
import static android.media.AudioAttributes.USAGE_MEDIA;
import static android.media.AudioManager.AUDIOFOCUS_GAIN;
import static android.media.AudioManager.AUDIOFOCUS_REQUEST_DELAYED;
import static android.media.AudioManager.AUDIOFOCUS_REQUEST_FAILED;
import static android.media.AudioManager.AUDIOFOCUS_REQUEST_GRANTED;

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.eq;
import static org.mockito.Mockito.description;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.car.media.CarAudioManager;
import android.car.oem.CarAudioFeaturesInfo;
import android.content.pm.PackageManager;
import android.media.AudioAttributes;
import android.media.AudioFocusInfo;
import android.media.AudioManager;
import android.media.audiopolicy.AudioPolicy;
import android.os.Build;
import android.os.Bundle;
import android.util.SparseArray;

import com.android.car.CarLocalServices;
import com.android.car.oem.CarOemProxyService;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;

import java.util.List;

@RunWith(MockitoJUnitRunner.class)
public final class CarZonesAudioFocusUnitTest {
    private static final String CLIENT_ID = "media-client-id";
    private static final String PACKAGE_NAME = "com.android.car.audio";
    private static final int AUDIOFOCUS_FLAG = 0;
    private static final int PRIMARY_ZONE_ID = CarAudioManager.PRIMARY_AUDIO_ZONE;
    private static final int SECONDARY_ZONE_ID = CarAudioManager.PRIMARY_AUDIO_ZONE + 1;
    private static final int CLIENT_UID = 1086753;

    private final SparseArray<CarAudioFocus> mFocusMocks = generateMockFocus();
    private final SparseArray<CarAudioZone> mMockZones = generateAudioZones();

    @Mock
    private AudioManagerWrapper mMockAudioManager;
    @Mock
    private AudioPolicy mAudioPolicy;
    @Mock
    private CarAudioService mCarAudioService;
    @Mock
    private CarAudioSettings mCarAudioSettings;
    @Mock
    private CarZonesAudioFocus.CarFocusCallback mMockCarFocusCallback;
    @Mock
    private PackageManager mMockPackageManager;
    @Mock
    private CarOemProxyService mMockCarOemProxyService;
    @Mock
    private CarVolumeInfoWrapper mMockCarVolumeInfoWrapper;

    private CarZonesAudioFocus mCarZonesAudioFocus;

    @Before
    public void setUp() {
        mCarZonesAudioFocus = getCarZonesAudioFocus();
        CarLocalServices.removeServiceForTest(CarOemProxyService.class);
        CarLocalServices.addService(CarOemProxyService.class, mMockCarOemProxyService);
    }

    @After
    public void tearDown() {
        CarLocalServices.removeServiceForTest(CarOemProxyService.class);
    }

    @Test
    public void newCarZonesAudioFocus_withNullAudioManager_throws() {
        NullPointerException thrown = assertThrows(NullPointerException.class,
                () -> CarZonesAudioFocus.createCarZonesAudioFocus(null,
                        mMockPackageManager, mMockZones, mCarAudioSettings, mMockCarFocusCallback,
                        mMockCarVolumeInfoWrapper, getCarAudioFeaturesInfo())
        );

        assertWithMessage("Create car audio zone with null audio manager exception")
                .that(thrown).hasMessageThat().contains("Audio manager");
    }

    @Test
    public void newCarZonesAudioFocus_withNullPackageManager_throws() {
        NullPointerException thrown = assertThrows(NullPointerException.class,
                () -> CarZonesAudioFocus.createCarZonesAudioFocus(mMockAudioManager,
                        null, mMockZones, mCarAudioSettings,  mMockCarFocusCallback,
                        mMockCarVolumeInfoWrapper, getCarAudioFeaturesInfo())
        );

        assertWithMessage("Create car audio zone with null package manager exception")
                .that(thrown).hasMessageThat().contains("Package manager");
    }

    @Test
    public void newCarZonesAudioFocus_withNullCarAudioZones_throws() {
        NullPointerException thrown = assertThrows(NullPointerException.class,
                () -> CarZonesAudioFocus.createCarZonesAudioFocus(mMockAudioManager,
                        mMockPackageManager, null, mCarAudioSettings, mMockCarFocusCallback,
                        mMockCarVolumeInfoWrapper, getCarAudioFeaturesInfo())
        );

        assertWithMessage("Create car audio zone with null zones exception")
                .that(thrown).hasMessageThat().contains("Car audio zones");
    }

    @Test
    public void newCarZonesAudioFocus_withEmptyCarAudioZones_throws() {
        IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class,
                () -> CarZonesAudioFocus.createCarZonesAudioFocus(mMockAudioManager,
                        mMockPackageManager, new SparseArray<>(), mCarAudioSettings,
                        mMockCarFocusCallback, mMockCarVolumeInfoWrapper, getCarAudioFeaturesInfo())
        );

        assertWithMessage("Create car audio zone with no audio zones exception")
                .that(thrown).hasMessageThat().contains("minimum of one audio zone");
    }

    @Test
    public void newCarZonesAudioFocus_withNullCarAudioSettings_throws() {
        NullPointerException thrown = assertThrows(NullPointerException.class,
                () -> CarZonesAudioFocus.createCarZonesAudioFocus(mMockAudioManager,
                        mMockPackageManager, mMockZones, null, mMockCarFocusCallback,
                        mMockCarVolumeInfoWrapper, getCarAudioFeaturesInfo())
        );

        assertWithMessage("Create car audio zone with null car settings exception")
                .that(thrown).hasMessageThat().contains("Car audio settings");
    }

    @Test
    public void newCarZonesAudioFocus_withNullCarFocusCallback_succeeds() {
        CarZonesAudioFocus.createCarZonesAudioFocus(mMockAudioManager, mMockPackageManager,
                mMockZones, mCarAudioSettings, null, mMockCarVolumeInfoWrapper,
                getCarAudioFeaturesInfo());
    }

    @Test
    public void newCarZonesAudioFocus_withNullCarVolumeInfo_fails() {
        NullPointerException thrown = assertThrows(NullPointerException.class,
                () -> CarZonesAudioFocus.createCarZonesAudioFocus(mMockAudioManager,
                        mMockPackageManager, mMockZones, mCarAudioSettings, mMockCarFocusCallback,
                        /* carVolumeInfoWrapper= */ null, getCarAudioFeaturesInfo())
        );

        assertWithMessage("Create car audio zone with null car volume exception")
                .that(thrown).hasMessageThat().contains("Car volume info");
    }

    @Test
    public void onAudioFocusRequest_withPrimaryZoneUid_passesRequestToPrimaryZone() {
        withUidRoutingToZone(PRIMARY_ZONE_ID);
        AudioFocusInfo audioFocusInfo = generateAudioFocusRequest();

        mCarZonesAudioFocus.onAudioFocusRequest(audioFocusInfo, AUDIOFOCUS_REQUEST_GRANTED);

        verify(mFocusMocks.get(PRIMARY_ZONE_ID))
                .onAudioFocusRequest(audioFocusInfo, AUDIOFOCUS_REQUEST_GRANTED);
    }

    @Test
    public void onAudioFocusRequest_withSecondaryZoneUid_passesRequestToSecondaryZone() {
        withUidRoutingToZone(SECONDARY_ZONE_ID);
        AudioFocusInfo audioFocusInfo = generateAudioFocusRequest();

        mCarZonesAudioFocus.onAudioFocusRequest(audioFocusInfo, AUDIOFOCUS_REQUEST_GRANTED);

        verify(mFocusMocks.get(SECONDARY_ZONE_ID))
                .onAudioFocusRequest(audioFocusInfo, AUDIOFOCUS_REQUEST_GRANTED);
    }

    @Test
    public void onAudioFocusRequest_withValidBundledZoneId_passesRequestToBundledZone() {
        withUidRoutingToZone(PRIMARY_ZONE_ID);
        when(mCarAudioService.isAudioZoneIdValid(SECONDARY_ZONE_ID)).thenReturn(true);
        AudioFocusInfo audioFocusInfo = generateAudioFocusInfoWithBundledZoneId(SECONDARY_ZONE_ID);

        mCarZonesAudioFocus.onAudioFocusRequest(audioFocusInfo, AUDIOFOCUS_REQUEST_GRANTED);

        verify(mFocusMocks.get(SECONDARY_ZONE_ID))
                .onAudioFocusRequest(audioFocusInfo, AUDIOFOCUS_REQUEST_GRANTED);
    }

    @Test
    public void onAudioFocusRequest_withInvalidBundledZoneId_passesRequestBasedOnUid() {
        int invalidZoneId = -1;
        withUidRoutingToZone(PRIMARY_ZONE_ID);
        when(mCarAudioService.isAudioZoneIdValid(invalidZoneId)).thenReturn(false);
        AudioFocusInfo audioFocusInfo = generateAudioFocusInfoWithBundledZoneId(invalidZoneId);

        mCarZonesAudioFocus.onAudioFocusRequest(audioFocusInfo, AUDIOFOCUS_REQUEST_GRANTED);

        verify(mFocusMocks.get(PRIMARY_ZONE_ID))
                .onAudioFocusRequest(audioFocusInfo, AUDIOFOCUS_REQUEST_GRANTED);
    }

    @Test
    public void onAudioFocusRequest_withFocusCallback_callsOnFocusChange() {
        List<AudioFocusInfo> focusHolders = List.of(generateAudioFocusRequest());
        when(mFocusMocks.get(PRIMARY_ZONE_ID).getAudioFocusHolders()).thenReturn(focusHolders);
        AudioFocusInfo audioFocusInfo = generateAudioFocusRequest();

        mCarZonesAudioFocus.onAudioFocusRequest(audioFocusInfo, AUDIOFOCUS_REQUEST_GRANTED);

        ArgumentCaptor<SparseArray<List<AudioFocusInfo>>> captor =
                ArgumentCaptor.forClass(SparseArray.class);
        verify(mMockCarFocusCallback)
                .onFocusChange(eq(new int[]{PRIMARY_ZONE_ID}), captor.capture());
        SparseArray<List<AudioFocusInfo>> results = captor.getValue();
        assertWithMessage("Number of lists returned in sparse array")
                .that(results.size()).isEqualTo(1);
        assertWithMessage("Focus holders for primary zone")
                .that(results.get(PRIMARY_ZONE_ID)).isEqualTo(focusHolders);
    }

    @Test
    public void onAudioFocusRequest_withNullAudioService() {
        AudioFocusInfo audioFocusInfo = generateAudioFocusRequest();
        mCarZonesAudioFocus.setOwningPolicy(/* carAudioService= */ null, /* parentPolicy= */null);

        mCarZonesAudioFocus.onAudioFocusRequest(audioFocusInfo, AUDIOFOCUS_REQUEST_GRANTED);

        verify(mFocusMocks.get(PRIMARY_ZONE_ID), never())
                .onAudioFocusRequest(audioFocusInfo, AUDIOFOCUS_REQUEST_GRANTED);
        verify(mMockCarFocusCallback, never()).onFocusChange(any(), any());
    }

    @Test
    public void onAudioFocusAbandon_withNullAudioService() {
        AudioFocusInfo audioFocusInfo = generateAudioFocusRequest();
        mCarZonesAudioFocus.onAudioFocusRequest(audioFocusInfo, AUDIOFOCUS_REQUEST_GRANTED);
        mCarZonesAudioFocus.setOwningPolicy(/* carAudioService= */ null, /* parentPolicy= */null);
        reset(mMockCarFocusCallback);

        mCarZonesAudioFocus.onAudioFocusAbandon(audioFocusInfo);

        verify(mMockCarFocusCallback, never()).onFocusChange(any(), any());
    }

    @Test
    public void onAudioFocusAbandon() {
        List<AudioFocusInfo> focusHolders = List.of(generateAudioFocusRequest());
        when(mFocusMocks.get(PRIMARY_ZONE_ID).getAudioFocusHolders()).thenReturn(focusHolders);
        AudioFocusInfo audioFocusInfo = generateAudioFocusRequest();
        mCarZonesAudioFocus.onAudioFocusRequest(audioFocusInfo, AUDIOFOCUS_REQUEST_GRANTED);

        mCarZonesAudioFocus.onAudioFocusAbandon(audioFocusInfo);

        verify(mFocusMocks.get(PRIMARY_ZONE_ID)).onAudioFocusAbandon(audioFocusInfo);
        ArgumentCaptor<SparseArray<List<AudioFocusInfo>>> captor =
                ArgumentCaptor.forClass(SparseArray.class);
        verify(mMockCarFocusCallback, times(2))
                .onFocusChange(eq(new int[]{PRIMARY_ZONE_ID}), captor.capture());
        SparseArray<List<AudioFocusInfo>> results = captor.getValue();
        assertWithMessage("Focus holder after abandoned focus called")
                .that(results.get(PRIMARY_ZONE_ID)).isEqualTo(focusHolders);
    }

    @Test
    public void setRestrictFocus_withTrue_restrictsFocusForAllZones() {
        mCarZonesAudioFocus.setRestrictFocus(true);

        verify(mFocusMocks.get(PRIMARY_ZONE_ID),
                description("Primary zone's CarAudioFocus#setRestrictFocus wasn't passed true"))
                .setRestrictFocus(true);
        verify(mFocusMocks.get(SECONDARY_ZONE_ID),
                description("Secondary zone's CarAudioFocus#setRestrictFocus wasn't passed true"))
                .setRestrictFocus(true);
    }

    @Test
    public void setRestrictFocus_withFalse_unrestrictsFocusForAllZones() {
        mCarZonesAudioFocus.setRestrictFocus(false);

        verify(mFocusMocks.get(PRIMARY_ZONE_ID),
                description("Primary zone's CarAudioFocus#setRestrictFocus wasn't passed false"))
                .setRestrictFocus(false);
        verify(mFocusMocks.get(SECONDARY_ZONE_ID),
                description("Secondary zone's CarAudioFocus#setRestrictFocus wasn't passed false"))
                .setRestrictFocus(false);
    }

    @Test
    public void setRestrictFocus_notifiesFocusCallbackForAllZones() {
        mCarZonesAudioFocus.setRestrictFocus(false);

        ArgumentCaptor<SparseArray<List<AudioFocusInfo>>> captor =
                ArgumentCaptor.forClass(SparseArray.class);
        int[] expectedZoneIds = new int[]{PRIMARY_ZONE_ID, SECONDARY_ZONE_ID};
        verify(mMockCarFocusCallback).onFocusChange(eq(expectedZoneIds), captor.capture());
        assertWithMessage("Number of focus holder lists")
                .that(captor.getValue().size()).isEqualTo(2);
    }

    @Test
    public void transientlyLoseAllFocusHoldersInZone() {
        AudioFocusInfo mediaFocusInfo = generateAudioFocusRequest();
        AudioFocusInfo navigationFocusInfo =
                generateAudioFocusRequestWithUsage(USAGE_ASSISTANCE_NAVIGATION_GUIDANCE);
        List<AudioFocusInfo> expectedFocusInfoList = List.of(mediaFocusInfo, navigationFocusInfo);
        when(mFocusMocks.get(SECONDARY_ZONE_ID).getAudioFocusHolders())
                .thenReturn(expectedFocusInfoList);

        List<AudioFocusInfo> audioFocusInfos =
                mCarZonesAudioFocus.transientlyLoseAllFocusHoldersInZone(SECONDARY_ZONE_ID);

        verify(mFocusMocks.get(SECONDARY_ZONE_ID))
                .removeAudioFocusInfoAndTransientlyLoseFocus(mediaFocusInfo);
        verify(mFocusMocks.get(SECONDARY_ZONE_ID))
                .removeAudioFocusInfoAndTransientlyLoseFocus(navigationFocusInfo);
        assertWithMessage("Focus holders in secondary zone")
                .that(audioFocusInfos).containsExactlyElementsIn(expectedFocusInfoList);
    }

    @Test
    public void reevaluateAndRegainAudioFocusList_regainsFocus() {
        AudioFocusInfo mediaFocusInfo = generateAudioFocusRequest();
        AudioFocusInfo navigationFocusInfo =
                generateAudioFocusRequestWithUsage(USAGE_ASSISTANCE_NAVIGATION_GUIDANCE);
        int mediaFocusRequestResult = AUDIOFOCUS_REQUEST_DELAYED;
        int navigationFocusRequestResult = AUDIOFOCUS_REQUEST_GRANTED;
        when(mCarAudioService.getZoneIdForAudioFocusInfo(mediaFocusInfo))
                .thenReturn(PRIMARY_ZONE_ID);
        when(mCarAudioService.getZoneIdForAudioFocusInfo(navigationFocusInfo))
                .thenReturn(SECONDARY_ZONE_ID);
        when(mFocusMocks.get(PRIMARY_ZONE_ID).reevaluateAndRegainAudioFocus(any()))
                .thenReturn(mediaFocusRequestResult);
        when(mFocusMocks.get(SECONDARY_ZONE_ID).reevaluateAndRegainAudioFocus(any()))
                .thenReturn(navigationFocusRequestResult);

        List<Integer> resList = mCarZonesAudioFocus.reevaluateAndRegainAudioFocusList(
                List.of(mediaFocusInfo, navigationFocusInfo));

        assertWithMessage("Result list size").that(resList.size()).isEqualTo(2);
        assertWithMessage("Results for regaining media focus in primary zone")
                .that(resList.get(0)).isEqualTo(mediaFocusRequestResult);
        assertWithMessage("Results for regaining navigation focus in primary zone")
                .that(resList.get(1)).isEqualTo(navigationFocusRequestResult);
    }

    @Test
    public void reevaluateAndRegainAudioFocusList_notifiesFocusListener() {
        AudioFocusInfo mediaFocusInfo = generateAudioFocusRequest();
        AudioFocusInfo navigationFocusInfo =
                generateAudioFocusRequestWithUsage(USAGE_ASSISTANCE_NAVIGATION_GUIDANCE);
        when(mCarAudioService.getZoneIdForAudioFocusInfo(mediaFocusInfo))
                .thenReturn(PRIMARY_ZONE_ID);
        when(mCarAudioService.getZoneIdForAudioFocusInfo(navigationFocusInfo))
                .thenReturn(SECONDARY_ZONE_ID);
        when(mFocusMocks.get(PRIMARY_ZONE_ID).getAudioFocusHolders())
                .thenReturn(List.of(mediaFocusInfo));
        when(mFocusMocks.get(SECONDARY_ZONE_ID).getAudioFocusHolders())
                .thenReturn(List.of(navigationFocusInfo));

        mCarZonesAudioFocus.reevaluateAndRegainAudioFocusList(List.of(mediaFocusInfo,
                navigationFocusInfo));

        ArgumentCaptor<SparseArray<List<AudioFocusInfo>>> captor =
                ArgumentCaptor.forClass(SparseArray.class);
        verify(mMockCarFocusCallback).onFocusChange(
                eq(new int[]{PRIMARY_ZONE_ID, SECONDARY_ZONE_ID}), captor.capture());
        SparseArray<List<AudioFocusInfo>> results = captor.getValue();
        assertWithMessage("Zones notified for focus changed")
                .that(results.size()).isEqualTo(2);
        assertWithMessage("Primary zone focus holders")
                .that(results.get(PRIMARY_ZONE_ID)).containsExactly(mediaFocusInfo);
        assertWithMessage("Secondary zone focus holders")
                .that(results.get(SECONDARY_ZONE_ID)).containsExactly(navigationFocusInfo);
    }

    @Test
    public void reevaluateAndRegainAudioFocusList_withCarAudioServiceUnavailable() {
        AudioFocusInfo mediaFocusInfo = generateAudioFocusRequest();
        int mediaFocusRequestResult = AUDIOFOCUS_REQUEST_GRANTED;
        when(mCarAudioService.getZoneIdForAudioFocusInfo(mediaFocusInfo))
                .thenReturn(PRIMARY_ZONE_ID);
        when(mFocusMocks.get(PRIMARY_ZONE_ID).reevaluateAndRegainAudioFocus(any()))
                .thenReturn(mediaFocusRequestResult);
        mCarZonesAudioFocus.setOwningPolicy(/* carAudioService= */ null, mAudioPolicy);

        List<Integer> resList = mCarZonesAudioFocus.reevaluateAndRegainAudioFocusList(
                List.of(mediaFocusInfo));

        assertWithMessage("Result list with car audio service unavailable")
                .that(resList).isEmpty();
    }

    @Test
    public void reevaluateAndRegainAudioFocus_withCarAudioServiceUnavailable() {
        AudioFocusInfo mediaFocusInfo = generateAudioFocusRequest();
        int mediaFocusRequestResult = AUDIOFOCUS_REQUEST_GRANTED;
        when(mCarAudioService.getZoneIdForAudioFocusInfo(mediaFocusInfo))
                .thenReturn(PRIMARY_ZONE_ID);
        when(mFocusMocks.get(PRIMARY_ZONE_ID).reevaluateAndRegainAudioFocus(any()))
                .thenReturn(mediaFocusRequestResult);
        mCarZonesAudioFocus.setOwningPolicy(/* carAudioService= */ null, mAudioPolicy);

        int result = mCarZonesAudioFocus.reevaluateAndRegainAudioFocus(mediaFocusInfo);

        assertWithMessage("Result list with car audio service unavailable")
                .that(result).isEqualTo(AUDIOFOCUS_REQUEST_FAILED);
    }

    @Test
    public void transientlyLoseAudioFocusForZone_forActiveFocusHolders() {
        AudioFocusInfo mediaFocusInfo = generateAudioFocusRequest();
        AudioFocusInfo navigationFocusInfo =
                generateAudioFocusRequestWithUsage(USAGE_ASSISTANCE_NAVIGATION_GUIDANCE);
        List<AudioFocusInfo> expectedFocusInfoList = List.of(mediaFocusInfo, navigationFocusInfo);
        when(mFocusMocks.get(SECONDARY_ZONE_ID).getAudioFocusHolders())
                .thenReturn(expectedFocusInfoList);
        when(mFocusMocks.get(SECONDARY_ZONE_ID).getAudioFocusLosers()).thenReturn(List.of());

        AudioFocusStack stack =
                mCarZonesAudioFocus.transientlyLoseAudioFocusForZone(SECONDARY_ZONE_ID);

        verify(mFocusMocks.get(SECONDARY_ZONE_ID))
                .removeAudioFocusInfoAndTransientlyLoseFocus(mediaFocusInfo);
        verify(mFocusMocks.get(SECONDARY_ZONE_ID))
                .removeAudioFocusInfoAndTransientlyLoseFocus(navigationFocusInfo);
        assertWithMessage("Stack active focus in secondary zone")
                .that(stack.getActiveFocusList()).containsExactlyElementsIn(expectedFocusInfoList);
        assertWithMessage("Empty stack inactive focus in secondary zone")
                .that(stack.getInactiveFocusList()).isEmpty();
    }

    @Test
    public void transientlyLoseAudioFocusForZone_forInactiveFocusHolders() {
        AudioFocusInfo mediaFocusInfo = generateAudioFocusRequest();
        AudioFocusInfo navigationFocusInfo =
                generateAudioFocusRequestWithUsage(USAGE_ASSISTANCE_NAVIGATION_GUIDANCE);
        List<AudioFocusInfo> expectedFocusInfoList = List.of(mediaFocusInfo, navigationFocusInfo);
        when(mFocusMocks.get(SECONDARY_ZONE_ID).getAudioFocusLosers())
                .thenReturn(expectedFocusInfoList);
        when(mFocusMocks.get(SECONDARY_ZONE_ID).getAudioFocusHolders()).thenReturn(List.of());

        AudioFocusStack stack =
                mCarZonesAudioFocus.transientlyLoseAudioFocusForZone(SECONDARY_ZONE_ID);

        verify(mFocusMocks.get(SECONDARY_ZONE_ID))
                .removeAudioFocusInfoAndTransientlyLoseFocus(mediaFocusInfo);
        verify(mFocusMocks.get(SECONDARY_ZONE_ID))
                .removeAudioFocusInfoAndTransientlyLoseFocus(navigationFocusInfo);
        assertWithMessage("Stack inactive focus in secondary zone")
                .that(stack.getInactiveFocusList())
                .containsExactlyElementsIn(expectedFocusInfoList);
        assertWithMessage("Empty stack active focus in secondary zone")
                .that(stack.getActiveFocusList()).isEmpty();
    }

    private static SparseArray<CarAudioZone> generateAudioZones() {
        CarAudioContext testCarAudioContext =
                new CarAudioContext(CarAudioContext.getAllContextsInfo(),
                        /* useCoreAudioRouting= */ false);
        int zoneConfigId = 0;
        CarAudioZoneConfig primaryZoneConfig =
                new CarAudioZoneConfig.Builder("Primary zone config",
                        PRIMARY_ZONE_ID, zoneConfigId, /* isDefault= */ true)
                        .build();
        CarAudioZoneConfig secondaryZoneConfig =
                new CarAudioZoneConfig.Builder("Secondary zone config",
                        SECONDARY_ZONE_ID, zoneConfigId, /* isDefault= */ true)
                        .build();
        CarAudioZone primaryZone = new CarAudioZone(testCarAudioContext, "Primary zone",
                PRIMARY_ZONE_ID);
        CarAudioZone secondaryZone = new CarAudioZone(testCarAudioContext, "Secondary zone",
                SECONDARY_ZONE_ID);
        primaryZone.addZoneConfig(primaryZoneConfig);
        secondaryZone.addZoneConfig(secondaryZoneConfig);
        SparseArray<CarAudioZone> zones = new SparseArray<>();
        zones.put(PRIMARY_ZONE_ID, primaryZone);
        zones.put(SECONDARY_ZONE_ID, secondaryZone);
        return zones;
    }

    private static SparseArray<CarAudioFocus> generateMockFocus() {
        SparseArray<CarAudioFocus> mockFocusZones = new SparseArray<>();
        mockFocusZones.put(PRIMARY_ZONE_ID, mock(CarAudioFocus.class));
        mockFocusZones.put(SECONDARY_ZONE_ID, mock(CarAudioFocus.class));
        return mockFocusZones;
    }

    private static AudioFocusInfo generateAudioFocusRequest() {
        return generateAudioFocusRequestWithUsage(USAGE_MEDIA);
    }

    private static AudioFocusInfo generateAudioFocusRequestWithUsage(
            @AudioAttributes.AttributeSdkUsage int usage) {
        AudioAttributes attributes = new AudioAttributes.Builder().setUsage(usage).build();
        return generateAudioFocusInfoWithAttributes(attributes);
    }

    private static AudioFocusInfo generateAudioFocusInfoWithBundledZoneId(int zoneId) {
        Bundle bundle = new Bundle();
        bundle.putInt(CarAudioManager.AUDIOFOCUS_EXTRA_REQUEST_ZONE_ID, zoneId);

        AudioAttributes attributes = new AudioAttributes.Builder()
                .setUsage(USAGE_MEDIA)
                .addBundle(bundle)
                .build();
        return generateAudioFocusInfoWithAttributes(attributes);
    }

    private static AudioFocusInfo generateAudioFocusInfoWithAttributes(AudioAttributes attributes) {
        return new AudioFocusInfo(attributes, CLIENT_UID, CLIENT_ID,
                PACKAGE_NAME, AUDIOFOCUS_GAIN, AudioManager.AUDIOFOCUS_NONE, AUDIOFOCUS_FLAG,
                Build.VERSION.SDK_INT);
    }

    private CarZonesAudioFocus getCarZonesAudioFocus() {
        CarZonesAudioFocus carZonesAudioFocus =
                new CarZonesAudioFocus(mFocusMocks, mMockCarFocusCallback);
        carZonesAudioFocus.setOwningPolicy(mCarAudioService, mAudioPolicy);

        return carZonesAudioFocus;
    }

    private void withUidRoutingToZone(int zoneId) {
        when(mCarAudioService.getZoneIdForAudioFocusInfo(any())).thenReturn(zoneId);
    }

    private CarAudioFeaturesInfo getCarAudioFeaturesInfo() {
        return new CarAudioFeaturesInfo.Builder(AUDIO_FEATURE_NO_FEATURE).build();
    }
}
