import AppCore
import BackupFeature
import ComposableArchitecture
import ContactsFeature
import CustomDump
import RegisterFeature
import UserSearchFeature
import XCTest
import XXClient
import XXMessengerClient
import XXModels
@testable import HomeFeature

final class HomeFeatureTests: XCTestCase {
  func testMessengerStartUnregistered() {
    let store = TestStore(
      initialState: HomeState(),
      reducer: homeReducer,
      environment: .unimplemented
    )

    var messengerDidStartWithTimeout: [Int] = []
    var messengerDidConnect = 0
    var messengerDidListenForMessages = 0
    var messengerDidStartFileTransfer = 0

    store.environment.bgQueue = .immediate
    store.environment.mainQueue = .immediate
    store.environment.messenger.start.run = { messengerDidStartWithTimeout.append($0) }
    store.environment.messenger.isConnected.run = { false }
    store.environment.messenger.connect.run = { messengerDidConnect += 1 }
    store.environment.messenger.isListeningForMessages.run = { false }
    store.environment.messenger.listenForMessages.run = { messengerDidListenForMessages += 1 }
    store.environment.messenger.isFileTransferRunning.run = { false }
    store.environment.messenger.startFileTransfer.run = { messengerDidStartFileTransfer += 1 }
    store.environment.messenger.isLoggedIn.run = { false }
    store.environment.messenger.isRegistered.run = { false }

    store.send(.messenger(.start))

    XCTAssertNoDifference(messengerDidStartWithTimeout, [30_000])
    XCTAssertNoDifference(messengerDidConnect, 1)
    XCTAssertNoDifference(messengerDidListenForMessages, 1)
    XCTAssertNoDifference(messengerDidStartFileTransfer, 1)

    store.receive(.networkMonitor(.stop))
    store.receive(.messenger(.didStartUnregistered)) {
      $0.register = RegisterState()
    }
  }

  func testMessengerStartRegistered() {
    let store = TestStore(
      initialState: HomeState(),
      reducer: homeReducer,
      environment: .unimplemented
    )

    var messengerDidStartWithTimeout: [Int] = []
    var messengerDidConnect = 0
    var messengerDidListenForMessages = 0
    var messengerDidStartFileTransfer = 0
    var messengerDidLogIn = 0
    var messengerDidResumeBackup = 0

    store.environment.bgQueue = .immediate
    store.environment.mainQueue = .immediate
    store.environment.messenger.start.run = { messengerDidStartWithTimeout.append($0) }
    store.environment.messenger.isConnected.run = { false }
    store.environment.messenger.connect.run = { messengerDidConnect += 1 }
    store.environment.messenger.isListeningForMessages.run = { false }
    store.environment.messenger.listenForMessages.run = { messengerDidListenForMessages += 1 }
    store.environment.messenger.isFileTransferRunning.run = { false }
    store.environment.messenger.startFileTransfer.run = { messengerDidStartFileTransfer += 1 }
    store.environment.messenger.isLoggedIn.run = { false }
    store.environment.messenger.isRegistered.run = { true }
    store.environment.messenger.logIn.run = { messengerDidLogIn += 1 }
    store.environment.messenger.isBackupRunning.run = { false }
    store.environment.messenger.resumeBackup.run = { messengerDidResumeBackup += 1 }
    store.environment.messenger.cMix.get = {
      var cMix: CMix = .unimplemented
      cMix.addHealthCallback.run = { _ in Cancellable {} }
      cMix.getNodeRegistrationStatus.run = {
        struct Unimplemented: Error {}
        throw Unimplemented()
      }
      return cMix
    }

    store.send(.messenger(.start))

    XCTAssertNoDifference(messengerDidStartWithTimeout, [30_000])
    XCTAssertNoDifference(messengerDidConnect, 1)
    XCTAssertNoDifference(messengerDidListenForMessages, 1)
    XCTAssertNoDifference(messengerDidStartFileTransfer, 1)
    XCTAssertNoDifference(messengerDidLogIn, 1)
    XCTAssertNoDifference(messengerDidResumeBackup, 1)

    store.receive(.networkMonitor(.stop))
    store.receive(.messenger(.didStartRegistered))
    store.receive(.networkMonitor(.start))

    store.send(.networkMonitor(.stop))
  }

  func testRegisterFinished() {
    let store = TestStore(
      initialState: HomeState(
        register: RegisterState()
      ),
      reducer: homeReducer,
      environment: .unimplemented
    )

    var messengerDidStartWithTimeout: [Int] = []
    var messengerDidLogIn = 0

    store.environment.bgQueue = .immediate
    store.environment.mainQueue = .immediate
    store.environment.messenger.start.run = { messengerDidStartWithTimeout.append($0) }
    store.environment.messenger.isConnected.run = { true }
    store.environment.messenger.isListeningForMessages.run = { true }
    store.environment.messenger.isFileTransferRunning.run = { true }
    store.environment.messenger.isLoggedIn.run = { false }
    store.environment.messenger.isRegistered.run = { true }
    store.environment.messenger.logIn.run = { messengerDidLogIn += 1 }
    store.environment.messenger.isBackupRunning.run = { true }
    store.environment.messenger.cMix.get = {
      var cMix: CMix = .unimplemented
      cMix.addHealthCallback.run = { _ in Cancellable {} }
      cMix.getNodeRegistrationStatus.run = {
        struct Unimplemented: Error {}
        throw Unimplemented()
      }
      return cMix
    }

    store.send(.register(.finished)) {
      $0.register = nil
    }

    store.receive(.messenger(.start))

    XCTAssertNoDifference(messengerDidStartWithTimeout, [30_000])
    XCTAssertNoDifference(messengerDidLogIn, 1)

    store.receive(.networkMonitor(.stop))
    store.receive(.messenger(.didStartRegistered))
    store.receive(.networkMonitor(.start))

    store.send(.networkMonitor(.stop))
  }

  func testMessengerStartFailure() {
    let store = TestStore(
      initialState: HomeState(),
      reducer: homeReducer,
      environment: .unimplemented
    )

    struct Failure: Error {}
    let error = Failure()

    store.environment.bgQueue = .immediate
    store.environment.mainQueue = .immediate
    store.environment.messenger.start.run = { _ in throw error }

    store.send(.messenger(.start))

    store.receive(.networkMonitor(.stop))
    store.receive(.messenger(.failure(error as NSError))) {
      $0.failure = error.localizedDescription
    }
  }

  func testMessengerStartConnectFailure() {
    let store = TestStore(
      initialState: HomeState(),
      reducer: homeReducer,
      environment: .unimplemented
    )

    struct Failure: Error {}
    let error = Failure()

    store.environment.bgQueue = .immediate
    store.environment.mainQueue = .immediate
    store.environment.messenger.start.run = { _ in }
    store.environment.messenger.isConnected.run = { false }
    store.environment.messenger.connect.run = { throw error }

    store.send(.messenger(.start))

    store.receive(.networkMonitor(.stop))
    store.receive(.messenger(.failure(error as NSError))) {
      $0.failure = error.localizedDescription
    }
  }

  func testMessengerStartIsRegisteredFailure() {
    let store = TestStore(
      initialState: HomeState(),
      reducer: homeReducer,
      environment: .unimplemented
    )

    struct Failure: Error {}
    let error = Failure()

    store.environment.bgQueue = .immediate
    store.environment.mainQueue = .immediate
    store.environment.messenger.start.run = { _ in }
    store.environment.messenger.isConnected.run = { true }
    store.environment.messenger.isListeningForMessages.run = { true }
    store.environment.messenger.isFileTransferRunning.run = { true }
    store.environment.messenger.isLoggedIn.run = { false }
    store.environment.messenger.isRegistered.run = { throw error }

    store.send(.messenger(.start))

    store.receive(.networkMonitor(.stop))
    store.receive(.messenger(.failure(error as NSError))) {
      $0.failure = error.localizedDescription
    }
  }

  func testMessengerStartLogInFailure() {
    let store = TestStore(
      initialState: HomeState(),
      reducer: homeReducer,
      environment: .unimplemented
    )

    struct Failure: Error {}
    let error = Failure()

    store.environment.bgQueue = .immediate
    store.environment.mainQueue = .immediate
    store.environment.messenger.start.run = { _ in }
    store.environment.messenger.isConnected.run = { true }
    store.environment.messenger.isListeningForMessages.run = { true }
    store.environment.messenger.isFileTransferRunning.run = { true }
    store.environment.messenger.isLoggedIn.run = { false }
    store.environment.messenger.isRegistered.run = { true }
    store.environment.messenger.logIn.run = { throw error }

    store.send(.messenger(.start))

    store.receive(.networkMonitor(.stop))
    store.receive(.messenger(.failure(error as NSError))) {
      $0.failure = error.localizedDescription
    }
  }

  func testNetworkMonitorStart() {
    let store = TestStore(
      initialState: HomeState(),
      reducer: homeReducer,
      environment: .unimplemented
    )

    let bgQueue = DispatchQueue.test
    let mainQueue = DispatchQueue.test

    var cMixDidAddHealthCallback: [HealthCallback] = []
    var healthCallbackDidCancel = 0
    var nodeRegistrationStatusIndex = 0
    let nodeRegistrationStatus: [NodeRegistrationReport] = [
      .init(registered: 0, total: 10),
      .init(registered: 1, total: 11),
      .init(registered: 2, total: 12),
    ]

    store.environment.bgQueue = bgQueue.eraseToAnyScheduler()
    store.environment.mainQueue = mainQueue.eraseToAnyScheduler()
    store.environment.messenger.cMix.get = {
      var cMix: CMix = .unimplemented
      cMix.addHealthCallback.run = { callback in
        cMixDidAddHealthCallback.append(callback)
        return Cancellable { healthCallbackDidCancel += 1 }
      }
      cMix.getNodeRegistrationStatus.run = {
        defer { nodeRegistrationStatusIndex += 1 }
        return nodeRegistrationStatus[nodeRegistrationStatusIndex]
      }
      return cMix
    }

    store.send(.networkMonitor(.start))

    bgQueue.advance()

    XCTAssertNoDifference(cMixDidAddHealthCallback.count, 1)

    cMixDidAddHealthCallback.first?.handle(true)
    mainQueue.advance()

    store.receive(.networkMonitor(.health(true))) {
      $0.isNetworkHealthy = true
    }

    cMixDidAddHealthCallback.first?.handle(false)
    mainQueue.advance()

    store.receive(.networkMonitor(.health(false))) {
      $0.isNetworkHealthy = false
    }

    bgQueue.advance(by: 2)
    mainQueue.advance()

    store.receive(.networkMonitor(.nodes(nodeRegistrationStatus[0]))) {
      $0.networkNodesReport = nodeRegistrationStatus[0]
    }

    bgQueue.advance(by: 2)
    mainQueue.advance()

    store.receive(.networkMonitor(.nodes(nodeRegistrationStatus[1]))) {
      $0.networkNodesReport = nodeRegistrationStatus[1]
    }

    bgQueue.advance(by: 2)
    mainQueue.advance()

    store.receive(.networkMonitor(.nodes(nodeRegistrationStatus[2]))) {
      $0.networkNodesReport = nodeRegistrationStatus[2]
    }

    store.send(.networkMonitor(.stop)) {
      $0.isNetworkHealthy = nil
      $0.networkNodesReport = nil
    }

    XCTAssertNoDifference(healthCallbackDidCancel, 1)

    mainQueue.advance()
  }

  func testAccountDeletion() {
    let store = TestStore(
      initialState: HomeState(),
      reducer: homeReducer,
      environment: .unimplemented
    )

    var dbDidFetchContacts: [XXModels.Contact.Query] = []
    var udDidPermanentDeleteAccount: [Fact] = []
    var messengerDidDestroy = 0
    var didRemoveDB = 0

    store.environment.bgQueue = .immediate
    store.environment.mainQueue = .immediate
    store.environment.messenger.e2e.get = {
      var e2e: E2E = .unimplemented
      e2e.getContact.run = {
        var contact = Contact.unimplemented("contact-data".data(using: .utf8)!)
        contact.getIdFromContact.run = { _ in "contact-id".data(using: .utf8)! }
        return contact
      }
      return e2e
    }
    store.environment.dbManager.getDB.run = {
      var db: Database = .unimplemented
      db.fetchContacts.run = { query in
        dbDidFetchContacts.append(query)
        return [
          XXModels.Contact(
            id: "contact-id".data(using: .utf8)!,
            marshaled: "contact-data".data(using: .utf8)!,
            username: "MyUsername"
          )
        ]
      }
      return db
    }
    store.environment.dbManager.removeDB.run = {
      didRemoveDB += 1
    }
    store.environment.messenger.ud.get = {
      var ud: UserDiscovery = .unimplemented
      ud.permanentDeleteAccount.run = { usernameFact in
        udDidPermanentDeleteAccount.append(usernameFact)
      }
      return ud
    }
    store.environment.messenger.destroy.run = {
      messengerDidDestroy += 1
    }

    store.send(.deleteAccount(.buttonTapped)) {
      $0.alert = .confirmAccountDeletion()
    }

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

    store.send(.deleteAccount(.confirmed)) {
      $0.isDeletingAccount = true
    }

    XCTAssertNoDifference(dbDidFetchContacts, [.init(id: ["contact-id".data(using: .utf8)!])])
    XCTAssertNoDifference(udDidPermanentDeleteAccount, [Fact(type: .username, value: "MyUsername")])
    XCTAssertNoDifference(messengerDidDestroy, 1)
    XCTAssertNoDifference(didRemoveDB, 1)

    store.receive(.deleteAccount(.success)) {
      $0.isDeletingAccount = false
    }
  }

  func testAccountDeletionFailure() {
    let store = TestStore(
      initialState: HomeState(),
      reducer: homeReducer,
      environment: .unimplemented
    )

    struct Failure: Error {}
    let error = Failure()

    store.environment.bgQueue = .immediate
    store.environment.mainQueue = .immediate
    store.environment.messenger.e2e.get = {
      var e2e: E2E = .unimplemented
      e2e.getContact.run = {
        var contact = Contact.unimplemented("contact-data".data(using: .utf8)!)
        contact.getIdFromContact.run = { _ in throw error }
        return contact
      }
      return e2e
    }

    store.send(.deleteAccount(.confirmed)) {
      $0.isDeletingAccount = true
    }

    store.receive(.deleteAccount(.failure(error as NSError))) {
      $0.isDeletingAccount = false
      $0.alert = .accountDeletionFailed(error)
    }
  }

  func testDidDismissAlert() {
    let store = TestStore(
      initialState: HomeState(
        alert: AlertState(title: TextState(""))
      ),
      reducer: homeReducer,
      environment: .unimplemented
    )

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

  func testDidDismissRegister() {
    let store = TestStore(
      initialState: HomeState(
        register: RegisterState()
      ),
      reducer: homeReducer,
      environment: .unimplemented
    )

    store.send(.didDismissRegister) {
      $0.register = nil
    }
  }

  func testUserSearchButtonTapped() {
    let store = TestStore(
      initialState: HomeState(),
      reducer: homeReducer,
      environment: .unimplemented
    )

    store.send(.userSearchButtonTapped) {
      $0.userSearch = UserSearchState()
    }
  }

  func testDidDismissUserSearch() {
    let store = TestStore(
      initialState: HomeState(
        userSearch: UserSearchState()
      ),
      reducer: homeReducer,
      environment: .unimplemented
    )

    store.send(.didDismissUserSearch) {
      $0.userSearch = nil
    }
  }

  func testContactsButtonTapped() {
    let store = TestStore(
      initialState: HomeState(),
      reducer: homeReducer,
      environment: .unimplemented
    )

    store.send(.contactsButtonTapped) {
      $0.contacts = ContactsState()
    }
  }

  func testDidDismissContacts() {
    let store = TestStore(
      initialState: HomeState(
        contacts: ContactsState()
      ),
      reducer: homeReducer,
      environment: .unimplemented
    )

    store.send(.didDismissContacts) {
      $0.contacts = nil
    }
  }

  func testBackupButtonTapped() {
    let store = TestStore(
      initialState: HomeState(),
      reducer: homeReducer,
      environment: .unimplemented
    )

    store.send(.backupButtonTapped) {
      $0.backup = BackupState()
    }
  }

  func testDidDismissBackup() {
    let store = TestStore(
      initialState: HomeState(
        backup: BackupState()
      ),
      reducer: homeReducer,
      environment: .unimplemented
    )

    store.send(.didDismissBackup) {
      $0.backup = nil
    }
  }
}