import * as utils from "../test-utils";
import {EventTimeline} from "../../src/models/event-timeline";
import {RoomState} from "../../src/models/room-state";

function mockRoomStates(timeline) {
    timeline._startState = utils.mock(RoomState, "startState");
    timeline._endState = utils.mock(RoomState, "endState");
}

describe("EventTimeline", function() {
    const roomId = "!foo:bar";
    const userA = "@alice:bar";
    const userB = "@bertha:bar";
    let timeline;

    beforeEach(function() {
        // XXX: this is a horrid hack; should use sinon or something instead to mock
        const timelineSet = { room: { roomId: roomId }};
        timelineSet.room.getUnfilteredTimelineSet = function() {
            return timelineSet;
        };

        timeline = new EventTimeline(timelineSet);
    });

    describe("construction", function() {
        it("getRoomId should get room id", function() {
            const v = timeline.getRoomId();
            expect(v).toEqual(roomId);
        });
    });

    describe("initialiseState", function() {
        beforeEach(function() {
            mockRoomStates(timeline);
        });

        it("should copy state events to start and end state", function() {
            const events = [
                utils.mkMembership({
                    room: roomId, mship: "invite", user: userB, skey: userA,
                    event: true,
                }),
                utils.mkEvent({
                    type: "m.room.name", room: roomId, user: userB,
                    event: true,
                    content: { name: "New room" },
                }),
            ];
            timeline.initialiseState(events);
            expect(timeline._startState.setStateEvents).toHaveBeenCalledWith(
                events,
            );
            expect(timeline._endState.setStateEvents).toHaveBeenCalledWith(
                events,
            );
        });

        it("should raise an exception if called after events are added", function() {
            const event =
                utils.mkMessage({
                    room: roomId, user: userA, msg: "Adam stole the plushies",
                    event: true,
                });

            const state = [
                utils.mkMembership({
                    room: roomId, mship: "invite", user: userB, skey: userA,
                    event: true,
                }),
            ];

            expect(function() {
                timeline.initialiseState(state);
            }).not.toThrow();
            timeline.addEvent(event, false);
            expect(function() {
                timeline.initialiseState(state);
            }).toThrow();
        });
    });

    describe("paginationTokens", function() {
        it("pagination tokens should start null", function() {
            expect(timeline.getPaginationToken(EventTimeline.BACKWARDS)).toBe(null);
            expect(timeline.getPaginationToken(EventTimeline.FORWARDS)).toBe(null);
        });

        it("setPaginationToken should set  token", function() {
            timeline.setPaginationToken("back", EventTimeline.BACKWARDS);
            timeline.setPaginationToken("fwd", EventTimeline.FORWARDS);
            expect(timeline.getPaginationToken(EventTimeline.BACKWARDS)).toEqual("back");
            expect(timeline.getPaginationToken(EventTimeline.FORWARDS)).toEqual("fwd");
        });
    });


    describe("neighbouringTimelines", function() {
        it("neighbouring timelines should start null", function() {
            expect(timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS)).toBe(null);
            expect(timeline.getNeighbouringTimeline(EventTimeline.FORWARDS)).toBe(null);
        });

        it("setNeighbouringTimeline should set neighbour", function() {
            const prev = {a: "a"};
            const next = {b: "b"};
            timeline.setNeighbouringTimeline(prev, EventTimeline.BACKWARDS);
            timeline.setNeighbouringTimeline(next, EventTimeline.FORWARDS);
            expect(timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS)).toBe(prev);
            expect(timeline.getNeighbouringTimeline(EventTimeline.FORWARDS)).toBe(next);
        });

        it("setNeighbouringTimeline should throw if called twice", function() {
            const prev = {a: "a"};
            const next = {b: "b"};
            expect(function() {
                timeline.setNeighbouringTimeline(prev, EventTimeline.BACKWARDS);
            }).not.toThrow();
            expect(timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS))
                .toBe(prev);
            expect(function() {
                timeline.setNeighbouringTimeline(prev, EventTimeline.BACKWARDS);
            }).toThrow();

            expect(function() {
                timeline.setNeighbouringTimeline(next, EventTimeline.FORWARDS);
            }).not.toThrow();
            expect(timeline.getNeighbouringTimeline(EventTimeline.FORWARDS))
                .toBe(next);
            expect(function() {
                timeline.setNeighbouringTimeline(next, EventTimeline.FORWARDS);
            }).toThrow();
        });
    });

    describe("addEvent", function() {
        beforeEach(function() {
            mockRoomStates(timeline);
        });

        const events = [
            utils.mkMessage({
                room: roomId, user: userA, msg: "hungry hungry hungry",
                event: true,
            }),
            utils.mkMessage({
                room: roomId, user: userB, msg: "nom nom nom",
                event: true,
            }),
        ];

        it("should be able to add events to the end", function() {
            timeline.addEvent(events[0], false);
            const initialIndex = timeline.getBaseIndex();
            timeline.addEvent(events[1], false);
            expect(timeline.getBaseIndex()).toEqual(initialIndex);
            expect(timeline.getEvents().length).toEqual(2);
            expect(timeline.getEvents()[0]).toEqual(events[0]);
            expect(timeline.getEvents()[1]).toEqual(events[1]);
        });

        it("should be able to add events to the start", function() {
            timeline.addEvent(events[0], true);
            const initialIndex = timeline.getBaseIndex();
            timeline.addEvent(events[1], true);
            expect(timeline.getBaseIndex()).toEqual(initialIndex + 1);
            expect(timeline.getEvents().length).toEqual(2);
            expect(timeline.getEvents()[0]).toEqual(events[1]);
            expect(timeline.getEvents()[1]).toEqual(events[0]);
        });

        it("should set event.sender for new and old events", function() {
            const sentinel = {
                userId: userA,
                membership: "join",
                name: "Alice",
            };
            const oldSentinel = {
                userId: userA,
                membership: "join",
                name: "Old Alice",
            };
            timeline.getState(EventTimeline.FORWARDS).getSentinelMember
                .mockImplementation(function(uid) {
                    if (uid === userA) {
                        return sentinel;
                    }
                    return null;
                });
            timeline.getState(EventTimeline.BACKWARDS).getSentinelMember
                .mockImplementation(function(uid) {
                    if (uid === userA) {
                        return oldSentinel;
                    }
                    return null;
                });

            const newEv = utils.mkEvent({
                type: "m.room.name", room: roomId, user: userA, event: true,
                content: { name: "New Room Name" },
            });
            const oldEv = utils.mkEvent({
                type: "m.room.name", room: roomId, user: userA, event: true,
                content: { name: "Old Room Name" },
            });

            timeline.addEvent(newEv, false);
            expect(newEv.sender).toEqual(sentinel);
            timeline.addEvent(oldEv, true);
            expect(oldEv.sender).toEqual(oldSentinel);
        });

        it("should set event.target for new and old m.room.member events",
        function() {
            const sentinel = {
                userId: userA,
                membership: "join",
                name: "Alice",
            };
            const oldSentinel = {
                userId: userA,
                membership: "join",
                name: "Old Alice",
            };
            timeline.getState(EventTimeline.FORWARDS).getSentinelMember
                .mockImplementation(function(uid) {
                    if (uid === userA) {
                        return sentinel;
                    }
                    return null;
                });
            timeline.getState(EventTimeline.BACKWARDS).getSentinelMember
                .mockImplementation(function(uid) {
                    if (uid === userA) {
                        return oldSentinel;
                    }
                    return null;
                });

            const newEv = utils.mkMembership({
                room: roomId, mship: "invite", user: userB, skey: userA, event: true,
            });
            const oldEv = utils.mkMembership({
                room: roomId, mship: "ban", user: userB, skey: userA, event: true,
            });
            timeline.addEvent(newEv, false);
            expect(newEv.target).toEqual(sentinel);
            timeline.addEvent(oldEv, true);
            expect(oldEv.target).toEqual(oldSentinel);
        });

        it("should call setStateEvents on the right RoomState with the right " +
           "forwardLooking value for new events", function() {
            const events = [
                utils.mkMembership({
                    room: roomId, mship: "invite", user: userB, skey: userA, event: true,
                }),
                utils.mkEvent({
                    type: "m.room.name", room: roomId, user: userB, event: true,
                    content: {
                        name: "New room",
                    },
                }),
            ];

            timeline.addEvent(events[0], false);
            timeline.addEvent(events[1], false);

            expect(timeline.getState(EventTimeline.FORWARDS).setStateEvents).
                toHaveBeenCalledWith([events[0]]);
            expect(timeline.getState(EventTimeline.FORWARDS).setStateEvents).
                toHaveBeenCalledWith([events[1]]);

            expect(events[0].forwardLooking).toBe(true);
            expect(events[1].forwardLooking).toBe(true);

            expect(timeline.getState(EventTimeline.BACKWARDS).setStateEvents).
                not.toHaveBeenCalled();
        });


        it("should call setStateEvents on the right RoomState with the right " +
           "forwardLooking value for old events", function() {
            const events = [
                utils.mkMembership({
                    room: roomId, mship: "invite", user: userB, skey: userA, event: true,
                }),
                utils.mkEvent({
                    type: "m.room.name", room: roomId, user: userB, event: true,
                    content: {
                        name: "New room",
                    },
                }),
            ];

            timeline.addEvent(events[0], true);
            timeline.addEvent(events[1], true);

            expect(timeline.getState(EventTimeline.BACKWARDS).setStateEvents).
                toHaveBeenCalledWith([events[0]]);
            expect(timeline.getState(EventTimeline.BACKWARDS).setStateEvents).
                toHaveBeenCalledWith([events[1]]);

            expect(events[0].forwardLooking).toBe(false);
            expect(events[1].forwardLooking).toBe(false);

            expect(timeline.getState(EventTimeline.FORWARDS).setStateEvents).
                not.toHaveBeenCalled();
        });
    });

    describe("removeEvent", function() {
        const events = [
            utils.mkMessage({
                room: roomId, user: userA, msg: "hungry hungry hungry",
                event: true,
            }),
            utils.mkMessage({
                room: roomId, user: userB, msg: "nom nom nom",
                event: true,
            }),
            utils.mkMessage({
                room: roomId, user: userB, msg: "piiie",
                event: true,
            }),
        ];

        it("should remove events", function() {
            timeline.addEvent(events[0], false);
            timeline.addEvent(events[1], false);
            expect(timeline.getEvents().length).toEqual(2);

            let ev = timeline.removeEvent(events[0].getId());
            expect(ev).toBe(events[0]);
            expect(timeline.getEvents().length).toEqual(1);

            ev = timeline.removeEvent(events[1].getId());
            expect(ev).toBe(events[1]);
            expect(timeline.getEvents().length).toEqual(0);
        });

        it("should update baseIndex", function() {
            timeline.addEvent(events[0], false);
            timeline.addEvent(events[1], true);
            timeline.addEvent(events[2], false);
            expect(timeline.getEvents().length).toEqual(3);
            expect(timeline.getBaseIndex()).toEqual(1);

            timeline.removeEvent(events[2].getId());
            expect(timeline.getEvents().length).toEqual(2);
            expect(timeline.getBaseIndex()).toEqual(1);

            timeline.removeEvent(events[1].getId());
            expect(timeline.getEvents().length).toEqual(1);
            expect(timeline.getBaseIndex()).toEqual(0);
        });

        // this is basically https://github.com/vector-im/vector-web/issues/937
        // - removing the last event got baseIndex into such a state that
        // further addEvent(ev, false) calls made the index increase.
        it("should not make baseIndex assplode when removing the last event",
           function() {
               timeline.addEvent(events[0], true);
               timeline.removeEvent(events[0].getId());
               const initialIndex = timeline.getBaseIndex();
               timeline.addEvent(events[1], false);
               timeline.addEvent(events[2], false);
               expect(timeline.getBaseIndex()).toEqual(initialIndex);
               expect(timeline.getEvents().length).toEqual(2);
           });
    });
});
