content: /*
   * Copyright (C) 2016 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.google.android.exoplayer2.text.webvtt;
  
  import static com.google.android.exoplayer2.testutil.truth.SpannedSubject.assertThat;
  import static com.google.common.truth.Truth.assertThat;
  import static org.junit.Assert.fail;
  
  import android.text.Layout.Alignment;
  import android.text.Spanned;
  import androidx.test.core.app.ApplicationProvider;
  import androidx.test.ext.junit.runners.AndroidJUnit4;
  import com.google.android.exoplayer2.testutil.TestUtil;
  import com.google.android.exoplayer2.text.Cue;
  import com.google.android.exoplayer2.text.SubtitleDecoderException;
  import com.google.android.exoplayer2.text.span.TextAnnotation;
  import com.google.android.exoplayer2.util.Assertions;
  import com.google.android.exoplayer2.util.ColorParser;
  import com.google.common.collect.Iterables;
  import com.google.common.truth.Expect;
  import java.io.IOException;
  import java.util.List;
  import org.junit.Rule;
  import org.junit.Test;
  import org.junit.runner.RunWith;
  
  /** Unit test for {@link WebvttDecoder}. */
  @RunWith(AndroidJUnit4.class)
  public class WebvttDecoderTest {
  
    private static final String TYPICAL_FILE = "media/webvtt/typical";
    private static final String TYPICAL_WITH_BAD_TIMESTAMPS =
        "media/webvtt/typical_with_bad_timestamps";
    private static final String TYPICAL_WITH_IDS_FILE = "media/webvtt/typical_with_identifiers";
    private static final String TYPICAL_WITH_COMMENTS_FILE = "media/webvtt/typical_with_comments";
    private static final String WITH_POSITIONING_FILE = "media/webvtt/with_positioning";
    private static final String WITH_OVERLAPPING_TIMESTAMPS_FILE =
        "media/webvtt/with_overlapping_timestamps";
    private static final String WITH_VERTICAL_FILE = "media/webvtt/with_vertical";
    private static final String WITH_RUBIES_FILE = "media/webvtt/with_rubies";
    private static final String WITH_BAD_CUE_HEADER_FILE = "media/webvtt/with_bad_cue_header";
    private static final String WITH_TAGS_FILE = "media/webvtt/with_tags";
    private static final String WITH_CSS_STYLES = "media/webvtt/with_css_styles";
    private static final String WITH_FONT_SIZE = "media/webvtt/with_font_size";
    private static final String WITH_CSS_COMPLEX_SELECTORS =
        "media/webvtt/with_css_complex_selectors";
    private static final String WITH_CSS_TEXT_COMBINE_UPRIGHT =
        "media/webvtt/with_css_text_combine_upright";
    private static final String WITH_BOM = "media/webvtt/with_bom";
    private static final String EMPTY_FILE = "media/webvtt/empty";
  
    @Rule public final Expect expect = Expect.create();
  
    @Test
    public void decodeEmpty() throws IOException {
      WebvttDecoder decoder = new WebvttDecoder();
      byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), EMPTY_FILE);
      try {
        decoder.decode(bytes, bytes.length, /* reset= */ false);
        fail();
      } catch (SubtitleDecoderException expected) {
        // Do nothing.
      }
    }
  
    @Test
    public void decodeTypical() throws Exception {
      WebvttSubtitle subtitle = getSubtitleForTestAsset(TYPICAL_FILE);
  
      assertThat(subtitle.getEventTimeCount()).isEqualTo(4);
  
      assertThat(subtitle.getEventTime(0)).isEqualTo(0L);
      assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L);
      Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0)));
      assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle.");
  
      assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L);
      assertThat(subtitle.getEventTime(3)).isEqualTo(3_456_000L);
      Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2)));
      assertThat(secondCue.text.toString()).isEqualTo("This is the second subtitle.");
    }
  
    @Test
    public void decodeWithBom() throws Exception {
      WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_BOM);
  
      assertThat(subtitle.getEventTimeCount()).isEqualTo(4);
  
      assertThat(subtitle.getEventTime(0)).isEqualTo(0L);
      assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L);
      Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0)));
      assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle.");
  
      assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L);
      assertThat(subtitle.getEventTime(3)).isEqualTo(3_456_000L);
      Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2)));
      assertThat(secondCue.text.toString()).isEqualTo("This is the second subtitle.");
    }
  
    @Test
    public void decodeTypicalWithBadTimestamps() throws Exception {
      WebvttSubtitle subtitle = getSubtitleForTestAsset(TYPICAL_WITH_BAD_TIMESTAMPS);
  
      assertThat(subtitle.getEventTimeCount()).isEqualTo(4);
  
      assertThat(subtitle.getEventTime(0)).isEqualTo(0L);
      assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L);
      Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0)));
      assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle.");
  
      assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L);
      assertThat(subtitle.getEventTime(3)).isEqualTo(3_456_000L);
      Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2)));
      assertThat(secondCue.text.toString()).isEqualTo("This is the second subtitle.");
    }
  
    @Test
    public void decodeTypicalWithIds() throws Exception {
      WebvttSubtitle subtitle = getSubtitleForTestAsset(TYPICAL_WITH_IDS_FILE);
  
      assertThat(subtitle.getEventTimeCount()).isEqualTo(4);
  
      assertThat(subtitle.getEventTime(0)).isEqualTo(0L);
      assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L);
      Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0)));
      assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle.");
  
      assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L);
      assertThat(subtitle.getEventTime(3)).isEqualTo(3_456_000L);
      Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2)));
      assertThat(secondCue.text.toString()).isEqualTo("This is the second subtitle.");
    }
  
    @Test
    public void decodeTypicalWithComments() throws Exception {
      WebvttSubtitle subtitle = getSubtitleForTestAsset(TYPICAL_WITH_COMMENTS_FILE);
  
      assertThat(subtitle.getEventTimeCount()).isEqualTo(4);
  
      assertThat(subtitle.getEventTime(0)).isEqualTo(0L);
      assertThat(subtitle.getEventTime(0 + 1)).isEqualTo(1_234_000L);
      Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0)));
      assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle.");
  
      assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L);
      assertThat(subtitle.getEventTime(2 + 1)).isEqualTo(3_456_000L);
      Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2)));
      assertThat(secondCue.text.toString()).isEqualTo("This is the second subtitle.");
    }
  
    @Test
    public void decodeWithTags() throws Exception {
      WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_TAGS_FILE);
  
      assertThat(subtitle.getEventTimeCount()).isEqualTo(8);
  
      assertThat(subtitle.getEventTime(0)).isEqualTo(0L);
      assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L);
      Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0)));
      assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle.");
  
      assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L);
      assertThat(subtitle.getEventTime(3)).isEqualTo(3_456_000L);
      Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2)));
      assertThat(secondCue.text.toString()).isEqualTo("This is the second subtitle.");
  
      assertThat(subtitle.getEventTime(4)).isEqualTo(4_000_000L);
      assertThat(subtitle.getEventTime(5)).isEqualTo(5_000_000L);
      Cue thirdCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4)));
      assertThat(thirdCue.text.toString()).isEqualTo("This is the third subtitle.");
  
      assertThat(subtitle.getEventTime(6)).isEqualTo(6_000_000L);
      assertThat(subtitle.getEventTime(7)).isEqualTo(7_000_000L);
      Cue fourthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(6)));
      assertThat(fourthCue.text.toString()).isEqualTo("This is the <fourth> &subtitle.");
    }
  
    @Test
    public void decodeWithPositioning() throws Exception {
      WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_POSITIONING_FILE);
  
      assertThat(subtitle.getEventTimeCount()).isEqualTo(16);
  
      assertThat(subtitle.getEventTime(0)).isEqualTo(0L);
      assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L);
      Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0)));
      assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle.");
      assertThat(firstCue.position).isEqualTo(0.6f);
      assertThat(firstCue.positionAnchor).isEqualTo(Cue.ANCHOR_TYPE_END);
      assertThat(firstCue.textAlignment).isEqualTo(Alignment.ALIGN_NORMAL);
      assertThat(firstCue.size).isEqualTo(0.35f);
  
      // Unspecified values should use WebVTT defaults
      assertThat(firstCue.line).isEqualTo(-1f);
      assertThat(firstCue.lineType).isEqualTo(Cue.LINE_TYPE_NUMBER);
      assertThat(firstCue.verticalType).isEqualTo(Cue.TYPE_UNSET);
  
      assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L);
      assertThat(subtitle.getEventTime(3)).isEqualTo(3_456_000L);
      Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2)));
      assertThat(secondCue.text.toString()).isEqualTo("This is the second subtitle.");
      // Position is invalid so defaults to 0.5
      assertThat(secondCue.position).isEqualTo(0.5f);
      assertThat(secondCue.textAlignment).isEqualTo(Alignment.ALIGN_OPPOSITE);
  
      assertThat(subtitle.getEventTime(4)).isEqualTo(4_000_000L);
      assertThat(subtitle.getEventTime(5)).isEqualTo(5_000_000L);
      Cue thirdCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4)));
      assertThat(thirdCue.text.toString()).isEqualTo("This is the third subtitle.");
      assertThat(thirdCue.line).isEqualTo(0.45f);
      assertThat(thirdCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION);
      assertThat(thirdCue.lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_END);
      assertThat(thirdCue.textAlignment).isEqualTo(Alignment.ALIGN_CENTER);
      // Derived from `align:middle`:
      assertThat(thirdCue.positionAnchor).isEqualTo(Cue.ANCHOR_TYPE_MIDDLE);
  
      assertThat(subtitle.getEventTime(6)).isEqualTo(6_000_000L);
      assertThat(subtitle.getEventTime(7)).isEqualTo(7_000_000L);
      Cue fourthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(6)));
      assertThat(fourthCue.text.toString()).isEqualTo("This is the fourth subtitle.");
      assertThat(fourthCue.line).isEqualTo(-10f);
      assertThat(fourthCue.lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_START);
      assertThat(fourthCue.textAlignment).isEqualTo(Alignment.ALIGN_CENTER);
      // Derived from `align:middle`:
      assertThat(fourthCue.position).isEqualTo(0.5f);
      assertThat(fourthCue.positionAnchor).isEqualTo(Cue.ANCHOR_TYPE_MIDDLE);
  
      assertThat(subtitle.getEventTime(8)).isEqualTo(8_000_000L);
      assertThat(subtitle.getEventTime(9)).isEqualTo(9_000_000L);
      Cue fifthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(8)));
      assertThat(fifthCue.text.toString()).isEqualTo("This is the fifth subtitle.");
      assertThat(fifthCue.textAlignment).isEqualTo(Alignment.ALIGN_OPPOSITE);
      // Derived from `align:right`:
      assertThat(fifthCue.position).isEqualTo(1.0f);
      assertThat(fifthCue.positionAnchor).isEqualTo(Cue.ANCHOR_TYPE_END);
  
      assertThat(subtitle.getEventTime(10)).isEqualTo(10_000_000L);
      assertThat(subtitle.getEventTime(11)).isEqualTo(11_000_000L);
      Cue sixthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(10)));
      assertThat(sixthCue.text.toString()).isEqualTo("This is the sixth subtitle.");
      assertThat(sixthCue.textAlignment).isEqualTo(Alignment.ALIGN_CENTER);
      // Derived from `align:center`:
      assertThat(sixthCue.position).isEqualTo(0.5f);
      assertThat(sixthCue.positionAnchor).isEqualTo(Cue.ANCHOR_TYPE_MIDDLE);
  
      assertThat(subtitle.getEventTime(12)).isEqualTo(12_000_000L);
      assertThat(subtitle.getEventTime(13)).isEqualTo(13_000_000L);
      Cue seventhCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(12)));
      assertThat(seventhCue.text.toString()).isEqualTo("This is the seventh subtitle.");
      assertThat(seventhCue.positionAnchor).isEqualTo(Cue.ANCHOR_TYPE_START);
  
      assertThat(subtitle.getEventTime(14)).isEqualTo(14_000_000L);
      assertThat(subtitle.getEventTime(15)).isEqualTo(15_000_000L);
      Cue eighthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(14)));
      assertThat(eighthCue.text.toString()).isEqualTo("This is the eighth subtitle.");
      assertThat(eighthCue.positionAnchor).isEqualTo(Cue.ANCHOR_TYPE_END);
    }
  
    @Test
    public void decodeWithOverlappingTimestamps() throws Exception {
      WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_OVERLAPPING_TIMESTAMPS_FILE);
  
      assertThat(subtitle.getEventTimeCount()).isEqualTo(8);
  
      Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0)));
      assertThat(firstCue.text.toString()).isEqualTo("Displayed at the bottom for 3 seconds.");
      assertThat(firstCue.line).isEqualTo(-1f);
      assertThat(firstCue.lineType).isEqualTo(Cue.LINE_TYPE_NUMBER);
  
      List<Cue> firstAndSecondCue = subtitle.getCues(subtitle.getEventTime(1));
      assertThat(firstAndSecondCue).hasSize(2);
      assertThat(firstAndSecondCue.get(0).text.toString())
          .isEqualTo("Displayed at the bottom for 3 seconds.");
      assertThat(firstAndSecondCue.get(0).line).isEqualTo(-1f);
      assertThat(firstAndSecondCue.get(0).lineType).isEqualTo(Cue.LINE_TYPE_NUMBER);
      assertThat(firstAndSecondCue.get(1).text.toString())
          .isEqualTo("Appears directly above for 1 second.");
      assertThat(firstAndSecondCue.get(1).line).isEqualTo(-2f);
      assertThat(firstAndSecondCue.get(1).lineType).isEqualTo(Cue.LINE_TYPE_NUMBER);
  
      Cue thirdCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4)));
      assertThat(thirdCue.text.toString()).isEqualTo("Displayed at the bottom for 2 seconds.");
      assertThat(thirdCue.line).isEqualTo(-1f);
      assertThat(thirdCue.lineType).isEqualTo(Cue.LINE_TYPE_NUMBER);
  
      List<Cue> thirdAndFourthCue = subtitle.getCues(subtitle.getEventTime(5));
      assertThat(thirdAndFourthCue).hasSize(2);
      assertThat(thirdAndFourthCue.get(0).text.toString())
          .isEqualTo("Displayed at the bottom for 2 seconds.");
      assertThat(thirdAndFourthCue.get(0).line).isEqualTo(-1f);
      assertThat(thirdAndFourthCue.get(0).lineType).isEqualTo(Cue.LINE_TYPE_NUMBER);
      assertThat(thirdAndFourthCue.get(1).text.toString())
          .isEqualTo("Appears directly above the previous cue, then replaces it after 1 second.");
      assertThat(thirdAndFourthCue.get(1).line).isEqualTo(-2f);
      assertThat(thirdAndFourthCue.get(1).lineType).isEqualTo(Cue.LINE_TYPE_NUMBER);
  
      Cue fourthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(6)));
      assertThat(fourthCue.text.toString())
          .isEqualTo("Appears directly above the previous cue, then replaces it after 1 second.");
      assertThat(fourthCue.line).isEqualTo(-1f);
      assertThat(fourthCue.lineType).isEqualTo(Cue.LINE_TYPE_NUMBER);
    }
  
    @Test
    public void decodeWithVertical() throws Exception {
      WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_VERTICAL_FILE);
  
      assertThat(subtitle.getEventTimeCount()).isEqualTo(6);
  
      assertThat(subtitle.getEventTime(0)).isEqualTo(0L);
      assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L);
      Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0)));
      assertThat(firstCue.text.toString()).isEqualTo("Vertical right-to-left (e.g. Japanese)");
      assertThat(firstCue.verticalType).isEqualTo(Cue.VERTICAL_TYPE_RL);
  
      assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L);
      assertThat(subtitle.getEventTime(3)).isEqualTo(3_456_000L);
      Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2)));
      assertThat(secondCue.text.toString()).isEqualTo("Vertical left-to-right (e.g. Mongolian)");
      assertThat(secondCue.verticalType).isEqualTo(Cue.VERTICAL_TYPE_LR);
  
      assertThat(subtitle.getEventTime(4)).isEqualTo(4_000_000L);
      assertThat(subtitle.getEventTime(5)).isEqualTo(5_000_000L);
      Cue thirdCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4)));
      assertThat(thirdCue.text.toString()).isEqualTo("No vertical setting (i.e. horizontal)");
      assertThat(thirdCue.verticalType).isEqualTo(Cue.TYPE_UNSET);
    }
  
    @Test
    public void decodeWithRubies() throws Exception {
      WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_RUBIES_FILE);
  
      assertThat(subtitle.getEventTimeCount()).isEqualTo(8);
  
      // Check that an explicit `over` position is read from CSS.
      Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0)));
      assertThat(firstCue.text.toString()).isEqualTo("Some text with over-ruby.");
      assertThat((Spanned) firstCue.text)
          .hasRubySpanBetween("Some ".length(), "Some text with over-ruby".length())
          .withTextAndPosition("over", TextAnnotation.POSITION_BEFORE);
  
      // Check that `under` is read from CSS and unspecified defaults to `over`.
      Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2)));
      assertThat(secondCue.text.toString())
          .isEqualTo("Some text with under-ruby and over-ruby (default).");
      assertThat((Spanned) secondCue.text)
          .hasRubySpanBetween("Some ".length(), "Some text with under-ruby".length())
          .withTextAndPosition("under", TextAnnotation.POSITION_AFTER);
      assertThat((Spanned) secondCue.text)
          .hasRubySpanBetween(
              "Some text with under-ruby and ".length(),
              "Some text with under-ruby and over-ruby (default)".length())
          .withTextAndPosition("over", TextAnnotation.POSITION_BEFORE);
  
      // Check many <rt> tags with different positions nested in a single <ruby> span.
      Cue thirdCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4)));
      assertThat(thirdCue.text.toString()).isEqualTo("base1base2base3.");
      assertThat((Spanned) thirdCue.text)
          .hasRubySpanBetween(/* start= */ 0, "base1".length())
          .withTextAndPosition("over1", TextAnnotation.POSITION_BEFORE);
      assertThat((Spanned) thirdCue.text)
          .hasRubySpanBetween("base1".length(), "base1base2".length())
          .withTextAndPosition("under2", TextAnnotation.POSITION_AFTER);
      assertThat((Spanned) thirdCue.text)
          .hasRubySpanBetween("base1base2".length(), "base1base2base3".length())
          .withTextAndPosition("under3", TextAnnotation.POSITION_AFTER);
  
      // Check a <ruby> span with no <rt> tags.
      Cue fourthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(6)));
      assertThat(fourthCue.text.toString()).isEqualTo("Some text with no ruby text.");
      assertThat((Spanned) fourthCue.text).hasNoSpans();
    }
  
    @Test
    public void decodeWithBadCueHeader() throws Exception {
      WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_BAD_CUE_HEADER_FILE);
  
      assertThat(subtitle.getEventTimeCount()).isEqualTo(4);
  
      assertThat(subtitle.getEventTime(0)).isEqualTo(0L);
      assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L);
      Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0)));
      assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle.");
  
      assertThat(subtitle.getEventTime(2)).isEqualTo(4_000_000L);
      assertThat(subtitle.getEventTime(3)).isEqualTo(5_000_000L);
      Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2)));
      assertThat(secondCue.text.toString()).isEqualTo("This is the third subtitle.");
    }
  
    @Test
    public void decodeWithCssFontSizeStyle() throws Exception {
      WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_FONT_SIZE);
  
      assertThat(subtitle.getEventTimeCount()).isEqualTo(12);
  
      assertThat(subtitle.getEventTime(0)).isEqualTo(0L);
      assertThat(subtitle.getEventTime(1)).isEqualTo(2_000_000L);
      Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0)));
      assertThat(firstCue.text.toString()).isEqualTo("Sentence with font-size set to 4.4em.");
      assertThat((Spanned) firstCue.text)
          .hasRelativeSizeSpanBetween(0, "Sentence with font-size set to 4.4em.".length())
          .withSizeChange(4.4f);
  
      assertThat(subtitle.getEventTime(2)).isEqualTo(2_100_000L);
      assertThat(subtitle.getEventTime(3)).isEqualTo(2_400_000L);
      Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2)));
      assertThat(secondCue.text.toString()).isEqualTo("Sentence with bad font-size unit.");
      assertThat((Spanned) secondCue.text).hasNoSpans();
  
      assertThat(subtitle.getEventTime(4)).isEqualTo(2_500_000L);
      assertThat(subtitle.getEventTime(5)).isEqualTo(4_000_000L);
      Cue thirdCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4)));
      assertThat(thirdCue.text.toString()).isEqualTo("Absolute font-size expressed in px unit!");
      assertThat((Spanned) thirdCue.text)
          .hasAbsoluteSizeSpanBetween(0, "Absolute font-size expressed in px unit!".length())
          .withAbsoluteSize(2);
  
      assertThat(subtitle.getEventTime(6)).isEqualTo(4_500_000L);
      assertThat(subtitle.getEventTime(7)).isEqualTo(6_000_000L);
      Cue fourthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(6)));
      assertThat(fourthCue.text.toString()).isEqualTo("Relative font-size expressed in % unit!");
      assertThat((Spanned) fourthCue.text)
          .hasRelativeSizeSpanBetween(0, "Relative font-size expressed in % unit!".length())
          .withSizeChange(0.035f);
  
      assertThat(subtitle.getEventTime(8)).isEqualTo(6_100_000L);
      assertThat(subtitle.getEventTime(9)).isEqualTo(6_400_000L);
      Cue fifthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(8)));
      assertThat(fifthCue.text.toString()).isEqualTo("Sentence with bad font-size value.");
      assertThat((Spanned) secondCue.text).hasNoSpans();
  
      assertThat(subtitle.getEventTime(10)).isEqualTo(6_500_000L);
      assertThat(subtitle.getEventTime(11)).isEqualTo(8_000_000L);
      Cue sixthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(10)));
      assertThat(sixthCue.text.toString())
          .isEqualTo("Upper and lower case letters in font-size unit.");
      assertThat((Spanned) sixthCue.text)
          .hasAbsoluteSizeSpanBetween(0, "Upper and lower case letters in font-size unit.".length())
          .withAbsoluteSize(2);
    }
  
    @Test
    public void webvttWithCssStyle() throws Exception {
      WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_CSS_STYLES);
  
      Spanned firstCueText = getUniqueSpanTextAt(subtitle, 0);
      assertThat(firstCueText.toString()).isEqualTo("This is the first subtitle.");
      assertThat(firstCueText)
          .hasForegroundColorSpanBetween(0, firstCueText.length())
          .withColor(ColorParser.parseCssColor("papayawhip"));
      assertThat(firstCueText)
          .hasBackgroundColorSpanBetween(0, firstCueText.length())
          .withColor(ColorParser.parseCssColor("green"));
  
      Spanned secondCueText = getUniqueSpanTextAt(subtitle, 2_345_000);
      assertThat(secondCueText.toString()).isEqualTo("This is the second subtitle.");
      assertThat(secondCueText)
          .hasForegroundColorSpanBetween(0, secondCueText.length())
          .withColor(ColorParser.parseCssColor("peachpuff"));
  
      Spanned thirdCueText = getUniqueSpanTextAt(subtitle, 20_000_000);
      assertThat(thirdCueText.toString()).isEqualTo("This is a reference by element");
      assertThat(thirdCueText).hasUnderlineSpanBetween("This is a ".length(), thirdCueText.length());
  
      Spanned fourthCueText = getUniqueSpanTextAt(subtitle, 25_000_000);
      assertThat(fourthCueText.toString()).isEqualTo("You are an idiot\nYou don't have the guts");
      assertThat(fourthCueText)
          .hasBackgroundColorSpanBetween(0, "You are an idiot".length())
          .withColor(ColorParser.parseCssColor("lime"));
      assertThat(fourthCueText)
          .hasBoldSpanBetween("You are an idiot\n".length(), fourthCueText.length());
    }
  
    @Test
    public void withComplexCssSelectors() throws Exception {
      WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_CSS_COMPLEX_SELECTORS);
      Spanned firstCueText = getUniqueSpanTextAt(subtitle, /* timeUs= */ 0);
      assertThat(firstCueText).hasUnderlineSpanBetween(0, firstCueText.length());
      assertThat(firstCueText)
          .hasForegroundColorSpanBetween(
              "This should be underlined and ".length(), firstCueText.length())
          .withColor(ColorParser.parseCssColor("violet"));
      assertThat(firstCueText)
          .hasTypefaceSpanBetween("This should be underlined and ".length(), firstCueText.length())
          .withFamily("courier");
  
      Spanned secondCueText = getUniqueSpanTextAt(subtitle, /* timeUs= */ 2_000_000);
      assertThat(secondCueText)
          .hasTypefaceSpanBetween("This ".length(), secondCueText.length())
          .withFamily("courier");
      assertThat(secondCueText)
          .hasNoForegroundColorSpanBetween("This ".length(), secondCueText.length());
  
      Spanned thirdCueText = getUniqueSpanTextAt(subtitle, /* timeUs= */ 2_500_000);
      assertThat(thirdCueText).hasBoldSpanBetween("This ".length(), thirdCueText.length());
      assertThat(thirdCueText)
          .hasTypefaceSpanBetween("This ".length(), thirdCueText.length())
          .withFamily("courier");
  
      Spanned fourthCueText = getUniqueSpanTextAt(subtitle, /* timeUs= */ 4_000_000);
      assertThat(fourthCueText)
          .hasNoStyleSpanBetween("This ".length(), "shouldn't be bold.".length());
      assertThat(fourthCueText)
          .hasBoldSpanBetween("This shouldn't be bold.\nThis ".length(), fourthCueText.length());
  
      Spanned fifthCueText = getUniqueSpanTextAt(subtitle, /* timeUs= */ 5_000_000);
      assertThat(fifthCueText)
          .hasNoStyleSpanBetween("This is ".length(), "This is specific".length());
      assertThat(fifthCueText)
          .hasItalicSpanBetween("This is specific\n".length(), fifthCueText.length());
    }
  
    @Test
    public void webvttWithCssTextCombineUpright() throws Exception {
      WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_CSS_TEXT_COMBINE_UPRIGHT);
  
      Spanned firstCueText = getUniqueSpanTextAt(subtitle, 500_000);
      assertThat(firstCueText)
          .hasHorizontalTextInVerticalContextSpanBetween("Combine ".length(), "Combine all".length());
  
      Spanned secondCueText = getUniqueSpanTextAt(subtitle, 3_500_000);
      assertThat(secondCueText)
          .hasHorizontalTextInVerticalContextSpanBetween(
              "Combine ".length(), "Combine 0004".length());
    }
  
    private WebvttSubtitle getSubtitleForTestAsset(String asset)
        throws IOException, SubtitleDecoderException {
      WebvttDecoder decoder = new WebvttDecoder();
      byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), asset);
      return (WebvttSubtitle) decoder.decode(bytes, bytes.length, /* reset= */ false);
    }
  
    private Spanned getUniqueSpanTextAt(WebvttSubtitle sub, long timeUs) {
      return (Spanned) Assertions.checkNotNull(sub.getCues(timeUs).get(0).text);
    }
  }
  