text: import * as utils from "../test-utils";
  import { EventTimeline } from "../../src/matrix";
  import { logger } from "../../src/logger";
  import { TestClient } from "../TestClient";
  
  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",
      },
  });
  
  const INITIAL_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,
                              },
                          }),
                      ],
                  },
              },
          },
      },
  };
  
  const EVENTS = [
      utils.mkMessage({
          room: roomId, user: userId, msg: "we",
      }),
      utils.mkMessage({
          room: roomId, user: userId, msg: "could",
      }),
      utils.mkMessage({
          room: roomId, user: userId, msg: "be",
      }),
      utils.mkMessage({
          room: roomId, user: userId, msg: "heroes",
      }),
  ];
  
  // start the client, and wait for it to initialise
  function startClient(httpBackend, client) {
      httpBackend.when("GET", "/capabilities").respond(200, { capabilities: {} });
      httpBackend.when("GET", "/pushrules").respond(200, {});
      httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
      httpBackend.when("GET", "/sync").respond(200, INITIAL_SYNC_DATA);
  
      client.startClient();
  
      // set up a promise which will resolve once the client is initialised
      const prom = new Promise((resolve) => {
          client.on("sync", function(state) {
              logger.log("sync", state);
              if (state != "SYNCING") {
                  return;
              }
              resolve();
          });
      });
  
      return Promise.all([
          httpBackend.flushAllExpected(),
          prom,
      ]);
  }
  
  describe("getEventTimeline support", function() {
      let httpBackend;
      let client;
  
      beforeEach(function() {
          const testClient = new TestClient(userId, "DEVICE", accessToken);
          client = testClient.client;
          httpBackend = testClient.httpBackend;
      });
  
      afterEach(function() {
          if (client) {
              client.stopClient();
          }
          return httpBackend.stop();
      });
  
      it("timeline support must be enabled to work", function() {
          return startClient(httpBackend, client).then(function() {
              const room = client.getRoom(roomId);
              const timelineSet = room.getTimelineSets()[0];
              expect(function() {
                  client.getEventTimeline(timelineSet, "event");
              }).toThrow();
          });
      });
  
      it("timeline support works when enabled", function() {
          const testClient = new TestClient(
              userId,
              "DEVICE",
              accessToken,
              undefined,
              { timelineSupport: true },
          );
          client = testClient.client;
          httpBackend = testClient.httpBackend;
  
          return startClient(httpBackend, client).then(() => {
              const room = client.getRoom(roomId);
              const timelineSet = room.getTimelineSets()[0];
              expect(function() {
                  client.getEventTimeline(timelineSet, "event");
              }).not.toThrow();
          });
      });
  
      it("scrollback should be able to scroll back to before a gappy /sync",
        function() {
          // need a client with timelineSupport disabled to make this work
  
          let room;
  
          return startClient(httpBackend, client).then(function() {
              room = client.getRoom(roomId);
  
              httpBackend.when("GET", "/sync").respond(200, {
                  next_batch: "s_5_4",
                  rooms: {
                      join: {
                          "!foo:bar": {
                              timeline: {
                                  events: [
                                      EVENTS[0],
                                  ],
                                  prev_batch: "f_1_1",
                              },
                          },
                      },
                  },
              });
  
              httpBackend.when("GET", "/sync").respond(200, {
                  next_batch: "s_5_5",
                  rooms: {
                      join: {
                          "!foo:bar": {
                              timeline: {
                                  events: [
                                      EVENTS[1],
                                  ],
                                  limited: true,
                                  prev_batch: "f_1_2",
                              },
                          },
                      },
                  },
              });
  
              return Promise.all([
                  httpBackend.flushAllExpected(),
                  utils.syncPromise(client, 2),
              ]);
          }).then(function() {
              expect(room.timeline.length).toEqual(1);
              expect(room.timeline[0].event).toEqual(EVENTS[1]);
  
              httpBackend.when("GET", "/messages").respond(200, {
                  chunk: [EVENTS[0]],
                  start: "pagin_start",
                  end: "pagin_end",
              });
              httpBackend.flush("/messages", 1);
              return client.scrollback(room);
          }).then(function() {
              expect(room.timeline.length).toEqual(2);
              expect(room.timeline[0].event).toEqual(EVENTS[0]);
              expect(room.timeline[1].event).toEqual(EVENTS[1]);
              expect(room.oldState.paginationToken).toEqual("pagin_end");
          });
      });
  });
  
  describe("MatrixClient event timelines", function() {
      let client = null;
      let httpBackend = null;
  
      beforeEach(function() {
          const testClient = new TestClient(
              userId,
              "DEVICE",
              accessToken,
              undefined,
              { timelineSupport: true },
          );
          client = testClient.client;
          httpBackend = testClient.httpBackend;
  
          return startClient(httpBackend, client);
      });
  
      afterEach(function() {
          httpBackend.verifyNoOutstandingExpectation();
          client.stopClient();
      });
  
      describe("getEventTimeline", function() {
          it("should create a new timeline for new events", function() {
              const room = client.getRoom(roomId);
              const timelineSet = room.getTimelineSets()[0];
              httpBackend.when("GET", "/rooms/!foo%3Abar/context/event1%3Abar")
                  .respond(200, function() {
                      return {
                          start: "start_token",
                          events_before: [EVENTS[1], EVENTS[0]],
                          event: EVENTS[2],
                          events_after: [EVENTS[3]],
                          state: [
                              ROOM_NAME_EVENT,
                              USER_MEMBERSHIP_EVENT,
                          ],
                          end: "end_token",
                      };
                  });
  
              return Promise.all([
                  client.getEventTimeline(timelineSet, "event1:bar").then(function(tl) {
                      expect(tl.getEvents().length).toEqual(4);
                      for (let i = 0; i < 4; i++) {
                          expect(tl.getEvents()[i].event).toEqual(EVENTS[i]);
                          expect(tl.getEvents()[i].sender.name).toEqual(userName);
                      }
                      expect(tl.getPaginationToken(EventTimeline.BACKWARDS))
                          .toEqual("start_token");
                      expect(tl.getPaginationToken(EventTimeline.FORWARDS))
                          .toEqual("end_token");
                  }),
                  httpBackend.flushAllExpected(),
              ]);
          });
  
          it("should return existing timeline for known events", function() {
              const room = client.getRoom(roomId);
              const timelineSet = room.getTimelineSets()[0];
              httpBackend.when("GET", "/sync").respond(200, {
                  next_batch: "s_5_4",
                  rooms: {
                      join: {
                          "!foo:bar": {
                              timeline: {
                                  events: [
                                      EVENTS[0],
                                  ],
                                  prev_batch: "f_1_2",
                              },
                          },
                      },
                  },
              });
  
              return Promise.all([
                  httpBackend.flush("/sync"),
                  utils.syncPromise(client),
              ]).then(function() {
                  return client.getEventTimeline(timelineSet, EVENTS[0].event_id);
              }).then(function(tl) {
                  expect(tl.getEvents().length).toEqual(2);
                  expect(tl.getEvents()[1].event).toEqual(EVENTS[0]);
                  expect(tl.getEvents()[1].sender.name).toEqual(userName);
                  expect(tl.getPaginationToken(EventTimeline.BACKWARDS))
                      .toEqual("f_1_1");
                  // expect(tl.getPaginationToken(EventTimeline.FORWARDS))
                  //    .toEqual("s_5_4");
              });
          });
  
          it("should update timelines where they overlap a previous /sync", function() {
              const room = client.getRoom(roomId);
              const timelineSet = room.getTimelineSets()[0];
              httpBackend.when("GET", "/sync").respond(200, {
                  next_batch: "s_5_4",
                  rooms: {
                      join: {
                          "!foo:bar": {
                              timeline: {
                                  events: [
                                      EVENTS[3],
                                  ],
                                  prev_batch: "f_1_2",
                              },
                          },
                      },
                  },
              });
  
              httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
                               encodeURIComponent(EVENTS[2].event_id))
                  .respond(200, function() {
                      return {
                          start: "start_token",
                          events_before: [EVENTS[1]],
                          event: EVENTS[2],
                          events_after: [EVENTS[3]],
                          end: "end_token",
                          state: [],
                      };
                  });
  
              const prom = new Promise((resolve, reject) => {
                  client.on("sync", function() {
                      client.getEventTimeline(timelineSet, EVENTS[2].event_id,
                      ).then(function(tl) {
                          expect(tl.getEvents().length).toEqual(4);
                          expect(tl.getEvents()[0].event).toEqual(EVENTS[1]);
                          expect(tl.getEvents()[1].event).toEqual(EVENTS[2]);
                          expect(tl.getEvents()[3].event).toEqual(EVENTS[3]);
                          expect(tl.getPaginationToken(EventTimeline.BACKWARDS))
                              .toEqual("start_token");
                          // expect(tl.getPaginationToken(EventTimeline.FORWARDS))
                          //    .toEqual("s_5_4");
                      }).then(resolve, reject);
                  });
              });
  
              return Promise.all([
                  httpBackend.flushAllExpected(),
                  prom,
              ]);
          });
  
          it("should join timelines where they overlap a previous /context",
            function() {
              const room = client.getRoom(roomId);
              const timelineSet = room.getTimelineSets()[0];
  
              // we fetch event 0, then 2, then 3, and finally 1. 1 is returned
              // with context which joins them all up.
              httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
                               encodeURIComponent(EVENTS[0].event_id))
                  .respond(200, function() {
                      return {
                          start: "start_token0",
                          events_before: [],
                          event: EVENTS[0],
                          events_after: [],
                          end: "end_token0",
                          state: [],
                      };
                  });
  
              httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
                               encodeURIComponent(EVENTS[2].event_id))
                  .respond(200, function() {
                      return {
                          start: "start_token2",
                          events_before: [],
                          event: EVENTS[2],
                          events_after: [],
                          end: "end_token2",
                          state: [],
                      };
                  });
  
              httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
                               encodeURIComponent(EVENTS[3].event_id))
                  .respond(200, function() {
                      return {
                          start: "start_token3",
                          events_before: [],
                          event: EVENTS[3],
                          events_after: [],
                          end: "end_token3",
                          state: [],
                      };
                  });
  
              httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
                               encodeURIComponent(EVENTS[1].event_id))
                  .respond(200, function() {
                      return {
                          start: "start_token4",
                          events_before: [EVENTS[0]],
                          event: EVENTS[1],
                          events_after: [EVENTS[2], EVENTS[3]],
                          end: "end_token4",
                          state: [],
                      };
                  });
  
              let tl0;
              let tl3;
              return Promise.all([
                  client.getEventTimeline(timelineSet, EVENTS[0].event_id,
                  ).then(function(tl) {
                      expect(tl.getEvents().length).toEqual(1);
                      tl0 = tl;
                      return client.getEventTimeline(timelineSet, EVENTS[2].event_id);
                  }).then(function(tl) {
                      expect(tl.getEvents().length).toEqual(1);
                      return client.getEventTimeline(timelineSet, EVENTS[3].event_id);
                  }).then(function(tl) {
                      expect(tl.getEvents().length).toEqual(1);
                      tl3 = tl;
                      return client.getEventTimeline(timelineSet, EVENTS[1].event_id);
                  }).then(function(tl) {
                      // we expect it to get merged in with event 2
                      expect(tl.getEvents().length).toEqual(2);
                      expect(tl.getEvents()[0].event).toEqual(EVENTS[1]);
                      expect(tl.getEvents()[1].event).toEqual(EVENTS[2]);
                      expect(tl.getNeighbouringTimeline(EventTimeline.BACKWARDS))
                          .toBe(tl0);
                      expect(tl.getNeighbouringTimeline(EventTimeline.FORWARDS))
                          .toBe(tl3);
                      expect(tl0.getPaginationToken(EventTimeline.BACKWARDS))
                          .toEqual("start_token0");
                      expect(tl0.getPaginationToken(EventTimeline.FORWARDS))
                          .toBe(null);
                      expect(tl3.getPaginationToken(EventTimeline.BACKWARDS))
                          .toBe(null);
                      expect(tl3.getPaginationToken(EventTimeline.FORWARDS))
                          .toEqual("end_token3");
                  }),
                  httpBackend.flushAllExpected(),
              ]);
          });
  
          it("should fail gracefully if there is no event field", function() {
              const room = client.getRoom(roomId);
              const timelineSet = room.getTimelineSets()[0];
              // we fetch event 0, then 2, then 3, and finally 1. 1 is returned
              // with context which joins them all up.
              httpBackend.when("GET", "/rooms/!foo%3Abar/context/event1")
                  .respond(200, function() {
                      return {
                          start: "start_token",
                          events_before: [],
                          events_after: [],
                          end: "end_token",
                          state: [],
                      };
                  });
  
              return Promise.all([
                  client.getEventTimeline(timelineSet, "event1",
                  ).then(function(tl) {
                      // could do with a fail()
                      expect(true).toBeFalsy();
                  }, function(e) {
                      expect(String(e)).toMatch(/'event'/);
                  }),
                  httpBackend.flushAllExpected(),
              ]);
          });
      });
  
      describe("paginateEventTimeline", function() {
          it("should allow you to paginate backwards", function() {
              const room = client.getRoom(roomId);
              const timelineSet = room.getTimelineSets()[0];
  
              httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
                               encodeURIComponent(EVENTS[0].event_id))
                  .respond(200, function() {
                      return {
                          start: "start_token0",
                          events_before: [],
                          event: EVENTS[0],
                          events_after: [],
                          end: "end_token0",
                          state: [],
                      };
                  });
  
              httpBackend.when("GET", "/rooms/!foo%3Abar/messages")
                  .check(function(req) {
                      const params = req.queryParams;
                      expect(params.dir).toEqual("b");
                      expect(params.from).toEqual("start_token0");
                      expect(params.limit).toEqual("30");
                  }).respond(200, function() {
                      return {
                          chunk: [EVENTS[1], EVENTS[2]],
                          end: "start_token1",
                      };
                  });
  
              let tl;
              return Promise.all([
                  client.getEventTimeline(timelineSet, EVENTS[0].event_id,
                  ).then(function(tl0) {
                      tl = tl0;
                      return client.paginateEventTimeline(tl, { backwards: true });
                  }).then(function(success) {
                      expect(success).toBeTruthy();
                      expect(tl.getEvents().length).toEqual(3);
                      expect(tl.getEvents()[0].event).toEqual(EVENTS[2]);
                      expect(tl.getEvents()[1].event).toEqual(EVENTS[1]);
                      expect(tl.getEvents()[2].event).toEqual(EVENTS[0]);
                      expect(tl.getPaginationToken(EventTimeline.BACKWARDS))
                          .toEqual("start_token1");
                      expect(tl.getPaginationToken(EventTimeline.FORWARDS))
                          .toEqual("end_token0");
                  }),
                  httpBackend.flushAllExpected(),
              ]);
          });
  
          it("should allow you to paginate forwards", function() {
              const room = client.getRoom(roomId);
              const timelineSet = room.getTimelineSets()[0];
  
              httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
                               encodeURIComponent(EVENTS[0].event_id))
                  .respond(200, function() {
                      return {
                          start: "start_token0",
                          events_before: [],
                          event: EVENTS[0],
                          events_after: [],
                          end: "end_token0",
                          state: [],
                      };
                  });
  
              httpBackend.when("GET", "/rooms/!foo%3Abar/messages")
                  .check(function(req) {
                      const params = req.queryParams;
                      expect(params.dir).toEqual("f");
                      expect(params.from).toEqual("end_token0");
                      expect(params.limit).toEqual("20");
                  }).respond(200, function() {
                      return {
                          chunk: [EVENTS[1], EVENTS[2]],
                          end: "end_token1",
                      };
                  });
  
              let tl;
              return Promise.all([
                  client.getEventTimeline(timelineSet, EVENTS[0].event_id,
                  ).then(function(tl0) {
                      tl = tl0;
                      return client.paginateEventTimeline(
                          tl, { backwards: false, limit: 20 });
                  }).then(function(success) {
                      expect(success).toBeTruthy();
                      expect(tl.getEvents().length).toEqual(3);
                      expect(tl.getEvents()[0].event).toEqual(EVENTS[0]);
                      expect(tl.getEvents()[1].event).toEqual(EVENTS[1]);
                      expect(tl.getEvents()[2].event).toEqual(EVENTS[2]);
                      expect(tl.getPaginationToken(EventTimeline.BACKWARDS))
                          .toEqual("start_token0");
                      expect(tl.getPaginationToken(EventTimeline.FORWARDS))
                          .toEqual("end_token1");
                  }),
                  httpBackend.flushAllExpected(),
              ]);
          });
      });
  
      describe("event timeline for sent events", function() {
          const TXN_ID = "txn1";
          const event = utils.mkMessage({
              room: roomId, user: userId, msg: "a body",
          });
          event.unsigned = { transaction_id: TXN_ID };
  
          beforeEach(function() {
              // set up handlers for both the message send, and the
              // /sync
              httpBackend.when("PUT", "/send/m.room.message/" + TXN_ID)
                  .respond(200, {
                      event_id: event.event_id,
                  });
              httpBackend.when("GET", "/sync").respond(200, {
                  next_batch: "s_5_4",
                  rooms: {
                      join: {
                          "!foo:bar": {
                              timeline: {
                                  events: [
                                      event,
                                  ],
                                  prev_batch: "f_1_1",
                              },
                          },
                      },
                  },
              });
          });
  
          it("should work when /send returns before /sync", function() {
              const room = client.getRoom(roomId);
              const timelineSet = room.getTimelineSets()[0];
  
              return Promise.all([
                  client.sendTextMessage(roomId, "a body", TXN_ID).then(function(res) {
                      expect(res.event_id).toEqual(event.event_id);
                      return client.getEventTimeline(timelineSet, event.event_id);
                  }).then(function(tl) {
                      // 2 because the initial sync contained an event
                      expect(tl.getEvents().length).toEqual(2);
                      expect(tl.getEvents()[1].getContent().body).toEqual("a body");
  
                      // now let the sync complete, and check it again
                      return Promise.all([
                          httpBackend.flush("/sync", 1),
                          utils.syncPromise(client),
                      ]);
                  }).then(function() {
                      return client.getEventTimeline(timelineSet, event.event_id);
                  }).then(function(tl) {
                      expect(tl.getEvents().length).toEqual(2);
                      expect(tl.getEvents()[1].event).toEqual(event);
                  }),
  
                  httpBackend.flush("/send/m.room.message/" + TXN_ID, 1),
              ]);
          });
  
          it("should work when /send returns after /sync", function() {
              const room = client.getRoom(roomId);
              const timelineSet = room.getTimelineSets()[0];
  
              return Promise.all([
                  // initiate the send, and set up checks to be done when it completes
                  // - but note that it won't complete until after the /sync does, below.
                  client.sendTextMessage(roomId, "a body", TXN_ID).then(function(res) {
                      logger.log("sendTextMessage completed");
                      expect(res.event_id).toEqual(event.event_id);
                      return client.getEventTimeline(timelineSet, event.event_id);
                  }).then(function(tl) {
                      logger.log("getEventTimeline completed (2)");
                      expect(tl.getEvents().length).toEqual(2);
                      expect(tl.getEvents()[1].getContent().body).toEqual("a body");
                  }),
  
                  Promise.all([
                      httpBackend.flush("/sync", 1),
                      utils.syncPromise(client),
                  ]).then(function() {
                      return client.getEventTimeline(timelineSet, event.event_id);
                  }).then(function(tl) {
                      logger.log("getEventTimeline completed (1)");
                      expect(tl.getEvents().length).toEqual(2);
                      expect(tl.getEvents()[1].event).toEqual(event);
  
                      // now let the send complete.
                      return httpBackend.flush("/send/m.room.message/" + TXN_ID, 1);
                  }),
              ]);
          });
      });
  
      it("should handle gappy syncs after redactions", function() {
          // https://github.com/vector-im/vector-web/issues/1389
  
          // a state event, followed by a redaction thereof
          const event = utils.mkMembership({
              room: roomId, mship: "join", user: otherUserId,
          });
          const redaction = utils.mkEvent({
              type: "m.room.redaction",
              room_id: roomId,
              sender: otherUserId,
              content: {},
          });
          redaction.redacts = event.event_id;
  
          const syncData = {
              next_batch: "batch1",
              rooms: {
                  join: {},
              },
          };
          syncData.rooms.join[roomId] = {
              timeline: {
                  events: [
                      event,
                      redaction,
                  ],
                  limited: false,
              },
          };
          httpBackend.when("GET", "/sync").respond(200, syncData);
  
          return Promise.all([
              httpBackend.flushAllExpected(),
              utils.syncPromise(client),
          ]).then(function() {
              const room = client.getRoom(roomId);
              const tl = room.getLiveTimeline();
              expect(tl.getEvents().length).toEqual(3);
              expect(tl.getEvents()[1].isRedacted()).toBe(true);
  
              const sync2 = {
                  next_batch: "batch2",
                  rooms: {
                      join: {},
                  },
              };
              sync2.rooms.join[roomId] = {
                  timeline: {
                      events: [
                          utils.mkMessage({
                              room: roomId, user: otherUserId, msg: "world",
                          }),
                      ],
                      limited: true,
                      prev_batch: "newerTok",
                  },
              };
              httpBackend.when("GET", "/sync").respond(200, sync2);
  
              return Promise.all([
                  httpBackend.flushAllExpected(),
                  utils.syncPromise(client),
              ]);
          }).then(function() {
              const room = client.getRoom(roomId);
              const tl = room.getLiveTimeline();
              expect(tl.getEvents().length).toEqual(1);
          });
      });
  });
  