"use strict";
import 'source-map-support/register';
const sdk = require("../..");
const EventStatus = sdk.EventStatus;
const HttpBackend = require("matrix-mock-request");
const utils = require("../test-utils");

import Promise from 'bluebird';
import expect from 'expect';

describe("MatrixClient room timelines", function() {
    const baseUrl = "http://localhost.or.something";
    let client = null;
    let httpBackend = null;
    const userId = "@alice:localhost";
    const userName = "Alice";
    const accessToken = "aseukfgwef";
    const roomId = "!foo:bar";
    const otherUserId = "@bob:localhost";
    const USER_MEMBERSHIP_EVENT = utils.mkMembership({
        room: roomId, mship: "join", user: userId, name: userName,
    });
    const ROOM_NAME_EVENT = utils.mkEvent({
        type: "m.room.name", room: roomId, user: otherUserId,
        content: {
            name: "Old room name",
        },
    });
    let NEXT_SYNC_DATA;
    const SYNC_DATA = {
        next_batch: "s_5_3",
        rooms: {
            join: {
                "!foo:bar": { // roomId
                    timeline: {
                        events: [
                            utils.mkMessage({
                                room: roomId, user: otherUserId, msg: "hello",
                            }),
                        ],
                        prev_batch: "f_1_1",
                    },
                    state: {
                        events: [
                            ROOM_NAME_EVENT,
                            utils.mkMembership({
                                room: roomId, mship: "join",
                                user: otherUserId, name: "Bob",
                            }),
                            USER_MEMBERSHIP_EVENT,
                            utils.mkEvent({
                                type: "m.room.create", room: roomId, user: userId,
                                content: {
                                    creator: userId,
                                },
                            }),
                        ],
                    },
                },
            },
        },
    };

    function setNextSyncData(events) {
        events = events || [];
        NEXT_SYNC_DATA = {
            next_batch: "n",
            presence: { events: [] },
            rooms: {
                invite: {},
                join: {
                    "!foo:bar": {
                        timeline: { events: [] },
                        state: { events: [] },
                        ephemeral: { events: [] },
                    },
                },
                leave: {},
            },
        };
        events.forEach(function(e) {
            if (e.room_id !== roomId) {
                throw new Error("setNextSyncData only works with one room id");
            }
            if (e.state_key) {
                if (e.__prev_event === undefined) {
                    throw new Error(
                        "setNextSyncData needs the prev state set to '__prev_event' " +
                        "for " + e.type,
                    );
                }
                if (e.__prev_event !== null) {
                    // push the previous state for this event type
                    NEXT_SYNC_DATA.rooms.join[roomId].state.events.push(e.__prev_event);
                }
                // push the current
                NEXT_SYNC_DATA.rooms.join[roomId].timeline.events.push(e);
            } else if (["m.typing", "m.receipt"].indexOf(e.type) !== -1) {
                NEXT_SYNC_DATA.rooms.join[roomId].ephemeral.events.push(e);
            } else {
                NEXT_SYNC_DATA.rooms.join[roomId].timeline.events.push(e);
            }
        });
    }

    beforeEach(function(done) {
        utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
        httpBackend = new HttpBackend();
        sdk.request(httpBackend.requestFn);
        client = sdk.createClient({
            baseUrl: baseUrl,
            userId: userId,
            accessToken: accessToken,
            // these tests should work with or without timelineSupport
            timelineSupport: true,
        });
        setNextSyncData();
        httpBackend.when("GET", "/pushrules").respond(200, {});
        httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
        httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
        httpBackend.when("GET", "/sync").respond(200, function() {
            return NEXT_SYNC_DATA;
        });
        client.startClient();
        httpBackend.flush("/pushrules").then(function() {
            return httpBackend.flush("/filter");
        }).nodeify(done);
    });

    afterEach(function() {
        httpBackend.verifyNoOutstandingExpectation();
        client.stopClient();
        return httpBackend.stop();
    });

    describe("local echo events", function() {
        it("should be added immediately after calling MatrixClient.sendEvent " +
        "with EventStatus.SENDING and the right event.sender", function(done) {
            client.on("sync", function(state) {
                if (state !== "PREPARED") {
                    return;
                }
                const room = client.getRoom(roomId);
                expect(room.timeline.length).toEqual(1);

                client.sendTextMessage(roomId, "I am a fish", "txn1");
                // check it was added
                expect(room.timeline.length).toEqual(2);
                // check status
                expect(room.timeline[1].status).toEqual(EventStatus.SENDING);
                // check member
                const member = room.timeline[1].sender;
                expect(member.userId).toEqual(userId);
                expect(member.name).toEqual(userName);

                httpBackend.flush("/sync", 1).done(function() {
                    done();
                });
            });
            httpBackend.flush("/sync", 1);
        });

        it("should be updated correctly when the send request finishes " +
        "BEFORE the event comes down the event stream", function(done) {
            const eventId = "$foo:bar";
            httpBackend.when("PUT", "/txn1").respond(200, {
                event_id: eventId,
            });

            const ev = utils.mkMessage({
                body: "I am a fish", user: userId, room: roomId,
            });
            ev.event_id = eventId;
            ev.unsigned = {transaction_id: "txn1"};
            setNextSyncData([ev]);

            client.on("sync", function(state) {
                if (state !== "PREPARED") {
                    return;
                }
                const room = client.getRoom(roomId);
                client.sendTextMessage(roomId, "I am a fish", "txn1").done(
                function() {
                    expect(room.timeline[1].getId()).toEqual(eventId);
                    httpBackend.flush("/sync", 1).done(function() {
                        expect(room.timeline[1].getId()).toEqual(eventId);
                        done();
                    });
                });
                httpBackend.flush("/txn1", 1);
            });
            httpBackend.flush("/sync", 1);
        });

        it("should be updated correctly when the send request finishes " +
        "AFTER the event comes down the event stream", function(done) {
            const eventId = "$foo:bar";
            httpBackend.when("PUT", "/txn1").respond(200, {
                event_id: eventId,
            });

            const ev = utils.mkMessage({
                body: "I am a fish", user: userId, room: roomId,
            });
            ev.event_id = eventId;
            ev.unsigned = {transaction_id: "txn1"};
            setNextSyncData([ev]);

            client.on("sync", function(state) {
                if (state !== "PREPARED") {
                    return;
                }
                const room = client.getRoom(roomId);
                const promise = client.sendTextMessage(roomId, "I am a fish", "txn1");
                httpBackend.flush("/sync", 1).done(function() {
                    expect(room.timeline.length).toEqual(2);
                    httpBackend.flush("/txn1", 1);
                    promise.done(function() {
                        expect(room.timeline.length).toEqual(2);
                        expect(room.timeline[1].getId()).toEqual(eventId);
                        done();
                    });
                });
            });
            httpBackend.flush("/sync", 1);
        });
    });

    describe("paginated events", function() {
        let sbEvents;
        const sbEndTok = "pagin_end";

        beforeEach(function() {
            sbEvents = [];
            httpBackend.when("GET", "/messages").respond(200, function() {
                return {
                    chunk: sbEvents,
                    start: "pagin_start",
                    end: sbEndTok,
                };
            });
        });

        it("should set Room.oldState.paginationToken to null at the start" +
        " of the timeline.", function(done) {
            client.on("sync", function(state) {
                if (state !== "PREPARED") {
                    return;
                }
                const room = client.getRoom(roomId);
                expect(room.timeline.length).toEqual(1);

                client.scrollback(room).done(function() {
                    expect(room.timeline.length).toEqual(1);
                    expect(room.oldState.paginationToken).toBe(null);

                    // still have a sync to flush
                    httpBackend.flush("/sync", 1).then(() => {
                        done();
                    });
                });

                httpBackend.flush("/messages", 1);
            });
            httpBackend.flush("/sync", 1);
        });

        it("should set the right event.sender values", function(done) {
            // We're aiming for an eventual timeline of:
            //
            // 'Old Alice' joined the room
            // <Old Alice> I'm old alice
            // @alice:localhost changed their name from 'Old Alice' to 'Alice'
            // <Alice> I'm alice
            // ------^ /messages results above this point, /sync result below
            // <Bob> hello

            // make an m.room.member event for alice's join
            const joinMshipEvent = utils.mkMembership({
                mship: "join", user: userId, room: roomId, name: "Old Alice",
                url: null,
            });

            // make an m.room.member event with prev_content for alice's nick
            // change
            const oldMshipEvent = utils.mkMembership({
                mship: "join", user: userId, room: roomId, name: userName,
                url: "mxc://some/url",
            });
            oldMshipEvent.prev_content = {
                displayname: "Old Alice",
                avatar_url: null,
                membership: "join",
            };

            // set the list of events to return on scrollback (/messages)
            // N.B. synapse returns /messages in reverse chronological order
            sbEvents = [
                utils.mkMessage({
                    user: userId, room: roomId, msg: "I'm alice",
                }),
                oldMshipEvent,
                utils.mkMessage({
                    user: userId, room: roomId, msg: "I'm old alice",
                }),
                joinMshipEvent,
            ];

            client.on("sync", function(state) {
                if (state !== "PREPARED") {
                    return;
                }
                const room = client.getRoom(roomId);
                // sync response
                expect(room.timeline.length).toEqual(1);

                client.scrollback(room).done(function() {
                    expect(room.timeline.length).toEqual(5);
                    const joinMsg = room.timeline[0];
                    expect(joinMsg.sender.name).toEqual("Old Alice");
                    const oldMsg = room.timeline[1];
                    expect(oldMsg.sender.name).toEqual("Old Alice");
                    const newMsg = room.timeline[3];
                    expect(newMsg.sender.name).toEqual(userName);

                    // still have a sync to flush
                    httpBackend.flush("/sync", 1).then(() => {
                        done();
                    });
                });

                httpBackend.flush("/messages", 1);
            });
            httpBackend.flush("/sync", 1);
        });

        it("should add it them to the right place in the timeline", function(done) {
            // set the list of events to return on scrollback
            sbEvents = [
                utils.mkMessage({
                    user: userId, room: roomId, msg: "I am new",
                }),
                utils.mkMessage({
                    user: userId, room: roomId, msg: "I am old",
                }),
            ];

            client.on("sync", function(state) {
                if (state !== "PREPARED") {
                    return;
                }
                const room = client.getRoom(roomId);
                expect(room.timeline.length).toEqual(1);

                client.scrollback(room).done(function() {
                    expect(room.timeline.length).toEqual(3);
                    expect(room.timeline[0].event).toEqual(sbEvents[1]);
                    expect(room.timeline[1].event).toEqual(sbEvents[0]);

                    // still have a sync to flush
                    httpBackend.flush("/sync", 1).then(() => {
                        done();
                    });
                });

                httpBackend.flush("/messages", 1);
            });
            httpBackend.flush("/sync", 1);
        });

        it("should use 'end' as the next pagination token", function(done) {
            // set the list of events to return on scrollback
            sbEvents = [
                utils.mkMessage({
                    user: userId, room: roomId, msg: "I am new",
                }),
            ];

            client.on("sync", function(state) {
                if (state !== "PREPARED") {
                    return;
                }
                const room = client.getRoom(roomId);
                expect(room.oldState.paginationToken).toBeTruthy();

                client.scrollback(room, 1).done(function() {
                    expect(room.oldState.paginationToken).toEqual(sbEndTok);
                });

                httpBackend.flush("/messages", 1).done(function() {
                    // still have a sync to flush
                    httpBackend.flush("/sync", 1).then(() => {
                        done();
                    });
                });
            });
            httpBackend.flush("/sync", 1);
        });
    });

    describe("new events", function() {
        it("should be added to the right place in the timeline", function() {
            const eventData = [
                utils.mkMessage({user: userId, room: roomId}),
                utils.mkMessage({user: userId, room: roomId}),
            ];
            setNextSyncData(eventData);

            return Promise.all([
                httpBackend.flush("/sync", 1),
                utils.syncPromise(client),
            ]).then(() => {
                const room = client.getRoom(roomId);

                let index = 0;
                client.on("Room.timeline", function(event, rm, toStart) {
                    expect(toStart).toBe(false);
                    expect(rm).toEqual(room);
                    expect(event.event).toEqual(eventData[index]);
                    index += 1;
                });

                httpBackend.flush("/messages", 1);
                return Promise.all([
                    httpBackend.flush("/sync", 1),
                    utils.syncPromise(client),
                ]).then(function() {
                    expect(index).toEqual(2);
                    expect(room.timeline.length).toEqual(3);
                    expect(room.timeline[2].event).toEqual(
                        eventData[1],
                    );
                    expect(room.timeline[1].event).toEqual(
                        eventData[0],
                    );
                });
            });
        });

        it("should set the right event.sender values", function() {
            const eventData = [
                utils.mkMessage({user: userId, room: roomId}),
                utils.mkMembership({
                    user: userId, room: roomId, mship: "join", name: "New Name",
                }),
                utils.mkMessage({user: userId, room: roomId}),
            ];
            eventData[1].__prev_event = USER_MEMBERSHIP_EVENT;
            setNextSyncData(eventData);

            return Promise.all([
                httpBackend.flush("/sync", 1),
                utils.syncPromise(client),
            ]).then(() => {
                const room = client.getRoom(roomId);
                return Promise.all([
                    httpBackend.flush("/sync", 1),
                    utils.syncPromise(client),
                ]).then(function() {
                    const preNameEvent = room.timeline[room.timeline.length - 3];
                    const postNameEvent = room.timeline[room.timeline.length - 1];
                    expect(preNameEvent.sender.name).toEqual(userName);
                    expect(postNameEvent.sender.name).toEqual("New Name");
                });
            });
        });

        it("should set the right room.name", function() {
            const secondRoomNameEvent = utils.mkEvent({
                user: userId, room: roomId, type: "m.room.name", content: {
                    name: "Room 2",
                },
            });
            secondRoomNameEvent.__prev_event = ROOM_NAME_EVENT;
            setNextSyncData([secondRoomNameEvent]);

            return Promise.all([
                httpBackend.flush("/sync", 1),
                utils.syncPromise(client),
            ]).then(() => {
                const room = client.getRoom(roomId);
                let nameEmitCount = 0;
                client.on("Room.name", function(rm) {
                    nameEmitCount += 1;
                });

                return Promise.all([
                    httpBackend.flush("/sync", 1),
                    utils.syncPromise(client),
                ]).then(function() {
                    expect(nameEmitCount).toEqual(1);
                    expect(room.name).toEqual("Room 2");
                    // do another round
                    const thirdRoomNameEvent = utils.mkEvent({
                        user: userId, room: roomId, type: "m.room.name", content: {
                            name: "Room 3",
                        },
                    });
                    thirdRoomNameEvent.__prev_event = secondRoomNameEvent;
                    setNextSyncData([thirdRoomNameEvent]);
                    httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
                    return Promise.all([
                        httpBackend.flush("/sync", 1),
                        utils.syncPromise(client),
                    ]);
                }).then(function() {
                    expect(nameEmitCount).toEqual(2);
                    expect(room.name).toEqual("Room 3");
                });
            });
        });

        it("should set the right room members", function() {
            const userC = "@cee:bar";
            const userD = "@dee:bar";
            const eventData = [
                utils.mkMembership({
                    user: userC, room: roomId, mship: "join", name: "C",
                }),
                utils.mkMembership({
                    user: userC, room: roomId, mship: "invite", skey: userD,
                }),
            ];
            eventData[0].__prev_event = null;
            eventData[1].__prev_event = null;
            setNextSyncData(eventData);

            return Promise.all([
                httpBackend.flush("/sync", 1),
                utils.syncPromise(client),
            ]).then(() => {
                const room = client.getRoom(roomId);
                return Promise.all([
                    httpBackend.flush("/sync", 1),
                    utils.syncPromise(client),
                ]).then(function() {
                    expect(room.currentState.getMembers().length).toEqual(4);
                    expect(room.currentState.getMember(userC).name).toEqual("C");
                    expect(room.currentState.getMember(userC).membership).toEqual(
                        "join",
                    );
                    expect(room.currentState.getMember(userD).name).toEqual(userD);
                    expect(room.currentState.getMember(userD).membership).toEqual(
                        "invite",
                    );
                });
            });
        });
    });

    describe("gappy sync", function() {
        it("should copy the last known state to the new timeline", function() {
            const eventData = [
                utils.mkMessage({user: userId, room: roomId}),
            ];
            setNextSyncData(eventData);
            NEXT_SYNC_DATA.rooms.join[roomId].timeline.limited = true;

            return Promise.all([
                httpBackend.flush("/sync", 1),
                utils.syncPromise(client),
            ]).then(() => {
                const room = client.getRoom(roomId);

                httpBackend.flush("/messages", 1);
                return Promise.all([
                    httpBackend.flush("/sync", 1),
                    utils.syncPromise(client),
                ]).then(function() {
                    expect(room.timeline.length).toEqual(1);
                    expect(room.timeline[0].event).toEqual(eventData[0]);
                    expect(room.currentState.getMembers().length).toEqual(2);
                    expect(room.currentState.getMember(userId).name).toEqual(userName);
                    expect(room.currentState.getMember(userId).membership).toEqual(
                        "join",
                    );
                    expect(room.currentState.getMember(otherUserId).name).toEqual("Bob");
                    expect(room.currentState.getMember(otherUserId).membership).toEqual(
                        "join",
                    );
                });
            });
        });

        it("should emit a 'Room.timelineReset' event", function() {
            const eventData = [
                utils.mkMessage({user: userId, room: roomId}),
            ];
            setNextSyncData(eventData);
            NEXT_SYNC_DATA.rooms.join[roomId].timeline.limited = true;

            return Promise.all([
                httpBackend.flush("/sync", 1),
                utils.syncPromise(client),
            ]).then(() => {
                const room = client.getRoom(roomId);

                let emitCount = 0;
                client.on("Room.timelineReset", function(emitRoom) {
                    expect(emitRoom).toEqual(room);
                    emitCount++;
                });

                httpBackend.flush("/messages", 1);
                return Promise.all([
                    httpBackend.flush("/sync", 1),
                    utils.syncPromise(client),
                ]).then(function() {
                    expect(emitCount).toEqual(1);
                });
            });
        });
    });
});
