/*
 * 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 androidx.media3.exoplayer.dash;

import static androidx.media3.exoplayer.dash.manifest.BaseUrl.DEFAULT_DVB_PRIORITY;
import static androidx.media3.exoplayer.dash.manifest.BaseUrl.DEFAULT_WEIGHT;
import static androidx.media3.exoplayer.upstream.DefaultLoadErrorHandlingPolicy.DEFAULT_LOCATION_EXCLUSION_MS;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;

import androidx.media3.exoplayer.dash.manifest.BaseUrl;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList;
import java.time.Duration;
import java.util.List;
import java.util.Random;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.shadows.ShadowSystemClock;

/** Unit tests for {@link BaseUrlExclusionList}. */
@RunWith(AndroidJUnit4.class)
public class BaseUrlExclusionListTest {

  @Test
  public void selectBaseUrl_excludeByServiceLocation_excludesAllBaseUrlOfSameServiceLocation() {
    BaseUrlExclusionList baseUrlExclusionList = new BaseUrlExclusionList();
    List<BaseUrl> baseUrls =
        ImmutableList.of(
            new BaseUrl(
                /* url= */ "a", /* serviceLocation= */ "a", /* priority= */ 1, /* weight= */ 1),
            new BaseUrl(
                /* url= */ "b", /* serviceLocation= */ "a", /* priority= */ 2, /* weight= */ 1),
            new BaseUrl(
                /* url= */ "c", /* serviceLocation= */ "c", /* priority= */ 3, /* weight= */ 1));

    baseUrlExclusionList.exclude(baseUrls.get(0), 5000);

    ShadowSystemClock.advanceBy(Duration.ofMillis(4999));
    assertThat(baseUrlExclusionList.selectBaseUrl(baseUrls).url).isEqualTo("c");
    ShadowSystemClock.advanceBy(Duration.ofMillis(1));
    assertThat(baseUrlExclusionList.selectBaseUrl(baseUrls).url).isEqualTo("a");
  }

  @Test
  public void selectBaseUrl_excludeByPriority_excludesAllBaseUrlsOfSamePriority() {
    Random mockRandom = mock(Random.class);
    when(mockRandom.nextInt(anyInt())).thenReturn(0);
    BaseUrlExclusionList baseUrlExclusionList = new BaseUrlExclusionList(mockRandom);
    List<BaseUrl> baseUrls =
        ImmutableList.of(
            new BaseUrl(
                /* url= */ "a", /* serviceLocation= */ "a", /* priority= */ 1, /* weight= */ 1),
            new BaseUrl(
                /* url= */ "b", /* serviceLocation= */ "b", /* priority= */ 1, /* weight= */ 99),
            new BaseUrl(
                /* url= */ "c", /* serviceLocation= */ "c", /* priority= */ 2, /* weight= */ 1));

    baseUrlExclusionList.exclude(baseUrls.get(0), 5000);

    ShadowSystemClock.advanceBy(Duration.ofMillis(4999));
    assertThat(baseUrlExclusionList.selectBaseUrl(baseUrls).url).isEqualTo("c");
    ShadowSystemClock.advanceBy(Duration.ofMillis(1));
    assertThat(baseUrlExclusionList.selectBaseUrl(baseUrls).url).isEqualTo("a");
  }

  @Test
  public void selectBaseUrl_samePriority_choiceIsRandom() {
    List<BaseUrl> baseUrls =
        ImmutableList.of(
            new BaseUrl(
                /* url= */ "a", /* serviceLocation= */ "a", /* priority= */ 1, /* weight= */ 99),
            new BaseUrl(
                /* url= */ "b", /* serviceLocation= */ "b", /* priority= */ 1, /* weight= */ 1));
    Random mockRandom = mock(Random.class);
    when(mockRandom.nextInt(anyInt())).thenReturn(99);

    assertThat(new BaseUrlExclusionList(mockRandom).selectBaseUrl(baseUrls))
        .isEqualTo(baseUrls.get(1));

    // Random is used for random choice.
    verify(mockRandom).nextInt(/* bound= */ 100);
    verifyNoMoreInteractions(mockRandom);
  }

  @Test
  public void selectBaseUrl_samePriority_choiceFromSameElementsRandomOnlyOnceSameAfterwards() {
    List<BaseUrl> baseUrlsVideo =
        ImmutableList.of(
            new BaseUrl(
                /* url= */ "a/v", /* serviceLocation= */ "a", /* priority= */ 1, /* weight= */ 99),
            new BaseUrl(
                /* url= */ "b/v", /* serviceLocation= */ "b", /* priority= */ 1, /* weight= */ 1));
    List<BaseUrl> baseUrlsAudio =
        ImmutableList.of(
            new BaseUrl(
                /* url= */ "a/a", /* serviceLocation= */ "a", /* priority= */ 1, /* weight= */ 99),
            new BaseUrl(
                /* url= */ "b/a", /* serviceLocation= */ "b", /* priority= */ 1, /* weight= */ 1));
    Random mockRandom = mock(Random.class);
    BaseUrlExclusionList baseUrlExclusionList = new BaseUrlExclusionList(mockRandom);
    when(mockRandom.nextInt(anyInt())).thenReturn(99);

    for (int i = 0; i < 5; i++) {
      assertThat(baseUrlExclusionList.selectBaseUrl(baseUrlsVideo).serviceLocation).isEqualTo("b");
      assertThat(baseUrlExclusionList.selectBaseUrl(baseUrlsAudio).serviceLocation).isEqualTo("b");
    }
    // Random is used only once.
    verify(mockRandom).nextInt(/* bound= */ 100);
    verifyNoMoreInteractions(mockRandom);
  }

  @Test
  public void selectBaseUrl_twiceTheSameLocationExcluded_correctExpirationDuration() {
    List<BaseUrl> baseUrls =
        ImmutableList.of(
            new BaseUrl(
                /* url= */ "a", /* serviceLocation= */ "a", /* priority= */ 1, /* weight= */ 1),
            new BaseUrl(
                /* url= */ "c", /* serviceLocation= */ "a", /* priority= */ 2, /* weight= */ 1),
            new BaseUrl(
                /* url= */ "d", /* serviceLocation= */ "d", /* priority= */ 2, /* weight= */ 1));
    BaseUrlExclusionList baseUrlExclusionList = new BaseUrlExclusionList();

    // Exclude location 'a'.
    baseUrlExclusionList.exclude(baseUrls.get(0), 5000);
    // Exclude location 'a' which increases exclusion duration of 'a'.
    baseUrlExclusionList.exclude(baseUrls.get(1), 10000);
    assertThat(baseUrlExclusionList.selectBaseUrl(baseUrls)).isNull();
    ShadowSystemClock.advanceBy(Duration.ofMillis(9999));
    // Location 'a' still excluded.
    assertThat(baseUrlExclusionList.selectBaseUrl(baseUrls)).isNull();
    ShadowSystemClock.advanceBy(Duration.ofMillis(1));
    assertThat(baseUrlExclusionList.getPriorityCountAfterExclusion(baseUrls)).isEqualTo(2);
    assertThat(baseUrlExclusionList.selectBaseUrl(baseUrls).url).isEqualTo("a");
  }

  @Test
  public void selectBaseUrl_twiceTheSamePriorityExcluded_correctExpirationDuration() {
    List<BaseUrl> baseUrls =
        ImmutableList.of(
            new BaseUrl(
                /* url= */ "a", /* serviceLocation= */ "a", /* priority= */ 1, /* weight= */ 1),
            new BaseUrl(
                /* url= */ "b", /* serviceLocation= */ "b", /* priority= */ 1, /* weight= */ 1),
            new BaseUrl(
                /* url= */ "c", /* serviceLocation= */ "c", /* priority= */ 2, /* weight= */ 1));
    BaseUrlExclusionList baseUrlExclusionList = new BaseUrlExclusionList();

    // Exclude priority 1.
    baseUrlExclusionList.exclude(baseUrls.get(0), 5000);
    // Exclude priority 1 again which increases the exclusion duration.
    baseUrlExclusionList.exclude(baseUrls.get(1), 10000);
    assertThat(baseUrlExclusionList.selectBaseUrl(baseUrls).url).isEqualTo("c");
    ShadowSystemClock.advanceBy(Duration.ofMillis(9999));
    assertThat(baseUrlExclusionList.getPriorityCountAfterExclusion(baseUrls)).isEqualTo(1);
    ShadowSystemClock.advanceBy(Duration.ofMillis(1));
    assertThat(baseUrlExclusionList.getPriorityCountAfterExclusion(baseUrls)).isEqualTo(2);
  }

  @Test
  public void selectBaseUrl_priorityUnset_isNotExcluded() {
    BaseUrlExclusionList baseUrlExclusionList = new BaseUrlExclusionList();
    ImmutableList<BaseUrl> baseUrls =
        ImmutableList.of(
            new BaseUrl(
                /* url= */ "a-1",
                /* serviceLocation= */ "a",
                BaseUrl.PRIORITY_UNSET,
                /* weight= */ 1),
            new BaseUrl(
                /* url= */ "a-2",
                /* serviceLocation= */ "a",
                BaseUrl.PRIORITY_UNSET,
                /* weight= */ 1),
            new BaseUrl(
                /* url= */ "b",
                /* serviceLocation= */ "b",
                BaseUrl.PRIORITY_UNSET,
                /* weight= */ 1));

    baseUrlExclusionList.exclude(baseUrls.get(0), 10_000);

    assertThat(baseUrlExclusionList.selectBaseUrl(baseUrls).serviceLocation).isEqualTo("b");
  }

  @Test
  public void selectBaseUrl_emptyBaseUrlList_selectionIsNull() {
    BaseUrlExclusionList baseUrlExclusionList = new BaseUrlExclusionList();

    assertThat(baseUrlExclusionList.selectBaseUrl(ImmutableList.of())).isNull();
  }

  @Test
  public void reset_dropsAllExclusions() {
    BaseUrlExclusionList baseUrlExclusionList = new BaseUrlExclusionList();
    ImmutableList<BaseUrl> baseUrls =
        ImmutableList.of(new BaseUrl("a", "a", DEFAULT_DVB_PRIORITY, DEFAULT_WEIGHT));
    baseUrlExclusionList.exclude(baseUrls.get(0), 5000);

    baseUrlExclusionList.reset();

    assertThat(baseUrlExclusionList.selectBaseUrl(baseUrls).url).isEqualTo("a");
  }

  @Test
  public void getPriorityCountAfterExclusion_correctPriorityCount() {
    List<BaseUrl> baseUrls =
        ImmutableList.of(
            new BaseUrl(
                /* url= */ "a", /* serviceLocation= */ "a", /* priority= */ 1, /* weight= */ 1),
            new BaseUrl(
                /* url= */ "b", /* serviceLocation= */ "b", /* priority= */ 2, /* weight= */ 1),
            new BaseUrl(
                /* url= */ "c", /* serviceLocation= */ "c", /* priority= */ 2, /* weight= */ 1),
            new BaseUrl(
                /* url= */ "d", /* serviceLocation= */ "d", /* priority= */ 3, /* weight= */ 1),
            new BaseUrl(
                /* url= */ "e", /* serviceLocation= */ "e", /* priority= */ 3, /* weight= */ 1));
    BaseUrlExclusionList baseUrlExclusionList = new BaseUrlExclusionList();

    // Empty base URL list.
    assertThat(baseUrlExclusionList.getPriorityCountAfterExclusion(ImmutableList.of()))
        .isEqualTo(0);

    assertThat(baseUrlExclusionList.getPriorityCountAfterExclusion(baseUrls)).isEqualTo(3);
    // Exclude base urls.
    baseUrlExclusionList.exclude(baseUrls.get(0), DEFAULT_LOCATION_EXCLUSION_MS);
    assertThat(baseUrlExclusionList.getPriorityCountAfterExclusion(baseUrls)).isEqualTo(2);
    baseUrlExclusionList.exclude(baseUrls.get(1), 2 * DEFAULT_LOCATION_EXCLUSION_MS);
    assertThat(baseUrlExclusionList.getPriorityCountAfterExclusion(baseUrls)).isEqualTo(1);
    baseUrlExclusionList.exclude(baseUrls.get(3), 3 * DEFAULT_LOCATION_EXCLUSION_MS);
    assertThat(baseUrlExclusionList.getPriorityCountAfterExclusion(baseUrls)).isEqualTo(0);
    // Time passes.
    ShadowSystemClock.advanceBy(Duration.ofMillis(DEFAULT_LOCATION_EXCLUSION_MS));
    assertThat(baseUrlExclusionList.getPriorityCountAfterExclusion(baseUrls)).isEqualTo(1);
    ShadowSystemClock.advanceBy(Duration.ofMillis(DEFAULT_LOCATION_EXCLUSION_MS));
    assertThat(baseUrlExclusionList.getPriorityCountAfterExclusion(baseUrls)).isEqualTo(2);
    ShadowSystemClock.advanceBy(Duration.ofMillis(DEFAULT_LOCATION_EXCLUSION_MS));
    assertThat(baseUrlExclusionList.getPriorityCountAfterExclusion(baseUrls)).isEqualTo(3);
  }

  @Test
  public void getPriorityCount_correctPriorityCount() {
    List<BaseUrl> baseUrls =
        ImmutableList.of(
            new BaseUrl(
                /* url= */ "a", /* serviceLocation= */ "a", /* priority= */ 1, /* weight= */ 1),
            new BaseUrl(
                /* url= */ "b", /* serviceLocation= */ "b", /* priority= */ 2, /* weight= */ 1),
            new BaseUrl(
                /* url= */ "c", /* serviceLocation= */ "c", /* priority= */ 2, /* weight= */ 1),
            new BaseUrl(
                /* url= */ "d", /* serviceLocation= */ "d", /* priority= */ 3, /* weight= */ 1));

    assertThat(BaseUrlExclusionList.getPriorityCount(baseUrls)).isEqualTo(3);
    assertThat(BaseUrlExclusionList.getPriorityCount(ImmutableList.of())).isEqualTo(0);
  }
}
