//
// Copyright (c) Double Symmetry GmbH
// Commercial use requires a license. See https://rntp.dev/pricing
//

import XCTest
@testable import PlayerCore

/// Tests for the pure HLS playlist rewriter. It resolves every child URI in a
/// playlist against the playlist's own absolute URL, then maps it through a
/// `transform` (in production: a proxy-URL builder) so AVPlayer routes segment
/// and variant requests back through the cache proxy.
final class HLSManifestRewriterTests: XCTestCase {

  // MARK: - Helpers

  /// Deterministic stand-in for the proxy-URL builder. Percent-encodes the
  /// absolute upstream URL into a predictable path so assertions are exact.
  private func proxify(_ url: URL) -> URL {
    let enc = url.absoluteString.addingPercentEncoding(withAllowedCharacters: .alphanumerics)!
    return URL(string: "https://proxy.test/p?u=\(enc)")!
  }

  /// The string `proxify` would produce for a given absolute upstream URL.
  private func expected(_ absolute: String) -> String {
    let enc = absolute.addingPercentEncoding(withAllowedCharacters: .alphanumerics)!
    return "https://proxy.test/p?u=\(enc)"
  }

  private func rewrite(_ manifest: String, base: String) -> String {
    HLSManifestRewriter.rewrite(
      manifest: manifest,
      baseURL: URL(string: base)!,
      transform: { self.proxify($0) }
    )
  }

  // MARK: - Bare URI lines (segments, variants)

  func testRewritesRelativeSegmentURI() {
    let manifest = """
    #EXTM3U
    #EXT-X-VERSION:3
    #EXT-X-TARGETDURATION:1
    #EXTINF:0.5,
    seg0.aac
    #EXT-X-ENDLIST
    """
    let out = rewrite(manifest, base: "https://cdn.example.com/hls/index.m3u8")

    XCTAssertTrue(
      out.contains(expected("https://cdn.example.com/hls/seg0.aac")),
      "Relative segment should resolve against the playlist URL and be proxied"
    )
    XCTAssertFalse(out.contains("\nseg0.aac"), "Original relative segment line should be replaced")
  }

  func testRewritesRelativeVariantPlaylistURI() {
    let manifest = """
    #EXTM3U
    #EXT-X-STREAM-INF:BANDWIDTH=128000,CODECS="mp4a.40.2"
    media_128k.m3u8
    """
    let out = rewrite(manifest, base: "https://cdn.example.com/master.m3u8")

    XCTAssertTrue(out.contains(expected("https://cdn.example.com/media_128k.m3u8")))
  }

  func testResolvesAndRewritesAbsoluteSegmentURI() {
    let manifest = """
    #EXTM3U
    #EXTINF:0.5,
    https://media.cdn.net/path/seg0.ts
    #EXT-X-ENDLIST
    """
    let out = rewrite(manifest, base: "https://cdn.example.com/hls/index.m3u8")

    XCTAssertTrue(out.contains(expected("https://media.cdn.net/path/seg0.ts")))
  }

  func testResolvesParentRelativeSegmentURI() {
    let manifest = """
    #EXTM3U
    #EXTINF:0.5,
    ../seg/0.aac
    #EXT-X-ENDLIST
    """
    let out = rewrite(manifest, base: "https://cdn.example.com/hls/v1/index.m3u8")

    XCTAssertTrue(out.contains(expected("https://cdn.example.com/hls/seg/0.aac")))
  }

  // MARK: - URI="..." attribute tags

  func testRewritesEncryptionKeyURIAttribute() {
    let manifest = """
    #EXTM3U
    #EXT-X-KEY:METHOD=AES-128,URI="enc.key",IV=0x00000000000000000000000000000000
    #EXTINF:0.5,
    seg0.aac
    #EXT-X-ENDLIST
    """
    let out = rewrite(manifest, base: "https://cdn.example.com/hls/index.m3u8")

    XCTAssertTrue(
      out.contains("URI=\"\(expected("https://cdn.example.com/hls/enc.key"))\""),
      "AES-128 key URI should be proxied"
    )
    XCTAssertTrue(out.contains("IV=0x00000000000000000000000000000000"),
                  "Other attributes on the tag must be preserved")
  }

  func testRewritesInitSegmentMapURIAttribute() {
    let manifest = """
    #EXTM3U
    #EXT-X-MAP:URI="init.mp4"
    #EXTINF:0.5,
    seg0.m4s
    #EXT-X-ENDLIST
    """
    let out = rewrite(manifest, base: "https://cdn.example.com/hls/index.m3u8")

    XCTAssertTrue(out.contains("URI=\"\(expected("https://cdn.example.com/hls/init.mp4"))\""))
    XCTAssertTrue(out.contains(expected("https://cdn.example.com/hls/seg0.m4s")))
  }

  func testRewritesMediaRenditionURIAttribute() {
    let manifest = """
    #EXTM3U
    #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud",NAME="English",URI="audio/eng.m3u8"
    #EXT-X-STREAM-INF:BANDWIDTH=128000,AUDIO="aud"
    video.m3u8
    """
    let out = rewrite(manifest, base: "https://cdn.example.com/master.m3u8")

    XCTAssertTrue(out.contains("URI=\"\(expected("https://cdn.example.com/audio/eng.m3u8"))\""))
    XCTAssertTrue(out.contains(expected("https://cdn.example.com/video.m3u8")))
  }

  func testRewritesIFrameStreamInfURIAttribute() {
    let manifest = """
    #EXTM3U
    #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=86000,URI="iframe.m3u8"
    """
    let out = rewrite(manifest, base: "https://cdn.example.com/master.m3u8")

    XCTAssertTrue(out.contains("URI=\"\(expected("https://cdn.example.com/iframe.m3u8"))\""))
  }

  // MARK: - Things that must be left untouched

  func testLeavesFairPlaySkdKeyUntouched() {
    let manifest = """
    #EXTM3U
    #EXT-X-KEY:METHOD=SAMPLE-AES,URI="skd://license-server/asset-id",KEYFORMAT="com.apple.streamingkeydelivery"
    #EXTINF:0.5,
    seg0.aac
    #EXT-X-ENDLIST
    """
    let out = rewrite(manifest, base: "https://cdn.example.com/hls/index.m3u8")

    XCTAssertTrue(out.contains("URI=\"skd://license-server/asset-id\""),
                  "FairPlay skd:// key URIs must not be proxied")
  }

  func testLeavesKeyMethodNoneUntouched() {
    let manifest = """
    #EXTM3U
    #EXT-X-KEY:METHOD=NONE
    #EXTINF:0.5,
    seg0.aac
    #EXT-X-ENDLIST
    """
    let out = rewrite(manifest, base: "https://cdn.example.com/hls/index.m3u8")

    XCTAssertTrue(out.contains("#EXT-X-KEY:METHOD=NONE"))
  }

  func testPreservesTagsCommentsAndOrder() throws {
    let manifest = """
    #EXTM3U
    #EXT-X-VERSION:6
    # a human comment
    #EXT-X-TARGETDURATION:2
    #EXT-X-MEDIA-SEQUENCE:0
    #EXTINF:1.0,
    seg0.aac
    #EXTINF:1.0,
    seg1.aac
    #EXT-X-ENDLIST
    """
    let out = rewrite(manifest, base: "https://cdn.example.com/hls/index.m3u8")
    let lines = out.components(separatedBy: "\n")

    XCTAssertEqual(lines.first, "#EXTM3U")
    XCTAssertEqual(lines.last, "#EXT-X-ENDLIST")
    XCTAssertTrue(out.contains("# a human comment"))
    XCTAssertTrue(out.contains("#EXT-X-MEDIA-SEQUENCE:0"))
    // Segment order preserved: seg0 proxied line appears before seg1 proxied line.
    let r0 = try XCTUnwrap(out.range(of: expected("https://cdn.example.com/hls/seg0.aac")))
    let r1 = try XCTUnwrap(out.range(of: expected("https://cdn.example.com/hls/seg1.aac")))
    XCTAssertTrue(r0.lowerBound < r1.lowerBound)
  }

  // MARK: - Detection: isPlaylist

  func testIsPlaylistByContentType() {
    XCTAssertTrue(HLSManifestRewriter.isPlaylist(
      contentType: "application/vnd.apple.mpegurl", url: nil, body: nil))
    XCTAssertTrue(HLSManifestRewriter.isPlaylist(
      contentType: "application/x-mpegURL; charset=utf-8", url: nil, body: nil))
    XCTAssertTrue(HLSManifestRewriter.isPlaylist(
      contentType: "audio/mpegurl", url: nil, body: nil))
  }

  func testIsPlaylistByExtension() {
    XCTAssertTrue(HLSManifestRewriter.isPlaylist(
      contentType: "application/octet-stream",
      url: URL(string: "https://cdn.example.com/a/index.m3u8?token=x"),
      body: nil))
  }

  func testIsPlaylistByBodySniff() {
    let body = Data("#EXTM3U\n#EXT-X-VERSION:3\n".utf8)
    XCTAssertTrue(HLSManifestRewriter.isPlaylist(
      contentType: "text/plain", url: URL(string: "https://cdn.example.com/x"), body: body))
  }

  func testIsNotPlaylistForAudio() {
    XCTAssertFalse(HLSManifestRewriter.isPlaylist(
      contentType: "audio/mpeg",
      url: URL(string: "https://cdn.example.com/a/track.mp3"),
      body: Data([0xFF, 0xFB])))
  }

  // MARK: - Detection: isVOD

  func testIsVODTrueWithEndlist() {
    XCTAssertTrue(HLSManifestRewriter.isVOD("#EXTM3U\n#EXTINF:1.0,\nseg0.ts\n#EXT-X-ENDLIST"))
  }

  func testIsVODFalseWithoutEndlist() {
    XCTAssertFalse(HLSManifestRewriter.isVOD("#EXTM3U\n#EXT-X-MEDIA-SEQUENCE:5\n#EXTINF:1.0,\nseg5.ts"))
  }

  // MARK: - Hardening: attribute parsing must not over-match

  /// A plain comment line that merely mentions `URI="..."` in prose must pass
  /// through verbatim — it is not an HLS tag and carries no rewritable URI.
  func testLeavesCommentLineWithURISubstringUntouched() {
    let manifest = """
    #EXTM3U
    # TODO: migrate URI="legacy/old.key" to the new format
    #EXTINF:0.5,
    seg0.aac
    #EXT-X-ENDLIST
    """
    let out = rewrite(manifest, base: "https://cdn.example.com/hls/index.m3u8")

    XCTAssertTrue(
      out.contains("# TODO: migrate URI=\"legacy/old.key\" to the new format"),
      "Comment-line URI=\"...\" prose must not be rewritten"
    )
  }

  /// When a non-URI quoted attribute value contains the literal `URI`, the
  /// parser must still rewrite only the real `URI` attribute by name.
  func testRewritesOnlyRealURIAttributeNotEmbeddedSubstring() {
    let manifest = """
    #EXTM3U
    #EXT-X-SESSION-DATA:DATA-ID="com.example.note",VALUE="see URI for details",URI="data.json"
    """
    let out = rewrite(manifest, base: "https://cdn.example.com/master.m3u8")

    XCTAssertTrue(out.contains("URI=\"\(expected("https://cdn.example.com/data.json"))\""))
    XCTAssertTrue(
      out.contains("VALUE=\"see URI for details\""),
      "The non-URI VALUE attribute must be preserved byte-for-byte"
    )
  }

  /// A vendor attribute whose name merely ends in `URI` (e.g. `X-COM-EXAMPLE-URI`)
  /// must not be treated as the `URI` attribute.
  func testDoesNotRewriteAttributeNameEndingInURI() {
    let manifest = """
    #EXTM3U
    #EXT-X-DATERANGE:ID="ad",X-COM-EXAMPLE-URI="should/stay.bin"
    """
    let out = rewrite(manifest, base: "https://cdn.example.com/master.m3u8")

    XCTAssertTrue(
      out.contains("X-COM-EXAMPLE-URI=\"should/stay.bin\""),
      "An attribute whose name only ends in URI must not be rewritten"
    )
  }

  // MARK: - Hardening: URI attribute edge cases

  /// An empty `URI=""` must be left exactly as-is, not resolved to the base URL.
  func testLeavesEmptyURIAttributeUntouched() {
    let manifest = """
    #EXTM3U
    #EXT-X-MAP:URI=""
    """
    let out = rewrite(manifest, base: "https://cdn.example.com/hls/index.m3u8")

    XCTAssertTrue(out.contains("#EXT-X-MAP:URI=\"\""))
    XCTAssertFalse(out.contains("proxy.test"), "Empty URI must not become a proxy of the base URL")
  }

  /// A `data:` URI on a key must be preserved (only `skd://` is exercised by the
  /// happy-path suite).
  func testLeavesDataURIKeyUntouched() {
    let manifest = """
    #EXTM3U
    #EXT-X-KEY:METHOD=AES-128,URI="data:text/plain;base64,AAAABBBB"
    #EXTINF:0.5,
    seg0.aac
    #EXT-X-ENDLIST
    """
    let out = rewrite(manifest, base: "https://cdn.example.com/hls/index.m3u8")

    XCTAssertTrue(out.contains("URI=\"data:text/plain;base64,AAAABBBB\""))
  }

  /// A lowercase `uri="..."` marker is rewritten (RFC 8216 names are uppercase
  /// but the parser is lenient on input).
  func testRewritesLowercaseURIAttribute() {
    let manifest = """
    #EXTM3U
    #EXT-X-MAP:uri="init.mp4"
    """
    let out = rewrite(manifest, base: "https://cdn.example.com/hls/index.m3u8")

    XCTAssertTrue(out.contains("uri=\"\(expected("https://cdn.example.com/hls/init.mp4"))\""))
  }

  /// A malformed `URI="` with no closing quote must return the line byte-for-byte
  /// (no crash, no drop).
  func testLeavesUnterminatedURIQuoteUntouched() {
    let manifest = """
    #EXTM3U
    #EXT-X-MAP:URI="init.mp4
    """
    let out = rewrite(manifest, base: "https://cdn.example.com/hls/index.m3u8")

    XCTAssertTrue(out.contains("#EXT-X-MAP:URI=\"init.mp4"))
    XCTAssertFalse(out.contains("proxy.test"))
  }

  /// A quoted attribute that precedes `URI` (e.g. `CODECS="..."` before the
  /// I-frame URI) must not derail the parser onto the wrong quote.
  func testRewritesURIAfterPrecedingQuotedAttribute() {
    let manifest = """
    #EXTM3U
    #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=86000,CODECS="avc1.4d401f",URI="iframe.m3u8"
    """
    let out = rewrite(manifest, base: "https://cdn.example.com/master.m3u8")

    XCTAssertTrue(out.contains("URI=\"\(expected("https://cdn.example.com/iframe.m3u8"))\""))
    XCTAssertTrue(out.contains("CODECS=\"avc1.4d401f\""))
  }

  /// `EXT-X-MAP` URI is proxied while its `BYTERANGE` attribute is preserved
  /// verbatim (the byte-range contract is honored downstream by the proxy).
  func testPreservesMapByteRangeWhileRewritingURI() {
    let manifest = """
    #EXTM3U
    #EXT-X-MAP:URI="init.mp4",BYTERANGE="1200@0"
    #EXTINF:0.5,
    seg0.m4s
    #EXT-X-ENDLIST
    """
    let out = rewrite(manifest, base: "https://cdn.example.com/hls/index.m3u8")

    XCTAssertTrue(out.contains("URI=\"\(expected("https://cdn.example.com/hls/init.mp4"))\""))
    XCTAssertTrue(out.contains("BYTERANGE=\"1200@0\""))
  }

  /// A `#EXT-X-BYTERANGE` tag following a segment is preserved while the segment
  /// URI is proxied.
  func testPreservesSegmentByteRangeTag() {
    let manifest = """
    #EXTM3U
    #EXT-X-BYTERANGE:75232@0
    seg.ts
    #EXT-X-ENDLIST
    """
    let out = rewrite(manifest, base: "https://cdn.example.com/hls/index.m3u8")

    XCTAssertTrue(out.contains("#EXT-X-BYTERANGE:75232@0"))
    XCTAssertTrue(out.contains(expected("https://cdn.example.com/hls/seg.ts")))
  }

  /// Subtitle renditions carry a `URI` just like audio renditions.
  func testRewritesSubtitleRenditionURIAttribute() {
    let manifest = """
    #EXTM3U
    #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="English",URI="subs/en.m3u8"
    """
    let out = rewrite(manifest, base: "https://cdn.example.com/master.m3u8")

    XCTAssertTrue(out.contains("URI=\"\(expected("https://cdn.example.com/subs/en.m3u8"))\""))
  }

  /// A CLOSED-CAPTIONS rendition has no `URI` and must be left untouched.
  func testLeavesClosedCaptionsMediaUntouched() {
    let manifest = """
    #EXTM3U
    #EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,GROUP-ID="cc",NAME="English",INSTREAM-ID="CC1"
    """
    let out = rewrite(manifest, base: "https://cdn.example.com/master.m3u8")

    XCTAssertTrue(out.contains("#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,GROUP-ID=\"cc\",NAME=\"English\",INSTREAM-ID=\"CC1\""))
    XCTAssertFalse(out.contains("proxy.test"))
  }

  /// A master-playlist `EXT-X-SESSION-KEY` URI is proxied like a media key.
  func testRewritesSessionKeyURIAttribute() {
    let manifest = """
    #EXTM3U
    #EXT-X-SESSION-KEY:METHOD=AES-128,URI="keys/session.key"
    """
    let out = rewrite(manifest, base: "https://cdn.example.com/master.m3u8")

    XCTAssertTrue(out.contains("URI=\"\(expected("https://cdn.example.com/keys/session.key"))\""))
  }

  // MARK: - Hardening: structural fidelity

  /// CRLF input is tolerated: segments resolve and the output is `\n`-joined
  /// with no stray `\r`.
  func testToleratesCRLFAndNormalisesToLF() {
    let manifest = "#EXTM3U\r\n#EXTINF:0.5,\r\nseg0.aac\r\n#EXT-X-ENDLIST\r\n"
    let out = rewrite(manifest, base: "https://cdn.example.com/hls/index.m3u8")

    XCTAssertFalse(out.contains("\r"), "CRLF must be normalised to LF")
    XCTAssertTrue(out.contains(expected("https://cdn.example.com/hls/seg0.aac")))
  }

  /// A trailing newline in the input is preserved in the output.
  func testPreservesTrailingNewline() {
    let manifest = "#EXTM3U\n#EXTINF:0.5,\nseg0.aac\n#EXT-X-ENDLIST\n"
    let out = rewrite(manifest, base: "https://cdn.example.com/hls/index.m3u8")

    XCTAssertTrue(out.hasSuffix("#EXT-X-ENDLIST\n"), "Trailing newline must round-trip")
  }

  /// A root-relative URI resolves against the playlist host, dropping the
  /// playlist path.
  func testResolvesRootRelativeSegmentURI() {
    let manifest = """
    #EXTM3U
    #EXTINF:0.5,
    /abs/seg.ts
    #EXT-X-ENDLIST
    """
    let out = rewrite(manifest, base: "https://cdn.example.com/hls/v1/index.m3u8")

    XCTAssertTrue(out.contains(expected("https://cdn.example.com/abs/seg.ts")))
  }

  /// The playlist base URL's own query is not inherited by a relative segment.
  func testDoesNotInheritBaseQuery() {
    let manifest = """
    #EXTM3U
    #EXTINF:0.5,
    seg0.aac
    #EXT-X-ENDLIST
    """
    let out = rewrite(manifest, base: "https://cdn.example.com/hls/index.m3u8?token=abc")

    XCTAssertTrue(out.contains(expected("https://cdn.example.com/hls/seg0.aac")))
  }

  /// A pre-percent-encoded segment must round-trip without double-encoding.
  func testDoesNotDoubleEncodePrePercentEncodedSegment() {
    let manifest = """
    #EXTM3U
    #EXTINF:0.5,
    seg%20a.ts
    #EXT-X-ENDLIST
    """
    let out = rewrite(manifest, base: "https://cdn.example.com/hls/index.m3u8")

    XCTAssertTrue(out.contains(expected("https://cdn.example.com/hls/seg%20a.ts")))
  }

  // MARK: - Detection: hardening

  /// A UTF-8 BOM before the `#EXTM3U` magic must still sniff as a playlist.
  func testIsPlaylistByBodySniffWithBOM() {
    var body = Data([0xEF, 0xBB, 0xBF])
    body.append(Data("#EXTM3U\n".utf8))
    XCTAssertTrue(HLSManifestRewriter.isPlaylist(
      contentType: "text/plain", url: URL(string: "https://cdn.example.com/x"), body: body))
  }

  /// `#EXT-X-PLAYLIST-TYPE:VOD` without an `#EXT-X-ENDLIST` is treated as VOD.
  func testIsVODTrueWithPlaylistTypeVOD() {
    XCTAssertTrue(HLSManifestRewriter.isVOD("#EXTM3U\n#EXT-X-PLAYLIST-TYPE:VOD\n#EXTINF:1.0,\nseg0.ts"))
  }

  /// A comment that merely mentions the `#EXT-X-ENDLIST` token is not VOD.
  func testIsVODFalseForCommentMentioningEndlist() {
    XCTAssertFalse(HLSManifestRewriter.isVOD("#EXTM3U\n# removed #EXT-X-ENDLIST last week\n#EXTINF:1.0,\nseg0.ts"))
  }
}
