/*
 Copyright 2021 The Matrix.org Foundation C.I.C

 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at

 http://www.apache.org/licenses/LICENSE-2.0

 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
 */

import Foundation
import XCTest

@testable import Element

class MXKRoomDataSourceTests: XCTestCase {

    // MARK: - Destruction tests

    func testDestroyRemovesAllBubbles() {
        let dataSource = StubMXKRoomDataSource()
        dataSource.destroy()
        XCTAssert(dataSource.getBubbles()?.isEmpty != false)
    }

    func testDestroyDeallocatesAllBubbles() throws {
        let dataSource = StubMXKRoomDataSource()
        weak var first = try XCTUnwrap(dataSource.getBubbles()?.first)
        weak var last = try XCTUnwrap(dataSource.getBubbles()?.last)
        dataSource.destroy()
        XCTAssertNil(first)
        XCTAssertNil(last)
    }

    // MARK: - Collapsing tests

    func testCollapseBubblesWhenProcessingTogether() throws {
        let dataSource = try FakeMXKRoomDataSource.make()
        try dataSource.queueEvent1()
        try dataSource.queueEvent2()
        awaitEventProcessing(for: dataSource)
        dataSource.verifyCollapsedEvents(2)
    }

    func testCollapseBubblesWhenProcessingAlone() throws {
        let dataSource = try FakeMXKRoomDataSource.make()
        try dataSource.queueEvent1()
        awaitEventProcessing(for: dataSource)
        try dataSource.queueEvent2()
        awaitEventProcessing(for: dataSource)
        dataSource.verifyCollapsedEvents(2)
    }

    private func awaitEventProcessing(for dataSource: MXKRoomDataSource) {
        let e = expectation(description: "The wai-ai-ting is the hardest part")
        dataSource.processQueuedEvents { _, _ in
            e.fulfill()
        }
        waitForExpectations(timeout: 2) { error in
            XCTAssertNil(error)
        }
    }

}

// MARK: - Test doubles

private final class StubMXKRoomDataSource: MXKRoomDataSource {

    override init() {
        super.init()

        let data1 = MXKRoomBubbleCellData()
        let data2 = MXKRoomBubbleCellData()
        let data3 = MXKRoomBubbleCellData()

        data1.nextCollapsableCellData = data2
        data2.prevCollapsableCellData = data1
        data2.nextCollapsableCellData = data3
        data3.prevCollapsableCellData = data2

        replaceBubbles([data1, data2, data3])
    }

}

private final class FakeMXKRoomDataSource: MXKRoomDataSource {

    class func make() throws -> FakeMXKRoomDataSource {
        let dataSource = try XCTUnwrap(FakeMXKRoomDataSource(roomId: "!foofoofoofoofoofoo:matrix.org", andMatrixSession: nil, threadId: nil))
        dataSource.registerCellDataClass(CollapsibleBubbleCellData.self, forCellIdentifier: kMXKRoomBubbleCellDataIdentifier)
        dataSource.eventFormatter = CountingEventFormatter(matrixSession: nil)
        return dataSource
    }

    override var state: MXKDataSourceState {
        MXKDataSourceStateReady
    }

    override var roomState: MXRoomState! {
        nil
    }

    func queueEvent1() throws {
        try queueEvent(json: #"{"sender":"@alice:matrix.org","content":{"displayname":"bob","membership":"invite"},"origin_server_ts":1616488993287,"state_key":"@bob:matrix.org","room_id":"!foofoofoofoofoofoo:matrix.org","event_id":"$lGK3budX5w009ErtQwE9ZFhwyUUAV9DqEN5yb2fI4Do","type":"m.room.member","unsigned":{"age":1204610,"prev_sender":"@alice:matrix.org","prev_content":{"membership":"leave"},"replaces_state":"$9mQ6RtscXqHCxWqOElI-eP_kwpkuPd2Czm3UHviGoyE"}}"#)
    }

    func queueEvent2() throws {
        try queueEvent(json: #"{"sender":"@alice:matrix.org","content":{"displayname":"john","membership":"invite"},"origin_server_ts":1616488967295,"state_key":"@john:matrix.org","room_id":"!foofoofoofoofoofoo:matrix.org","event_id":"$-00slfAluxVTP2VWytgDThTmh3nLd0WJD6gzBo2scJM","type":"m.room.member","unsigned":{"age":1712006,"prev_sender":"@alice:matrix.org","prev_content":{"membership":"leave"},"replaces_state":"$NRNkCMKeKK5NtTfWkMfTlMr5Ygw60Q2CQYnJNkbzyrs"}}"#)
    }

    private func queueEvent(json: String) throws {
        let data = try XCTUnwrap(json.data(using: .utf8))
        let dict = try XCTUnwrap((try JSONSerialization.jsonObject(with: data, options: [])) as? [AnyHashable: Any])
        let event = MXEvent(fromJSON: dict)
        queueEvent(forProcessing: event, with: nil, direction: .forwards)
    }

    func verifyCollapsedEvents(_ number: Int) {
        let message = getBubbles()?.first?.collapsedAttributedTextMessage.string
        XCTAssertEqual(message, "\(number)")
    }

}

private final class CollapsibleBubbleCellData: MXKRoomBubbleCellData {

    override init() {
        super.init()
    }

    required init!(event: MXEvent!, andRoomState roomState: MXRoomState!, andRoomDataSource roomDataSource: MXKRoomDataSource!) {
        super.init(event: event, andRoomState: roomState, andRoomDataSource: roomDataSource)
        collapsable = true
    }

    override func collapse(with cellData: MXKRoomBubbleCellDataStoring!) -> Bool {
        true
    }

}

private final class CountingEventFormatter: MXKEventFormatter {

    override func attributedString(from events: [MXEvent]!,
                                   with roomState: MXRoomState!,
                                   andLatestRoomState latestRoomState: MXRoomState!,
                                   error: UnsafeMutablePointer<MXKEventFormatterError>!) -> NSAttributedString! {
        NSAttributedString(string: "\(events.count)")
    }
}
