/*
 * Copyright (C) 2017 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 com.google.common.truth.Truth.assertThat;

import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.MimeTypes;
import androidx.media3.decoder.DecoderInputBuffer;
import androidx.media3.exoplayer.FormatHolder;
import androidx.media3.exoplayer.dash.manifest.EventStream;
import androidx.media3.exoplayer.source.SampleStream;
import androidx.media3.extractor.metadata.MetadataInputBuffer;
import androidx.media3.extractor.metadata.emsg.EventMessage;
import androidx.media3.extractor.metadata.emsg.EventMessageEncoder;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

/** Unit test for {@link EventSampleStream}. */
@RunWith(AndroidJUnit4.class)
public final class EventSampleStreamTest {

  private static final String SCHEME_ID = "urn:test";
  private static final String VALUE = "123";
  private static final Format FORMAT =
      new Format.Builder()
          .setId("urn:test/123")
          .setSampleMimeType(MimeTypes.APPLICATION_EMSG)
          .build();
  private static final byte[] MESSAGE_DATA = new byte[] {1, 2, 3, 4};
  private static final long DURATION_MS = 3000;
  private static final long TIME_SCALE = 1000;

  private FormatHolder formatHolder;
  private MetadataInputBuffer inputBuffer;
  private EventMessageEncoder eventMessageEncoder;

  @Before
  public void setUp() {
    formatHolder = new FormatHolder();
    inputBuffer = new MetadataInputBuffer();
    eventMessageEncoder = new EventMessageEncoder();
  }

  /**
   * Tests that {@link EventSampleStream#readData(FormatHolder, DecoderInputBuffer, int)} will
   * return format for the first call.
   */
  @Test
  public void readDataReturnFormatForFirstRead() {
    EventStream eventStream =
        new EventStream(SCHEME_ID, VALUE, TIME_SCALE, new long[0], new EventMessage[0]);
    // Make the event stream appendable so that the format is read. Otherwise, the format will be
    // skipped and the end of input will be read.
    EventSampleStream sampleStream =
        new EventSampleStream(eventStream, FORMAT, /* eventStreamAppendable= */ true);

    int result = readData(sampleStream);
    assertThat(result).isEqualTo(C.RESULT_FORMAT_READ);
    assertThat(formatHolder.format).isEqualTo(FORMAT);
  }

  /**
   * Tests that a non-dynamic {@link EventSampleStream} will return a buffer with {@link
   * C#BUFFER_FLAG_END_OF_STREAM} when trying to read sample out-of-bound.
   */
  @Test
  public void readDataOutOfBoundReturnEndOfStreamAfterFormatForNonDynamicEventSampleStream() {
    EventStream eventStream =
        new EventStream(SCHEME_ID, VALUE, TIME_SCALE, new long[0], new EventMessage[0]);
    EventSampleStream sampleStream = new EventSampleStream(eventStream, FORMAT, false);
    // first read - read format
    readData(sampleStream);

    int result = readData(sampleStream);
    assertThat(result).isEqualTo(C.RESULT_BUFFER_READ);
    assertThat(inputBuffer.isEndOfStream()).isTrue();
  }

  /**
   * Tests that a dynamic {@link EventSampleStream} will return {@link C#RESULT_NOTHING_READ} when
   * trying to read sample out-of-bound.
   */
  @Test
  public void readDataOutOfBoundReturnEndOfStreamAfterFormatForDynamicEventSampleStream() {
    EventStream eventStream =
        new EventStream(SCHEME_ID, VALUE, TIME_SCALE, new long[0], new EventMessage[0]);
    EventSampleStream sampleStream = new EventSampleStream(eventStream, FORMAT, true);
    // first read - read format
    readData(sampleStream);

    int result = readData(sampleStream);
    assertThat(result).isEqualTo(C.RESULT_NOTHING_READ);
  }

  /**
   * Tests that {@link EventSampleStream#readData(FormatHolder, DecoderInputBuffer, int)} will
   * return sample data after the first call.
   */
  @Test
  public void readDataReturnDataAfterFormat() {
    long presentationTimeUs = 1000000;
    EventMessage eventMessage = newEventMessageWithId(1);
    EventStream eventStream =
        new EventStream(
            SCHEME_ID,
            VALUE,
            TIME_SCALE,
            new long[] {presentationTimeUs},
            new EventMessage[] {eventMessage});
    EventSampleStream sampleStream = new EventSampleStream(eventStream, FORMAT, false);
    // first read - read format
    readData(sampleStream);

    int result = readData(sampleStream);
    assertThat(result).isEqualTo(C.RESULT_BUFFER_READ);
    assertThat(inputBuffer.data.array()).isEqualTo(getEncodedMessage(eventMessage));
  }

  @Test
  public void readData_peek_doesNotAdvanceSampleIndex() {
    long presentationTimeUs = 1000000;
    EventMessage eventMessage = newEventMessageWithId(1);
    EventStream eventStream =
        new EventStream(
            SCHEME_ID,
            VALUE,
            TIME_SCALE,
            new long[] {presentationTimeUs},
            new EventMessage[] {eventMessage});
    EventSampleStream sampleStream = new EventSampleStream(eventStream, FORMAT, false);
    // first read - read format
    readData(sampleStream);

    // read the event with FLAG_PEEK
    int result = readData(sampleStream, SampleStream.FLAG_PEEK);
    assertThat(result).isEqualTo(C.RESULT_BUFFER_READ);
    assertThat(inputBuffer.isKeyFrame()).isTrue();
    assertThat(inputBuffer.data.array()).isEqualTo(getEncodedMessage(eventMessage));
    // read again gives the same message again (there is only one msg)
    result = readData(sampleStream);
    assertThat(result).isEqualTo(C.RESULT_BUFFER_READ);
    assertThat(inputBuffer.isKeyFrame()).isTrue();
    assertThat(inputBuffer.data.array()).isEqualTo(getEncodedMessage(eventMessage));
    // read again gives end of stream
    result = readData(sampleStream);
    assertThat(result).isEqualTo(C.RESULT_BUFFER_READ);
    assertThat(inputBuffer.isEndOfStream()).isTrue();
  }

  @Test
  public void readData_omitSampleData_doesOmitSampleData() {
    long presentationTimeUs = 1000000;
    EventMessage eventMessage = newEventMessageWithId(1);
    EventStream eventStream =
        new EventStream(
            SCHEME_ID,
            VALUE,
            TIME_SCALE,
            new long[] {presentationTimeUs},
            new EventMessage[] {eventMessage});
    EventSampleStream sampleStream = new EventSampleStream(eventStream, FORMAT, false);
    // first read - read format
    readData(sampleStream);

    // read the event with FLAG_OMIT_SAMPLE_DATA
    int result = readData(sampleStream, SampleStream.FLAG_OMIT_SAMPLE_DATA);
    assertThat(result).isEqualTo(C.RESULT_BUFFER_READ);
    assertThat(inputBuffer.data).isNull();
  }

  /**
   * Tests that {@link EventSampleStream#skipData(long)} will skip until the given position, and the
   * next {@link EventSampleStream#readData(FormatHolder, DecoderInputBuffer, int)} call will return
   * sample data from that position.
   */
  @Test
  public void skipDataThenReadDataReturnDataFromSkippedPosition() {
    long presentationTimeUs1 = 1000000;
    long presentationTimeUs2 = 2000000;
    EventMessage eventMessage1 = newEventMessageWithId(1);
    EventMessage eventMessage2 = newEventMessageWithId(2);
    EventStream eventStream =
        new EventStream(
            SCHEME_ID,
            VALUE,
            TIME_SCALE,
            new long[] {presentationTimeUs1, presentationTimeUs2},
            new EventMessage[] {eventMessage1, eventMessage2});
    EventSampleStream sampleStream = new EventSampleStream(eventStream, FORMAT, false);
    // first read - read format
    readData(sampleStream);

    int skipped = sampleStream.skipData(presentationTimeUs2);
    int result = readData(sampleStream);
    assertThat(skipped).isEqualTo(1);
    assertThat(result).isEqualTo(C.RESULT_BUFFER_READ);
    assertThat(inputBuffer.data.array()).isEqualTo(getEncodedMessage(eventMessage2));
  }

  /**
   * Tests that {@link EventSampleStream#seekToUs(long)} (long)} will seek to the given position,
   * and the next {@link EventSampleStream#readData(FormatHolder, DecoderInputBuffer, int)} call
   * will return sample data from that position.
   */
  @Test
  public void seekToUsThenReadDataReturnDataFromSeekPosition() {
    long presentationTimeUs1 = 1000000;
    long presentationTimeUs2 = 2000000;
    EventMessage eventMessage1 = newEventMessageWithId(1);
    EventMessage eventMessage2 = newEventMessageWithId(2);
    EventStream eventStream =
        new EventStream(
            SCHEME_ID,
            VALUE,
            TIME_SCALE,
            new long[] {presentationTimeUs1, presentationTimeUs2},
            new EventMessage[] {eventMessage1, eventMessage2});
    EventSampleStream sampleStream = new EventSampleStream(eventStream, FORMAT, false);
    // first read - read format
    readData(sampleStream);

    sampleStream.seekToUs(presentationTimeUs2);
    int result = readData(sampleStream);
    assertThat(result).isEqualTo(C.RESULT_BUFFER_READ);
    assertThat(inputBuffer.data.array()).isEqualTo(getEncodedMessage(eventMessage2));
  }

  /**
   * Tests that {@link EventSampleStream#updateEventStream(EventStream, boolean)} will update the
   * underlying event stream, but keep the read timestamp, so the next {@link
   * EventSampleStream#readData(FormatHolder, DecoderInputBuffer, int)} call will return sample data
   * from after the last read sample timestamp.
   */
  @Test
  public void updateEventStreamContinueToReadAfterLastReadSamplePresentationTime() {
    long presentationTimeUs1 = 1000000;
    long presentationTimeUs2 = 2000000;
    long presentationTimeUs3 = 3000000;
    EventMessage eventMessage1 = newEventMessageWithId(1);
    EventMessage eventMessage2 = newEventMessageWithId(2);
    EventMessage eventMessage3 = newEventMessageWithId(3);
    EventStream eventStream1 =
        new EventStream(
            SCHEME_ID,
            VALUE,
            TIME_SCALE,
            new long[] {presentationTimeUs1, presentationTimeUs2},
            new EventMessage[] {eventMessage1, eventMessage2});
    EventStream eventStream2 =
        new EventStream(
            SCHEME_ID,
            VALUE,
            TIME_SCALE,
            new long[] {presentationTimeUs1, presentationTimeUs2, presentationTimeUs3},
            new EventMessage[] {eventMessage1, eventMessage2, eventMessage3});
    EventSampleStream sampleStream = new EventSampleStream(eventStream1, FORMAT, true);
    // first read - read format
    readData(sampleStream);
    // read first and second sample.
    readData(sampleStream);
    readData(sampleStream);

    sampleStream.updateEventStream(eventStream2, true);
    int result = readData(sampleStream);
    assertThat(result).isEqualTo(C.RESULT_BUFFER_READ);
    assertThat(inputBuffer.data.array()).isEqualTo(getEncodedMessage(eventMessage3));
  }

  /**
   * Tests that {@link EventSampleStream#updateEventStream(EventStream, boolean)} will update the
   * underlying event stream, but keep the timestamp the stream has skipped to, so the next {@link
   * EventSampleStream#readData(FormatHolder, DecoderInputBuffer, int)} call will return sample data
   * from the skipped position.
   */
  @Test
  public void skipDataThenUpdateStreamContinueToReadFromSkippedPosition() {
    long presentationTimeUs1 = 1000000;
    long presentationTimeUs2 = 2000000;
    long presentationTimeUs3 = 3000000;
    EventMessage eventMessage1 = newEventMessageWithId(1);
    EventMessage eventMessage2 = newEventMessageWithId(2);
    EventMessage eventMessage3 = newEventMessageWithId(3);
    EventStream eventStream1 =
        new EventStream(
            SCHEME_ID,
            VALUE,
            TIME_SCALE,
            new long[] {presentationTimeUs1, presentationTimeUs2},
            new EventMessage[] {eventMessage1, eventMessage2});
    EventStream eventStream2 =
        new EventStream(
            SCHEME_ID,
            VALUE,
            TIME_SCALE,
            new long[] {presentationTimeUs1, presentationTimeUs2, presentationTimeUs3},
            new EventMessage[] {eventMessage1, eventMessage2, eventMessage3});
    EventSampleStream sampleStream = new EventSampleStream(eventStream1, FORMAT, true);
    // first read - read format
    readData(sampleStream);
    sampleStream.skipData(presentationTimeUs2 + 1);

    sampleStream.updateEventStream(eventStream2, true);
    int result = readData(sampleStream);
    assertThat(result).isEqualTo(C.RESULT_BUFFER_READ);
    assertThat(inputBuffer.data.array()).isEqualTo(getEncodedMessage(eventMessage3));
  }

  /**
   * Tests that {@link EventSampleStream#skipData(long)} will only skip to the point right after it
   * last event. A following {@link EventSampleStream#updateEventStream(EventStream, boolean)} will
   * update the underlying event stream and keep the timestamp the stream has skipped to, so the
   * next {@link EventSampleStream#readData(FormatHolder, DecoderInputBuffer, int)} call will return
   * sample data from the skipped position.
   */
  @Test
  public void skipDataThenUpdateStreamContinueToReadDoNotSkippedMoreThanAvailable() {
    long presentationTimeUs1 = 1000000;
    long presentationTimeUs2 = 2000000;
    long presentationTimeUs3 = 3000000;
    EventMessage eventMessage1 = newEventMessageWithId(1);
    EventMessage eventMessage2 = newEventMessageWithId(2);
    EventMessage eventMessage3 = newEventMessageWithId(3);
    EventStream eventStream1 =
        new EventStream(
            SCHEME_ID,
            VALUE,
            TIME_SCALE,
            new long[] {presentationTimeUs1},
            new EventMessage[] {eventMessage1});
    EventStream eventStream2 =
        new EventStream(
            SCHEME_ID,
            VALUE,
            TIME_SCALE,
            new long[] {presentationTimeUs1, presentationTimeUs2, presentationTimeUs3},
            new EventMessage[] {eventMessage1, eventMessage2, eventMessage3});
    EventSampleStream sampleStream = new EventSampleStream(eventStream1, FORMAT, true);
    // first read - read format
    readData(sampleStream);
    // even though the skip call is to 2000001, since eventStream1 only contains sample until
    // 1000000, it will only skip to 1000001.
    sampleStream.skipData(presentationTimeUs2 + 1);

    sampleStream.updateEventStream(eventStream2, true);
    int result = readData(sampleStream);
    assertThat(result).isEqualTo(C.RESULT_BUFFER_READ);
    assertThat(inputBuffer.data.array()).isEqualTo(getEncodedMessage(eventMessage2));
  }

  /**
   * Tests that {@link EventSampleStream#updateEventStream(EventStream, boolean)} will update the
   * underlying event stream, but keep the timestamp the stream has seek to, so the next {@link
   * EventSampleStream#readData(FormatHolder, DecoderInputBuffer, int)} call will return sample data
   * from the seek position.
   */
  @Test
  public void seekToUsThenUpdateStreamContinueToReadFromSeekPosition() {
    long presentationTimeUs1 = 1000000;
    long presentationTimeUs2 = 2000000;
    long presentationTimeUs3 = 3000000;
    EventMessage eventMessage1 = newEventMessageWithId(1);
    EventMessage eventMessage2 = newEventMessageWithId(2);
    EventMessage eventMessage3 = newEventMessageWithId(3);
    EventStream eventStream1 =
        new EventStream(
            SCHEME_ID,
            VALUE,
            TIME_SCALE,
            new long[] {presentationTimeUs1, presentationTimeUs2},
            new EventMessage[] {eventMessage1, eventMessage2});
    EventStream eventStream2 =
        new EventStream(
            SCHEME_ID,
            VALUE,
            TIME_SCALE,
            new long[] {presentationTimeUs1, presentationTimeUs2, presentationTimeUs3},
            new EventMessage[] {eventMessage1, eventMessage2, eventMessage3});
    EventSampleStream sampleStream = new EventSampleStream(eventStream1, FORMAT, true);
    // first read - read format
    readData(sampleStream);
    sampleStream.seekToUs(presentationTimeUs2);

    sampleStream.updateEventStream(eventStream2, true);
    int result = readData(sampleStream);
    assertThat(result).isEqualTo(C.RESULT_BUFFER_READ);
    assertThat(inputBuffer.data.array()).isEqualTo(getEncodedMessage(eventMessage2));
  }

  /**
   * Tests that {@link EventSampleStream#updateEventStream(EventStream, boolean)} will update the
   * underlying event stream, but keep the timestamp the stream has seek to, so the next {@link
   * EventSampleStream#readData(FormatHolder, DecoderInputBuffer, int)} call will return sample data
   * from the seek position.
   */
  @Test
  public void seekToThenUpdateStreamContinueToReadFromSeekPositionEvenSeekMoreThanAvailable() {
    long presentationTimeUs1 = 1000000;
    long presentationTimeUs2 = 2000000;
    long presentationTimeUs3 = 3000000;
    EventMessage eventMessage1 = newEventMessageWithId(1);
    EventMessage eventMessage2 = newEventMessageWithId(2);
    EventMessage eventMessage3 = newEventMessageWithId(3);
    EventStream eventStream1 =
        new EventStream(
            SCHEME_ID,
            VALUE,
            TIME_SCALE,
            new long[] {presentationTimeUs1},
            new EventMessage[] {eventMessage1});
    EventStream eventStream2 =
        new EventStream(
            SCHEME_ID,
            VALUE,
            TIME_SCALE,
            new long[] {presentationTimeUs1, presentationTimeUs2, presentationTimeUs3},
            new EventMessage[] {eventMessage1, eventMessage2, eventMessage3});
    EventSampleStream sampleStream = new EventSampleStream(eventStream1, FORMAT, true);
    // first read - read format
    readData(sampleStream);
    sampleStream.seekToUs(presentationTimeUs2 + 1);

    sampleStream.updateEventStream(eventStream2, true);
    int result = readData(sampleStream);
    assertThat(result).isEqualTo(C.RESULT_BUFFER_READ);
    assertThat(inputBuffer.data.array()).isEqualTo(getEncodedMessage(eventMessage3));
  }

  private int readData(EventSampleStream sampleStream) {
    return readData(sampleStream, /* readFlags= */ 0);
  }

  private int readData(EventSampleStream sampleStream, @SampleStream.ReadFlags int readFlags) {
    inputBuffer.clear();
    return sampleStream.readData(formatHolder, inputBuffer, readFlags);
  }

  private EventMessage newEventMessageWithId(int id) {
    return new EventMessage(SCHEME_ID, VALUE, DURATION_MS, id, MESSAGE_DATA);
  }

  private byte[] getEncodedMessage(EventMessage eventMessage) {
    return eventMessageEncoder.encode(eventMessage);
  }
}
