describe('EventHandlerService', function() {
    var scope, $q, $interval, $window;
    
    var testContainsBingWords, testPresenceState, testRoomName; // mPresence, mRoomNameFilter, notificationService
    var testUserId, testDisplayName, testBingWords; // matrixService.config
    var testResolvedRoomId, testJoinSuccess, testRoomInitialSync, testConfig; // matrixService
    var testNowState, testOldState, testEvents, testChangedKey; // modelService
    
    var modelService = {
        getRoom: function(roomId) {
            // FIXME: This shouldn't return a new object every time, we should be spyOn'ing rather the all this crap
            return {
                room_id: roomId,
                current_room_state: testNowState,
                now: testNowState,
                old_room_state: testOldState,
                aevents: testEvents,
                addMessageEvent: function(event, toFront) {
                    var annotatedEvent = {event: event};
                    if (toFront) {
                        testEvents.unshift(annotatedEvent);
                    }
                    else {
                        testEvents.push(annotatedEvent);
                    }
                    return annotatedEvent;
                },
                addOrReplaceMessageEvent: function(event, toFront) {
                    for (var i = this.aevents.length - 1; i >= 0; i--) {
                        var storedEvent = this.aevents[i].event;
                        if (storedEvent.event_id === event.event_id) {
                            this.aevents[i] = {event: event};
                            return this.aevents[i];
                        }
                    }
                    return this.addMessageEvent(event, toFront);
                },
                getAnnotatedEvent: function(eventId) {
                    for (var i = this.aevents.length - 1; i >= 0; i--) {
                        var storedEvent = this.aevents[i];
                        if (storedEvent.event.event_id === eventId) {
                            return storedEvent;
                        }
                    }
                },
                removeEvent: function(event) {
                    for (var i=0; i<this.aevents.length; i++) {
                        if (this.aevents[i].event == event) {
                            this.aevents.splice(i, 1);
                            break;
                        }
                    }
                },
                mutateRoomMemberState: function(){},
                getChangedKeyForMemberEvent: function(){
                    return testChangedKey;
                },
                isJoinedRoom: function(userId){
                    return testNowState.members[userId] && testNowState.members[userId].event.content.membership === "join";
                }
            };
        },
        createRoomIdToAliasMapping: function(roomId, alias) {
        
        },
        getMember: function(roomId, userId) {
        
        },
        setUser: function(presenceEvent) {
        
        }
    };
    
    var matrixService = {
        resolveRoomAlias: function(alias) {
            var defer = $q.defer();
            if (testResolvedRoomId) {
                defer.resolve({ data: { room_id: testResolvedRoomId} });
            }
            else {
                defer.reject({ data: { error: "some resolveRoomAlias error", errcode: "M_UNKNOWN" } });
            }
            return defer.promise;
        },
        join: function(roomId) {
            var defer = $q.defer();
            if (testJoinSuccess) {
                defer.resolve({});
            }
            else {
                defer.reject({ data: { error: "some join error", errcode: "M_UNKNOWN" } });
            }
            return defer.promise;
        },
        roomInitialSync: function(roomId) {
            var defer = $q.defer();
            if (testRoomInitialSync) {
                defer.resolve({ data: testRoomInitialSync });
            }
            else {
                defer.reject({ data: { error: "some roomState error", errcode: "M_UNKNOWN" } });
            }
            return defer.promise;
        },
        config: function() {
            return testConfig;
        },
        sendTextMessage: function(roomId, input) {
            var defer = $q.defer();
            if (testSendMessage) {
                defer.resolve({ data: testSendMessage });
            }
            else {
                defer.reject({ data: { error: "some testSendMessage error", errcode: "M_UNKNOWN" } });
            }
            return defer.promise;
        },
        create: function(){},
        presence: { unavailable: "unavailable", online: "online" }
    };
    
    var notificationService = {
        processEvent: function(ev) {
        },
        shouldHighlightEvent: function(ev) {
            return testContainsBingWords;
        },
        showNotification: function(title, msg, pic, onclick) {
        
        }
    };
    
    var mPresence = {
        getState: function() {
            return testPresenceState;
        }
    };
    
    var commandsService = {
        processInput: function(roomId, input) {}
    };
    
    var mRoomNameFilter = function(){
        return function() {
            return testRoomName;
        };
    };
    
    var mUserDisplayNameFilter = function() {
        return function(input) {
            return input;
        };
    };
    
    // helper function
    var mkEvent = function(evType, content, rmId, stateKey, usrId) {
        var eventId = Math.random().toString(36);
        if (!usrId) {
            usrId = testUserId;
        }
        return {
            event_id: eventId,
            user_id: usrId,
            state_key: stateKey,
            room_id: rmId,
            content: content,
            type: evType
        };
    };
    

    // setup the service and mocked dependencies
    beforeEach(function() {
        // reset test vars
        testContainsBingWords = false;
        testPresenceState = matrixService.presence.unavailable;
        
        testUserId = "@me:matrix.org";
        testDisplayName = "Me";
        testBingWords = [];
        testChangedKey = undefined;
        
        testRoomName = "Room Name";
        testJoinSuccess = true;
        testResolvedRoomId = "!foo:matrix.org";
        testRoomInitialSync = {
            state: [],
            messages: {
                chunk: [],
                start: "s",
                end: "e"
            },
            presence: []
        };
        testEvents = [];
        testSendMessage = {
            event_id: "foobar"
        };
        testInitialSync = {
            data: {
                rooms: [],
                presence: []
            }
        };
        
        testConfig = {
            user_id: testUserId,
            display_name: testDisplayName,
            bingWords: testBingWords
        };
        
        testNowState = {
            members: {},
            s: {},
            state: function(t,k) { 
                return k ? this.s[t+k] : this.s[t]; 
            },
            getStateEvent: function(t,k) {
                return this.state(t,k);
            },
            storeStateEvents: function(events) {
                if (!events) { return; }
                for (var i=0; i<events.length; i++) {
                    this.storeStateEvent(events[i]);
                }
            },
            storeStateEvent: function(event) {
                if (!event) { return; }
                if (event.state_key) {
                    this.s[event.type + event.state_key] = event;
                }
                else {
                    this.s[event.type] = event;
                }
            },
            // test helper methods
            setRoomName: function(name) {
                this.s["m.room.name"] = {
                    content: {
                        name: name
                    }
                };
            },
            setMember: function(user_id, membership, inviter_user_id) {
                if (!inviter_user_id) {
                    inviter_user_id = user_id;
                }
                this.s["m.room.member" + user_id] = {
                    event: {
                        content: {
                            membership: membership
                        },
                        state_key: user_id,
                        user_id: inviter_user_id 
                    }
                };
                this.members[user_id] = this.s["m.room.member" + user_id];
            }
        };
        testOldState = angular.copy(testNowState);
        
        // mocked dependencies
        module(function ($provide) {
            $provide.value('modelService', modelService);
            $provide.value('matrixService', matrixService);
            $provide.value('notificationService', notificationService);
            $provide.value('mPresence', mPresence);
            $provide.value('commandsService', commandsService);
            $provide.factory('mRoomNameFilter', mRoomNameFilter);
            $provide.factory('mUserDisplayNameFilter', mUserDisplayNameFilter);
        });
        
        // tested service
        module('eventHandlerService');
    });
    
    beforeEach(inject(function($rootScope, _$q_, _$interval_, _$window_) {
        scope = $rootScope;
        $q = _$q_;
        $interval = _$interval_;
        $window = _$window_;
    }));

    it('joinRoom: should be able to join a room from a room ID.', inject(
    function(eventHandlerService) {
        eventHandlerService.handleInitialSyncDone(testInitialSync);
        var roomId = "!foobar:matrix.org";
        testRoomInitialSync.state = [
            {
                event_id: "a",
                user_id: "@bob:matrix.org",
                state_key: "@bob:matrix.org",
                room_id: roomId,
                content: { membership: "join" },
            },
            {
                event_id: "b",
                user_id: testUserId,
                state_key: testUserId,
                room_id: roomId,
                content: { membership: "join" },
            }
        
        ];
        
        spyOn(matrixService, "join").and.callThrough();
        spyOn(matrixService, "roomInitialSync").and.callThrough();
        var promiseResult = undefined;
        eventHandlerService.joinRoom(roomId).then(function(r) {
            promiseResult = r;
        }, function(err) {
            promiseResult = err;
        });
        scope.$digest(); // resolve stuff
        expect(matrixService.join).toHaveBeenCalledWith(roomId);
        expect(matrixService.roomInitialSync).toHaveBeenCalledWith(roomId, jasmine.any(Number));
        expect(promiseResult).toEqual(roomId);
    }));
    
    it('joinRoom: should be able to join a room from a room alias.', inject(
    function(eventHandlerService) {
        eventHandlerService.handleInitialSyncDone(testInitialSync);
        var roomAlias = "#flibble:matrix.org";
        testRoomInitialSync.state = [
            {
                event_id: "a",
                user_id: "@bob:matrix.org",
                state_key: "@bob:matrix.org",
                room_id: testResolvedRoomId,
                content: { membership: "join" },
            },
            {
                event_id: "b",
                user_id: testUserId,
                state_key: testUserId,
                room_id: testResolvedRoomId,
                content: { membership: "join" },
            }
        
        ];
        
        spyOn(matrixService, "resolveRoomAlias").and.callThrough();
        spyOn(matrixService, "roomInitialSync").and.callThrough();
        spyOn(testNowState, "storeStateEvents").and.callThrough();
        var promiseResult = undefined;
        eventHandlerService.joinRoom(roomAlias).then(function(r) {
            promiseResult = r;
        }, function(err) {
            promiseResult = err;
        });
        scope.$digest(); // resolve stuff
        expect(matrixService.resolveRoomAlias).toHaveBeenCalledWith(roomAlias);
        expect(matrixService.roomInitialSync).toHaveBeenCalledWith(testResolvedRoomId, jasmine.any(Number));
        expect(testNowState.storeStateEvents).toHaveBeenCalledWith(testRoomInitialSync.state);
        expect(promiseResult).toEqual(testResolvedRoomId);
    }));
    
    // SYWEB-181
    it('should inject events into a room when joining if there are some.', inject(
    function(eventHandlerService) {
        eventHandlerService.handleInitialSyncDone(testInitialSync);
        var roomId = "!foobar:matrix.org";
        testRoomInitialSync.state = [
            {
                event_id: "a",
                user_id: "@bob:matrix.org",
                state_key: "@bob:matrix.org",
                room_id: roomId,
                content: { membership: "join" },
                type: "m.room.member"
            },
            {
                event_id: "b",
                user_id: testUserId,
                state_key: testUserId,
                room_id: roomId,
                content: { membership: "join" },
                type: "m.room.member"
            }
        
        ];
        testRoomInitialSync.messages.chunk = [
            {
                event_id: "c",
                user_id: "@bob:matrix.org",
                room_id: roomId,
                content: { 
                    msgtype: "m.text",
                    body: "Hello1"
                },
                type: "m.room.message"
            },
            {
                event_id: "d",
                user_id: "@bob:matrix.org",
                room_id: roomId,
                content: { 
                    msgtype: "m.text",
                    body: "Hello2"
                },
                type: "m.room.message"
            }
        ];
        
        var promiseResult = undefined;
        eventHandlerService.joinRoom(roomId).then(function(r) {
            promiseResult = r;
        }, function(err) {
            promiseResult = err;
        });
        scope.$digest(); // resolve stuff
        
        for (var i=0; i<testEvents.length; i++) {
            expect(testEvents[i].event).toEqual(testRoomInitialSync.messages.chunk[i]);
        }
        
        expect(promiseResult).toEqual(roomId);
    }));
    
    it('should NOT join a room that has been joined already.', inject(
    function(eventHandlerService) {
        eventHandlerService.handleInitialSyncDone(testInitialSync);
        var roomId = "!foobar:matrix.org";
        testNowState.setMember(testUserId, "join");
        
        var promiseResult = undefined;
        eventHandlerService.joinRoom(roomId).then(function(r) {
            promiseResult = r;
        }, function(err) {
            promiseResult = err;
        });
        
        spyOn(matrixService, "join");
        scope.$digest(); // resolve stuff
        expect(matrixService.join).not.toHaveBeenCalled();
        expect(promiseResult).toEqual(roomId);
    }));
    
    it('should waitForInitialSyncCompletion.', inject(
    function(eventHandlerService) {
        var promiseResult = undefined;
        eventHandlerService.waitForInitialSyncCompletion().then(function(r) {
            promiseResult = r;
        }, function(err) {
            promiseResult = err;
        });
        expect(promiseResult).toBeUndefined();
        eventHandlerService.handleInitialSyncDone(testInitialSync);
        scope.$digest(); // resolve stuff
        expect(promiseResult).toBeDefined();
    }));
    
    it('should be able to handle multiple events.', inject(
    function(eventHandlerService) {
        spyOn(modelService, "setUser");
        eventHandlerService.handleInitialSyncDone(testInitialSync);
        
        var eventA = {
            content: {
                user_id: "@alice:matrix.org",
                presence: "online"
            },
            user_id: "@alice:matrix.org",
            type: "m.presence"
        };
        
        var eventB = {
            content: {
                user_id: "@bob:matrix.org",
                presence: "unavailable"
            },
            user_id: "@bob:matrix.org",
            type: "m.presence"
        };
        
        var eventC = {
            content: {
                user_id: "@claire:matrix.org",
                presence: "online"
            },
            user_id: "@claire:matrix.org",
            type: "m.presence"
        };
        
        eventHandlerService.handleEvents([eventA, eventB, eventC]);
        expect(modelService.setUser).toHaveBeenCalledWith(eventA);
        expect(modelService.setUser).toHaveBeenCalledWith(eventB);
        expect(modelService.setUser).toHaveBeenCalledWith(eventC);
    }));
    
    it('should be able to process m.presence events.', inject(
    function(eventHandlerService) {
        eventHandlerService.handleInitialSyncDone(testInitialSync);
        
        spyOn(modelService, "setUser");
        var event = {
            content: {
                user_id: "@claire:matrix.org",
                presence: "online"
            },
            user_id: "@claire:matrix.org",
            type: "m.presence"
        };
        eventHandlerService.handleEvent(event);
        expect(modelService.setUser).toHaveBeenCalledWith(event);
    }));
    
    it('should be able to process redaction events and remove the redacted event.', inject(
    function(eventHandlerService) {
        var badEventId = "wefiuwehf";
        eventHandlerService.handleInitialSyncDone(testInitialSync);
        
        var event = {
            content: {
                body: "something naughty",
                msgtype: "m.text"
            },
            user_id: "@claire:matrix.org",
            room_id: "!foobar:matrix.org",
            type: "m.room.message",
            event_id: badEventId
        };
        eventHandlerService.handleEvent(event, true);
        expect(testEvents[0].event).toEqual(event);
        
        var redaction = {
            content: {},
            user_id: "@bob:matrix.org",
            room_id: "!foobar:matrix.org",
            event_id: "wefuiwehfuiw",
            redacts: badEventId,
            type: "m.room.redaction"
        };
        eventHandlerService.handleEvent(redaction, true);
        expect(testEvents).toEqual([]);
    }));
    
    it('should be able to process generic room state events.', inject(
    function(eventHandlerService) {
        eventHandlerService.handleInitialSyncDone(testInitialSync);
        
        spyOn(testNowState, "storeStateEvent");
        var event = {
            content: {
                something: "for nothing"
            },
            user_id: "@claire:matrix.org",
            room_id: "!foobar:matrix.org",
            state_key: "Rubber_Biscuit",
            type: "org.matrix.test.random",
            event_id: "wefiuwehf"
        };
        eventHandlerService.handleEvent(event, true);
        expect(testNowState.storeStateEvent).toHaveBeenCalledWith(event);
    }));
    
    it('should suppress duplicate event IDs received from /events.', inject(
    function(eventHandlerService) {
        eventHandlerService.handleInitialSyncDone(testInitialSync);
        var eventId = "wefiuwehf";
        
        var event = {
            content: {
                body: "hello",
                msgtype: "m.text"
            },
            user_id: "@claire:matrix.org",
            room_id: "!foobar:matrix.org",
            type: "m.room.message",
            event_id: eventId
        };
        
        var dupeEvent = {
            content: {
                body: "goodbye", // should suppress based on event ID
                msgtype: "m.text"
            },
            user_id: "@claire:matrix.org",
            room_id: "!foobar:matrix.org",
            type: "m.room.message",
            event_id: eventId
        };
        eventHandlerService.handleEvent(event, true);
        eventHandlerService.handleEvent(dupeEvent, true);
        expect(testEvents.length).toEqual(1);
        expect(testEvents[0].event).toEqual(event);
    }));
    
    it('should suppress duplicate event IDs when sending messages.', inject(
    function(eventHandlerService) {
        var roomId = "!flibble:matrix.org";
        var sendCallback = {
            onSendEcho: function(echo){},
            onSent: function(response, isEcho){},
            onError: function(error){}
        };
        spyOn(sendCallback, "onSendEcho");
        spyOn(sendCallback, "onSent");
        spyOn(sendCallback, "onError");
        
        
        var eventId = "someEventId";
        testSendMessage = {
            event_id: eventId
        };
        
        eventHandlerService.sendMessage(roomId, "some text", sendCallback);
        expect(sendCallback.onSendEcho).toHaveBeenCalled();
        expect(testEvents.length).toEqual(1);
        
        // oh noes, the message comes down the event stream before this request finishes
        var event = {
            content: {
                body: "some text",
                msgtype: "m.text"
            },
            user_id: testUserId,
            room_id: roomId,
            type: "m.room.message",
            event_id: eventId
        };
        eventHandlerService.handleEvent(event, true);
        
        scope.$digest(); // process the send message request
        
        expect(testEvents.length).toEqual(1);
        expect(testEvents[0].event).toEqual(event);
    }));
    
    it('should be able to send a text message with echo.', inject(
    function(eventHandlerService) {
        var roomId = "!flibble:matrix.org";
        var sendCallback = {
            onSendEcho: function(echo){},
            onSent: function(response, isEcho){},
            onError: function(error){}
        };
        spyOn(sendCallback, "onSendEcho");
        spyOn(sendCallback, "onSent");
        spyOn(sendCallback, "onError");
        
        var eventId = "someEventId";
        testSendMessage = {
            event_id: eventId
        };
        
        eventHandlerService.sendMessage(roomId, "some text", sendCallback);
        
        expect(sendCallback.onSendEcho).toHaveBeenCalled();
        scope.$digest(); // process the matrix request
        expect(sendCallback.onSent).toHaveBeenCalledWith({ data: testSendMessage}, true);
    }));
    
    // NB: We can only indirectly test this by making sure there is no dupe
    //     suppression after EVENT_ID_LIFETIME_MS.
    it('should reap event IDs after EVENT_ID_LIFETIME_MS.', inject(
    function(eventHandlerService) {
        eventHandlerService.handleInitialSyncDone(testInitialSync);
        var eventId = "wefiuwehf";
        
        var event = {
            content: {
                body: "hello",
                msgtype: "m.text"
            },
            user_id: "@claire:matrix.org",
            room_id: "!foobar:matrix.org",
            type: "m.room.message",
            event_id: eventId
        };
        eventHandlerService.handleEvent(event, true);
        
        var dupeEvent = {
            content: {
                body: "goodbye", // should suppress based on event ID
                msgtype: "m.text"
            },
            user_id: "@claire:matrix.org",
            room_id: "!foobar:matrix.org",
            type: "m.room.message",
            event_id: eventId
        };
        
        // mock date
        var testTime = new Date().getTime() + 
                       eventHandlerService.EVENT_ID_LIFETIME_MS + 1000;
        var oldDate = Date;
        spyOn($window, 'Date').and.callFake(function() {
            return new oldDate(testTime);
        });
        
        $interval.flush(eventHandlerService.REAP_POLL_MS); // force a recheck
        
        // should not suppress since it forgot about it.
        eventHandlerService.handleEvent(dupeEvent, true);
        expect(testEvents.length).toEqual(2);
    }));
    
    it('should be able to create a room and do an initial sync on the room.', inject(
    function(eventHandlerService) {
        var alias = "bob";
        var isPublic = true;
        var invitee = "@alicia:matrix.org";
        var roomId = "!avauyga:matrix.org";
        var defer = $q.defer();
        spyOn(matrixService, "create").and.returnValue(defer.promise);
        spyOn(matrixService, "roomInitialSync").and.callThrough();
        
        var promise = eventHandlerService.createRoom(alias, isPublic, invitee);
        
        expect(matrixService.create).toHaveBeenCalled();
        defer.resolve({data:{room_id:roomId}});
        scope.$digest();
        expect(matrixService.roomInitialSync).toHaveBeenCalledWith(
            roomId, 
            jasmine.any(Number)
        );
    })); 
    
    it('should be able to handle global initial sync data', inject(
    function(eventHandlerService) {
        // a joined room with messages
        var roomId = "!rooma:matrix.org";
        var theRoom = {
            current_room_state: {
                storeStateEvents: function(){}
            },
            old_room_state: {
                storeStateEvents: function(){}
            },
            room_id: roomId,
            addMessageEvent: function(){},
            addOrReplaceMessageEvent: function(){},
            mutateRoomMemberState: function(){},
            getChangedKeyForMemberEvent: function(){},
            isJoinedRoom: function(){return true;}
        };
        testInitialSync.data.rooms.push(angular.copy(testRoomInitialSync));
        
        var roomAmember = "@roomamember:matrix.org";
        testInitialSync.data.rooms[0].state = [
            mkEvent("m.room.member", {membership:"join"}, roomId, testUserId),
            mkEvent("m.room.member", {membership:"join"}, roomId, roomAmember, roomAmember)
        ];
        testInitialSync.data.rooms[0].messages.chunk = [
            mkEvent("m.room.member", {membership:"join"}, roomId, testUserId),
            mkEvent("m.room.member", {membership:"join"}, roomId, roomAmember, roomAmember),
            mkEvent("m.room.message", {body:"hi",msgtype:"m.text"}, roomId),
            mkEvent("m.room.message", {body:"bye",msgtype:"m.text"}, roomId, undefined, roomAmember)
        ];
        testInitialSync.data.rooms[0].room_id = roomId;
        spyOn(modelService, "getRoom").and.returnValue(theRoom);
        spyOn(theRoom.current_room_state, "storeStateEvents");
        spyOn(theRoom.old_room_state, "storeStateEvents");
        spyOn(theRoom, "addOrReplaceMessageEvent");
        spyOn(theRoom, "addMessageEvent");
        spyOn(theRoom, "getChangedKeyForMemberEvent");
        
        // an invited room
        var inviterUserId = "@inviter:matrix.org";
        var inviteRoomId = "!invite:matrix.org";
        testInitialSync.data.rooms.push({
            inviter: inviterUserId,
            room_id: inviteRoomId,
            membership: "invite"
        });
        
        // some presence
        testInitialSync.data.presence.push({
            content: {status:"online"},
            user_id: roomAmember,
            type: "m.presence"
        });
        spyOn(modelService, "setUser");
    
        eventHandlerService.handleInitialSyncDone(testInitialSync);
        
        // expect injected fake invite event
        expect(testInitialSync.data.rooms[1].state.length).toEqual(1);
        
        // expect presence stuff
        expect(modelService.setUser).toHaveBeenCalledWith(
            testInitialSync.data.presence[0]
        );
        
        // expect room state & msgs stored
        expect(theRoom.current_room_state.storeStateEvents).toHaveBeenCalledWith(
            testInitialSync.data.rooms[0].state
        );
        // addOrReplace since it was sent by us and we may need to replace local echo
        expect(theRoom.addOrReplaceMessageEvent).toHaveBeenCalledWith(
            testInitialSync.data.rooms[0].messages.chunk[2],
            true
        );
        // add since it wasn't sent by us and so doesn't need to replace a local echo
        expect(theRoom.addMessageEvent).toHaveBeenCalledWith(
            testInitialSync.data.rooms[0].messages.chunk[3],
            true
        );
    }));
    
    it('should be able to process m.typing events.', inject(
    function(eventHandlerService) {
        eventHandlerService.handleInitialSyncDone(testInitialSync);
        testNowState.members = {
            "@aaa:matrix.org": {
                typing: false
            },
            "@bbb:matrix.org": {
                typing: false
            }
        };
        
        var event = {
            content: {
                user_ids: ["@aaa:matrix.org"]
            },
            user_id: "matrix.org",
            type: "m.typing"
        };
        eventHandlerService.handleEvent(event, true);
        expect(testNowState.members["@aaa:matrix.org"].typing).toBe(true);
    }));
    
    it('should clobber m.typing events.', inject(
    function(eventHandlerService) {
        eventHandlerService.handleInitialSyncDone(testInitialSync);
        testNowState.members = {
            "@aaa:matrix.org": {
                typing: false
            },
            "@bbb:matrix.org": {
                typing: false
            }
        };
        
        var event = {
            content: {
                user_ids: ["@aaa:matrix.org"]
            },
            user_id: "matrix.org",
            type: "m.typing"
        };
        eventHandlerService.handleEvent(event, true);
        expect(testNowState.members["@aaa:matrix.org"].typing).toBe(true);
        
        // clobber with bbb
        event = {
            content: {
                user_ids: ["@bbb:matrix.org"]
            },
            user_id: "matrix.org",
            type: "m.typing"
        };
        eventHandlerService.handleEvent(event, true);
        expect(testNowState.members["@aaa:matrix.org"].typing).toBe(false); // reset
        expect(testNowState.members["@bbb:matrix.org"].typing).toBe(true);
    }));
    
    it('should treat an empty m.typing user_ids array as no-one is typing.', inject(
    function(eventHandlerService) {
        eventHandlerService.handleInitialSyncDone(testInitialSync);
        testNowState.members = {
            "@aaa:matrix.org": {
                typing: false
            },
            "@bbb:matrix.org": {
                typing: false
            }
        };
        
        var event = {
            content: {
                user_ids: []
            },
            user_id: "matrix.org",
            type: "m.typing"
        };
        eventHandlerService.handleEvent(event, true);
        expect(testNowState.members["@aaa:matrix.org"].typing).toBe(false);
        expect(testNowState.members["@bbb:matrix.org"].typing).toBe(false);
    }));
    
    it('should ignore unknown user_ids in m.typing.', inject(
    function(eventHandlerService) {
        eventHandlerService.handleInitialSyncDone(testInitialSync);
        testNowState.members = {
            "@aaa:matrix.org": {
                typing: false
            },
            "@bbb:matrix.org": {
                typing: false
            }
        };
        
        var event = {
            content: {
                user_ids: ["@ccc:matrix.org"]
            },
            user_id: "matrix.org",
            type: "m.typing"
        };
        eventHandlerService.handleEvent(event, true);
        expect(testNowState.members["@aaa:matrix.org"].typing).toBe(false);
        expect(testNowState.members["@bbb:matrix.org"].typing).toBe(false);
        expect(testNowState.members["@ccc:matrix.org"]).toBeUndefined();
    }));
    
    it('should pass the event through to notificationService.', inject(
    function(eventHandlerService) {
        eventHandlerService.handleInitialSyncDone(testInitialSync);
        testContainsBingWords = true;
        
        var event = {
            content: {
                body: "ping",
                msgtype: "m.text"
            },
            user_id: "@someone:matrix.org",
            room_id: "!foobar:matrix.org",
            type: "m.room.message",
            event_id: "wf3"
        };
        spyOn(notificationService, "processEvent");
        eventHandlerService.handleEvent(event, true);
        expect(notificationService.processEvent).toHaveBeenCalled();
    }));
    
});
