/*
 * Copyright 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.rtsp;

import static androidx.media3.exoplayer.rtsp.MediaDescription.MEDIA_TYPE_AUDIO;
import static androidx.media3.exoplayer.rtsp.MediaDescription.MEDIA_TYPE_VIDEO;
import static androidx.media3.exoplayer.rtsp.MediaDescription.RTP_AVP_PROFILE;
import static androidx.media3.exoplayer.rtsp.SessionDescription.ATTR_CONTROL;
import static androidx.media3.exoplayer.rtsp.SessionDescription.ATTR_FMTP;
import static androidx.media3.exoplayer.rtsp.SessionDescription.ATTR_RANGE;
import static androidx.media3.exoplayer.rtsp.SessionDescription.ATTR_RTPMAP;
import static androidx.media3.exoplayer.rtsp.SessionDescription.ATTR_TOOL;
import static androidx.media3.exoplayer.rtsp.SessionDescription.ATTR_TYPE;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;

import android.net.Uri;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;

/** Unit test for {@link SessionDescription}. */
@RunWith(AndroidJUnit4.class)
public class SessionDescriptionTest {

  @Test
  public void parse_sdpString_succeeds() throws Exception {
    String testMediaSdpInfo =
        "v=0\r\n"
            + "o=MNobody 2890844526 2890842807 IN IP4 192.0.2.46\r\n"
            + "s=SDP Seminar\r\n"
            + "i=A Seminar on the session description protocol\r\n"
            + "u=http://www.example.com/lectures/sdp.ps\r\n"
            + "e=seminar@example.com (Seminar Management)\r\n"
            + "c=IN IP4 0.0.0.0\r\n"
            + "a=control:*\r\n"
            + "t=2873397496 2873404696\r\n"
            + "m=audio 3456 RTP/AVP 0\r\n"
            + "a=control:audio\r\n"
            + "a=rtpmap:0 PCMU/8000\r\n"
            + "a=3GPP-Adaption-Support:1\r\n"
            + "m=video 2232 RTP/AVP 31\r\n"
            + "a=control:video\r\n"
            + "a=rtpmap:31 H261/90000\r\n";

    SessionDescription sessionDescription = SessionDescriptionParser.parse(testMediaSdpInfo);

    SessionDescription expectedSession =
        new SessionDescription.Builder()
            .setOrigin("MNobody 2890844526 2890842807 IN IP4 192.0.2.46")
            .setSessionName("SDP Seminar")
            .setSessionInfo("A Seminar on the session description protocol")
            .setUri(Uri.parse("http://www.example.com/lectures/sdp.ps"))
            .setEmailAddress("seminar@example.com (Seminar Management)")
            .setConnection("IN IP4 0.0.0.0")
            .setTiming("2873397496 2873404696")
            .addAttribute(ATTR_CONTROL, "*")
            .addMediaDescription(
                new MediaDescription.Builder(MEDIA_TYPE_AUDIO, 3456, RTP_AVP_PROFILE, 0)
                    .addAttribute(ATTR_CONTROL, "audio")
                    .addAttribute(ATTR_RTPMAP, "0 PCMU/8000")
                    .addAttribute("3GPP-Adaption-Support", "1")
                    .build())
            .addMediaDescription(
                new MediaDescription.Builder(MEDIA_TYPE_VIDEO, 2232, RTP_AVP_PROFILE, 31)
                    .addAttribute(ATTR_CONTROL, "video")
                    .addAttribute(ATTR_RTPMAP, "31 H261/90000")
                    .build())
            .build();

    assertThat(sessionDescription).isEqualTo(expectedSession);
  }

  @Test
  public void parse_sdpStringWithoutMandatoryFields_succeeds() throws Exception {
    // This SDP string is similar to the one in parse_sdpString_succeeds(), it doesn't include the
    // mandatory SDP fields origin, session name, session info and timing.
    String testMediaSdpInfo =
        "v=0\r\n"
            + "u=http://www.example.com/lectures/sdp.ps\r\n"
            + "e=seminar@example.com (Seminar Management)\r\n"
            + "c=IN IP4 0.0.0.0\r\n"
            + "a=control:*\r\n"
            + "m=audio 3456 RTP/AVP 0\r\n"
            + "a=control:audio\r\n"
            + "a=rtpmap:0 PCMU/8000\r\n"
            + "a=3GPP-Adaption-Support:1\r\n"
            + "m=video 2232 RTP/AVP 31\r\n"
            + "a=control:video\r\n"
            + "a=rtpmap:31 H261/90000\r\n";

    SessionDescription sessionDescription = SessionDescriptionParser.parse(testMediaSdpInfo);

    SessionDescription expectedSession =
        new SessionDescription.Builder()
            .setUri(Uri.parse("http://www.example.com/lectures/sdp.ps"))
            .setEmailAddress("seminar@example.com (Seminar Management)")
            .setConnection("IN IP4 0.0.0.0")
            .addAttribute(ATTR_CONTROL, "*")
            .addMediaDescription(
                new MediaDescription.Builder(MEDIA_TYPE_AUDIO, 3456, RTP_AVP_PROFILE, 0)
                    .addAttribute(ATTR_CONTROL, "audio")
                    .addAttribute(ATTR_RTPMAP, "0 PCMU/8000")
                    .addAttribute("3GPP-Adaption-Support", "1")
                    .build())
            .addMediaDescription(
                new MediaDescription.Builder(MEDIA_TYPE_VIDEO, 2232, RTP_AVP_PROFILE, 31)
                    .addAttribute(ATTR_CONTROL, "video")
                    .addAttribute(ATTR_RTPMAP, "31 H261/90000")
                    .build())
            .build();

    assertThat(sessionDescription).isEqualTo(expectedSession);
  }

  @Test
  public void parse_sdpString2_succeeds() throws Exception {
    String testMediaSdpInfo =
        "v=0\r\n"
            + "o=- 1600785369059721 1 IN IP4 192.168.2.176\r\n"
            + "s=video+audio, streamed by ExoPlayer\r\n"
            + "i=test.mkv\r\n"
            + "t=0 0\r\n"
            + "a=tool:ExoPlayer\r\n"
            + "a=type:broadcast\r\n"
            + "a=control:*\r\n"
            + "a=range:npt=0-30.102\r\n"
            + "m=video 0 RTP/AVP 96\r\n"
            + "c=IN IP4 0.0.0.0\r\n"
            + "b=AS:500\r\n"
            + "a=rtpmap:96 H264/90000\r\n"
            + "a=fmtp:96"
            + " packetization-mode=1;profile-level-id=64001F;sprop-parameter-sets=Z2QAH6zZQPARabIAAAMACAAAAwGcHjBjLA==,aOvjyyLAAA==\r\n"
            + "a=control:track1\r\n"
            + "m=audio 0 RTP/AVP 97\r\n"
            + "c=IN IP4 0.0.0.0\r\n"
            + "b=AS:96\r\n"
            + "a=rtpmap:97 MPEG4-GENERIC/44100\r\n"
            + "a=fmtp:97"
            + " streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1208\r\n"
            + "a=control:track2\r\n";

    SessionDescription sessionDescription = SessionDescriptionParser.parse(testMediaSdpInfo);

    SessionDescription expectedSession =
        new SessionDescription.Builder()
            .setOrigin("- 1600785369059721 1 IN IP4 192.168.2.176")
            .setSessionName("video+audio, streamed by ExoPlayer")
            .setSessionInfo("test.mkv")
            .setTiming("0 0")
            .addAttribute(ATTR_TOOL, "ExoPlayer")
            .addAttribute(ATTR_TYPE, "broadcast")
            .addAttribute(ATTR_CONTROL, "*")
            .addAttribute(ATTR_RANGE, "npt=0-30.102")
            .addMediaDescription(
                new MediaDescription.Builder(MEDIA_TYPE_VIDEO, 0, RTP_AVP_PROFILE, 96)
                    .setConnection("IN IP4 0.0.0.0")
                    .setBitrate(500_000)
                    .addAttribute(ATTR_RTPMAP, "96 H264/90000")
                    .addAttribute(
                        ATTR_FMTP,
                        "96 packetization-mode=1;profile-level-id=64001F;sprop-parameter-sets=Z2QAH6zZQPARabIAAAMACAAAAwGcHjBjLA==,aOvjyyLAAA==")
                    .addAttribute(ATTR_CONTROL, "track1")
                    .build())
            .addMediaDescription(
                new MediaDescription.Builder(MEDIA_TYPE_AUDIO, 0, RTP_AVP_PROFILE, 97)
                    .setConnection("IN IP4 0.0.0.0")
                    .setBitrate(96_000)
                    .addAttribute(ATTR_RTPMAP, "97 MPEG4-GENERIC/44100")
                    .addAttribute(
                        ATTR_FMTP,
                        "97 streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1208")
                    .addAttribute(ATTR_CONTROL, "track2")
                    .build())
            .build();

    assertThat(sessionDescription).isEqualTo(expectedSession);
  }

  @Test
  public void parse_sdpStringWithDuplicatedMediaAttribute_recordsTheMostRecentValue()
      throws Exception {
    String testMediaSdpInfo =
        "v=0\r\n"
            + "o=MNobody 2890844526 2890842807 IN IP4 192.0.2.46\r\n"
            + "s=SDP Seminar\r\n"
            + "t=0 0\r\n"
            + "i=A Seminar on the session description protocol\r\n"
            + "m=audio 3456 RTP/AVP 0\r\n"
            + "a=control:video\r\n"
            + "a=rtpmap:97 AC3/44100\r\n"
            // Duplicate attribute.
            + "a=control:audio\r\n";

    SessionDescription sessionDescription = SessionDescriptionParser.parse(testMediaSdpInfo);

    assertThat(sessionDescription.mediaDescriptionList.get(0).attributes)
        .containsEntry(ATTR_CONTROL, "audio");
  }

  @Test
  public void parse_sdpStringWithSpecialAttributeField_succeeds() throws Exception {
    String testMediaSdpInfo =
        "v=0\r\n"
            + "o=MNobody 2890844526 2890842807 IN IP4 192.0.2.46\r\n"
            + "s=SDP Seminar\r\n"
            + "t=0 0\r\n"
            + "i=A Seminar on the session description protocol\r\n"
            + "m=audio 3456 RTP/AVP 0\r\n"
            + "a=rtpmap:97 AC3/44100\r\n"
            + "a=A!#$%&'*+-.^_`{|}~:special\r\n";

    SessionDescription sessionDescription = SessionDescriptionParser.parse(testMediaSdpInfo);

    assertThat(sessionDescription.mediaDescriptionList.get(0).attributes)
        .containsEntry("A!#$%&'*+-.^_`{|}~", "special");
  }

  @Test
  public void parse_sdpStringWithDuplicatedSessionAttribute_recordsTheMostRecentValue()
      throws Exception {
    String testMediaSdpInfo =
        "v=0\r\n"
            + "o=MNobody 2890844526 2890842807 IN IP4 192.0.2.46\r\n"
            + "s=SDP Seminar\r\n"
            + "t=0 0\r\n"
            + "a=control:*\r\n"
            // Duplicate attribute.
            + "a=control:session1\r\n"
            + "i=A Seminar on the session description protocol\r\n"
            + "m=audio 3456 RTP/AVP 0\r\n"
            + "a=rtpmap:97 AC3/44100\r\n"
            + "a=control:audio\r\n";

    SessionDescription sessionDescription = SessionDescriptionParser.parse(testMediaSdpInfo);

    assertThat(sessionDescription.attributes).containsEntry(ATTR_CONTROL, "session1");
  }

  @Test
  public void parse_sdpStringWithExtraSpaceInRtpMapAttribute_succeeds() throws Exception {
    String testMediaSdpInfo =
        "v=0\r\n"
            + "o=MNobody 2890844526 2890842807 IN IP4 192.0.2.46\r\n"
            + "s=SDP Seminar\r\n"
            + "t=0 0\r\n"
            + "a=control:*\r\n"
            + "m=audio 3456 RTP/AVP 0\r\n"
            + "a=rtpmap:97 AC3/44100  \r\n";

    SessionDescription sessionDescription = SessionDescriptionParser.parse(testMediaSdpInfo);
    MediaDescription.RtpMapAttribute rtpMapAttribute =
        sessionDescription.mediaDescriptionList.get(0).rtpMapAttribute;
    assertThat(rtpMapAttribute.payloadType).isEqualTo(97);
    assertThat(rtpMapAttribute.mediaEncoding).isEqualTo("AC3");
    assertThat(rtpMapAttribute.clockRate).isEqualTo(44100);
  }

  @Test
  public void buildMediaDescription_withInvalidRtpmapAttribute_throwsIllegalStateException() {
    assertThrows(
        IllegalStateException.class,
        () ->
            new MediaDescription.Builder(
                    MEDIA_TYPE_AUDIO, /* port= */ 0, RTP_AVP_PROFILE, /* payloadType= */ 97)
                .addAttribute(ATTR_RTPMAP, "AF AC3/44100")
                .build());
  }

  @Test
  public void buildMediaDescription_withInvalidRtpmapAttribute2_throwsIllegalStateException() {
    assertThrows(
        IllegalStateException.class,
        () ->
            new MediaDescription.Builder(
                    MEDIA_TYPE_AUDIO, /* port= */ 0, RTP_AVP_PROFILE, /* payloadType= */ 97)
                .addAttribute(ATTR_RTPMAP, "97 AC3/441A0")
                .build());
  }
}
