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

import TestClient from '../TestClient';
import testUtils from '../test-utils';

const ROOM_ID = "!room:id";

/**
 * get a /sync response which contains a single e2e room (ROOM_ID), with the
 * members given
 *
 * @param {string[]} roomMembers
 *
 * @return {object} sync response
 */
function getSyncResponse(roomMembers) {
    const stateEvents = [
        testUtils.mkEvent({
            type: 'm.room.encryption',
            skey: '',
            content: {
                algorithm: 'm.megolm.v1.aes-sha2',
            },
        }),
    ];

    Array.prototype.push.apply(
        stateEvents,
        roomMembers.map(
            (m) => testUtils.mkMembership({
                mship: 'join',
                sender: m,
            }),
        ),
    );

    const syncResponse = {
        next_batch: 1,
        rooms: {
            join: {
                [ROOM_ID]: {
                    state: {
                        events: stateEvents,
                    },
                },
            },
        },
    };

    return syncResponse;
}


describe("DeviceList management:", function() {
    if (!global.Olm) {
        console.warn('not running deviceList tests: Olm not present');
        return;
    }

    let sessionStoreBackend;
    let aliceTestClient;

    async function createTestClient() {
        const testClient = new TestClient(
            "@alice:localhost", "xzcvb", "akjgkrgjs", sessionStoreBackend,
        );
        await testClient.client.initCrypto();
        return testClient;
    }

    beforeEach(async function() {
        testUtils.beforeEach(this); // eslint-disable-line no-invalid-this

        // we create our own sessionStoreBackend so that we can use it for
        // another TestClient.
        sessionStoreBackend = new testUtils.MockStorageApi();

        aliceTestClient = await createTestClient();
    });

    afterEach(function() {
        aliceTestClient.stop();
    });

    it("Alice shouldn't do a second /query for non-e2e-capable devices", function() {
        return aliceTestClient.start().then(function() {
            const syncResponse = getSyncResponse(['@bob:xyz']);
            aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);

            return aliceTestClient.flushSync();
        }).then(function() {
            console.log("Forcing alice to download our device keys");

            aliceTestClient.httpBackend.when('POST', '/keys/query').respond(200, {
                device_keys: {
                    '@bob:xyz': {},
                },
            });

            return Promise.all([
                aliceTestClient.client.downloadKeys(['@bob:xyz']),
                aliceTestClient.httpBackend.flush('/keys/query', 1),
            ]);
        }).then(function() {
            console.log("Telling alice to send a megolm message");

            aliceTestClient.httpBackend.when(
                'PUT', '/send/',
            ).respond(200, {
                    event_id: '$event_id',
            });

            return Promise.all([
                aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),

                // the crypto stuff can take a while, so give the requests a whole second.
                aliceTestClient.httpBackend.flushAllExpected({
                    timeout: 1000,
                }),
            ]);
        });
    });


    it("We should not get confused by out-of-order device query responses",
       () => {
           // https://github.com/vector-im/riot-web/issues/3126
           return aliceTestClient.start().then(() => {
               aliceTestClient.httpBackend.when('GET', '/sync').respond(
                   200, getSyncResponse(['@bob:xyz', '@chris:abc']));
               return aliceTestClient.flushSync();
           }).then(() => {
               // to make sure the initial device queries are flushed out, we
               // attempt to send a message.

               aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
                   200, {
                       device_keys: {
                           '@bob:xyz': {},
                           '@chris:abc': {},
                       },
                   },
               );

               aliceTestClient.httpBackend.when('PUT', '/send/').respond(
                   200, {event_id: '$event1'});

               return Promise.all([
                   aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
                   aliceTestClient.httpBackend.flush('/keys/query', 1).then(
                       () => aliceTestClient.httpBackend.flush('/send/', 1),
                   ),
               ]);
           }).then(() => {
               expect(aliceTestClient.storage.getEndToEndDeviceSyncToken()).toEqual(1);

               // invalidate bob's and chris's device lists in separate syncs
               aliceTestClient.httpBackend.when('GET', '/sync').respond(200, {
                   next_batch: '2',
                   device_lists: {
                       changed: ['@bob:xyz'],
                   },
               });
               aliceTestClient.httpBackend.when('GET', '/sync').respond(200, {
                   next_batch: '3',
                   device_lists: {
                       changed: ['@chris:abc'],
                   },
               });
               // flush both syncs
               return aliceTestClient.flushSync().then(() => {
                   return aliceTestClient.flushSync();
               });
           }).then(() => {
               // check that we don't yet have a request for chris's devices.
               aliceTestClient.httpBackend.when('POST', '/keys/query', {
                   device_keys: {
                       '@chris:abc': {},
                   },
                   token: '3',
               }).respond(200, {
                   device_keys: {'@chris:abc': {}},
               });
               return aliceTestClient.httpBackend.flush('/keys/query', 1);
           }).then((flushed) => {
               expect(flushed).toEqual(0);
               const bobStat = aliceTestClient.storage
                     .getEndToEndDeviceTrackingStatus()['@bob:xyz'];
               if (bobStat != 1 && bobStat != 2) {
                   throw new Error('Unexpected status for bob: wanted 1 or 2, got ' +
                                   bobStat);
               }

               const chrisStat = aliceTestClient.storage
                     .getEndToEndDeviceTrackingStatus()['@chris:abc'];
               if (chrisStat != 1 && chrisStat != 2) {
                   throw new Error('Unexpected status for chris: wanted 1 or 2, got ' +
                                   chrisStat);
               }

               // now add an expectation for a query for bob's devices, and let
               // it complete.
               aliceTestClient.httpBackend.when('POST', '/keys/query', {
                   device_keys: {
                       '@bob:xyz': {},
                   },
                   token: '2',
               }).respond(200, {
                   device_keys: {'@bob:xyz': {}},
               });
               return aliceTestClient.httpBackend.flush('/keys/query', 1);
           }).then((flushed) => {
               expect(flushed).toEqual(1);

               // wait for the client to stop processing the response
               return aliceTestClient.client.downloadKeys(['@bob:xyz']);
           }).then(() => {
               const bobStat = aliceTestClient.storage
                     .getEndToEndDeviceTrackingStatus()['@bob:xyz'];
               expect(bobStat).toEqual(3);
               const chrisStat = aliceTestClient.storage
                     .getEndToEndDeviceTrackingStatus()['@chris:abc'];
               if (chrisStat != 1 && chrisStat != 2) {
                   throw new Error('Unexpected status for chris: wanted 1 or 2, got ' +
                                   bobStat);
               }

               // now let the query for chris's devices complete.
               return aliceTestClient.httpBackend.flush('/keys/query', 1);
           }).then((flushed) => {
               expect(flushed).toEqual(1);

               // wait for the client to stop processing the response
               return aliceTestClient.client.downloadKeys(['@chris:abc']);
           }).then(() => {
               const bobStat = aliceTestClient.storage
                     .getEndToEndDeviceTrackingStatus()['@bob:xyz'];
               const chrisStat = aliceTestClient.storage
                     .getEndToEndDeviceTrackingStatus()['@chris:abc'];

               expect(bobStat).toEqual(3);
               expect(chrisStat).toEqual(3);
               expect(aliceTestClient.storage.getEndToEndDeviceSyncToken()).toEqual(3);
           });
       });

    // https://github.com/vector-im/riot-web/issues/4983
    describe("Alice should know she has stale device lists", () => {
        beforeEach(async function() {
            await aliceTestClient.start();

            aliceTestClient.httpBackend.when('GET', '/sync').respond(
                200, getSyncResponse(['@bob:xyz']));
            await aliceTestClient.flushSync();

            aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
                200, {
                    device_keys: {
                        '@bob:xyz': {},
                    },
                },
            );
            await aliceTestClient.httpBackend.flush('/keys/query', 1);

            const bobStat = aliceTestClient.storage
                      .getEndToEndDeviceTrackingStatus()['@bob:xyz'];

            expect(bobStat).toBeGreaterThan(
                0, "Alice should be tracking bob's device list",
            );
        });

        it("when Bob leaves", async function() {
            aliceTestClient.httpBackend.when('GET', '/sync').respond(
                200, {
                    next_batch: 2,
                    device_lists: {
                        left: ['@bob:xyz'],
                    },
                    rooms: {
                        join: {
                            [ROOM_ID]: {
                                timeline: {
                                    events: [
                                        testUtils.mkMembership({
                                            mship: 'leave',
                                            sender: '@bob:xyz',
                                        }),
                                    ],
                                },
                            },
                        },
                    },
                },
            );


            await aliceTestClient.flushSync();

            const bobStat = aliceTestClient.storage
                      .getEndToEndDeviceTrackingStatus()['@bob:xyz'];
            expect(bobStat).toEqual(
                0, "Alice should have marked bob's device list as untracked",
            );
        });

        it("when Alice leaves", async function() {
            aliceTestClient.httpBackend.when('GET', '/sync').respond(
                200, {
                    next_batch: 2,
                    device_lists: {
                        left: ['@bob:xyz'],
                    },
                    rooms: {
                        leave: {
                            [ROOM_ID]: {
                                timeline: {
                                    events: [
                                        testUtils.mkMembership({
                                            mship: 'leave',
                                            sender: '@bob:xyz',
                                        }),
                                    ],
                                },
                            },
                        },
                    },
                },
            );

            await aliceTestClient.flushSync();

            const bobStat = aliceTestClient.storage
                      .getEndToEndDeviceTrackingStatus()['@bob:xyz'];
            expect(bobStat).toEqual(
                0, "Alice should have marked bob's device list as untracked",
            );
        });

        it("when Bob leaves whilst Alice is offline", async function() {
            aliceTestClient.stop();

            const anotherTestClient = await createTestClient();

            try {
                anotherTestClient.httpBackend.when('GET', '/keys/changes').respond(
                    200, {
                        changed: [],
                        left: ['@bob:xyz'],
                    },
                );
                await anotherTestClient.start();
                anotherTestClient.httpBackend.when('GET', '/sync').respond(
                    200, getSyncResponse([]));
                await anotherTestClient.flushSync();

                const bobStat = anotherTestClient.storage
                      .getEndToEndDeviceTrackingStatus()['@bob:xyz'];

                expect(bobStat).toEqual(
                    0, "Alice should have marked bob's device list as untracked",
                );
            } finally {
                anotherTestClient.stop();
            }
        });
    });
});
