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

import XCTest
@testable import PlayerCore

/// Tests that preloading an HLS playlist warms the cache with the actual media
/// SEGMENTS (and init/key), not just the manifest text — matching Android's
/// segment-level preload rather than the shallow manifest-only behavior.
final class PreloaderTests: XCTestCase {
  private var cacheDir: URL!
  private var cache: AudioCache!
  private var coordinator: DownloadCoordinator!
  private var preloader: Preloader!

  override func setUp() {
    super.setUp()
    cacheDir = FileManager.default.temporaryDirectory
      .appendingPathComponent("PreloaderTests-\(UUID().uuidString)", isDirectory: true)
    cache = AudioCache(maxSizeBytes: 10 * 1024 * 1024, directory: cacheDir)
    coordinator = DownloadCoordinator(cache: cache)
    preloader = Preloader(cache: cache, coordinator: coordinator)
  }

  override func tearDown() {
    preloader.cancelAll()
    coordinator.cancelAll()
    try? FileManager.default.removeItem(at: cacheDir)
    super.tearDown()
  }

  // MARK: - Helpers

  private func waitUntil(timeout: TimeInterval = 5.0, _ condition: () -> Bool) -> Bool {
    let deadline = Date().addingTimeInterval(timeout)
    while Date() < deadline {
      if condition() { return true }
      Thread.sleep(forTimeInterval: 0.05)
    }
    return condition()
  }

  private func vodMediaPlaylist() -> String {
    return """
    #EXTM3U
    #EXT-X-VERSION:3
    #EXT-X-TARGETDURATION:1
    #EXT-X-MEDIA-SEQUENCE:0
    #EXTINF:1.0,
    seg0.aac
    #EXTINF:1.0,
    seg1.aac
    #EXT-X-ENDLIST
    """
  }

  private func makeHLSServer(seg0: Data, seg1: Data) throws -> LocalAudioServer {
    return try LocalAudioServer(routes: [
      "/index.m3u8": .init(data: Data(vodMediaPlaylist().utf8), contentType: "application/vnd.apple.mpegurl"),
      "/seg0.aac": .init(data: seg0, contentType: "audio/aac"),
      "/seg1.aac": .init(data: seg1, contentType: "audio/aac"),
    ])
  }

  // MARK: - HLS preload caches the segments, not just the manifest

  func testPreloadHLSCachesManifestAndSegments() throws {
    let seg0 = Data(repeating: 0xA0, count: 2048)
    let seg1 = Data(repeating: 0xB1, count: 2048)
    let server = try makeHLSServer(seg0: seg0, seg1: seg1)
    server.start()
    defer { server.stop() }

    let manifestURL = server.url(forPath: "/index.m3u8")
    preloader.preload(url: manifestURL, headers: nil)

    let manifestKey = cache.cacheKey(for: manifestURL)
    let seg0Key = cache.cacheKey(for: server.url(forPath: "/seg0.aac"))
    let seg1Key = cache.cacheKey(for: server.url(forPath: "/seg1.aac"))

    XCTAssertTrue(
      waitUntil { self.cache.isFullyCached(key: seg0Key) && self.cache.isFullyCached(key: seg1Key) },
      "preloading an HLS playlist must cache its segments, not just the manifest"
    )
    XCTAssertTrue(cache.isFullyCached(key: manifestKey), "the manifest itself must be cached for offline serve")
  }

  // MARK: - duration window only warms the covering segment prefix

  func testPreloadHLSDurationWindowCachesOnlyFirstSegment() throws {
    // vodMediaPlaylist has two 1.0s segments; a 0.5s window is covered by the
    // first segment alone, so seg1 must never be fetched.
    let seg0 = Data(repeating: 0xA0, count: 2048)
    let seg1 = Data(repeating: 0xB1, count: 2048)
    let server = try makeHLSServer(seg0: seg0, seg1: seg1)
    server.start()
    defer { server.stop() }

    let manifestURL = server.url(forPath: "/index.m3u8")
    preloader.preload(url: manifestURL, headers: nil, duration: 0.5)

    let seg0Key = cache.cacheKey(for: server.url(forPath: "/seg0.aac"))
    let seg1Key = cache.cacheKey(for: server.url(forPath: "/seg1.aac"))

    XCTAssertTrue(
      waitUntil { self.cache.isFullyCached(key: seg0Key) },
      "the first segment must be cached to cover the requested duration window"
    )
    // seg0 is fully cached, so seg1 (if it were ever going to download) has had
    // ample time; assert the window excluded it.
    XCTAssertFalse(
      cache.isFullyCached(key: seg1Key),
      "a 0.5s window over 1.0s segments must not warm the second segment"
    )
  }

  // MARK: - master playlist preloads the lowest-bandwidth variant's segments

  func testPreloadHLSMasterPreloadsLowestBandwidthVariant() throws {
    let lowSeg = Data(repeating: 0xC2, count: 2048)
    let highSeg = Data(repeating: 0xD3, count: 2048)

    let master = """
    #EXTM3U
    #EXT-X-STREAM-INF:BANDWIDTH=800000
    high.m3u8
    #EXT-X-STREAM-INF:BANDWIDTH=200000
    low.m3u8
    """

    let lowMedia = """
    #EXTM3U
    #EXT-X-VERSION:3
    #EXT-X-TARGETDURATION:1
    #EXT-X-MEDIA-SEQUENCE:0
    #EXTINF:1.0,
    low_seg0.aac
    #EXT-X-ENDLIST
    """

    let highMedia = """
    #EXTM3U
    #EXT-X-VERSION:3
    #EXT-X-TARGETDURATION:1
    #EXT-X-MEDIA-SEQUENCE:0
    #EXTINF:1.0,
    high_seg0.aac
    #EXT-X-ENDLIST
    """

    let server = try LocalAudioServer(routes: [
      "/master.m3u8": .init(data: Data(master.utf8), contentType: "application/vnd.apple.mpegurl"),
      "/low.m3u8": .init(data: Data(lowMedia.utf8), contentType: "application/vnd.apple.mpegurl"),
      "/high.m3u8": .init(data: Data(highMedia.utf8), contentType: "application/vnd.apple.mpegurl"),
      "/low_seg0.aac": .init(data: lowSeg, contentType: "audio/aac"),
      "/high_seg0.aac": .init(data: highSeg, contentType: "audio/aac"),
    ])
    server.start()
    defer { server.stop() }

    let masterURL = server.url(forPath: "/master.m3u8")
    preloader.preload(url: masterURL, headers: nil)

    let lowSegKey = cache.cacheKey(for: server.url(forPath: "/low_seg0.aac"))
    let highSegKey = cache.cacheKey(for: server.url(forPath: "/high_seg0.aac"))

    XCTAssertTrue(
      waitUntil { self.cache.isFullyCached(key: lowSegKey) },
      "a master playlist must warm the lowest-bandwidth variant's segments"
    )
    XCTAssertFalse(
      cache.isFullyCached(key: highSegKey),
      "the higher-bandwidth variant's segments must not be warmed"
    )
  }

  // MARK: - cancel() tears the whole job down before segments are warmed

  func testCancelStopsPreloadJob() throws {
    let seg0 = Data(repeating: 0xA0, count: 2048)
    let seg1 = Data(repeating: 0xB1, count: 2048)
    // A slow manifest gives us a window to cancel before any segment is fetched.
    let server = try LocalAudioServer(
      routes: [
        "/index.m3u8": .init(data: Data(vodMediaPlaylist().utf8), contentType: "application/vnd.apple.mpegurl"),
        "/seg0.aac": .init(data: seg0, contentType: "audio/aac"),
        "/seg1.aac": .init(data: seg1, contentType: "audio/aac"),
      ],
      simulateSlowNetwork: true
    )
    server.start()
    defer { server.stop() }

    let manifestURL = server.url(forPath: "/index.m3u8")
    preloader.preload(url: manifestURL, headers: nil)
    preloader.cancel(url: manifestURL)

    let seg0Key = cache.cacheKey(for: server.url(forPath: "/seg0.aac"))
    let seg1Key = cache.cacheKey(for: server.url(forPath: "/seg1.aac"))

    // The whole job is torn down: segments never get cached and the coordinator
    // settles back to zero active downloads.
    XCTAssertTrue(
      waitUntil { self.coordinator.activeDownloadCount == 0 },
      "cancelling a preload must drain all of its in-flight downloads"
    )
    XCTAssertFalse(cache.isFullyCached(key: seg0Key), "cancelled preload must not warm segment 0")
    XCTAssertFalse(cache.isFullyCached(key: seg1Key), "cancelled preload must not warm segment 1")
  }
}
