//! Definitions and parsing for all tags defined in the HLS specification.
//!
//! At the time of writing `draft-pantos-hls-rfc8216bis-18-preliminary-v1` was used for all tag
//! definitions.

use crate::{
    error::ValidationError,
    tag::{IntoInnerTag, TagInner, UnknownTag},
};
use std::fmt::Debug;

mod bitrate;
mod byterange;
mod content_steering;
mod daterange;
mod define;
mod discontinuity;
mod discontinuity_sequence;
mod endlist;
mod enumerated_string;
mod gap;
mod i_frame_stream_inf;
mod i_frames_only;
mod independent_segments;
mod inf;
mod key;
mod m3u;
mod map;
mod media;
mod media_sequence;
mod part;
mod part_inf;
mod playlist_type;
mod preload_hint;
mod program_date_time;
mod rendition_report;
mod server_control;
mod session_data;
mod session_key;
mod skip;
mod start;
mod stream_inf;
mod targetduration;
#[cfg(test)]
mod test_macro;
mod version;

// Re-exporting all HLS tag types to `hls` module level.
pub use bitrate::*;
pub use byterange::*;
pub use content_steering::*;
pub use daterange::*;
pub use define::*;
pub use discontinuity::*;
pub use discontinuity_sequence::*;
pub use endlist::*;
pub use enumerated_string::*;
pub use gap::*;
pub use i_frame_stream_inf::*;
pub use i_frames_only::*;
pub use independent_segments::*;
pub use inf::*;
pub use key::*;
pub use m3u::*;
pub use map::*;
pub use media::*;
pub use media_sequence::*;
pub use part::*;
pub use part_inf::*;
pub use playlist_type::*;
pub use preload_hint::*;
pub use program_date_time::*;
pub use rendition_report::*;
pub use server_control::*;
pub use session_data::*;
pub use session_key::*;
pub use skip::*;
pub use start::*;
pub use stream_inf::*;
pub use targetduration::*;
pub use version::*;

/// A HLS tag.
///
/// This includes all 32 known tags defined in the draft-pantos-hls specification. The associated
/// value of each enum case contains the strongly typed tag definition which will have more
/// documentation on its parameters.
#[derive(Debug, PartialEq, Clone)]
pub enum Tag<'a> {
    /// <https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-17#section-4.4.1.1>
    M3u(M3u),
    /// <https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-17#section-4.4.1.2>
    Version(Version<'a>),
    /// <https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-17#section-4.4.2.1>
    IndependentSegments(IndependentSegments),
    /// <https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-17#section-4.4.2.2>
    Start(Start<'a>),
    /// <https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-17#section-4.4.2.3>
    Define(Define<'a>),
    /// <https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-17#section-4.4.3.1>
    Targetduration(Targetduration<'a>),
    /// <https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-17#section-4.4.3.2>
    MediaSequence(MediaSequence<'a>),
    /// <https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-17#section-4.4.3.3>
    DiscontinuitySequence(DiscontinuitySequence<'a>),
    /// <https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-17#section-4.4.3.4>
    Endlist(Endlist),
    /// <https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-17#section-4.4.3.5>
    PlaylistType(PlaylistType),
    /// <https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-17#section-4.4.3.6>
    IFramesOnly(IFramesOnly),
    /// <https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-17#section-4.4.3.7>
    PartInf(PartInf<'a>),
    /// <https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-17#section-4.4.3.8>
    ServerControl(ServerControl<'a>),
    /// <https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-17#section-4.4.4.1>
    Inf(Inf<'a>),
    /// <https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-17#section-4.4.4.2>
    Byterange(Byterange<'a>),
    /// <https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-17#section-4.4.4.3>
    Discontinuity(Discontinuity),
    /// <https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-17#section-4.4.4.4>
    Key(Key<'a>),
    /// <https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-17#section-4.4.4.5>
    Map(Map<'a>),
    /// <https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-17#section-4.4.4.6>
    ProgramDateTime(ProgramDateTime<'a>),
    /// <https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-17#section-4.4.4.7>
    Gap(Gap),
    /// <https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-17#section-4.4.4.8>
    Bitrate(Bitrate<'a>),
    /// <https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-17#section-4.4.4.9>
    Part(Part<'a>),
    /// <https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-17#section-4.4.5.1>
    Daterange(Daterange<'a>),
    /// <https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-17#section-4.4.5.2>
    Skip(Skip<'a>),
    /// <https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-17#section-4.4.5.3>
    PreloadHint(PreloadHint<'a>),
    /// <https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-17#section-4.4.5.4>
    RenditionReport(RenditionReport<'a>),
    /// <https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-17#section-4.4.6.1>
    Media(Media<'a>),
    /// <https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-17#section-4.4.6.2>
    StreamInf(StreamInf<'a>),
    /// <https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-17#section-4.4.6.3>
    IFrameStreamInf(IFrameStreamInf<'a>),
    /// <https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-17#section-4.4.6.4>
    SessionData(SessionData<'a>),
    /// <https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-17#section-4.4.6.5>
    SessionKey(SessionKey<'a>),
    /// <https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-17#section-4.4.6.6>
    ContentSteering(ContentSteering<'a>),
}

impl<'a> TryFrom<UnknownTag<'a>> for Tag<'a> {
    type Error = ValidationError;

    fn try_from(tag: UnknownTag<'a>) -> Result<Self, Self::Error> {
        let tag_name = TagName::try_from(tag.name)?;
        match tag_name {
            TagName::M3u => Ok(Self::M3u(M3u::try_from(tag)?)),
            TagName::Version => Ok(Self::Version(Version::try_from(tag)?)),
            TagName::IndependentSegments => Ok(Self::IndependentSegments(
                IndependentSegments::try_from(tag)?,
            )),
            TagName::Start => Ok(Self::Start(Start::try_from(tag)?)),
            TagName::Define => Ok(Self::Define(Define::try_from(tag)?)),
            TagName::Targetduration => Ok(Self::Targetduration(Targetduration::try_from(tag)?)),
            TagName::MediaSequence => Ok(Self::MediaSequence(MediaSequence::try_from(tag)?)),
            TagName::DiscontinuitySequence => Ok(Self::DiscontinuitySequence(
                DiscontinuitySequence::try_from(tag)?,
            )),
            TagName::Endlist => Ok(Self::Endlist(Endlist::try_from(tag)?)),
            TagName::PlaylistType => Ok(Self::PlaylistType(PlaylistType::try_from(tag)?)),
            TagName::IFramesOnly => Ok(Self::IFramesOnly(IFramesOnly::try_from(tag)?)),
            TagName::PartInf => Ok(Self::PartInf(PartInf::try_from(tag)?)),
            TagName::ServerControl => Ok(Self::ServerControl(ServerControl::try_from(tag)?)),
            TagName::Inf => Ok(Self::Inf(Inf::try_from(tag)?)),
            TagName::Byterange => Ok(Self::Byterange(Byterange::try_from(tag)?)),
            TagName::Discontinuity => Ok(Self::Discontinuity(Discontinuity::try_from(tag)?)),
            TagName::Key => Ok(Self::Key(Key::try_from(tag)?)),
            TagName::Map => Ok(Self::Map(Map::try_from(tag)?)),
            TagName::ProgramDateTime => Ok(Self::ProgramDateTime(ProgramDateTime::try_from(tag)?)),
            TagName::Gap => Ok(Self::Gap(Gap::try_from(tag)?)),
            TagName::Bitrate => Ok(Self::Bitrate(Bitrate::try_from(tag)?)),
            TagName::Part => Ok(Self::Part(Part::try_from(tag)?)),
            TagName::Daterange => Ok(Self::Daterange(Daterange::try_from(tag)?)),
            TagName::Skip => Ok(Self::Skip(Skip::try_from(tag)?)),
            TagName::PreloadHint => Ok(Self::PreloadHint(PreloadHint::try_from(tag)?)),
            TagName::RenditionReport => Ok(Self::RenditionReport(RenditionReport::try_from(tag)?)),
            TagName::Media => Ok(Self::Media(Media::try_from(tag)?)),
            TagName::StreamInf => Ok(Self::StreamInf(StreamInf::try_from(tag)?)),
            TagName::IFrameStreamInf => Ok(Self::IFrameStreamInf(IFrameStreamInf::try_from(tag)?)),
            TagName::SessionData => Ok(Self::SessionData(SessionData::try_from(tag)?)),
            TagName::SessionKey => Ok(Self::SessionKey(SessionKey::try_from(tag)?)),
            TagName::ContentSteering => Ok(Self::ContentSteering(ContentSteering::try_from(tag)?)),
        }
    }
}

// Helper macro for implementing IntoInnerTag for each of the HLS tag implementations. It works
// because all of the HLS tag implementations have a "recalculate_output_line" method. In the future
// I may look to move the entire tag implementation into a macro (given that, at least the attribute
// list tags, share a lot of the same implementation details).
macro_rules! into_inner_tag {
    ($name:ident) => {
        impl<'a> $crate::tag::IntoInnerTag<'a> for $name<'a> {
            fn into_inner(mut self) -> $crate::tag::TagInner<'a> {
                if self.output_line_is_dirty {
                    self.recalculate_output_line();
                }
                $crate::tag::TagInner {
                    output_line: self.output_line,
                }
            }
        }
    };
    ($name:ident @Static $val:literal) => {
        impl $crate::tag::IntoInnerTag<'static> for $name {
            fn into_inner(self) -> $crate::tag::TagInner<'static> {
                $crate::tag::TagInner {
                    output_line: std::borrow::Cow::Borrowed($val),
                }
            }
        }
    };
}
use into_inner_tag;

impl<'a> IntoInnerTag<'a> for Tag<'a> {
    fn into_inner(self) -> TagInner<'a> {
        match self {
            Tag::M3u(t) => t.into_inner(),
            Tag::Version(t) => t.into_inner(),
            Tag::IndependentSegments(t) => t.into_inner(),
            Tag::Start(t) => t.into_inner(),
            Tag::Define(t) => t.into_inner(),
            Tag::Targetduration(t) => t.into_inner(),
            Tag::MediaSequence(t) => t.into_inner(),
            Tag::DiscontinuitySequence(t) => t.into_inner(),
            Tag::Endlist(t) => t.into_inner(),
            Tag::PlaylistType(t) => t.into_inner(),
            Tag::IFramesOnly(t) => t.into_inner(),
            Tag::PartInf(t) => t.into_inner(),
            Tag::ServerControl(t) => t.into_inner(),
            Tag::Inf(t) => t.into_inner(),
            Tag::Byterange(t) => t.into_inner(),
            Tag::Discontinuity(t) => t.into_inner(),
            Tag::Key(t) => t.into_inner(),
            Tag::Map(t) => t.into_inner(),
            Tag::ProgramDateTime(t) => t.into_inner(),
            Tag::Gap(t) => t.into_inner(),
            Tag::Bitrate(t) => t.into_inner(),
            Tag::Part(t) => t.into_inner(),
            Tag::Daterange(t) => t.into_inner(),
            Tag::Skip(t) => t.into_inner(),
            Tag::PreloadHint(t) => t.into_inner(),
            Tag::RenditionReport(t) => t.into_inner(),
            Tag::Media(t) => t.into_inner(),
            Tag::StreamInf(t) => t.into_inner(),
            Tag::IFrameStreamInf(t) => t.into_inner(),
            Tag::SessionData(t) => t.into_inner(),
            Tag::SessionKey(t) => t.into_inner(),
            Tag::ContentSteering(t) => t.into_inner(),
        }
    }
}

impl Tag<'_> {
    /// Get the known name of the tag.
    pub fn name(&self) -> TagName {
        match self {
            Tag::M3u(_) => TagName::M3u,
            Tag::Version(_) => TagName::Version,
            Tag::IndependentSegments(_) => TagName::IndependentSegments,
            Tag::Start(_) => TagName::Start,
            Tag::Define(_) => TagName::Define,
            Tag::Targetduration(_) => TagName::Targetduration,
            Tag::MediaSequence(_) => TagName::MediaSequence,
            Tag::DiscontinuitySequence(_) => TagName::DiscontinuitySequence,
            Tag::Endlist(_) => TagName::Endlist,
            Tag::PlaylistType(_) => TagName::PlaylistType,
            Tag::IFramesOnly(_) => TagName::IFramesOnly,
            Tag::PartInf(_) => TagName::PartInf,
            Tag::ServerControl(_) => TagName::ServerControl,
            Tag::Inf(_) => TagName::Inf,
            Tag::Byterange(_) => TagName::Byterange,
            Tag::Discontinuity(_) => TagName::Discontinuity,
            Tag::Key(_) => TagName::Key,
            Tag::Map(_) => TagName::Map,
            Tag::ProgramDateTime(_) => TagName::ProgramDateTime,
            Tag::Gap(_) => TagName::Gap,
            Tag::Bitrate(_) => TagName::Bitrate,
            Tag::Part(_) => TagName::Part,
            Tag::Daterange(_) => TagName::Daterange,
            Tag::Skip(_) => TagName::Skip,
            Tag::PreloadHint(_) => TagName::PreloadHint,
            Tag::RenditionReport(_) => TagName::RenditionReport,
            Tag::Media(_) => TagName::Media,
            Tag::StreamInf(_) => TagName::StreamInf,
            Tag::IFrameStreamInf(_) => TagName::IFrameStreamInf,
            Tag::SessionData(_) => TagName::SessionData,
            Tag::SessionKey(_) => TagName::SessionKey,
            Tag::ContentSteering(_) => TagName::ContentSteering,
        }
    }
}

/// An enumeration of all the tag names defined in the HLS specification.
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
pub enum TagName {
    /// Corresponds to `#EXTM3U`.
    M3u,
    /// Corresponds to `#EXT-X-VERSION`.
    Version,
    /// Corresponds to `#EXT-X-INDEPENDENT-SEGMENTS`.
    IndependentSegments,
    /// Corresponds to `#EXT-X-START`.
    Start,
    /// Corresponds to `#EXT-X-DEFINE`.
    Define,
    /// Corresponds to `#EXT-X-TARGETDURATION`.
    Targetduration,
    /// Corresponds to `#EXT-X-MEDIA-SEQUENCE`.
    MediaSequence,
    /// Corresponds to `#EXT-X-DISCONTINUITY-SEQUENCE`.
    DiscontinuitySequence,
    /// Corresponds to `#EXT-X-ENDLIST`.
    Endlist,
    /// Corresponds to `#EXT-X-PLAYLIST-TYPE`.
    PlaylistType,
    /// Corresponds to `#EXT-X-I-FRAMES-ONLY`.
    IFramesOnly,
    /// Corresponds to `#EXT-X-PART-INF`.
    PartInf,
    /// Corresponds to `#EXT-X-SERVER-CONTROL`.
    ServerControl,
    /// Corresponds to `#EXTINF`.
    Inf,
    /// Corresponds to `#EXT-X-BYTERANGE`.
    Byterange,
    /// Corresponds to `#EXT-X-DISCONTINUITY`.
    Discontinuity,
    /// Corresponds to `#EXT-X-KEY`.
    Key,
    /// Corresponds to `#EXT-X-MAP`.
    Map,
    /// Corresponds to `#EXT-X-PROGRAM-DATE-TIME`.
    ProgramDateTime,
    /// Corresponds to `#EXT-X-GAP`.
    Gap,
    /// Corresponds to `#EXT-X-BITRATE`.
    Bitrate,
    /// Corresponds to `#EXT-X-PART`.
    Part,
    /// Corresponds to `#EXT-X-DATERANGE`.
    Daterange,
    /// Corresponds to `#EXT-X-SKIP`.
    Skip,
    /// Corresponds to `#EXT-X-PRELOAD-HINT`.
    PreloadHint,
    /// Corresponds to `#EXT-X-RENDITION-REPORT`.
    RenditionReport,
    /// Corresponds to `#EXT-X-MEDIA`.
    Media,
    /// Corresponds to `#EXT-X-STREAM-INF`.
    StreamInf,
    /// Corresponds to `#EXT-X-I-FRAME-STREAM-INF`.
    IFrameStreamInf,
    /// Corresponds to `#EXT-X-SESSION-DATA`.
    SessionData,
    /// Corresponds to `#EXT-X-SESSION-KEY`.
    SessionKey,
    /// Corresponds to `#EXT-X-CONTENT-STEERING`.
    ContentSteering,
}

impl TryFrom<&'_ str> for TagName {
    type Error = ValidationError;

    fn try_from(value: &'_ str) -> Result<Self, Self::Error> {
        match value {
            "M3U" => Ok(Self::M3u),
            "-X-VERSION" => Ok(Self::Version),
            "-X-INDEPENDENT-SEGMENTS" => Ok(Self::IndependentSegments),
            "-X-START" => Ok(Self::Start),
            "-X-DEFINE" => Ok(Self::Define),
            "-X-TARGETDURATION" => Ok(Self::Targetduration),
            "-X-MEDIA-SEQUENCE" => Ok(Self::MediaSequence),
            "-X-DISCONTINUITY-SEQUENCE" => Ok(Self::DiscontinuitySequence),
            "-X-ENDLIST" => Ok(Self::Endlist),
            "-X-PLAYLIST-TYPE" => Ok(Self::PlaylistType),
            "-X-I-FRAMES-ONLY" => Ok(Self::IFramesOnly),
            "-X-PART-INF" => Ok(Self::PartInf),
            "-X-SERVER-CONTROL" => Ok(Self::ServerControl),
            "INF" => Ok(Self::Inf),
            "-X-BYTERANGE" => Ok(Self::Byterange),
            "-X-DISCONTINUITY" => Ok(Self::Discontinuity),
            "-X-KEY" => Ok(Self::Key),
            "-X-MAP" => Ok(Self::Map),
            "-X-PROGRAM-DATE-TIME" => Ok(Self::ProgramDateTime),
            "-X-GAP" => Ok(Self::Gap),
            "-X-BITRATE" => Ok(Self::Bitrate),
            "-X-PART" => Ok(Self::Part),
            "-X-DATERANGE" => Ok(Self::Daterange),
            "-X-SKIP" => Ok(Self::Skip),
            "-X-PRELOAD-HINT" => Ok(Self::PreloadHint),
            "-X-RENDITION-REPORT" => Ok(Self::RenditionReport),
            "-X-MEDIA" => Ok(Self::Media),
            "-X-STREAM-INF" => Ok(Self::StreamInf),
            "-X-I-FRAME-STREAM-INF" => Ok(Self::IFrameStreamInf),
            "-X-SESSION-DATA" => Ok(Self::SessionData),
            "-X-SESSION-KEY" => Ok(Self::SessionKey),
            "-X-CONTENT-STEERING" => Ok(Self::ContentSteering),
            _ => Err(ValidationError::UnexpectedTagName),
        }
    }
}

impl TagName {
    /// The string value that the library will parse as the name of the tag (i.e. the name portion
    /// after the `#EXT` prefix).
    pub fn as_str(&self) -> &'static str {
        match self {
            Self::M3u => "M3U",
            Self::Version => "-X-VERSION",
            Self::IndependentSegments => "-X-INDEPENDENT-SEGMENTS",
            Self::Start => "-X-START",
            Self::Define => "-X-DEFINE",
            Self::Targetduration => "-X-TARGETDURATION",
            Self::MediaSequence => "-X-MEDIA-SEQUENCE",
            Self::DiscontinuitySequence => "-X-DISCONTINUITY-SEQUENCE",
            Self::Endlist => "-X-ENDLIST",
            Self::PlaylistType => "-X-PLAYLIST-TYPE",
            Self::IFramesOnly => "-X-I-FRAMES-ONLY",
            Self::PartInf => "-X-PART-INF",
            Self::ServerControl => "-X-SERVER-CONTROL",
            Self::Inf => "INF",
            Self::Byterange => "-X-BYTERANGE",
            Self::Discontinuity => "-X-DISCONTINUITY",
            Self::Key => "-X-KEY",
            Self::Map => "-X-MAP",
            Self::ProgramDateTime => "-X-PROGRAM-DATE-TIME",
            Self::Gap => "-X-GAP",
            Self::Bitrate => "-X-BITRATE",
            Self::Part => "-X-PART",
            Self::Daterange => "-X-DATERANGE",
            Self::Skip => "-X-SKIP",
            Self::PreloadHint => "-X-PRELOAD-HINT",
            Self::RenditionReport => "-X-RENDITION-REPORT",
            Self::Media => "-X-MEDIA",
            Self::StreamInf => "-X-STREAM-INF",
            Self::IFrameStreamInf => "-X-I-FRAME-STREAM-INF",
            Self::SessionData => "-X-SESSION-DATA",
            Self::SessionKey => "-X-SESSION-KEY",
            Self::ContentSteering => "-X-CONTENT-STEERING",
        }
    }

    /// The type of tag this is, as defined by what sub-section of section 4 the tag appears in.
    pub fn tag_type(&self) -> TagType {
        match self {
            Self::M3u => TagType::Basic,
            Self::Version => TagType::Basic,
            Self::IndependentSegments => TagType::MediaOrMultivariantPlaylist,
            Self::Start => TagType::MediaOrMultivariantPlaylist,
            Self::Define => TagType::MediaOrMultivariantPlaylist,
            Self::Targetduration => TagType::MediaPlaylist,
            Self::MediaSequence => TagType::MediaPlaylist,
            Self::DiscontinuitySequence => TagType::MediaPlaylist,
            Self::Endlist => TagType::MediaPlaylist,
            Self::PlaylistType => TagType::MediaPlaylist,
            Self::IFramesOnly => TagType::MediaPlaylist,
            Self::PartInf => TagType::MediaPlaylist,
            Self::ServerControl => TagType::MediaPlaylist,
            Self::Inf => TagType::MediaSegment,
            Self::Byterange => TagType::MediaSegment,
            Self::Discontinuity => TagType::MediaSegment,
            Self::Key => TagType::MediaSegment,
            Self::Map => TagType::MediaSegment,
            Self::ProgramDateTime => TagType::MediaSegment,
            Self::Gap => TagType::MediaSegment,
            Self::Bitrate => TagType::MediaSegment,
            Self::Part => TagType::MediaSegment,
            Self::Daterange => TagType::MediaMetadata,
            Self::Skip => TagType::MediaMetadata,
            Self::PreloadHint => TagType::MediaMetadata,
            Self::RenditionReport => TagType::MediaMetadata,
            Self::Media => TagType::MultivariantPlaylist,
            Self::StreamInf => TagType::MultivariantPlaylist,
            Self::IFrameStreamInf => TagType::MultivariantPlaylist,
            Self::SessionData => TagType::MultivariantPlaylist,
            Self::SessionKey => TagType::MultivariantPlaylist,
            Self::ContentSteering => TagType::MultivariantPlaylist,
        }
    }
}

/// The type of tag, as determined by the tag sections in the HLS specification.
///
/// The sections that are defined are:
/// * 4.4.1. Basic Tags
///     * These tags are allowed in both Media Playlists and Multivariant Playlists.
/// * 4.4.2. Media or Multivariant Playlist Tags
///     * The tags in this section can appear in either Multivariant Playlists or Media Playlists.
///       
///       Tags in this section MUST NOT appear more than once in a Playlist. If one does, clients
///       MUST fail to parse the Playlist. The only exception to this rule is EXT-X-DEFINE, which
///       MAY appear more than once.
/// * 4.4.3. Media Playlist Tags
///     * Media Playlist tags describe global parameters of the Media Playlist. There MUST NOT be
///       more than one Media Playlist tag of each type in any Media Playlist.
///       
///       A Media Playlist tag MUST NOT appear in a Multivariant Playlist.
/// * 4.4.4. Media Segment Tags
///     * Each Media Segment is specified by a series of Media Segment tags followed by a URI. Some
///       Media Segment tags apply to just the next segment; others apply to all subsequent segments
///       until another instance of the same tag.
///       
///       A Media Segment tag MUST NOT appear in a Multivariant Playlist. Clients MUST fail to parse
///       Playlists that contain both Media Segment tags and Multivariant Playlist tags.
/// * 4.4.5. Media Metadata Tags
///     * Media Metadata tags provide information about the playlist that is not associated with
///       specific Media Segments. There MAY be more than one Media Metadata tag of each type in any
///       Media Playlist. The only exception to this rule is an EXT-X-SKIP, which MUST NOT appear
///       more than once.
/// * 4.4.6. Multivariant Playlist Tags
///     * Multivariant Playlist tags define the Variant Streams, Renditions, and other global
///       parameters of the presentation.
///       
///       Multivariant Playlist tags MUST NOT appear in a Media Playlist; clients MUST fail to parse
///       any Playlist that contains both a Multivariant Playlist tag and either a Media Playlist
///       tag or a Media Segment tag.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum TagType {
    /// <https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-17#section-4.4.1>
    Basic,
    /// <https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-17#section-4.4.2>
    MediaOrMultivariantPlaylist,
    /// <https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-17#section-4.4.3>
    MediaPlaylist,
    /// <https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-17#section-4.4.4>
    MediaSegment,
    /// <https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-17#section-4.4.5>
    MediaMetadata,
    /// <https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-17#section-4.4.6>
    MultivariantPlaylist,
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{
        date_time,
        tag::{
            DecimalResolution, HlsPlaylistType, TagValue,
            hls::{daterange::ExtensionAttributeValue, map::MapByterange, part::PartByterange},
        },
    };
    use pretty_assertions::assert_eq;

    macro_rules! unknown_tag {
        ($name:literal) => {
            Tag::try_from(UnknownTag {
                name: $name,
                value: None,
                original_input: format!("#EXT{}", $name).as_bytes(),
                validation_error: None,
            })
        };
        ($name:literal $($value:literal)+) => {
            Tag::try_from(UnknownTag {
                name: $name,
                value: Some(TagValue([$($value,)+].join("").as_bytes())),
                original_input: format!("#EXT{}:{}", $name, [$($value,)+].join("")).as_bytes(),
                validation_error: None,
            })
        };
    }

    #[test]
    fn m3u() {
        assert_eq!(Ok(Tag::M3u(M3u)), unknown_tag!("M3U"))
    }

    #[test]
    fn version() {
        assert_eq!(
            Ok(Tag::Version(Version::new(9))),
            unknown_tag!("-X-VERSION" "9")
        )
    }

    #[test]
    fn independent_segments() {
        assert_eq!(
            Ok(Tag::IndependentSegments(IndependentSegments)),
            unknown_tag!("-X-INDEPENDENT-SEGMENTS")
        )
    }

    #[test]
    fn start() {
        assert_eq!(
            Ok(Tag::Start(Start::builder().with_time_offset(10.5).finish())),
            unknown_tag!("-X-START" "TIME-OFFSET=10.5")
        );
        let expected = Tag::Start(
            Start::builder()
                .with_time_offset(10.0)
                .with_precise()
                .finish(),
        );
        let actual = Tag::try_from(UnknownTag {
            name: "-X-START",
            value: Some(TagValue(b"TIME-OFFSET=10,PRECISE=YES")),
            original_input: b"#EXT-X-START:TIME-OFFSET=10,PRECISE=YES",
            validation_error: None,
        })
        .unwrap();
        match (expected, actual) {
            (Tag::Start(expected), Tag::Start(actual)) => {
                assert_eq!(expected.time_offset(), actual.time_offset());
                assert_eq!(expected.precise(), actual.precise());
            }
            _ => panic!("Unexpected tag type"),
        }
    }

    #[test]
    fn define() {
        assert_eq!(
            Ok(Tag::Define(Define::new_name(
                "TEST".to_string(),
                "GOOD".to_string()
            ))),
            unknown_tag!("-X-DEFINE" "NAME=\"TEST\",VALUE=\"GOOD\"")
        );
        assert_eq!(
            Ok(Tag::Define(Define::new_import("TEST".to_string()))),
            unknown_tag!("-X-DEFINE" r#"IMPORT="TEST""#)
        );
        assert_eq!(
            Ok(Tag::Define(Define::new_queryparam(
                "testQueryParam".to_string()
            ))),
            unknown_tag!("-X-DEFINE" r#"QUERYPARAM="testQueryParam""#)
        );
    }

    #[test]
    fn targetduration() {
        assert_eq!(
            Ok(Tag::Targetduration(Targetduration::new(6))),
            unknown_tag!("-X-TARGETDURATION" "6")
        );
    }

    #[test]
    fn media_sequence() {
        assert_eq!(
            Ok(Tag::MediaSequence(MediaSequence::new(100))),
            unknown_tag!("-X-MEDIA-SEQUENCE" "100")
        );
    }

    #[test]
    fn discontinuity_sequencee() {
        assert_eq!(
            Ok(Tag::DiscontinuitySequence(DiscontinuitySequence::new(100))),
            unknown_tag!("-X-DISCONTINUITY-SEQUENCE" "100")
        );
    }

    #[test]
    fn endlist() {
        assert_eq!(Ok(Tag::Endlist(Endlist)), unknown_tag!("-X-ENDLIST"))
    }

    #[test]
    fn playlist_type() {
        assert_eq!(
            Ok(Tag::PlaylistType(PlaylistType::new(HlsPlaylistType::Event))),
            unknown_tag!("-X-PLAYLIST-TYPE" "EVENT")
        );
        assert_eq!(
            Ok(Tag::PlaylistType(PlaylistType::new(HlsPlaylistType::Vod))),
            unknown_tag!("-X-PLAYLIST-TYPE" "VOD")
        );
    }

    #[test]
    fn i_frames_only() {
        assert_eq!(
            Ok(Tag::IFramesOnly(IFramesOnly)),
            unknown_tag!("-X-I-FRAMES-ONLY")
        )
    }

    #[test]
    fn part_inf() {
        assert_eq!(
            Ok(Tag::PartInf(PartInf::new(0.5))),
            unknown_tag!("-X-PART-INF" "PART-TARGET=0.5")
        )
    }

    #[test]
    fn server_control() {
        assert_eq!(
            Ok(Tag::ServerControl(
                ServerControl::builder()
                    .with_can_skip_until(36.0)
                    .with_can_skip_dateranges()
                    .with_can_block_reload()
                    .with_hold_back(12.0)
                    .with_part_hold_back(1.5)
                    .finish()
            )),
            unknown_tag!(
                "-X-SERVER-CONTROL"
                "CAN-SKIP-UNTIL=36,CAN-SKIP-DATERANGES=YES,HOLD-BACK=12,PART-HOLD-BACK=1.5,"
                "CAN-BLOCK-RELOAD=YES"
            )
        );
    }

    #[test]
    fn inf() {
        assert_eq!(
            Ok(Tag::Inf(Inf::new(6.0, "".to_string()))),
            unknown_tag!("INF" "6")
        );
        assert_eq!(
            Ok(Tag::Inf(Inf::new(6.006, "".to_string()))),
            unknown_tag!("INF" "6.006,")
        );
        assert_eq!(
            Ok(Tag::Inf(Inf::new(6.006, "A useful title".to_string()))),
            unknown_tag!("INF" "6.006,A useful title")
        );
    }

    #[test]
    fn byterange() {
        assert_eq!(
            Ok(Tag::Byterange(Byterange::new(1024, Some(512)))),
            unknown_tag!("-X-BYTERANGE" "1024@512")
        );
        assert_eq!(
            Ok(Tag::Byterange(Byterange::new(1024, None))),
            unknown_tag!("-X-BYTERANGE" "1024")
        );
    }

    #[test]
    fn discontinuity() {
        assert_eq!(
            Ok(Tag::Discontinuity(Discontinuity)),
            unknown_tag!("-X-DISCONTINUITY")
        );
    }

    #[test]
    fn key() {
        assert_eq!(
            Ok(Tag::Key(
                Key::builder()
                    .with_method(key::Method::SampleAes)
                    .with_uri("skd://some-key-id")
                    .with_iv("0xABCD")
                    .with_keyformat("com.apple.streamingkeydelivery")
                    .with_keyformatversions("1")
                    .finish()
            )),
            unknown_tag!(
                "-X-KEY"
                "METHOD=SAMPLE-AES,URI=\"skd://some-key-id\",IV=0xABCD,"
                "KEYFORMAT=\"com.apple.streamingkeydelivery\",KEYFORMATVERSIONS=\"1\""
            )
        );
        assert_eq!(
            Ok(Tag::Key(
                Key::builder().with_method(key::Method::None).finish()
            )),
            unknown_tag!("-X-KEY" "METHOD=NONE")
        );
    }

    #[test]
    fn map() {
        assert_eq!(
            Ok(Tag::Map(
                Map::builder()
                    .with_uri("init.mp4")
                    .with_byterange(MapByterange {
                        length: 1024,
                        offset: 0
                    })
                    .finish()
            )),
            unknown_tag!("-X-MAP" r#"URI="init.mp4",BYTERANGE="1024@0""#)
        );
        assert_eq!(
            Ok(Tag::Map(Map::builder().with_uri("init.mp4").finish())),
            unknown_tag!("-X-MAP" "URI=\"init.mp4\"")
        );
    }

    #[test]
    fn program_date_time() {
        assert_eq!(
            Ok(Tag::ProgramDateTime(ProgramDateTime::new(
                date_time!(2025-06-05 T 16:46:42.123 -05:00)
            ))),
            unknown_tag!("-X-PROGRAM-DATE-TIME" "2025-06-05T16:46:42.123-05:00")
        );
    }

    #[test]
    fn gap() {
        assert_eq!(
            Ok(Tag::Gap(Gap)),
            Tag::try_from(UnknownTag {
                name: "-X-GAP",
                value: None,
                original_input: b"#EXT-X-GAP",
                validation_error: None,
            })
        );
    }

    #[test]
    fn bitrate() {
        assert_eq!(
            Ok(Tag::Bitrate(Bitrate::new(10000000))),
            unknown_tag!("-X-BITRATE" "10000000")
        );
    }

    #[test]
    fn part() {
        assert_eq!(
            Ok(Tag::Part(
                Part::builder()
                    .with_uri("part.1.mp4")
                    .with_duration(0.5)
                    .with_independent()
                    .with_byterange(PartByterange {
                        length: 1024,
                        offset: Some(512)
                    })
                    .with_gap()
                    .finish()
            )),
            unknown_tag!(
                "-X-PART"
                "URI=\"part.1.mp4\",DURATION=0.5,INDEPENDENT=YES,BYTERANGE=\"1024@512\",GAP=YES"
            )
        );
        assert_eq!(
            Ok(Tag::Part(
                Part::builder()
                    .with_uri("part.1.mp4")
                    .with_duration(0.5)
                    .with_byterange(PartByterange {
                        length: 1024,
                        offset: None
                    })
                    .finish()
            )),
            unknown_tag!("-X-PART" "URI=\"part.1.mp4\",DURATION=0.5,BYTERANGE=\"1024\"")
        );
        assert_eq!(
            Ok(Tag::Part(
                Part::builder()
                    .with_uri("part.1.mp4")
                    .with_duration(0.5)
                    .finish()
            )),
            unknown_tag!("-X-PART" "URI=\"part.1.mp4\",DURATION=0.5")
        );
    }

    #[test]
    fn daterange() {
        assert_eq!(
            Ok(Tag::Daterange(
                Daterange::builder()
                    .with_id("test")
                    .with_start_date(date_time!(2025-06-05 T 20:38:42.149 -05:00))
                    .with_class("com.m3u8.test")
                    .with_cue(EnumeratedStringList::from([daterange::Cue::Once]))
                    .with_end_date(date_time!(2025-06-05 T 20:40:42.149 -05:00))
                    .with_duration(120.0)
                    .with_planned_duration(180.0)
                    .with_extension_attribute(
                        "X-COM-M3U8-TEST",
                        ExtensionAttributeValue::QuotedString("YES".into())
                    )
                    .with_scte35_cmd("0xABCD")
                    .with_scte35_in("0xABCD")
                    .with_scte35_out("0xABCD")
                    .with_end_on_next()
                    .finish()
            )),
            unknown_tag!(
                "-X-DATERANGE"
                r#"ID="test",CLASS="com.m3u8.test",START-DATE="2025-06-05T20:38:42.149-05:00","#
                r#"CUE="ONCE",END-DATE="2025-06-05T20:40:42.149-05:00",DURATION=120,"#
                r#"PLANNED-DURATION=180,X-COM-M3U8-TEST="YES",SCTE35-CMD=0xABCD,SCTE35-OUT=0xABCD,"#
                r#"SCTE35-IN=0xABCD,END-ON-NEXT=YES"#)
        );
        assert_eq!(
            Ok(Tag::Daterange(
                Daterange::builder()
                    .with_id("test")
                    .with_start_date(date_time!(2025-06-05 T 20:38:42.149 -05:00))
                    .finish()
            )),
            unknown_tag!("-X-DATERANGE" r#"ID="test",START-DATE="2025-06-05T20:38:42.149-05:00""#)
        );
    }

    #[test]
    fn skip() {
        assert_eq!(
            Ok(Tag::Skip(
                Skip::builder()
                    .with_skipped_segments(100)
                    .with_recently_removed_dateranges("1234\tabcd")
                    .finish()
            )),
            unknown_tag!(
                "-X-SKIP"
                "SKIPPED-SEGMENTS=100,RECENTLY-REMOVED-DATERANGES=\"1234\tabcd\""
            )
        );
        assert_eq!(
            Ok(Tag::Skip(
                Skip::builder().with_skipped_segments(100).finish()
            )),
            unknown_tag!("-X-SKIP" "SKIPPED-SEGMENTS=100")
        );
    }

    #[test]
    fn preload_hint() {
        assert_eq!(
            Ok(Tag::PreloadHint(
                PreloadHint::builder()
                    .with_hint_type(preload_hint::PreloadHintType::Part)
                    .with_uri("part.2.mp4")
                    .with_byterange_start(512)
                    .with_byterange_length(1024)
                    .finish()
            )),
            unknown_tag!(
                "-X-PRELOAD-HINT"
                "TYPE=PART,URI=\"part.2.mp4\",BYTERANGE-START=512,BYTERANGE-LENGTH=1024"
            )
        );
        assert_eq!(
            Ok(Tag::PreloadHint(
                PreloadHint::builder()
                    .with_hint_type(preload_hint::PreloadHintType::Part)
                    .with_uri("part.2.mp4")
                    .finish()
            )),
            unknown_tag!(
                "-X-PRELOAD-HINT"
                "TYPE=PART,URI=\"part.2.mp4\""
            )
        );
    }

    #[test]
    fn rendition_report() {
        assert_eq!(
            Ok(Tag::RenditionReport(
                RenditionReport::builder()
                    .with_uri("high.m3u8")
                    .with_last_msn(1000)
                    .with_last_part(2)
                    .finish()
            )),
            unknown_tag!("-X-RENDITION-REPORT" "URI=\"high.m3u8\",LAST-MSN=1000,LAST-PART=2")
        );
        assert_eq!(
            Ok(Tag::RenditionReport(
                RenditionReport::builder()
                    .with_uri("high.m3u8")
                    .with_last_msn(1000)
                    .finish()
            )),
            unknown_tag!("-X-RENDITION-REPORT" "URI=\"high.m3u8\",LAST-MSN=1000")
        );
    }

    #[test]
    fn media() {
        assert_eq!(
            Ok(Tag::Media(
                Media::builder()
                    .with_media_type(media::MediaType::Audio)
                    .with_name("English")
                    .with_group_id("stereo")
                    .with_uri("audio/en/stereo.m3u8")
                    .with_language("en")
                    .with_assoc_language("en")
                    .with_stable_rendition_id("1234")
                    .with_default()
                    .with_autoselect()
                    .with_forced()
                    .with_bit_depth(8)
                    .with_sample_rate(48000)
                    .with_characteristics("public.accessibility.describes-video")
                    .with_channels("2")
                    .finish()
            )),
            unknown_tag!(
                "-X-MEDIA"
                "TYPE=AUDIO,URI=\"audio/en/stereo.m3u8\",GROUP-ID=\"stereo\",LANGUAGE=\"en\","
                "ASSOC-LANGUAGE=\"en\",NAME=\"English\",STABLE-RENDITION-ID=\"1234\",DEFAULT=YES,"
                "AUTOSELECT=YES,FORCED=YES,BIT-DEPTH=8,SAMPLE-RATE=48000,"
                "CHARACTERISTICS=\"public.accessibility.describes-video\",CHANNELS=\"2\""
            )
        );
        assert_eq!(
            Ok(Tag::Media(
                Media::builder()
                    .with_media_type(media::MediaType::ClosedCaptions)
                    .with_name("English")
                    .with_group_id("cc")
                    .with_instream_id("CC1")
                    .finish()
            )),
            unknown_tag!(
                "-X-MEDIA"
                "TYPE=CLOSED-CAPTIONS,GROUP-ID=\"cc\",NAME=\"English\",INSTREAM-ID=\"CC1\""
            )
        );
    }

    #[test]
    fn stream_inf() {
        assert_eq!(
            Ok(Tag::StreamInf(
                StreamInf::builder()
                    .with_bandwidth(10000000)
                    .with_average_bandwidth(9000000)
                    .with_score(2.0)
                    .with_codecs("hvc1.2.4.L153.b0,ec-3")
                    .with_supplemental_codecs("dvh1.08.07/db4h")
                    .with_resolution(DecimalResolution {
                        width: 3840,
                        height: 2160
                    })
                    .with_frame_rate(23.976)
                    .with_hdcp_level("TYPE-1")
                    .with_allowed_cpc("com.example.drm1:SMART-TV/PC")
                    .with_video_range("PQ")
                    .with_req_video_layout("CH-STEREO,CH-MONO")
                    .with_stable_variant_id("1234")
                    .with_audio("surround")
                    .with_video("alternate-view")
                    .with_subtitles("subs")
                    .with_closed_captions("cc")
                    .with_pathway_id("1234")
                    .finish()
            )),
            unknown_tag!(
                "-X-STREAM-INF"
                "BANDWIDTH=10000000,AVERAGE-BANDWIDTH=9000000,SCORE=2.0,"
                "CODECS=\"hvc1.2.4.L153.b0,ec-3\",SUPPLEMENTAL-CODECS=\"dvh1.08.07/db4h\","
                "RESOLUTION=3840x2160,FRAME-RATE=23.976,HDCP-LEVEL=TYPE-1,"
                "ALLOWED-CPC=\"com.example.drm1:SMART-TV/PC\",VIDEO-RANGE=PQ,"
                "REQ-VIDEO-LAYOUT=\"CH-STEREO,CH-MONO\",STABLE-VARIANT-ID=\"1234\","
                "AUDIO=\"surround\",VIDEO=\"alternate-view\",SUBTITLES=\"subs\","
                "CLOSED-CAPTIONS=\"cc\",PATHWAY-ID=\"1234\""
            )
        );
        // One more test to check that integer frame rate parses well
        assert_eq!(
            Ok(Tag::StreamInf(
                StreamInf::builder()
                    .with_bandwidth(10000000)
                    .with_frame_rate(25.0)
                    .finish()
            )),
            unknown_tag!("-X-STREAM-INF" "BANDWIDTH=10000000,FRAME-RATE=25")
        );
        // Final check with all options unset
        assert_eq!(
            Ok(Tag::StreamInf(
                StreamInf::builder().with_bandwidth(10000000).finish()
            )),
            unknown_tag!("-X-STREAM-INF" "BANDWIDTH=10000000")
        );
    }

    #[test]
    fn i_frame_stream_inf() {
        assert_eq!(
            Ok(Tag::IFrameStreamInf(
                IFrameStreamInf::builder()
                    .with_uri("iframe.high.m3u8")
                    .with_bandwidth(10000000)
                    .with_average_bandwidth(9000000)
                    .with_score(2.0)
                    .with_codecs("hvc1.2.4.L153.b0,ec-3")
                    .with_supplemental_codecs("dvh1.08.07/db4h")
                    .with_resolution(DecimalResolution {
                        width: 3840,
                        height: 2160
                    })
                    .with_hdcp_level("TYPE-1")
                    .with_allowed_cpc("com.example.drm1:SMART-TV/PC")
                    .with_video_range("PQ")
                    .with_req_video_layout("CH-STEREO,CH-MONO")
                    .with_stable_variant_id("1234")
                    .with_video("alternate-view")
                    .with_pathway_id("1234")
                    .finish()
            )),
            unknown_tag!(
                "-X-I-FRAME-STREAM-INF"
                "URI=\"iframe.high.m3u8\",BANDWIDTH=10000000,AVERAGE-BANDWIDTH=9000000,SCORE=2.0,"
                "CODECS=\"hvc1.2.4.L153.b0,ec-3\",SUPPLEMENTAL-CODECS=\"dvh1.08.07/db4h\","
                "RESOLUTION=3840x2160,HDCP-LEVEL=TYPE-1,"
                "ALLOWED-CPC=\"com.example.drm1:SMART-TV/PC\",VIDEO-RANGE=PQ,"
                "REQ-VIDEO-LAYOUT=\"CH-STEREO,CH-MONO\",STABLE-VARIANT-ID=\"1234\","
                "VIDEO=\"alternate-view\",PATHWAY-ID=\"1234\""
            )
        );
        assert_eq!(
            Ok(Tag::IFrameStreamInf(
                IFrameStreamInf::builder()
                    .with_uri("iframe.high.m3u8")
                    .with_bandwidth(10000000)
                    .finish()
            )),
            unknown_tag!("-X-I-FRAME-STREAM-INF" "URI=\"iframe.high.m3u8\",BANDWIDTH=10000000")
        );
    }

    #[test]
    fn session_data() {
        assert_eq!(
            Ok(Tag::SessionData(
                SessionData::builder()
                    .with_data_id("1234")
                    .with_value("test")
                    .with_language("en")
                    .finish()
            )),
            unknown_tag!("-X-SESSION-DATA" "DATA-ID=\"1234\",VALUE=\"test\",LANGUAGE=\"en\"")
        );
        assert_eq!(
            Ok(Tag::SessionData(
                SessionData::builder()
                    .with_data_id("1234")
                    .with_uri("test.bin")
                    .with_format("RAW")
                    .finish()
            )),
            unknown_tag!("-X-SESSION-DATA" "DATA-ID=\"1234\",URI=\"test.bin\",FORMAT=RAW")
        );
    }

    #[test]
    fn session_key() {
        assert_eq!(
            Ok(Tag::SessionKey(
                SessionKey::builder()
                    .with_method("SAMPLE-AES")
                    .with_uri("skd://some-key-id")
                    .with_iv("0xABCD")
                    .with_keyformat("com.apple.streamingkeydelivery")
                    .with_keyformatversions("1")
                    .finish()
            )),
            unknown_tag!(
                "-X-SESSION-KEY"
                "METHOD=SAMPLE-AES,URI=\"skd://some-key-id\",IV=0xABCD,"
                "KEYFORMAT=\"com.apple.streamingkeydelivery\",KEYFORMATVERSIONS=\"1\""
            )
        );
        assert_eq!(
            Ok(Tag::SessionKey(
                SessionKey::builder()
                    .with_method("AES-128")
                    .with_uri("skd://some-key-id")
                    .finish()
            )),
            unknown_tag!("-X-SESSION-KEY" "METHOD=AES-128,URI=\"skd://some-key-id\"")
        );
    }

    #[test]
    fn content_steering() {
        assert_eq!(
            Ok(Tag::ContentSteering(
                ContentSteering::builder()
                    .with_server_uri("content-steering.json")
                    .with_pathway_id("1234")
                    .finish()
            )),
            unknown_tag!(
                "-X-CONTENT-STEERING"
                "SERVER-URI=\"content-steering.json\",PATHWAY-ID=\"1234\""
            )
        );
        assert_eq!(
            Ok(Tag::ContentSteering(
                ContentSteering::builder()
                    .with_server_uri("content-steering.json")
                    .finish()
            )),
            unknown_tag!("-X-CONTENT-STEERING" "SERVER-URI=\"content-steering.json\"")
        );
    }
}
