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

import XCTest

@testable import PlayerCore

final class AudioCacheTests: XCTestCase {
  private var cacheDir: URL!
  private var cache: AudioCache!

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

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

  // MARK: - Cache Key

  func testCacheKeyIsDeterministic() {
    let url = URL(string: "https://example.com/song.mp3")!
    let key1 = cache.cacheKey(for: url)
    let key2 = cache.cacheKey(for: url)
    XCTAssertEqual(key1, key2)
  }

  func testCacheKeyDiffersForDifferentURLs() {
    let url1 = URL(string: "https://example.com/a.mp3")!
    let url2 = URL(string: "https://example.com/b.mp3")!
    XCTAssertNotEqual(cache.cacheKey(for: url1), cache.cacheKey(for: url2))
  }

  // MARK: - Content Info

  func testStoreAndRetrieveContentInfo() {
    let url = URL(string: "https://example.com/song.mp3")!
    let key = cache.cacheKey(for: url)
    let info = AudioCache.ContentInfo(contentType: "audio/mpeg", contentLength: 5000, isByteRangeAccessSupported: true)

    cache.storeContentInfo(info, for: key, url: url)

    let retrieved = cache.contentInfo(for: key)
    XCTAssertNotNil(retrieved)
    XCTAssertEqual(retrieved?.contentType, "audio/mpeg")
    XCTAssertEqual(retrieved?.contentLength, 5000)
    XCTAssertTrue(retrieved?.isByteRangeAccessSupported ?? false)
  }

  func testContentInfoReturnsNilForUnknownKey() {
    XCTAssertNil(cache.contentInfo(for: "nonexistent"))
  }

  // MARK: - Data Storage

  func testAppendAndReadData() {
    let key = "testkey"
    let data = Data(repeating: 0xAB, count: 100)

    cache.appendData(data, for: key)

    XCTAssertEqual(cache.cachedBytes(for: key), 100)

    let read = cache.readData(for: key, offset: 0, length: 100)
    XCTAssertEqual(read, data)
  }

  func testAppendDataSequentially() {
    let key = "testkey"
    let chunk1 = Data(repeating: 0x01, count: 50)
    let chunk2 = Data(repeating: 0x02, count: 50)

    cache.appendData(chunk1, for: key)
    cache.appendData(chunk2, for: key)

    XCTAssertEqual(cache.cachedBytes(for: key), 100)

    let firstHalf = cache.readData(for: key, offset: 0, length: 50)
    XCTAssertEqual(firstHalf, chunk1)

    let secondHalf = cache.readData(for: key, offset: 50, length: 50)
    XCTAssertEqual(secondHalf, chunk2)
  }

  func testReadDataPartialRange() {
    let key = "testkey"
    let data = Data([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

    cache.appendData(data, for: key)

    let middle = cache.readData(for: key, offset: 3, length: 4)
    XCTAssertEqual(middle, Data([3, 4, 5, 6]))
  }

  func testReadDataReturnsNilForUnknownKey() {
    XCTAssertNil(cache.readData(for: "nonexistent", offset: 0, length: 10))
  }

  func testCachedBytesForUnknownKeyIsZero() {
    XCTAssertEqual(cache.cachedBytes(for: "nonexistent"), 0)
  }

  // MARK: - isFullyCached

  func testIsFullyCachedReturnsFalseWithoutContentInfo() {
    let key = "testkey"
    cache.appendData(Data(repeating: 0, count: 100), for: key)

    XCTAssertFalse(cache.isFullyCached(key: key))
  }

  func testIsFullyCachedReturnsFalseWhenPartial() {
    let url = URL(string: "https://example.com/song.mp3")!
    let key = cache.cacheKey(for: url)
    let info = AudioCache.ContentInfo(contentType: "audio/mpeg", contentLength: 1000, isByteRangeAccessSupported: true)
    cache.storeContentInfo(info, for: key, url: url)
    cache.appendData(Data(repeating: 0, count: 500), for: key)

    XCTAssertFalse(cache.isFullyCached(key: key))
  }

  func testIsFullyCachedReturnsTrueWhenComplete() {
    let url = URL(string: "https://example.com/song.mp3")!
    let key = cache.cacheKey(for: url)
    let info = AudioCache.ContentInfo(contentType: "audio/mpeg", contentLength: 100, isByteRangeAccessSupported: true)
    cache.storeContentInfo(info, for: key, url: url)
    cache.appendData(Data(repeating: 0, count: 100), for: key)

    XCTAssertTrue(cache.isFullyCached(key: key))
  }

  // MARK: - Eviction

  func testEvictionRemovesLeastRecentlyUsed() {
    let smallCache = AudioCache(maxSizeBytes: 200, directory: cacheDir)

    let url1 = URL(string: "https://example.com/old.mp3")!
    let url2 = URL(string: "https://example.com/new.mp3")!
    let key1 = smallCache.cacheKey(for: url1)
    let key2 = smallCache.cacheKey(for: url2)

    let info = AudioCache.ContentInfo(contentType: "audio/mpeg", contentLength: 150, isByteRangeAccessSupported: true)
    smallCache.storeContentInfo(info, for: key1, url: url1)
    smallCache.appendData(Data(repeating: 0x01, count: 150), for: key1)

    // Ensure key1 has an older access date
    Thread.sleep(forTimeInterval: 0.05)

    smallCache.storeContentInfo(info, for: key2, url: url2)
    smallCache.appendData(Data(repeating: 0x02, count: 150), for: key2)

    // Total is 300, max is 200 → eviction should remove key1
    smallCache.evictIfNeeded()

    XCTAssertEqual(smallCache.cachedBytes(for: key1), 0, "old item should be evicted")
    XCTAssertEqual(smallCache.cachedBytes(for: key2), 150, "new item should survive")
  }

  // MARK: - Remove All

  func testRemoveAll() {
    let key = "testkey"
    cache.appendData(Data(repeating: 0, count: 50), for: key)
    XCTAssertEqual(cache.cachedBytes(for: key), 50)

    cache.removeAll()

    XCTAssertEqual(cache.cachedBytes(for: key), 0)
  }
}
