//
// 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 parser used by the preloader to extract the
/// variant / segment / init / key URIs it must warm into the cache.
final class HLSPlaylistParserTests: XCTestCase {

  private func url(_ s: String) -> URL { URL(string: s)! }

  // MARK: - Master vs media detection

  func testIsMasterTrueForStreamInf() {
    let m = "#EXTM3U\n#EXT-X-STREAM-INF:BANDWIDTH=128000\nmedia.m3u8\n"
    XCTAssertTrue(HLSPlaylistParser.isMaster(m))
  }

  func testIsMasterFalseForMediaPlaylist() {
    let m = "#EXTM3U\n#EXT-X-TARGETDURATION:2\n#EXTINF:1.0,\nseg0.ts\n#EXT-X-ENDLIST"
    XCTAssertFalse(HLSPlaylistParser.isMaster(m))
  }

  // MARK: - Variants (master)

  func testVariantsParsedWithBandwidthAndResolvedURLs() {
    let m = """
    #EXTM3U
    #EXT-X-STREAM-INF:BANDWIDTH=256000,CODECS="mp4a.40.2"
    low/media.m3u8
    #EXT-X-STREAM-INF:BANDWIDTH=512000
    https://cdn2.net/hi/media.m3u8
    """
    let v = HLSPlaylistParser.variants(manifest: m, baseURL: url("https://cdn.example.com/master.m3u8"))
    XCTAssertEqual(v.count, 2)
    guard v.count == 2 else { return }
    XCTAssertEqual(v[0].bandwidth, 256000)
    XCTAssertEqual(v[0].url, url("https://cdn.example.com/low/media.m3u8"))
    XCTAssertEqual(v[1].bandwidth, 512000)
    XCTAssertEqual(v[1].url, url("https://cdn2.net/hi/media.m3u8"))
  }

  func testVariantsEmptyForMediaPlaylist() {
    let m = "#EXTM3U\n#EXTINF:1.0,\nseg0.ts\n#EXT-X-ENDLIST"
    XCTAssertTrue(HLSPlaylistParser.variants(manifest: m, baseURL: url("https://x.com/a.m3u8")).isEmpty)
  }

  // MARK: - Segments (media)

  func testSegmentsParsedWithDurationsAndOrder() {
    let m = """
    #EXTM3U
    #EXT-X-TARGETDURATION:2
    #EXTINF:1.5,
    seg0.aac
    #EXTINF:2.0,a title
    sub/seg1.aac
    #EXT-X-ENDLIST
    """
    let s = HLSPlaylistParser.segments(manifest: m, baseURL: url("https://cdn.example.com/hls/index.m3u8"))
    XCTAssertEqual(s.count, 2)
    guard s.count == 2 else { return }
    XCTAssertEqual(s[0].url, url("https://cdn.example.com/hls/seg0.aac"))
    XCTAssertEqual(s[0].duration, 1.5, accuracy: 0.001)
    XCTAssertEqual(s[1].url, url("https://cdn.example.com/hls/sub/seg1.aac"))
    XCTAssertEqual(s[1].duration, 2.0, accuracy: 0.001)
  }

  func testSegmentsEmptyForMaster() {
    let m = "#EXTM3U\n#EXT-X-STREAM-INF:BANDWIDTH=1\nmedia.m3u8\n"
    XCTAssertTrue(HLSPlaylistParser.segments(manifest: m, baseURL: url("https://x.com/a.m3u8")).isEmpty)
  }

  // MARK: - Auxiliary resources (init map + encryption key)

  func testAuxiliaryURIsIncludesMapAndKey() {
    let m = """
    #EXTM3U
    #EXT-X-MAP:URI="init.mp4"
    #EXT-X-KEY:METHOD=AES-128,URI="keys/k.key",IV=0x00000000000000000000000000000000
    #EXTINF:1.0,
    seg0.m4s
    #EXT-X-ENDLIST
    """
    let aux = HLSPlaylistParser.auxiliaryURIs(manifest: m, baseURL: url("https://cdn.example.com/hls/index.m3u8"))
    XCTAssertTrue(aux.contains(url("https://cdn.example.com/hls/init.mp4")), "EXT-X-MAP init segment must be preloaded")
    XCTAssertTrue(aux.contains(url("https://cdn.example.com/hls/keys/k.key")), "EXT-X-KEY AES key must be preloaded")
  }

  func testAuxiliaryURIsExcludesKeyNoneAndFairPlay() {
    let m = """
    #EXTM3U
    #EXT-X-KEY:METHOD=NONE
    #EXT-X-KEY:METHOD=SAMPLE-AES,URI="skd://license/asset",KEYFORMAT="com.apple.streamingkeydelivery"
    #EXTINF:1.0,
    seg0.aac
    #EXT-X-ENDLIST
    """
    let aux = HLSPlaylistParser.auxiliaryURIs(manifest: m, baseURL: url("https://cdn.example.com/hls/index.m3u8"))
    XCTAssertTrue(aux.isEmpty, "METHOD=NONE has no URI and skd:// FairPlay keys must be excluded")
  }

  // MARK: - Variants: BANDWIDTH parsing (quote-aware)

  func testVariantBandwidthDefaultsToZeroWhenAbsent() {
    let m = "#EXTM3U\n#EXT-X-STREAM-INF:CODECS=\"mp4a.40.2\"\nmedia.m3u8\n"
    let v = HLSPlaylistParser.variants(manifest: m, baseURL: url("https://cdn.example.com/master.m3u8"))
    XCTAssertEqual(v.count, 1)
    guard v.count == 1 else { return }
    XCTAssertEqual(v[0].bandwidth, 0, "Absent BANDWIDTH defaults to 0; variant is not dropped")
    XCTAssertEqual(v[0].url, url("https://cdn.example.com/media.m3u8"))
  }

  func testVariantBandwidthAfterQuotedCodecsWithComma() {
    // A comma inside the quoted CODECS value must not corrupt the BANDWIDTH read.
    let m = "#EXTM3U\n#EXT-X-STREAM-INF:CODECS=\"avc1.4d401f,mp4a.40.2\",BANDWIDTH=800000\nmedia.m3u8\n"
    let v = HLSPlaylistParser.variants(manifest: m, baseURL: url("https://cdn.example.com/master.m3u8"))
    XCTAssertEqual(v.count, 1)
    guard v.count == 1 else { return }
    XCTAssertEqual(v[0].bandwidth, 800000)
  }

  func testVariantBandwidthWithEqualsInsideQuotedValueBeforeIt() {
    // A quoted value containing a "key=value" lookalike before BANDWIDTH must
    // not be mistaken for it; the real BANDWIDTH after it wins.
    let m = "#EXTM3U\n#EXT-X-STREAM-INF:CODECS=\"x,BANDWIDTH=999\",BANDWIDTH=512000\nmedia.m3u8\n"
    let v = HLSPlaylistParser.variants(manifest: m, baseURL: url("https://cdn.example.com/master.m3u8"))
    XCTAssertEqual(v.count, 1)
    guard v.count == 1 else { return }
    XCTAssertEqual(v[0].bandwidth, 512000)
  }

  func testVariantBandwidthAfterResolutionAndAudioAttributes() {
    let m = "#EXTM3U\n#EXT-X-STREAM-INF:RESOLUTION=1920x1080,AUDIO=\"aud\",BANDWIDTH=4000000\nv.m3u8\n"
    let v = HLSPlaylistParser.variants(manifest: m, baseURL: url("https://cdn.example.com/master.m3u8"))
    XCTAssertEqual(v.count, 1)
    guard v.count == 1 else { return }
    XCTAssertEqual(v[0].bandwidth, 4000000)
  }

  // MARK: - Variants: URI resolution + comment tolerance

  func testVariantParentRelativeURIResolved() {
    let m = "#EXTM3U\n#EXT-X-STREAM-INF:BANDWIDTH=1\n../up/v.m3u8\n"
    let v = HLSPlaylistParser.variants(manifest: m, baseURL: url("https://cdn.example.com/a/b/master.m3u8"))
    XCTAssertEqual(v.count, 1)
    guard v.count == 1 else { return }
    XCTAssertEqual(v[0].url, url("https://cdn.example.com/a/up/v.m3u8"))
  }

  func testVariantWithCommentBetweenStreamInfAndURI() {
    // A plain comment between STREAM-INF and the URI must not drop the variant
    // (matches the rewriter, which rewrites the bare URI regardless).
    let m = "#EXTM3U\n#EXT-X-STREAM-INF:BANDWIDTH=256000\n# a comment\nlow/media.m3u8\n"
    let v = HLSPlaylistParser.variants(manifest: m, baseURL: url("https://cdn.example.com/master.m3u8"))
    XCTAssertEqual(v.count, 1)
    guard v.count == 1 else { return }
    XCTAssertEqual(v[0].bandwidth, 256000)
    XCTAssertEqual(v[0].url, url("https://cdn.example.com/low/media.m3u8"))
  }

  func testConsecutiveStreamInfWithoutURISkipsFirstOnly() {
    // First STREAM-INF has no bare URI before the next tag: it is dropped, the
    // second's URI is NOT stolen, and the second variant is parsed.
    let m = "#EXTM3U\n#EXT-X-STREAM-INF:BANDWIDTH=1\n#EXT-X-STREAM-INF:BANDWIDTH=2\nsecond.m3u8\n"
    let v = HLSPlaylistParser.variants(manifest: m, baseURL: url("https://cdn.example.com/master.m3u8"))
    XCTAssertEqual(v.count, 1)
    guard v.count == 1 else { return }
    XCTAssertEqual(v[0].bandwidth, 2)
    XCTAssertEqual(v[0].url, url("https://cdn.example.com/second.m3u8"))
  }

  // MARK: - Segments: duration edge cases

  func testSegmentIntegerDurationAndTitleWithComma() {
    let m = "#EXTM3U\n#EXTINF:8,Ad, break\nseg.ts\n#EXT-X-ENDLIST"
    let s = HLSPlaylistParser.segments(manifest: m, baseURL: url("https://cdn.example.com/hls/i.m3u8"))
    XCTAssertEqual(s.count, 1)
    guard s.count == 1 else { return }
    XCTAssertEqual(s[0].duration, 8.0, accuracy: 0.001, "duration is the value before the first comma")
    XCTAssertEqual(s[0].url, url("https://cdn.example.com/hls/seg.ts"))
  }

  func testSegmentNonFiniteDurationNormalisedToZero() {
    // inf/nan parse as valid Doubles but would poison preload-window arithmetic.
    let m = "#EXTM3U\n#EXTINF:inf,\na.ts\n#EXTINF:nan,\nb.ts\n#EXT-X-ENDLIST"
    let s = HLSPlaylistParser.segments(manifest: m, baseURL: url("https://cdn.example.com/hls/i.m3u8"))
    XCTAssertEqual(s.count, 2)
    guard s.count == 2 else { return }
    XCTAssertEqual(s[0].duration, 0.0, accuracy: 0.001)
    XCTAssertEqual(s[1].duration, 0.0, accuracy: 0.001)
  }

  func testSegmentLocaleCommaDurationTakesValueBeforeFirstComma() {
    // C-locale parse: "1,5" is the integer 1 (the comma starts the title).
    let m = "#EXTM3U\n#EXTINF:1,5,title\nseg.ts\n#EXT-X-ENDLIST"
    let s = HLSPlaylistParser.segments(manifest: m, baseURL: url("https://cdn.example.com/hls/i.m3u8"))
    XCTAssertEqual(s.count, 1)
    guard s.count == 1 else { return }
    XCTAssertEqual(s[0].duration, 1.0, accuracy: 0.001)
  }

  // MARK: - Segments: URI location

  func testSegmentURIAfterInterleavedTags() {
    let m = """
    #EXTM3U
    #EXTINF:2.0,
    #EXT-X-BYTERANGE:1000@0
    #EXT-X-DISCONTINUITY
    #EXT-X-PROGRAM-DATE-TIME:2026-01-01T00:00:00Z
    seg.ts
    #EXT-X-ENDLIST
    """
    let s = HLSPlaylistParser.segments(manifest: m, baseURL: url("https://cdn.example.com/hls/i.m3u8"))
    XCTAssertEqual(s.count, 1)
    guard s.count == 1 else { return }
    XCTAssertEqual(s[0].url, url("https://cdn.example.com/hls/seg.ts"))
    XCTAssertEqual(s[0].duration, 2.0, accuracy: 0.001)
  }

  func testSegmentURIAfterBlankAndWhitespaceLines() {
    let m = "#EXTM3U\n#EXTINF:1.0,\n\n   \nseg.ts\n#EXT-X-ENDLIST"
    let s = HLSPlaylistParser.segments(manifest: m, baseURL: url("https://cdn.example.com/hls/i.m3u8"))
    XCTAssertEqual(s.count, 1)
    guard s.count == 1 else { return }
    XCTAssertEqual(s[0].url, url("https://cdn.example.com/hls/seg.ts"))
  }

  func testExtinfWithoutURIBeforeNextExtinfDoesNotStealURI() {
    // First EXTINF has no URI before the second EXTINF: it is dropped, and the
    // second EXTINF keeps its own URI (no desync).
    let m = "#EXTM3U\n#EXTINF:1.0,\n#EXTINF:2.0,\nseg1.ts\n#EXT-X-ENDLIST"
    let s = HLSPlaylistParser.segments(manifest: m, baseURL: url("https://cdn.example.com/hls/i.m3u8"))
    XCTAssertEqual(s.count, 1)
    guard s.count == 1 else { return }
    XCTAssertEqual(s[0].duration, 2.0, accuracy: 0.001)
    XCTAssertEqual(s[0].url, url("https://cdn.example.com/hls/seg1.ts"))
  }

  func testSegmentRootRelativeAndSchemeRelativeURIs() {
    let m = """
    #EXTM3U
    #EXTINF:1.0,
    /root/seg.ts
    #EXTINF:1.0,
    //other.com/seg.ts
    #EXT-X-ENDLIST
    """
    let s = HLSPlaylistParser.segments(manifest: m, baseURL: url("https://cdn.example.com/a/b/i.m3u8"))
    XCTAssertEqual(s.count, 2)
    guard s.count == 2 else { return }
    XCTAssertEqual(s[0].url, url("https://cdn.example.com/root/seg.ts"))
    XCTAssertEqual(s[1].url, url("https://other.com/seg.ts"))
  }

  // MARK: - Auxiliary resources: METHOD, dedup, preserved schemes

  func testAuxiliaryKeyWithCommaInQuotedURIQueryNotTreatedAsNone() {
    // A real AES key whose URI query contains ",METHOD=NONE" must still be
    // collected — the quote-aware scanner must not read METHOD from inside it.
    let m = """
    #EXTM3U
    #EXT-X-KEY:METHOD=AES-128,URI="https://k.example.com/x?a=1,METHOD=NONE"
    #EXTINF:1.0,
    seg.ts
    #EXT-X-ENDLIST
    """
    let aux = HLSPlaylistParser.auxiliaryURIs(manifest: m, baseURL: url("https://cdn.example.com/hls/i.m3u8"))
    XCTAssertEqual(aux, [url("https://k.example.com/x?a=1,METHOD=NONE")])
  }

  func testAuxiliaryKeyMethodNoneInIsolationExcluded() {
    let m = "#EXTM3U\n#EXT-X-KEY:METHOD=NONE\n#EXTINF:1.0,\nseg.ts\n#EXT-X-ENDLIST"
    let aux = HLSPlaylistParser.auxiliaryURIs(manifest: m, baseURL: url("https://cdn.example.com/hls/i.m3u8"))
    XCTAssertTrue(aux.isEmpty)
  }

  func testAuxiliaryKeyMethodNoneWithStrayURIExcluded() {
    // Malformed: METHOD=NONE per spec carries no URI, so any stray URI is ignored.
    let m = "#EXTM3U\n#EXT-X-KEY:METHOD=NONE,URI=\"x.key\"\n#EXTINF:1.0,\nseg.ts\n#EXT-X-ENDLIST"
    let aux = HLSPlaylistParser.auxiliaryURIs(manifest: m, baseURL: url("https://cdn.example.com/hls/i.m3u8"))
    XCTAssertTrue(aux.isEmpty)
  }

  func testAuxiliaryDataURIKeyAndMapExcluded() {
    let m = """
    #EXTM3U
    #EXT-X-MAP:URI="data:application/octet-stream;base64,AAAA"
    #EXT-X-KEY:METHOD=AES-128,URI="data:text/plain;base64,BBBB"
    #EXTINF:1.0,
    seg.ts
    #EXT-X-ENDLIST
    """
    let aux = HLSPlaylistParser.auxiliaryURIs(manifest: m, baseURL: url("https://cdn.example.com/hls/i.m3u8"))
    XCTAssertTrue(aux.isEmpty, "data: map and key URIs must be excluded")
  }

  func testAuxiliaryDedupPreservesFirstSeenOrder() {
    let m = """
    #EXTM3U
    #EXT-X-MAP:URI="init.mp4"
    #EXT-X-KEY:METHOD=AES-128,URI="k1.key"
    #EXTINF:1.0,
    s0.m4s
    #EXT-X-KEY:METHOD=AES-128,URI="k1.key"
    #EXTINF:1.0,
    s1.m4s
    #EXT-X-KEY:METHOD=AES-128,URI="k2.key"
    #EXTINF:1.0,
    s2.m4s
    #EXT-X-ENDLIST
    """
    let aux = HLSPlaylistParser.auxiliaryURIs(manifest: m, baseURL: url("https://cdn.example.com/hls/i.m3u8"))
    XCTAssertEqual(aux, [
      url("https://cdn.example.com/hls/init.mp4"),
      url("https://cdn.example.com/hls/k1.key"),
      url("https://cdn.example.com/hls/k2.key"),
    ], "repeated key deduped; distinct resources kept in first-seen order")
  }

  func testAuxiliaryMapWithByteRangeAttributeStillCollected() {
    let m = "#EXTM3U\n#EXT-X-MAP:URI=\"init.mp4\",BYTERANGE=\"1200@0\"\n#EXTINF:1.0,\nseg.ts\n#EXT-X-ENDLIST"
    let aux = HLSPlaylistParser.auxiliaryURIs(manifest: m, baseURL: url("https://cdn.example.com/hls/i.m3u8"))
    XCTAssertEqual(aux, [url("https://cdn.example.com/hls/init.mp4")])
  }

  // MARK: - CRLF handling

  func testCRLFManifestParsedForVariantsSegmentsAndAux() {
    let master = "#EXTM3U\r\n#EXT-X-STREAM-INF:BANDWIDTH=128000\r\nmedia.m3u8\r\n"
    let v = HLSPlaylistParser.variants(manifest: master, baseURL: url("https://cdn.example.com/master.m3u8"))
    XCTAssertEqual(v.count, 1)
    XCTAssertEqual(v.first?.url, url("https://cdn.example.com/media.m3u8"))

    let media = "#EXTM3U\r\n#EXT-X-MAP:URI=\"init.mp4\"\r\n#EXTINF:1.5,\r\nseg0.ts\r\n#EXT-X-ENDLIST\r\n"
    let s = HLSPlaylistParser.segments(manifest: media, baseURL: url("https://cdn.example.com/hls/i.m3u8"))
    XCTAssertEqual(s.count, 1)
    XCTAssertEqual(s.first?.url, url("https://cdn.example.com/hls/seg0.ts"))
    XCTAssertEqual(s.first?.duration ?? -1, 1.5, accuracy: 0.001)

    let aux = HLSPlaylistParser.auxiliaryURIs(manifest: media, baseURL: url("https://cdn.example.com/hls/i.m3u8"))
    XCTAssertEqual(aux, [url("https://cdn.example.com/hls/init.mp4")])
  }

  // MARK: - Empty / no-crash contract

  func testEmptyManifestReturnsEmptyForAllAccessors() {
    let base = url("https://cdn.example.com/i.m3u8")
    XCTAssertFalse(HLSPlaylistParser.isMaster(""))
    XCTAssertTrue(HLSPlaylistParser.variants(manifest: "", baseURL: base).isEmpty)
    XCTAssertTrue(HLSPlaylistParser.segments(manifest: "", baseURL: base).isEmpty)
    XCTAssertTrue(HLSPlaylistParser.auxiliaryURIs(manifest: "", baseURL: base).isEmpty)
  }
}
