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

import XCTest

@testable import PlayerCore

// MARK: - Test Helpers

private struct MockItem: AudioItem {
  var sourceUrl: String
  var sourceType: SourceType = .stream
  var headers: [String: String]?
  var isLive: Bool = false
  var title: String?
  var artist: String?
  var albumTitle: String?

  init(_ id: String) {
    self.sourceUrl = id
    self.title = id
  }

  #if canImport(UIKit)
  func getArtwork(_ handler: @escaping (UIImage?) -> Void) {
    handler(nil)
  }
  #endif
}

private func items(_ ids: String...) -> [AudioItem] {
  ids.map { MockItem($0) }
}

// MARK: - Tests

final class QueueManagerTests: XCTestCase {

  // MARK: - Initial State

  func testEmptyQueue() {
    let q = QueueManager()
    XCTAssertEqual(q.items.count, 0)
    XCTAssertEqual(q.currentIndex, -1)
    XCTAssertNil(q.current)
  }

  // MARK: - Add

  func testAddToEmptyQueue() {
    let q = QueueManager()
    q.add(items("a", "b", "c"))

    XCTAssertEqual(q.items.count, 3)
    XCTAssertEqual(q.currentIndex, -1, "add does not set currentIndex")
  }

  func testAddAppendsToEnd() {
    let q = QueueManager()
    q.add(items("a"))
    try! q.jump(to: 0)
    q.add(items("b", "c"))

    XCTAssertEqual(q.items.count, 3)
    XCTAssertEqual(q.items[2].sourceUrl, "c")
    XCTAssertEqual(q.currentIndex, 0)
  }

  // MARK: - Insert At Index

  func testInsertBeforeCurrent() {
    let q = QueueManager()
    q.add(items("a", "b"))
    try! q.jump(to: 1)
    XCTAssertEqual(q.current?.sourceUrl, "b")

    try! q.add(items("x"), at: 0)

    XCTAssertEqual(q.items.count, 3)
    XCTAssertEqual(q.currentIndex, 2, "currentIndex shifts forward")
    XCTAssertEqual(q.current?.sourceUrl, "b")
  }

  func testInsertAfterCurrent() {
    let q = QueueManager()
    q.add(items("a", "b"))
    try! q.jump(to: 0)

    try! q.add(items("x"), at: 1)

    XCTAssertEqual(q.items.count, 3)
    XCTAssertEqual(q.currentIndex, 0, "currentIndex unchanged")
    XCTAssertEqual(q.items[1].sourceUrl, "x")
  }

  func testInsertAtCurrent() {
    let q = QueueManager()
    q.add(items("a", "b"))
    try! q.jump(to: 1)

    try! q.add(items("x"), at: 1)

    XCTAssertEqual(q.currentIndex, 2, "currentIndex shifts forward")
    XCTAssertEqual(q.current?.sourceUrl, "b")
    XCTAssertEqual(q.items[1].sourceUrl, "x")
  }

  func testInsertAtEnd() {
    let q = QueueManager()
    q.add(items("a"))
    try! q.jump(to: 0)

    try! q.add(items("b"), at: 1)

    XCTAssertEqual(q.items.count, 2)
    XCTAssertEqual(q.items[1].sourceUrl, "b")
    XCTAssertEqual(q.currentIndex, 0)
  }

  func testInsertMultipleBeforeCurrent() {
    let q = QueueManager()
    q.add(items("a", "b"))
    try! q.jump(to: 1)

    try! q.add(items("x", "y"), at: 0)

    XCTAssertEqual(q.currentIndex, 3, "shifts by inserted count")
    XCTAssertEqual(q.current?.sourceUrl, "b")
  }

  func testInsertAtInvalidIndexThrows() {
    let q = QueueManager()
    q.add(items("a"))

    XCTAssertThrowsError(try q.add(items("x"), at: 5))
    XCTAssertThrowsError(try q.add(items("x"), at: -1))
  }

  // MARK: - Remove

  func testRemoveBeforeCurrent() {
    let q = QueueManager()
    q.add(items("a", "b", "c"))
    try! q.jump(to: 2)

    try! q.removeItem(at: 0)

    XCTAssertEqual(q.items.count, 2)
    XCTAssertEqual(q.currentIndex, 1, "currentIndex shifts back")
    XCTAssertEqual(q.current?.sourceUrl, "c")
  }

  func testRemoveAfterCurrent() {
    let q = QueueManager()
    q.add(items("a", "b", "c"))
    try! q.jump(to: 0)

    try! q.removeItem(at: 2)

    XCTAssertEqual(q.items.count, 2)
    XCTAssertEqual(q.currentIndex, 0, "currentIndex unchanged")
  }

  func testRemoveCurrentItemMidQueue() {
    let q = QueueManager()
    q.add(items("a", "b", "c"))
    try! q.jump(to: 1)

    try! q.removeItem(at: 1)

    XCTAssertEqual(q.items.count, 2)
    XCTAssertEqual(q.currentIndex, 1, "stays at same index, now pointing to next item")
    XCTAssertEqual(q.current?.sourceUrl, "c")
  }

  func testRemoveCurrentItemAtEnd() {
    let q = QueueManager()
    q.add(items("a", "b", "c"))
    try! q.jump(to: 2)

    try! q.removeItem(at: 2)

    XCTAssertEqual(q.items.count, 2)
    XCTAssertEqual(q.currentIndex, 1, "clamps to last item")
    XCTAssertEqual(q.current?.sourceUrl, "b")
  }

  func testRemoveLastItem() {
    let q = QueueManager()
    q.add(items("a"))
    try! q.jump(to: 0)

    try! q.removeItem(at: 0)

    XCTAssertEqual(q.items.count, 0)
    XCTAssertEqual(q.currentIndex, -1)
    XCTAssertNil(q.current)
  }

  func testRemoveAtInvalidIndexThrows() {
    let q = QueueManager()
    q.add(items("a"))

    XCTAssertThrowsError(try q.removeItem(at: 1))
    XCTAssertThrowsError(try q.removeItem(at: -1))
  }

  // MARK: - Navigate: Next

  func testNextMovesForward() {
    let q = QueueManager()
    q.add(items("a", "b", "c"))
    try! q.jump(to: 0)

    XCTAssertTrue(q.next(wrap: false))
    XCTAssertEqual(q.currentIndex, 1)
    XCTAssertEqual(q.current?.sourceUrl, "b")
  }

  func testNextAtEndNoWrap() {
    let q = QueueManager()
    q.add(items("a", "b"))
    try! q.jump(to: 1)

    XCTAssertFalse(q.next(wrap: false))
    XCTAssertEqual(q.currentIndex, 1, "stays at end")
  }

  func testNextAtEndWithWrap() {
    let q = QueueManager()
    q.add(items("a", "b"))
    try! q.jump(to: 1)

    XCTAssertTrue(q.next(wrap: true))
    XCTAssertEqual(q.currentIndex, 0)
    XCTAssertEqual(q.current?.sourceUrl, "a")
  }

  func testNextOnEmptyQueue() {
    let q = QueueManager()
    XCTAssertFalse(q.next(wrap: false))
    XCTAssertFalse(q.next(wrap: true))
  }

  // MARK: - Navigate: Previous

  func testPreviousMovesBack() {
    let q = QueueManager()
    q.add(items("a", "b", "c"))
    try! q.jump(to: 2)

    XCTAssertTrue(q.previous(wrap: false))
    XCTAssertEqual(q.currentIndex, 1)
  }

  func testPreviousAtStartNoWrap() {
    let q = QueueManager()
    q.add(items("a", "b"))
    try! q.jump(to: 0)

    XCTAssertFalse(q.previous(wrap: false))
    XCTAssertEqual(q.currentIndex, 0, "stays at start")
  }

  func testPreviousAtStartWithWrap() {
    let q = QueueManager()
    q.add(items("a", "b", "c"))
    try! q.jump(to: 0)

    XCTAssertTrue(q.previous(wrap: true))
    XCTAssertEqual(q.currentIndex, 2)
    XCTAssertEqual(q.current?.sourceUrl, "c")
  }

  // MARK: - Jump

  func testJumpToValidIndex() {
    let q = QueueManager()
    q.add(items("a", "b", "c"))

    try! q.jump(to: 2)
    XCTAssertEqual(q.currentIndex, 2)
    XCTAssertEqual(q.current?.sourceUrl, "c")
  }

  func testJumpToInvalidIndexThrows() {
    let q = QueueManager()
    q.add(items("a"))

    XCTAssertThrowsError(try q.jump(to: 1))
    XCTAssertThrowsError(try q.jump(to: -1))
  }

  func testJumpOnEmptyQueueThrows() {
    let q = QueueManager()
    XCTAssertThrowsError(try q.jump(to: 0))
  }

  // MARK: - Replace Current Item

  func testReplaceCurrentItemInNonEmptyQueue() {
    let q = QueueManager()
    q.add(items("a", "b", "c"))
    try! q.jump(to: 1)

    q.replaceCurrentItem(with: MockItem("x"))

    XCTAssertEqual(q.items.count, 3)
    XCTAssertEqual(q.currentIndex, 1)
    XCTAssertEqual(q.current?.sourceUrl, "x")
    XCTAssertEqual(q.items[0].sourceUrl, "a")
    XCTAssertEqual(q.items[2].sourceUrl, "c")
  }

  func testReplaceCurrentItemInEmptyQueue() {
    let q = QueueManager()

    q.replaceCurrentItem(with: MockItem("x"))

    XCTAssertEqual(q.items.count, 1)
    XCTAssertEqual(q.currentIndex, 0)
    XCTAssertEqual(q.current?.sourceUrl, "x")
  }

  // MARK: - Clear

  func testClear() {
    let q = QueueManager()
    q.add(items("a", "b", "c"))
    try! q.jump(to: 1)

    q.clear()

    XCTAssertEqual(q.items.count, 0)
    XCTAssertEqual(q.currentIndex, -1)
    XCTAssertNil(q.current)
  }

  func testClearEmptyQueue() {
    let q = QueueManager()
    q.clear()

    XCTAssertEqual(q.items.count, 0)
    XCTAssertEqual(q.currentIndex, -1)
  }

  // MARK: - Single Item Queue

  func testSingleItemNextNoWrap() {
    let q = QueueManager()
    q.add(items("a"))
    try! q.jump(to: 0)

    XCTAssertFalse(q.next(wrap: false))
    XCTAssertEqual(q.currentIndex, 0)
  }

  func testSingleItemNextWithWrap() {
    let q = QueueManager()
    q.add(items("a"))
    try! q.jump(to: 0)

    XCTAssertTrue(q.next(wrap: true))
    XCTAssertEqual(q.currentIndex, 0, "wraps to same item")
  }

  func testSingleItemPreviousWithWrap() {
    let q = QueueManager()
    q.add(items("a"))
    try! q.jump(to: 0)

    XCTAssertTrue(q.previous(wrap: true))
    XCTAssertEqual(q.currentIndex, 0, "wraps to same item")
  }

  // MARK: - Compound Operations

  func testAddThenRemoveThenNavigate() {
    let q = QueueManager()
    q.add(items("a", "b", "c", "d"))
    try! q.jump(to: 1)

    try! q.removeItem(at: 0)
    // items: [b, c, d], currentIndex: 0

    XCTAssertEqual(q.current?.sourceUrl, "b")

    XCTAssertTrue(q.next(wrap: false))
    XCTAssertEqual(q.current?.sourceUrl, "c")

    XCTAssertTrue(q.next(wrap: false))
    XCTAssertEqual(q.current?.sourceUrl, "d")

    XCTAssertFalse(q.next(wrap: false))
  }

  func testInsertThenRemovePreservesOrder() {
    let q = QueueManager()
    q.add(items("a", "c"))
    try! q.jump(to: 0)

    try! q.add(items("b"), at: 1)
    // items: [a, b, c], currentIndex: 0

    try! q.removeItem(at: 0)
    // items: [b, c], currentIndex: 0

    XCTAssertEqual(q.current?.sourceUrl, "b")
    XCTAssertEqual(q.items.map { $0.sourceUrl }, ["b", "c"])
  }

  func testRemoveAllItemsOneByOne() {
    let q = QueueManager()
    q.add(items("a", "b", "c"))
    try! q.jump(to: 0)

    try! q.removeItem(at: 2)
    try! q.removeItem(at: 1)
    try! q.removeItem(at: 0)

    XCTAssertEqual(q.items.count, 0)
    XCTAssertEqual(q.currentIndex, -1)
    XCTAssertNil(q.current)
  }

  // MARK: - Shuffle: Enable/Disable

  func testShuffleEnable() {
    let q = QueueManager()
    q.add(items("a", "b", "c"))
    try! q.jump(to: 0)

    q.setShuffle(enabled: true)

    XCTAssertTrue(q.isShuffled)
  }

  func testShuffleDisable() {
    let q = QueueManager()
    q.add(items("a", "b", "c"))
    try! q.jump(to: 0)
    q.setShuffle(enabled: true)

    q.setShuffle(enabled: false)

    XCTAssertFalse(q.isShuffled)
  }

  func testShufflePreservesCurrentItem() {
    let q = QueueManager()
    q.add(items("a", "b", "c"))
    try! q.jump(to: 1)
    XCTAssertEqual(q.current?.sourceUrl, "b")

    q.setShuffle(enabled: true)

    XCTAssertEqual(q.current?.sourceUrl, "b", "current stays the same")
    XCTAssertEqual(q.currentIndex, 1)
  }

  // MARK: - Shuffle: Navigation

  func testShuffleNextVisitsAllItems() {
    let q = QueueManager()
    q.add(items("a", "b", "c", "d"))
    try! q.jump(to: 0)
    q.setShuffle(enabled: true)

    var visited = Set<String>()
    visited.insert(q.current!.sourceUrl)

    for _ in 0..<3 {
      XCTAssertTrue(q.next(wrap: false))
      visited.insert(q.current!.sourceUrl)
    }

    XCTAssertEqual(visited.count, 4, "all items visited")
  }

  func testShuffleNextAtEndNoWrap() {
    let q = QueueManager()
    q.add(items("a", "b"))
    try! q.jump(to: 0)
    q.setShuffle(enabled: true)

    XCTAssertTrue(q.next(wrap: false))
    XCTAssertFalse(q.next(wrap: false), "no more items")
  }

  func testShuffleNextAtEndWithWrap() {
    let q = QueueManager()
    q.add(items("a", "b"))
    try! q.jump(to: 0)
    q.setShuffle(enabled: true)

    q.next(wrap: false)
    XCTAssertTrue(q.next(wrap: true), "wraps around")
  }

  func testShufflePrevious() {
    let q = QueueManager()
    q.add(items("a", "b", "c"))
    try! q.jump(to: 0)
    q.setShuffle(enabled: true)

    q.next(wrap: false)
    let afterNext = q.current!.sourceUrl

    q.previous(wrap: false)
    let afterPrev = q.current!.sourceUrl

    q.next(wrap: false)
    XCTAssertEqual(q.current?.sourceUrl, afterNext, "next after previous returns to same item")
    _ = afterPrev
  }

  func testShufflePreviousAtStartNoWrap() {
    let q = QueueManager()
    q.add(items("a", "b"))
    try! q.jump(to: 0)
    q.setShuffle(enabled: true)

    XCTAssertFalse(q.previous(wrap: false))
  }

  // MARK: - Shuffle: Disable Reverts to Sequential

  func testDisableShuffleRevertsToSequentialNav() {
    let q = QueueManager()
    q.add(items("a", "b", "c", "d", "e"))
    try! q.jump(to: 0)
    q.setShuffle(enabled: true)
    q.next(wrap: false)

    q.setShuffle(enabled: false)

    // After disabling shuffle, jump to a known position and verify sequential navigation
    try! q.jump(to: 1)
    q.next(wrap: false)
    XCTAssertEqual(q.currentIndex, 2, "sequential after disabling shuffle")
    q.next(wrap: false)
    XCTAssertEqual(q.currentIndex, 3, "continues sequentially")
  }

  // MARK: - Shuffle: Jump

  func testJumpWhileShuffled() {
    let q = QueueManager()
    q.add(items("a", "b", "c", "d"))
    try! q.jump(to: 0)
    q.setShuffle(enabled: true)

    try! q.jump(to: 2)

    XCTAssertEqual(q.currentIndex, 2)
    XCTAssertEqual(q.current?.sourceUrl, "c")
  }

  // MARK: - Shuffle: Clear Resets

  func testClearResetsShuffle() {
    let q = QueueManager()
    q.add(items("a", "b"))
    try! q.jump(to: 0)
    q.setShuffle(enabled: true)

    q.clear()

    XCTAssertFalse(q.isShuffled)
  }

  // MARK: - Shuffle: Add Items

  // MARK: - Replace Item

  func testReplaceItemAtValidIndex() {
    let q = QueueManager()
    q.add(items("a", "b", "c"))
    try! q.jump(to: 1)

    let replacement = MockItem("x")
    try! q.replaceItem(at: 1, with: replacement)

    XCTAssertEqual(q.items[1].title, "x")
    XCTAssertEqual(q.currentIndex, 1, "currentIndex unchanged")
    XCTAssertEqual(q.items.count, 3)
  }

  func testReplaceItemAtInvalidIndexThrows() {
    let q = QueueManager()
    q.add(items("a"))

    let replacement = MockItem("x")
    XCTAssertThrowsError(try q.replaceItem(at: 5, with: replacement))
  }

  // MARK: - Shuffle + Add

  func testAddItemsWhileShuffled() {
    let q = QueueManager()
    q.add(items("a", "b"))
    try! q.jump(to: 0)
    q.setShuffle(enabled: true)

    q.add(items("c", "d"))

    var visited = Set<String>()
    visited.insert(q.current!.sourceUrl)
    for _ in 0..<3 {
      q.next(wrap: false)
      visited.insert(q.current!.sourceUrl)
    }

    XCTAssertEqual(visited.count, 4, "new items are reachable")
  }
}
