import ComposableArchitecture
import XCTest
import XXClient
import XXMessengerClient
@testable import BackupFeature

final class BackupFeatureTests: XCTestCase {
  func testTask() {
    var isBackupRunning: [Bool] = [false]
    var observers: [UUID: BackupStorage.Observer] = [:]
    let storedBackup = BackupStorage.Backup(
      date: .init(timeIntervalSince1970: 1),
      data: "stored".data(using: .utf8)!
    )

    let store = TestStore(
      initialState: BackupComponent.State(),
      reducer: BackupComponent()
    )
    store.dependencies.app.mainQueue = .immediate
    store.dependencies.app.bgQueue = .immediate
    store.dependencies.app.messenger.isBackupRunning.run = {
      isBackupRunning.removeFirst()
    }
    store.dependencies.app.backupStorage.stored = {
      storedBackup
    }
    store.dependencies.app.backupStorage.observe = {
      let id = UUID()
      observers[id] = $0
      return Cancellable { observers[id] = nil }
    }

    store.send(.task)

    XCTAssertNoDifference(observers.count, 1)

    store.receive(.backupUpdated(storedBackup)) {
      $0.backup = storedBackup
    }

    let observedBackup = BackupStorage.Backup(
      date: .init(timeIntervalSince1970: 2),
      data: "observed".data(using: .utf8)!
    )
    observers.values.forEach { $0(observedBackup) }

    store.receive(.backupUpdated(observedBackup)) {
      $0.backup = observedBackup
    }

    observers.values.forEach { $0(nil) }

    store.receive(.backupUpdated(nil)) {
      $0.backup = nil
    }

    store.send(.cancelTask)

    XCTAssertNoDifference(observers.count, 0)
  }

  func testStartBackup() {
    var actions: [Action]!
    var isBackupRunning: [Bool] = [true]
    let passphrase = "backup-password"

    let store = TestStore(
      initialState: BackupComponent.State(),
      reducer: BackupComponent()
    )
    store.dependencies.app.mainQueue = .immediate
    store.dependencies.app.bgQueue = .immediate
    store.dependencies.app.messenger.startBackup.run = { passphrase, params in
      actions.append(.didStartBackup(passphrase: passphrase, params: params))
    }
    store.dependencies.app.messenger.isBackupRunning.run = {
      isBackupRunning.removeFirst()
    }

    actions = []
    store.send(.set(\.$focusedField, .passphrase)) {
      $0.focusedField = .passphrase
    }
    store.send(.set(\.$passphrase, passphrase)) {
      $0.passphrase = passphrase
    }

    XCTAssertNoDifference(actions, [])

    actions = []
    store.send(.startTapped) {
      $0.isStarting = true
      $0.focusedField = nil
    }

    XCTAssertNoDifference(actions, [
      .didStartBackup(
        passphrase: passphrase,
        params: nil
      )
    ])

    store.receive(.didStart(failure: nil)) {
      $0.isRunning = true
      $0.isStarting = false
      $0.passphrase = ""
    }
  }

  func testStartBackupMyContactFailure() {
    struct Failure: Error {}
    let failure = Failure()
    var isBackupRunning: [Bool] = [false]

    let store = TestStore(
      initialState: BackupComponent.State(
        passphrase: "1234"
      ),
      reducer: BackupComponent()
    )
    store.dependencies.app.mainQueue = .immediate
    store.dependencies.app.bgQueue = .immediate
    store.dependencies.app.messenger.startBackup.run = { _, _ in throw failure }
    store.dependencies.app.messenger.isBackupRunning.run = {
      isBackupRunning.removeFirst()
    }

    store.send(.startTapped) {
      $0.isStarting = true
    }

    store.receive(.didStart(failure: failure as NSError)) {
      $0.isRunning = false
      $0.isStarting = false
      $0.alert = .error(failure)
    }
  }

  func testStartBackupStartFailure() {
    struct Failure: Error {}
    let failure = Failure()
    var isBackupRunning: [Bool] = [false]

    let store = TestStore(
      initialState: BackupComponent.State(
        passphrase: "1234"
      ),
      reducer: BackupComponent()
    )
    store.dependencies.app.mainQueue = .immediate
    store.dependencies.app.bgQueue = .immediate
    store.dependencies.app.messenger.startBackup.run = { _, _ in
      throw failure
    }
    store.dependencies.app.messenger.isBackupRunning.run = {
      isBackupRunning.removeFirst()
    }

    store.send(.startTapped) {
      $0.isStarting = true
    }

    store.receive(.didStart(failure: failure as NSError)) {
      $0.isRunning = false
      $0.isStarting = false
      $0.alert = .error(failure)
    }
  }

  func testResumeBackup() {
    var actions: [Action]!
    var isBackupRunning: [Bool] = [true]

    let store = TestStore(
      initialState: BackupComponent.State(),
      reducer: BackupComponent()
    )
    store.dependencies.app.mainQueue = .immediate
    store.dependencies.app.bgQueue = .immediate
    store.dependencies.app.messenger.resumeBackup.run = {
      actions.append(.didResumeBackup)
    }
    store.dependencies.app.messenger.isBackupRunning.run = {
      isBackupRunning.removeFirst()
    }

    actions = []
    store.send(.resumeTapped) {
      $0.isResuming = true
    }

    XCTAssertNoDifference(actions, [.didResumeBackup])

    actions = []
    store.receive(.didResume(failure: nil)) {
      $0.isRunning = true
      $0.isResuming = false
    }

    XCTAssertNoDifference(actions, [])
  }

  func testResumeBackupFailure() {
    struct Failure: Error {}
    let failure = Failure()
    var isBackupRunning: [Bool] = [false]

    let store = TestStore(
      initialState: BackupComponent.State(),
      reducer: BackupComponent()
    )
    store.dependencies.app.mainQueue = .immediate
    store.dependencies.app.bgQueue = .immediate
    store.dependencies.app.messenger.resumeBackup.run = {
      throw failure
    }
    store.dependencies.app.messenger.isBackupRunning.run = {
      isBackupRunning.removeFirst()
    }

    store.send(.resumeTapped) {
      $0.isResuming = true
    }

    store.receive(.didResume(failure: failure as NSError)) {
      $0.isRunning = false
      $0.isResuming = false
      $0.alert = .error(failure)
    }
  }

  func testStopBackup() {
    var actions: [Action]!
    var isBackupRunning: [Bool] = [false]

    let store = TestStore(
      initialState: BackupComponent.State(),
      reducer: BackupComponent()
    )
    store.dependencies.app.mainQueue = .immediate
    store.dependencies.app.bgQueue = .immediate
    store.dependencies.app.messenger.stopBackup.run = {
      actions.append(.didStopBackup)
    }
    store.dependencies.app.messenger.isBackupRunning.run = {
      isBackupRunning.removeFirst()
    }
    store.dependencies.app.backupStorage.remove = {
      actions.append(.didRemoveBackup)
    }

    actions = []
    store.send(.stopTapped) {
      $0.isStopping = true
    }

    XCTAssertNoDifference(actions, [
      .didStopBackup,
      .didRemoveBackup,
    ])

    actions = []
    store.receive(.didStop(failure: nil)) {
      $0.isRunning = false
      $0.isStopping = false
    }

    XCTAssertNoDifference(actions, [])
  }

  func testStopBackupFailure() {
    struct Failure: Error {}
    let failure = Failure()
    var isBackupRunning: [Bool] = [true]

    let store = TestStore(
      initialState: BackupComponent.State(),
      reducer: BackupComponent()
    )
    store.dependencies.app.mainQueue = .immediate
    store.dependencies.app.bgQueue = .immediate
    store.dependencies.app.messenger.stopBackup.run = {
      throw failure
    }
    store.dependencies.app.messenger.isBackupRunning.run = {
      isBackupRunning.removeFirst()
    }

    store.send(.stopTapped) {
      $0.isStopping = true
    }

    store.receive(.didStop(failure: failure as NSError)) {
      $0.isRunning = true
      $0.isStopping = false
      $0.alert = .error(failure)
    }
  }

  func testAlertDismissed() {
    let store = TestStore(
      initialState: BackupComponent.State(
        alert: .error(NSError(domain: "test", code: 0))
      ),
      reducer: BackupComponent()
    )

    store.send(.alertDismissed) {
      $0.alert = nil
    }
  }

  func testExportBackup() {
    let backupData = "backup-data".data(using: .utf8)!

    let store = TestStore(
      initialState: BackupComponent.State(
        backup: .init(
          date: Date(),
          data: backupData
        )
      ),
      reducer: BackupComponent()
    )

    store.send(.exportTapped) {
      $0.isExporting = true
      $0.exportData = backupData
    }

    store.send(.didExport(failure: nil)) {
      $0.isExporting = false
      $0.exportData = nil
    }

    store.send(.exportTapped) {
      $0.isExporting = true
      $0.exportData = backupData
    }

    let failure = NSError(domain: "test", code: 0)
    store.send(.didExport(failure: failure)) {
      $0.isExporting = false
      $0.exportData = nil
      $0.alert = .error(failure)
    }
  }
}

private enum Action: Equatable {
  case didRegisterObserver
  case didStartBackup(passphrase: String, params: String?)
  case didResumeBackup
  case didStopBackup
  case didRemoveBackup
}