import ComposableArchitecture
import CustomDump
import XCTest
import XXClient
import XXMessengerClient
import XXModels
@testable import RestoreFeature

final class RestoreFeatureTests: XCTestCase {
  func testFileImport() {
    let fileURL = URL(string: "file-url")!
    var didLoadDataFromURL: [URL] = []
    let dataFromURL = "data-from-url".data(using: .utf8)!

    let store = TestStore(
      initialState: RestoreState(),
      reducer: restoreReducer,
      environment: .unimplemented
    )

    store.environment.loadData.load = { url in
      didLoadDataFromURL.append(url)
      return dataFromURL
    }

    store.send(.importFileTapped) {
      $0.isImportingFile = true
    }

    store.send(.fileImport(.success(fileURL))) {
      $0.isImportingFile = false
      $0.file = .init(name: fileURL.lastPathComponent, data: dataFromURL)
      $0.fileImportFailure = nil
    }

    XCTAssertNoDifference(didLoadDataFromURL, [fileURL])
  }

  func testFileImportFailure() {
    struct Failure: Error {}
    let failure = Failure()

    let store = TestStore(
      initialState: RestoreState(
        isImportingFile: true
      ),
      reducer: restoreReducer,
      environment: .unimplemented
    )

    store.send(.fileImport(.failure(failure as NSError))) {
      $0.isImportingFile = false
      $0.file = nil
      $0.fileImportFailure = failure.localizedDescription
    }
  }

  func testFileImportLoadingFailure() {
    struct Failure: Error {}
    let failure = Failure()

    let store = TestStore(
      initialState: RestoreState(
        isImportingFile: true
      ),
      reducer: restoreReducer,
      environment: .unimplemented
    )

    store.environment.loadData.load = { _ in throw failure }

    store.send(.fileImport(.success(URL(string: "test")!))) {
      $0.isImportingFile = false
      $0.file = nil
      $0.fileImportFailure = failure.localizedDescription
    }
  }

  func testRestore() {
    let backupData = "backup-data".data(using: .utf8)!
    let backupPassphrase = "backup-passphrase"
    let restoredFacts = [
      Fact(type: .email, value: "restored-email"),
      Fact(type: .phone, value: "restored-phone"),
    ]
    let restoreResult = MessengerRestoreBackup.Result(
      restoredParams: BackupParams(username: "restored-username"),
      restoredContacts: [
        "contact-1-id".data(using: .utf8)!,
        "contact-2-id".data(using: .utf8)!,
        "contact-3-id".data(using: .utf8)!,
      ]
    )
    let lookedUpContacts = [XXClient.Contact.stub(1), .stub(2), .stub(3)]
    let now = Date()
    let contactId = "contact-id".data(using: .utf8)!


    var udFacts: [Fact] = []
    var didRestoreWithData: [Data] = []
    var didRestoreWithPassphrase: [String] = []
    var didSaveContact: [XXModels.Contact] = []
    var didLookupContactIds: [[Data]] = []

    let store = TestStore(
      initialState: RestoreState(
        file: .init(name: "file-name", data: backupData)
      ),
      reducer: restoreReducer,
      environment: .unimplemented
    )

    store.environment.bgQueue = .immediate
    store.environment.mainQueue = .immediate
    store.environment.now = { now }
    store.environment.messenger.restoreBackup.run = { data, passphrase in
      didRestoreWithData.append(data)
      didRestoreWithPassphrase.append(passphrase)
      udFacts = restoredFacts
      return restoreResult
    }
    store.environment.messenger.e2e.get = {
      var e2e: E2E = .unimplemented
      e2e.getContact.run = {
        var contact: XXClient.Contact = .unimplemented(Data())
        contact.getIdFromContact.run = { _ in contactId }
        return contact
      }
      return e2e
    }
    store.environment.messenger.ud.get = {
      var ud: UserDiscovery = .unimplemented
      ud.getFacts.run = { udFacts }
      return ud
    }
    store.environment.messenger.lookupContacts.run = { contactIds in
      didLookupContactIds.append(contactIds)
      return .init(
        contacts: lookedUpContacts,
        failedIds: [],
        errors: []
      )
    }
    store.environment.db.run = {
      var db: Database = .unimplemented
      db.saveContact.run = { contact in
        didSaveContact.append(contact)
        return contact
      }
      return db
    }

    store.send(.set(\.$passphrase, backupPassphrase)) {
      $0.passphrase = backupPassphrase
    }

    store.send(.restoreTapped) {
      $0.isRestoring = true
    }

    XCTAssertNoDifference(didRestoreWithData, [backupData])
    XCTAssertNoDifference(didRestoreWithPassphrase, [backupPassphrase])
    XCTAssertNoDifference(didLookupContactIds, [restoreResult.restoredContacts])
    XCTAssertNoDifference(didSaveContact, [
      Contact(
        id: contactId,
        username: restoreResult.restoredParams.username,
        email: restoredFacts.get(.email)?.value,
        phone: restoredFacts.get(.phone)?.value,
        createdAt: now
      ),
      Contact(
        id: "contact-\(1)-id".data(using: .utf8)!,
        marshaled: "contact-\(1)-data".data(using: .utf8)!,
        username: "contact-\(1)-username",
        email: "contact-\(1)-email",
        phone: "contact-\(1)-phone",
        authStatus: .friend,
        createdAt: now
      ),
      Contact(
        id: "contact-\(2)-id".data(using: .utf8)!,
        marshaled: "contact-\(2)-data".data(using: .utf8)!,
        username: "contact-\(2)-username",
        email: "contact-\(2)-email",
        phone: "contact-\(2)-phone",
        authStatus: .friend,
        createdAt: now
      ),
      Contact(
        id: "contact-\(3)-id".data(using: .utf8)!,
        marshaled: "contact-\(3)-data".data(using: .utf8)!,
        username: "contact-\(3)-username",
        email: "contact-\(3)-email",
        phone: "contact-\(3)-phone",
        authStatus: .friend,
        createdAt: now
      ),
    ])

    store.receive(.finished) {
      $0.isRestoring = false
    }
  }

  func testRestoreWithoutFile() {
    let store = TestStore(
      initialState: RestoreState(
        file: nil
      ),
      reducer: restoreReducer,
      environment: .unimplemented
    )

    store.send(.restoreTapped)
  }

  func testRestoreFailure() {
    struct Failure: Error {}
    let failure = Failure()

    var didDestroyMessenger = 0

    let store = TestStore(
      initialState: RestoreState(
        file: .init(name: "name", data: "data".data(using: .utf8)!)
      ),
      reducer: restoreReducer,
      environment: .unimplemented
    )

    store.environment.bgQueue = .immediate
    store.environment.mainQueue = .immediate
    store.environment.messenger.restoreBackup.run = { _, _ in throw failure }
    store.environment.messenger.destroy.run = { didDestroyMessenger += 1 }

    store.send(.restoreTapped) {
      $0.isRestoring = true
    }

    XCTAssertEqual(didDestroyMessenger, 1)

    store.receive(.failed([failure as NSError])) {
      $0.isRestoring = false
      $0.restoreFailures = [failure.localizedDescription]
    }
  }
}

private extension XXClient.Contact {
  static func stub(_ id: Int) -> XXClient.Contact {
    var contact = XXClient.Contact.unimplemented(
      "contact-\(id)-data".data(using: .utf8)!
    )
    contact.getIdFromContact.run = { _ in
      "contact-\(id)-id".data(using: .utf8)!
    }
    contact.getFactsFromContact.run = { _ in
      [
        Fact(type: .username, value: "contact-\(id)-username"),
        Fact(type: .email, value: "contact-\(id)-email"),
        Fact(type: .phone, value: "contact-\(id)-phone"),
      ]
    }
    return contact
  }
}