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

final class GroupComponentTests: XCTestCase {
  enum Action: Equatable {
    case didFetchGroupInfos(GroupInfo.Query)
    case didJoinGroup(Data)
    case didSaveGroup(XXModels.Group)
  }

  var actions: [Action]!

  override func setUp() {
    actions = []
  }

  override func tearDown() {
    actions = nil
  }

  func testStart() {
    let groupId = "group-id".data(using: .utf8)!
    let groupInfosSubject = PassthroughSubject<[GroupInfo], Error>()

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

    store.dependencies.app.mainQueue = .immediate
    store.dependencies.app.bgQueue = .immediate
    store.dependencies.app.dbManager.getDB.run = {
      var db: Database = .unimplemented
      db.fetchGroupInfosPublisher.run = { query in
        self.actions.append(.didFetchGroupInfos(query))
        return groupInfosSubject.eraseToAnyPublisher()
      }
      return db
    }

    store.send(.start)

    XCTAssertNoDifference(actions, [
      .didFetchGroupInfos(.init(groupId: groupId)),
    ])

    let groupInfo = GroupInfo.stub()
    groupInfosSubject.send([groupInfo])

    store.receive(.didFetchGroupInfo(groupInfo)) {
      $0.groupInfo = groupInfo
    }

    groupInfosSubject.send(completion: .finished)
  }

  func testJoinGroup() {
    var groupInfo = GroupInfo.stub()
    groupInfo.group.authStatus = .pending

    let store = TestStore(
      initialState: GroupComponent.State(
        groupId: groupInfo.group.id,
        groupInfo: groupInfo
      ),
      reducer: GroupComponent()
    )

    store.dependencies.app.mainQueue = .immediate
    store.dependencies.app.bgQueue = .immediate
    store.dependencies.app.messenger.groupChat.get = {
      var groupChat: GroupChat = .unimplemented
      groupChat.joinGroup.run = { serializedGroupData in
        self.actions.append(.didJoinGroup(serializedGroupData))
      }
      return groupChat
    }
    store.dependencies.app.dbManager.getDB.run = {
      var db: Database = .unimplemented
      db.saveGroup.run = { group in
        self.actions.append(.didSaveGroup(group))
        return group
      }
      return db
    }

    store.send(.joinButtonTapped) {
      $0.isJoining = true
    }

    XCTAssertNoDifference(actions, [
      .didJoinGroup(groupInfo.group.serialized),
      .didSaveGroup({
        var group = groupInfo.group
        group.authStatus = .participating
        return group
      }())
    ])

    store.receive(.didJoin) {
      $0.isJoining = false
    }
  }

  func testJoinGroupFailure() {
    let groupInfo = GroupInfo.stub()
    struct Failure: Error {}
    let failure = Failure()

    let store = TestStore(
      initialState: GroupComponent.State(
        groupId: groupInfo.group.id,
        groupInfo: groupInfo
      ),
      reducer: GroupComponent()
    )

    store.dependencies.app.mainQueue = .immediate
    store.dependencies.app.bgQueue = .immediate
    store.dependencies.app.messenger.groupChat.get = {
      var groupChat: GroupChat = .unimplemented
      groupChat.joinGroup.run = { _ in throw failure }
      return groupChat
    }

    store.send(.joinButtonTapped) {
      $0.isJoining = true
    }

    store.receive(.didFailToJoin(failure.localizedDescription)) {
      $0.isJoining = false
      $0.joinFailure = failure.localizedDescription
    }
  }
}

private extension XXModels.GroupInfo {
  static func stub() -> XXModels.GroupInfo {
    XXModels.GroupInfo(
      group: .init(
        id: "group-id".data(using: .utf8)!,
        name: "Group Name",
        leaderId: "group-leader-id".data(using: .utf8)!,
        createdAt: Date(timeIntervalSince1970: TimeInterval(86_400)),
        authStatus: .participating,
        serialized: "group-serialized".data(using: .utf8)!
      ),
      leader: .init(
        id: "group-leader-id".data(using: .utf8)!,
        username: "Group leader"
      ),
      members: [
        .init(
          id: "member-1-id".data(using: .utf8)!,
          username: "Member 1"
        ),
        .init(
          id: "member-2-id".data(using: .utf8)!,
          username: "Member 2"
        ),
      ]
    )
  }
}