import Combine
import ComposableArchitecture
import CustomDump
import XCTest
import XXClient
import XXMessengerClient
import XXModels
@testable import MyContactFeature

final class MyContactComponentTests: XCTestCase {
  func testStart() {
    let contactId = "contact-id".data(using: .utf8)!

    let store = TestStore(
      initialState: MyContactComponent.State(),
      reducer: MyContactComponent()
    )

    var dbDidFetchContacts: [XXModels.Contact.Query] = []
    let dbContactsPublisher = PassthroughSubject<[XXModels.Contact], Error>()

    store.dependencies.app.mainQueue = .immediate
    store.dependencies.app.bgQueue = .immediate
    store.dependencies.app.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.dependencies.app.dbManager.getDB.run = {
      var db: Database = .unimplemented
      db.fetchContactsPublisher.run = { query in
        dbDidFetchContacts.append(query)
        return dbContactsPublisher.eraseToAnyPublisher()
      }
      return db
    }

    store.send(.start)

    XCTAssertNoDifference(dbDidFetchContacts, [.init(id: [contactId])])

    dbContactsPublisher.send([])

    store.receive(.contactFetched(nil))

    let contact = XXModels.Contact(id: contactId)
    dbContactsPublisher.send([contact])

    store.receive(.contactFetched(contact)) {
      $0.contact = contact
    }

    dbContactsPublisher.send(completion: .finished)
  }

  func testRegisterEmail() {
    let email = "test@email.com"
    let confirmationID = "123"

    var didSendRegisterFact: [Fact] = []

    let store = TestStore(
      initialState: MyContactComponent.State(),
      reducer: MyContactComponent()
    )

    store.dependencies.app.mainQueue = .immediate
    store.dependencies.app.bgQueue = .immediate
    store.dependencies.app.messenger.ud.get = {
      var ud: UserDiscovery = .unimplemented
      ud.sendRegisterFact.run = { fact in
        didSendRegisterFact.append(fact)
        return confirmationID
      }
      return ud
    }

    store.send(.set(\.$focusedField, .email)) {
      $0.focusedField = .email
    }

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

    store.send(.registerEmailTapped) {
      $0.focusedField = nil
      $0.isRegisteringEmail = true
    }

    XCTAssertNoDifference(didSendRegisterFact, [.init(type: .email, value: email)])

    store.receive(.set(\.$emailConfirmationID, confirmationID)) {
      $0.emailConfirmationID = confirmationID
    }

    store.receive(.set(\.$isRegisteringEmail, false)) {
      $0.isRegisteringEmail = false
    }
  }

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

    let store = TestStore(
      initialState: MyContactComponent.State(),
      reducer: MyContactComponent()
    )

    store.dependencies.app.mainQueue = .immediate
    store.dependencies.app.bgQueue = .immediate
    store.dependencies.app.messenger.ud.get = {
      var ud: UserDiscovery = .unimplemented
      ud.sendRegisterFact.run = { _ in throw failure }
      return ud
    }

    store.send(.registerEmailTapped) {
      $0.isRegisteringEmail = true
    }

    store.receive(.didFail(failure.localizedDescription)) {
      $0.alert = .error(failure.localizedDescription)
    }

    store.receive(.set(\.$isRegisteringEmail, false)) {
      $0.isRegisteringEmail = false
    }
  }

  func testConfirmEmail() {
    let contactID = "contact-id".data(using: .utf8)!
    let email = "test@email.com"
    let confirmationID = "123"
    let confirmationCode = "321"
    let dbContact = XXModels.Contact(id: contactID)

    var didConfirmWithID: [String] = []
    var didConfirmWithCode: [String] = []
    var didFetchContacts: [XXModels.Contact.Query] = []
    var didSaveContact: [XXModels.Contact] = []

    let store = TestStore(
      initialState: MyContactComponent.State(
        email: email,
        emailConfirmationID: confirmationID
      ),
      reducer: MyContactComponent()
    )

    store.dependencies.app.mainQueue = .immediate
    store.dependencies.app.bgQueue = .immediate
    store.dependencies.app.messenger.ud.get = {
      var ud: UserDiscovery = .unimplemented
      ud.confirmFact.run = { id, code in
        didConfirmWithID.append(id)
        didConfirmWithCode.append(code)
      }
      return ud
    }
    store.dependencies.app.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.dependencies.app.dbManager.getDB.run = {
      var db: Database = .unimplemented
      db.fetchContacts.run = { query in
        didFetchContacts.append(query)
        return [dbContact]
      }
      db.saveContact.run = { contact in
        didSaveContact.append(contact)
        return contact
      }
      return db
    }

    store.send(.set(\.$focusedField, .emailCode)) {
      $0.focusedField = .emailCode
    }

    store.send(.set(\.$emailConfirmationCode, confirmationCode)) {
      $0.emailConfirmationCode = confirmationCode
    }

    store.send(.confirmEmailTapped) {
      $0.focusedField = nil
      $0.isConfirmingEmail = true
    }

    XCTAssertNoDifference(didConfirmWithID, [confirmationID])
    XCTAssertNoDifference(didConfirmWithCode, [confirmationCode])
    XCTAssertNoDifference(didFetchContacts, [.init(id: [contactID])])
    var expectedSavedContact = dbContact
    expectedSavedContact.email = email
    XCTAssertNoDifference(didSaveContact, [expectedSavedContact])

    store.receive(.set(\.$email, "")) {
      $0.email = ""
    }
    store.receive(.set(\.$emailConfirmationID, nil)) {
      $0.emailConfirmationID = nil
    }
    store.receive(.set(\.$emailConfirmationCode, "")) {
      $0.emailConfirmationCode = ""
    }
    store.receive(.set(\.$isConfirmingEmail, false)) {
      $0.isConfirmingEmail = false
    }
  }

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

    let store = TestStore(
      initialState: MyContactComponent.State(
        emailConfirmationID: "123"
      ),
      reducer: MyContactComponent()
    )

    store.dependencies.app.mainQueue = .immediate
    store.dependencies.app.bgQueue = .immediate
    store.dependencies.app.messenger.ud.get = {
      var ud: UserDiscovery = .unimplemented
      ud.confirmFact.run = { _, _ in throw failure }
      return ud
    }

    store.send(.confirmEmailTapped) {
      $0.isConfirmingEmail = true
    }

    store.receive(.didFail(failure.localizedDescription)) {
      $0.alert = .error(failure.localizedDescription)
    }

    store.receive(.set(\.$isConfirmingEmail, false)) {
      $0.isConfirmingEmail = false
    }
  }

  func testUnregisterEmail() {
    let contactID = "contact-id".data(using: .utf8)!
    let email = "test@email.com"
    let dbContact = XXModels.Contact(id: contactID, email: email)

    var didRemoveFact: [Fact] = []
    var didFetchContacts: [XXModels.Contact.Query] = []
    var didSaveContact: [XXModels.Contact] = []

    let store = TestStore(
      initialState: MyContactComponent.State(
        contact: dbContact
      ),
      reducer: MyContactComponent()
    )

    store.dependencies.app.mainQueue = .immediate
    store.dependencies.app.bgQueue = .immediate
    store.dependencies.app.messenger.ud.get = {
      var ud: UserDiscovery = .unimplemented
      ud.removeFact.run = { didRemoveFact.append($0) }
      return ud
    }
    store.dependencies.app.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.dependencies.app.dbManager.getDB.run = {
      var db: Database = .unimplemented
      db.fetchContacts.run = { query in
        didFetchContacts.append(query)
        return [dbContact]
      }
      db.saveContact.run = { contact in
        didSaveContact.append(contact)
        return contact
      }
      return db
    }

    store.send(.unregisterEmailTapped) {
      $0.isUnregisteringEmail = true
    }

    XCTAssertNoDifference(didRemoveFact, [.init(type: .email, value: email)])
    XCTAssertNoDifference(didFetchContacts, [.init(id: [contactID])])
    var expectedSavedContact = dbContact
    expectedSavedContact.email = nil
    XCTAssertNoDifference(didSaveContact, [expectedSavedContact])

    store.receive(.set(\.$isUnregisteringEmail, false)) {
      $0.isUnregisteringEmail = false
    }
  }

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

    let store = TestStore(
      initialState: MyContactComponent.State(
        contact: .init(id: Data(), email: "test@email.com")
      ),
      reducer: MyContactComponent()
    )

    store.dependencies.app.mainQueue = .immediate
    store.dependencies.app.bgQueue = .immediate
    store.dependencies.app.messenger.ud.get = {
      var ud: UserDiscovery = .unimplemented
      ud.removeFact.run = { _ in throw failure }
      return ud
    }

    store.send(.unregisterEmailTapped) {
      $0.isUnregisteringEmail = true
    }

    store.receive(.didFail(failure.localizedDescription)) {
      $0.alert = .error(failure.localizedDescription)
    }

    store.receive(.set(\.$isUnregisteringEmail, false)) {
      $0.isUnregisteringEmail = false
    }
  }

  func testRegisterPhone() {
    let phone = "+123456789"
    let confirmationID = "123"

    var didSendRegisterFact: [Fact] = []

    let store = TestStore(
      initialState: MyContactComponent.State(),
      reducer: MyContactComponent()
    )

    store.dependencies.app.mainQueue = .immediate
    store.dependencies.app.bgQueue = .immediate
    store.dependencies.app.messenger.ud.get = {
      var ud: UserDiscovery = .unimplemented
      ud.sendRegisterFact.run = { fact in
        didSendRegisterFact.append(fact)
        return confirmationID
      }
      return ud
    }

    store.send(.set(\.$focusedField, .phone)) {
      $0.focusedField = .phone
    }

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

    store.send(.registerPhoneTapped) {
      $0.focusedField = nil
      $0.isRegisteringPhone = true
    }

    XCTAssertNoDifference(didSendRegisterFact, [.init(type: .phone, value: phone)])

    store.receive(.set(\.$phoneConfirmationID, confirmationID)) {
      $0.phoneConfirmationID = confirmationID
    }

    store.receive(.set(\.$isRegisteringPhone, false)) {
      $0.isRegisteringPhone = false
    }
  }

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

    let store = TestStore(
      initialState: MyContactComponent.State(),
      reducer: MyContactComponent()
    )

    store.dependencies.app.mainQueue = .immediate
    store.dependencies.app.bgQueue = .immediate
    store.dependencies.app.messenger.ud.get = {
      var ud: UserDiscovery = .unimplemented
      ud.sendRegisterFact.run = { _ in throw failure }
      return ud
    }

    store.send(.registerPhoneTapped) {
      $0.isRegisteringPhone = true
    }

    store.receive(.didFail(failure.localizedDescription)) {
      $0.alert = .error(failure.localizedDescription)
    }

    store.receive(.set(\.$isRegisteringPhone, false)) {
      $0.isRegisteringPhone = false
    }
  }

  func testConfirmPhone() {
    let contactID = "contact-id".data(using: .utf8)!
    let phone = "+123456789"
    let confirmationID = "123"
    let confirmationCode = "321"
    let dbContact = XXModels.Contact(id: contactID)

    var didConfirmWithID: [String] = []
    var didConfirmWithCode: [String] = []
    var didFetchContacts: [XXModels.Contact.Query] = []
    var didSaveContact: [XXModels.Contact] = []

    let store = TestStore(
      initialState: MyContactComponent.State(
        phone: phone,
        phoneConfirmationID: confirmationID
      ),
      reducer: MyContactComponent()
    )

    store.dependencies.app.mainQueue = .immediate
    store.dependencies.app.bgQueue = .immediate
    store.dependencies.app.messenger.ud.get = {
      var ud: UserDiscovery = .unimplemented
      ud.confirmFact.run = { id, code in
        didConfirmWithID.append(id)
        didConfirmWithCode.append(code)
      }
      return ud
    }
    store.dependencies.app.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.dependencies.app.dbManager.getDB.run = {
      var db: Database = .unimplemented
      db.fetchContacts.run = { query in
        didFetchContacts.append(query)
        return [dbContact]
      }
      db.saveContact.run = { contact in
        didSaveContact.append(contact)
        return contact
      }
      return db
    }

    store.send(.set(\.$focusedField, .phoneCode)) {
      $0.focusedField = .phoneCode
    }

    store.send(.set(\.$phoneConfirmationCode, confirmationCode)) {
      $0.phoneConfirmationCode = confirmationCode
    }

    store.send(.confirmPhoneTapped) {
      $0.focusedField = nil
      $0.isConfirmingPhone = true
    }

    XCTAssertNoDifference(didConfirmWithID, [confirmationID])
    XCTAssertNoDifference(didConfirmWithCode, [confirmationCode])
    XCTAssertNoDifference(didFetchContacts, [.init(id: [contactID])])
    var expectedSavedContact = dbContact
    expectedSavedContact.phone = phone
    XCTAssertNoDifference(didSaveContact, [expectedSavedContact])

    store.receive(.set(\.$phone, "")) {
      $0.phone = ""
    }
    store.receive(.set(\.$phoneConfirmationID, nil)) {
      $0.phoneConfirmationID = nil
    }
    store.receive(.set(\.$phoneConfirmationCode, "")) {
      $0.phoneConfirmationCode = ""
    }
    store.receive(.set(\.$isConfirmingPhone, false)) {
      $0.isConfirmingPhone = false
    }
  }

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

    let store = TestStore(
      initialState: MyContactComponent.State(
        phoneConfirmationID: "123"
      ),
      reducer: MyContactComponent()
    )

    store.dependencies.app.mainQueue = .immediate
    store.dependencies.app.bgQueue = .immediate
    store.dependencies.app.messenger.ud.get = {
      var ud: UserDiscovery = .unimplemented
      ud.confirmFact.run = { _, _ in throw failure }
      return ud
    }

    store.send(.confirmPhoneTapped) {
      $0.isConfirmingPhone = true
    }

    store.receive(.didFail(failure.localizedDescription)) {
      $0.alert = .error(failure.localizedDescription)
    }

    store.receive(.set(\.$isConfirmingPhone, false)) {
      $0.isConfirmingPhone = false
    }
  }
  
  func testUnregisterPhone() {
    let contactID = "contact-id".data(using: .utf8)!
    let phone = "+123456789"
    let dbContact = XXModels.Contact(id: contactID, phone: phone)

    var didRemoveFact: [Fact] = []
    var didFetchContacts: [XXModels.Contact.Query] = []
    var didSaveContact: [XXModels.Contact] = []

    let store = TestStore(
      initialState: MyContactComponent.State(
        contact: dbContact
      ),
      reducer: MyContactComponent()
    )

    store.dependencies.app.mainQueue = .immediate
    store.dependencies.app.bgQueue = .immediate
    store.dependencies.app.messenger.ud.get = {
      var ud: UserDiscovery = .unimplemented
      ud.removeFact.run = { didRemoveFact.append($0) }
      return ud
    }
    store.dependencies.app.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.dependencies.app.dbManager.getDB.run = {
      var db: Database = .unimplemented
      db.fetchContacts.run = { query in
        didFetchContacts.append(query)
        return [dbContact]
      }
      db.saveContact.run = { contact in
        didSaveContact.append(contact)
        return contact
      }
      return db
    }

    store.send(.unregisterPhoneTapped) {
      $0.isUnregisteringPhone = true
    }

    XCTAssertNoDifference(didRemoveFact, [.init(type: .phone, value: phone)])
    XCTAssertNoDifference(didFetchContacts, [.init(id: [contactID])])
    var expectedSavedContact = dbContact
    expectedSavedContact.phone = nil
    XCTAssertNoDifference(didSaveContact, [expectedSavedContact])

    store.receive(.set(\.$isUnregisteringPhone, false)) {
      $0.isUnregisteringPhone = false
    }
  }

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

    let store = TestStore(
      initialState: MyContactComponent.State(
        contact: .init(id: Data(), phone: "+123456789")
      ),
      reducer: MyContactComponent()
    )

    store.dependencies.app.mainQueue = .immediate
    store.dependencies.app.bgQueue = .immediate
    store.dependencies.app.messenger.ud.get = {
      var ud: UserDiscovery = .unimplemented
      ud.removeFact.run = { _ in throw failure }
      return ud
    }

    store.send(.unregisterPhoneTapped) {
      $0.isUnregisteringPhone = true
    }

    store.receive(.didFail(failure.localizedDescription)) {
      $0.alert = .error(failure.localizedDescription)
    }

    store.receive(.set(\.$isUnregisteringPhone, false)) {
      $0.isUnregisteringPhone = false
    }
  }

  func testLoadFactsFromClient() {
    let contactId = "contact-id".data(using: .utf8)!
    let dbContact = XXModels.Contact(id: contactId)
    let username = "user234"
    let email = "test@email.com"
    let phone = "123456789"

    var didFetchContacts: [XXModels.Contact.Query] = []
    var didSaveContact: [XXModels.Contact] = []

    let store = TestStore(
      initialState: MyContactComponent.State(),
      reducer: MyContactComponent()
    )

    store.dependencies.app.mainQueue = .immediate
    store.dependencies.app.bgQueue = .immediate
    store.dependencies.app.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.dependencies.app.messenger.ud.get = {
      var ud: UserDiscovery = .unimplemented
      ud.getFacts.run = {
        [
          Fact(type: .username, value: username),
          Fact(type: .email, value: email),
          Fact(type: .phone, value: phone),
        ]
      }
      return ud
    }
    store.dependencies.app.dbManager.getDB.run = {
      var db: Database = .unimplemented
      db.fetchContacts.run = { query in
        didFetchContacts.append(query)
        return [dbContact]
      }
      db.saveContact.run = { contact in
        didSaveContact.append(contact)
        return contact
      }
      return db
    }

    store.send(.loadFactsTapped) {
      $0.isLoadingFacts = true
    }

    XCTAssertNoDifference(didFetchContacts, [.init(id: [contactId])])
    var expectedSavedContact = dbContact
    expectedSavedContact.username = username
    expectedSavedContact.email = email
    expectedSavedContact.phone = phone
    XCTAssertNoDifference(didSaveContact, [expectedSavedContact])

    store.receive(.set(\.$isLoadingFacts, false)) {
      $0.isLoadingFacts = false
    }
  }

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

    let store = TestStore(
      initialState: MyContactComponent.State(),
      reducer: MyContactComponent()
    )

    store.dependencies.app.mainQueue = .immediate
    store.dependencies.app.bgQueue = .immediate
    store.dependencies.app.messenger.e2e.get = {
      var e2e: E2E = .unimplemented
      e2e.getContact.run = {
        var contact: XXClient.Contact = .unimplemented(Data())
        contact.getIdFromContact.run = { _ in throw failure }
        return contact
      }
      return e2e
    }

    store.send(.loadFactsTapped) {
      $0.isLoadingFacts = true
    }

    store.receive(.didFail(failure.localizedDescription)) {
      $0.alert = .error(failure.localizedDescription)
    }

    store.receive(.set(\.$isLoadingFacts, false)) {
      $0.isLoadingFacts = false
    }
  }

  func testErrorAlert() {
    let store = TestStore(
      initialState: MyContactComponent.State(),
      reducer: MyContactComponent()
    )

    let failure = "Something went wrong"

    store.send(.didFail(failure)) {
      $0.alert = .error(failure)
    }

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