import AppCore
import ComposableArchitecture
import CustomDump
import HomeFeature
import RestoreFeature
import WelcomeFeature
import XCTest
import XXClient
@testable import AppFeature

final class AppFeatureTests: XCTestCase {
  func testStartWithoutMessengerCreated() {
    var actions: [Action] = []

    let store = TestStore(
      initialState: AppState(),
      reducer: appReducer,
      environment: .unimplemented
    )

    store.environment.mainQueue = .immediate
    store.environment.bgQueue = .immediate
    store.environment.dbManager.hasDB.run = { false }
    store.environment.messenger.isLoaded.run = { false }
    store.environment.messenger.isCreated.run = { false }
    store.environment.dbManager.makeDB.run = {
      actions.append(.didMakeDB)
    }
    store.environment.authHandler.run = { _ in
      actions.append(.didStartAuthHandler)
      return Cancellable {}
    }
    store.environment.messageListener.run = { _ in
      actions.append(.didStartMessageListener)
      return Cancellable {}
    }

    store.send(.start)

    store.receive(.set(\.$screen, .welcome(WelcomeState()))) {
      $0.screen = .welcome(WelcomeState())
    }

    XCTAssertNoDifference(actions, [
      .didMakeDB,
      .didStartAuthHandler,
      .didStartMessageListener,
    ])

    store.send(.stop)
  }

  func testStartWithMessengerCreated() {
    var actions: [Action] = []

    let store = TestStore(
      initialState: AppState(),
      reducer: appReducer,
      environment: .unimplemented
    )

    store.environment.mainQueue = .immediate
    store.environment.bgQueue = .immediate
    store.environment.dbManager.hasDB.run = { false }
    store.environment.messenger.isLoaded.run = { false }
    store.environment.messenger.isCreated.run = { true }
    store.environment.dbManager.makeDB.run = {
      actions.append(.didMakeDB)
    }
    store.environment.messenger.load.run = {
      actions.append(.didLoadMessenger)
    }
    store.environment.authHandler.run = { _ in
      actions.append(.didStartAuthHandler)
      return Cancellable {}
    }
    store.environment.messageListener.run = { _ in
      actions.append(.didStartMessageListener)
      return Cancellable {}
    }

    store.send(.start)

    store.receive(.set(\.$screen, .home(HomeState()))) {
      $0.screen = .home(HomeState())
    }

    XCTAssertNoDifference(actions, [
      .didMakeDB,
      .didStartAuthHandler,
      .didStartMessageListener,
      .didLoadMessenger,
    ])

    store.send(.stop)
  }

  func testWelcomeFinished() {
    var actions: [Action] = []

    let store = TestStore(
      initialState: AppState(
        screen: .welcome(WelcomeState())
      ),
      reducer: appReducer,
      environment: .unimplemented
    )

    store.environment.mainQueue = .immediate
    store.environment.bgQueue = .immediate
    store.environment.dbManager.hasDB.run = { true }
    store.environment.messenger.isLoaded.run = { false }
    store.environment.messenger.isCreated.run = { true }
    store.environment.messenger.load.run = {
      actions.append(.didLoadMessenger)
    }
    store.environment.authHandler.run = { _ in
      actions.append(.didStartAuthHandler)
      return Cancellable {}
    }
    store.environment.messageListener.run = { _ in
      actions.append(.didStartMessageListener)
      return Cancellable {}
    }

    store.send(.welcome(.finished)) {
      $0.screen = .loading
    }

    store.receive(.set(\.$screen, .home(HomeState()))) {
      $0.screen = .home(HomeState())
    }

    XCTAssertNoDifference(actions, [
      .didStartAuthHandler,
      .didStartMessageListener,
      .didLoadMessenger,
    ])

    store.send(.stop)
  }

  func testRestoreFinished() {
    var actions: [Action] = []

    let store = TestStore(
      initialState: AppState(
        screen: .restore(RestoreState())
      ),
      reducer: appReducer,
      environment: .unimplemented
    )

    store.environment.mainQueue = .immediate
    store.environment.bgQueue = .immediate
    store.environment.dbManager.hasDB.run = { true }
    store.environment.messenger.isLoaded.run = { false }
    store.environment.messenger.isCreated.run = { true }
    store.environment.messenger.load.run = {
      actions.append(.didLoadMessenger)
    }
    store.environment.authHandler.run = { _ in
      actions.append(.didStartAuthHandler)
      return Cancellable {}
    }
    store.environment.messageListener.run = { _ in
      actions.append(.didStartMessageListener)
      return Cancellable {}
    }

    store.send(.restore(.finished)) {
      $0.screen = .loading
    }

    store.receive(.set(\.$screen, .home(HomeState()))) {
      $0.screen = .home(HomeState())
    }

    XCTAssertNoDifference(actions, [
      .didStartAuthHandler,
      .didStartMessageListener,
      .didLoadMessenger,
    ])

    store.send(.stop)
  }

  func testHomeDidDeleteAccount() {
    var actions: [Action] = []

    let store = TestStore(
      initialState: AppState(
        screen: .home(HomeState())
      ),
      reducer: appReducer,
      environment: .unimplemented
    )

    store.environment.mainQueue = .immediate
    store.environment.bgQueue = .immediate
    store.environment.dbManager.hasDB.run = { true }
    store.environment.messenger.isLoaded.run = { false }
    store.environment.messenger.isCreated.run = { false }
    store.environment.authHandler.run = { _ in
      actions.append(.didStartAuthHandler)
      return Cancellable {}
    }
    store.environment.messageListener.run = { _ in
      actions.append(.didStartMessageListener)
      return Cancellable {}
    }

    store.send(.home(.deleteAccount(.success))) {
      $0.screen = .loading
    }

    store.receive(.set(\.$screen, .welcome(WelcomeState()))) {
      $0.screen = .welcome(WelcomeState())
    }

    XCTAssertNoDifference(actions, [
      .didStartAuthHandler,
      .didStartMessageListener,
    ])

    store.send(.stop)
  }

  func testWelcomeRestoreTapped() {
    let store = TestStore(
      initialState: AppState(
        screen: .welcome(WelcomeState())
      ),
      reducer: appReducer,
      environment: .unimplemented
    )

    store.send(.welcome(.restoreTapped)) {
      $0.screen = .restore(RestoreState())
    }
  }

  func testWelcomeFailed() {
    let failure = "Something went wrong"

    let store = TestStore(
      initialState: AppState(
        screen: .welcome(WelcomeState())
      ),
      reducer: appReducer,
      environment: .unimplemented
    )

    store.send(.welcome(.failed(failure))) {
      $0.screen = .failure(failure)
    }
  }

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

    let store = TestStore(
      initialState: AppState(),
      reducer: appReducer,
      environment: .unimplemented
    )

    store.environment.mainQueue = .immediate
    store.environment.bgQueue = .immediate
    store.environment.dbManager.hasDB.run = { false }
    store.environment.dbManager.makeDB.run = { throw error }

    store.send(.start)

    store.receive(.set(\.$screen, .failure(error.localizedDescription))) {
      $0.screen = .failure(error.localizedDescription)
    }

    store.send(.stop)
  }

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

    var actions: [Action] = []

    let store = TestStore(
      initialState: AppState(),
      reducer: appReducer,
      environment: .unimplemented
    )

    store.environment.mainQueue = .immediate
    store.environment.bgQueue = .immediate
    store.environment.dbManager.hasDB.run = { true }
    store.environment.messenger.isLoaded.run = { false }
    store.environment.messenger.isCreated.run = { true }
    store.environment.messenger.load.run = { throw error }
    store.environment.authHandler.run = { _ in
      actions.append(.didStartAuthHandler)
      return Cancellable {}
    }
    store.environment.messageListener.run = { _ in
      actions.append(.didStartMessageListener)
      return Cancellable {}
    }

    store.send(.start)

    store.receive(.set(\.$screen, .failure(error.localizedDescription))) {
      $0.screen = .failure(error.localizedDescription)
    }

    XCTAssertNoDifference(actions, [
      .didStartAuthHandler,
      .didStartMessageListener,
    ])

    store.send(.stop)
  }

  func testStartHandlersAndListeners() {
    var actions: [Action] = []
    var authHandlerOnError: [AuthCallbackHandler.OnError] = []
    var messageListenerOnError: [MessageListenerHandler.OnError] = []

    let store = TestStore(
      initialState: AppState(),
      reducer: appReducer,
      environment: .unimplemented
    )

    store.environment.mainQueue = .immediate
    store.environment.bgQueue = .immediate
    store.environment.dbManager.hasDB.run = { true }
    store.environment.messenger.isLoaded.run = { true }
    store.environment.messenger.isCreated.run = { true }
    store.environment.authHandler.run = { onError in
      authHandlerOnError.append(onError)
      actions.append(.didStartAuthHandler)
      return Cancellable {
        actions.append(.didCancelAuthHandler)
      }
    }
    store.environment.messageListener.run = { onError in
      messageListenerOnError.append(onError)
      actions.append(.didStartMessageListener)
      return Cancellable {
        actions.append(.didCancelMessageListener)
      }
    }

    store.send(.start)

    store.receive(.set(\.$screen, .home(HomeState()))) {
      $0.screen = .home(HomeState())
    }

    XCTAssertNoDifference(actions, [
      .didStartAuthHandler,
      .didStartMessageListener,
    ])
    actions = []

    store.send(.start) {
      $0.screen = .loading
    }

    store.receive(.set(\.$screen, .home(HomeState()))) {
      $0.screen = .home(HomeState())
    }

    XCTAssertNoDifference(actions, [
      .didCancelAuthHandler,
      .didCancelMessageListener,
      .didStartAuthHandler,
      .didStartMessageListener,
    ])
    actions = []

    struct AuthError: Error {}
    authHandlerOnError.first?(AuthError())

    XCTAssertNoDifference(actions, [])
    actions = []

    struct MessageError: Error {}
    messageListenerOnError.first?(MessageError())

    XCTAssertNoDifference(actions, [])
    actions = []

    store.send(.stop)

    XCTAssertNoDifference(actions, [
      .didCancelAuthHandler,
      .didCancelMessageListener,
    ])
  }
}

private enum Action: Equatable {
  case didMakeDB
  case didStartAuthHandler
  case didStartMessageListener
  case didLoadMessenger
  case didCancelAuthHandler
  case didCancelMessageListener
}