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

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

import androidx.media3.common.util.ParsableByteArray;
import androidx.media3.extractor.FlacFrameReader.SampleNumberHolder;
import androidx.media3.extractor.FlacMetadataReader.FlacStreamMetadataHolder;
import androidx.media3.extractor.flac.FlacConstants;
import androidx.media3.test.utils.FakeExtractorInput;
import androidx.media3.test.utils.TestUtil;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.io.IOException;
import org.junit.Test;
import org.junit.runner.RunWith;

/**
 * Unit tests for {@link FlacFrameReader}.
 *
 * <p>Some expected results in these tests have been retrieved using the <a
 * href="https://xiph.org/flac/documentation_tools_flac.html">flac</a> command.
 */
@RunWith(AndroidJUnit4.class)
public class FlacFrameReaderTest {

  @Test
  public void checkAndReadFrameHeader_validData_updatesPosition() throws Exception {
    FlacStreamMetadataHolder streamMetadataHolder =
        new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null);
    ExtractorInput input =
        buildExtractorInputReadingFromFirstFrame(
            "media/flac/bear_one_metadata_block.flac", streamMetadataHolder);
    int frameStartMarker = FlacMetadataReader.getFrameStartMarker(input);
    ParsableByteArray scratch = new ParsableByteArray(FlacConstants.MAX_FRAME_HEADER_SIZE);
    input.read(scratch.getData(), 0, FlacConstants.MAX_FRAME_HEADER_SIZE);

    FlacFrameReader.checkAndReadFrameHeader(
        scratch,
        streamMetadataHolder.flacStreamMetadata,
        frameStartMarker,
        new SampleNumberHolder());

    assertThat(scratch.getPosition()).isEqualTo(FlacConstants.MIN_FRAME_HEADER_SIZE);
  }

  @Test
  public void checkAndReadFrameHeader_validData_isTrue() throws Exception {
    FlacStreamMetadataHolder streamMetadataHolder =
        new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null);
    ExtractorInput input =
        buildExtractorInputReadingFromFirstFrame(
            "media/flac/bear_one_metadata_block.flac", streamMetadataHolder);
    int frameStartMarker = FlacMetadataReader.getFrameStartMarker(input);
    ParsableByteArray scratch = new ParsableByteArray(FlacConstants.MAX_FRAME_HEADER_SIZE);
    input.read(scratch.getData(), 0, FlacConstants.MAX_FRAME_HEADER_SIZE);

    boolean result =
        FlacFrameReader.checkAndReadFrameHeader(
            scratch,
            streamMetadataHolder.flacStreamMetadata,
            frameStartMarker,
            new SampleNumberHolder());

    assertThat(result).isTrue();
  }

  @Test
  public void checkAndReadFrameHeader_validData_writesSampleNumber() throws Exception {
    FlacStreamMetadataHolder streamMetadataHolder =
        new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null);
    ExtractorInput input =
        buildExtractorInputReadingFromFirstFrame(
            "media/flac/bear_one_metadata_block.flac", streamMetadataHolder);
    int frameStartMarker = FlacMetadataReader.getFrameStartMarker(input);
    // Skip first frame.
    input.skip(5030);
    ParsableByteArray scratch = new ParsableByteArray(FlacConstants.MAX_FRAME_HEADER_SIZE);
    input.read(scratch.getData(), 0, FlacConstants.MAX_FRAME_HEADER_SIZE);
    SampleNumberHolder sampleNumberHolder = new SampleNumberHolder();

    FlacFrameReader.checkAndReadFrameHeader(
        scratch, streamMetadataHolder.flacStreamMetadata, frameStartMarker, sampleNumberHolder);

    assertThat(sampleNumberHolder.sampleNumber).isEqualTo(4096);
  }

  @Test
  public void checkAndReadFrameHeader_invalidData_isFalse() throws Exception {
    FlacStreamMetadataHolder streamMetadataHolder =
        new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null);
    ExtractorInput input =
        buildExtractorInputReadingFromFirstFrame(
            "media/flac/bear_one_metadata_block.flac", streamMetadataHolder);
    ParsableByteArray scratch = new ParsableByteArray(FlacConstants.MAX_FRAME_HEADER_SIZE);
    input.read(scratch.getData(), 0, FlacConstants.MAX_FRAME_HEADER_SIZE);

    // The first bytes of the frame are not equal to the frame start marker.
    boolean result =
        FlacFrameReader.checkAndReadFrameHeader(
            scratch,
            streamMetadataHolder.flacStreamMetadata,
            /* frameStartMarker= */ -1,
            new SampleNumberHolder());

    assertThat(result).isFalse();
  }

  @Test
  public void checkFrameHeaderFromPeek_validData_doesNotUpdatePositions() throws Exception {
    String file = "media/flac/bear_one_metadata_block.flac";
    FlacStreamMetadataHolder streamMetadataHolder =
        new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null);
    ExtractorInput input = buildExtractorInputReadingFromFirstFrame(file, streamMetadataHolder);
    int frameStartMarker = FlacMetadataReader.getFrameStartMarker(input);
    long peekPosition = input.getPosition();
    // Set read position to 0.
    input = buildExtractorInput(file);
    input.advancePeekPosition((int) peekPosition);

    FlacFrameReader.checkFrameHeaderFromPeek(
        input, streamMetadataHolder.flacStreamMetadata, frameStartMarker, new SampleNumberHolder());

    assertThat(input.getPosition()).isEqualTo(0);
    assertThat(input.getPeekPosition()).isEqualTo(peekPosition);
  }

  @Test
  public void checkFrameHeaderFromPeek_validData_isTrue() throws Exception {
    FlacStreamMetadataHolder streamMetadataHolder =
        new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null);
    ExtractorInput input =
        buildExtractorInputReadingFromFirstFrame(
            "media/flac/bear_one_metadata_block.flac", streamMetadataHolder);
    int frameStartMarker = FlacMetadataReader.getFrameStartMarker(input);

    boolean result =
        FlacFrameReader.checkFrameHeaderFromPeek(
            input,
            streamMetadataHolder.flacStreamMetadata,
            frameStartMarker,
            new SampleNumberHolder());

    assertThat(result).isTrue();
  }

  @Test
  public void checkFrameHeaderFromPeek_validData_writesSampleNumber() throws Exception {
    FlacStreamMetadataHolder streamMetadataHolder =
        new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null);
    ExtractorInput input =
        buildExtractorInputReadingFromFirstFrame(
            "media/flac/bear_one_metadata_block.flac", streamMetadataHolder);
    int frameStartMarker = FlacMetadataReader.getFrameStartMarker(input);
    // Skip first frame.
    input.skip(5030);
    SampleNumberHolder sampleNumberHolder = new SampleNumberHolder();

    FlacFrameReader.checkFrameHeaderFromPeek(
        input, streamMetadataHolder.flacStreamMetadata, frameStartMarker, sampleNumberHolder);

    assertThat(sampleNumberHolder.sampleNumber).isEqualTo(4096);
  }

  @Test
  public void checkFrameHeaderFromPeek_invalidData_isFalse() throws Exception {
    FlacStreamMetadataHolder streamMetadataHolder =
        new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null);
    ExtractorInput input =
        buildExtractorInputReadingFromFirstFrame(
            "media/flac/bear_one_metadata_block.flac", streamMetadataHolder);

    // The first bytes of the frame are not equal to the frame start marker.
    boolean result =
        FlacFrameReader.checkFrameHeaderFromPeek(
            input,
            streamMetadataHolder.flacStreamMetadata,
            /* frameStartMarker= */ -1,
            new SampleNumberHolder());

    assertThat(result).isFalse();
  }

  @Test
  public void checkFrameHeaderFromPeek_invalidData_doesNotUpdatePositions() throws Exception {
    String file = "media/flac/bear_one_metadata_block.flac";
    FlacStreamMetadataHolder streamMetadataHolder =
        new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null);
    ExtractorInput input = buildExtractorInputReadingFromFirstFrame(file, streamMetadataHolder);
    long peekPosition = input.getPosition();
    // Set read position to 0.
    input = buildExtractorInput(file);
    input.advancePeekPosition((int) peekPosition);

    // The first bytes of the frame are not equal to the frame start marker.
    FlacFrameReader.checkFrameHeaderFromPeek(
        input,
        streamMetadataHolder.flacStreamMetadata,
        /* frameStartMarker= */ -1,
        new SampleNumberHolder());

    assertThat(input.getPosition()).isEqualTo(0);
    assertThat(input.getPeekPosition()).isEqualTo(peekPosition);
  }

  @Test
  public void getFirstSampleNumber_doesNotUpdateReadPositionAndAlignsPeekPosition()
      throws Exception {
    FlacStreamMetadataHolder streamMetadataHolder =
        new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null);
    ExtractorInput input =
        buildExtractorInputReadingFromFirstFrame(
            "media/flac/bear_one_metadata_block.flac", streamMetadataHolder);
    long initialReadPosition = input.getPosition();
    // Advance peek position after block size bits.
    input.advancePeekPosition(FlacConstants.MAX_FRAME_HEADER_SIZE);

    FlacFrameReader.getFirstSampleNumber(input, streamMetadataHolder.flacStreamMetadata);

    assertThat(input.getPosition()).isEqualTo(initialReadPosition);
    assertThat(input.getPeekPosition()).isEqualTo(input.getPosition());
  }

  @Test
  public void getFirstSampleNumber_returnsSampleNumber() throws Exception {
    FlacStreamMetadataHolder streamMetadataHolder =
        new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null);
    ExtractorInput input =
        buildExtractorInputReadingFromFirstFrame(
            "media/flac/bear_one_metadata_block.flac", streamMetadataHolder);
    // Skip first frame.
    input.skip(5030);

    long result =
        FlacFrameReader.getFirstSampleNumber(input, streamMetadataHolder.flacStreamMetadata);

    assertThat(result).isEqualTo(4096);
  }

  @Test
  public void readFrameBlockSizeSamplesFromKey_keyIs1_returnsCorrectBlockSize() {
    int result =
        FlacFrameReader.readFrameBlockSizeSamplesFromKey(
            new ParsableByteArray(/* limit= */ 0), /* blockSizeKey= */ 1);

    assertThat(result).isEqualTo(192);
  }

  @Test
  public void readFrameBlockSizeSamplesFromKey_keyBetween2and5_returnsCorrectBlockSize() {
    int result =
        FlacFrameReader.readFrameBlockSizeSamplesFromKey(
            new ParsableByteArray(/* limit= */ 0), /* blockSizeKey= */ 3);

    assertThat(result).isEqualTo(1152);
  }

  @Test
  public void readFrameBlockSizeSamplesFromKey_keyBetween6And7_returnsCorrectBlockSize()
      throws Exception {
    ExtractorInput input = buildExtractorInput("media/flac/bear_one_metadata_block.flac");
    // Skip to block size bits of last frame.
    input.skipFully(164033);
    ParsableByteArray scratch = new ParsableByteArray(2);
    input.readFully(scratch.getData(), 0, 2);

    int result = FlacFrameReader.readFrameBlockSizeSamplesFromKey(scratch, /* blockSizeKey= */ 7);

    assertThat(result).isEqualTo(496);
  }

  @Test
  public void readFrameBlockSizeSamplesFromKey_keyBetween8and15_returnsCorrectBlockSize() {
    int result =
        FlacFrameReader.readFrameBlockSizeSamplesFromKey(
            new ParsableByteArray(/* limit= */ 0), /* blockSizeKey= */ 11);

    assertThat(result).isEqualTo(2048);
  }

  @Test
  public void readFrameBlockSizeSamplesFromKey_invalidKey_returnsCorrectBlockSize() {
    int result =
        FlacFrameReader.readFrameBlockSizeSamplesFromKey(
            new ParsableByteArray(/* limit= */ 0), /* blockSizeKey= */ 25);

    assertThat(result).isEqualTo(-1);
  }

  private static ExtractorInput buildExtractorInput(String file) throws IOException {
    byte[] fileData = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), file);
    return new FakeExtractorInput.Builder().setData(fileData).build();
  }

  private ExtractorInput buildExtractorInputReadingFromFirstFrame(
      String file, FlacStreamMetadataHolder streamMetadataHolder) throws IOException {
    ExtractorInput input = buildExtractorInput(file);

    input.skipFully(FlacConstants.STREAM_MARKER_SIZE);

    boolean lastMetadataBlock = false;
    while (!lastMetadataBlock) {
      lastMetadataBlock = FlacMetadataReader.readMetadataBlock(input, streamMetadataHolder);
    }

    return input;
  }
}
