// load olm before the sdk if possible
import './olm-loader';

import {logger} from '../src/logger';
import {MatrixEvent} from "../src/models/event";

/**
 * Return a promise that is resolved when the client next emits a
 * SYNCING event.
 * @param {Object} client The client
 * @param {Number=} count Number of syncs to wait for (default 1)
 * @return {Promise} Resolves once the client has emitted a SYNCING event
 */
export function syncPromise(client, count) {
    if (count === undefined) {
        count = 1;
    }
    if (count <= 0) {
        return Promise.resolve();
    }

    const p = new Promise((resolve, reject) => {
        const cb = (state) => {
            logger.log(`${Date.now()} syncPromise(${count}): ${state}`);
            if (state === 'SYNCING') {
                resolve();
            } else {
                client.once('sync', cb);
            }
        };
        client.once('sync', cb);
    });

    return p.then(() => {
        return syncPromise(client, count-1);
    });
}

/**
 * Create a spy for an object and automatically spy its methods.
 * @param {*} constr The class constructor (used with 'new')
 * @param {string} name The name of the class
 * @return {Object} An instantiated object with spied methods/properties.
 */
export function mock(constr, name) {
    // Based on
    // http://eclipsesource.com/blogs/2014/03/27/mocks-in-jasmine-tests/
    const HelperConstr = new Function(); // jshint ignore:line
    HelperConstr.prototype = constr.prototype;
    const result = new HelperConstr();
    result.toString = function() {
        return "mock" + (name ? " of " + name : "");
    };
    for (const key in constr.prototype) { // eslint-disable-line guard-for-in
        try {
            if (constr.prototype[key] instanceof Function) {
                result[key] = jest.fn();
            }
        } catch (ex) {
            // Direct access to some non-function fields of DOM prototypes may
            // cause exceptions.
            // Overwriting will not work either in that case.
        }
    }
    return result;
}

/**
 * Create an Event.
 * @param {Object} opts Values for the event.
 * @param {string} opts.type The event.type
 * @param {string} opts.room The event.room_id
 * @param {string} opts.sender The event.sender
 * @param {string} opts.skey Optional. The state key (auto inserts empty string)
 * @param {Object} opts.content The event.content
 * @param {boolean} opts.event True to make a MatrixEvent.
 * @return {Object} a JSON object representing this event.
 */
export function mkEvent(opts) {
    if (!opts.type || !opts.content) {
        throw new Error("Missing .type or .content =>" + JSON.stringify(opts));
    }
    const event = {
        type: opts.type,
        room_id: opts.room,
        sender: opts.sender || opts.user, // opts.user for backwards-compat
        content: opts.content,
        event_id: "$" + Math.random() + "-" + Math.random(),
    };
    if (opts.skey !== undefined) {
        event.state_key = opts.skey;
    } else if (["m.room.name", "m.room.topic", "m.room.create", "m.room.join_rules",
         "m.room.power_levels", "m.room.topic",
         "com.example.state"].indexOf(opts.type) !== -1) {
        event.state_key = "";
    }
    return opts.event ? new MatrixEvent(event) : event;
}

/**
 * Create an m.presence event.
 * @param {Object} opts Values for the presence.
 * @return {Object|MatrixEvent} The event
 */
export function mkPresence(opts) {
    if (!opts.user) {
        throw new Error("Missing user");
    }
    const event = {
        event_id: "$" + Math.random() + "-" + Math.random(),
        type: "m.presence",
        sender: opts.sender || opts.user, // opts.user for backwards-compat
        content: {
            avatar_url: opts.url,
            displayname: opts.name,
            last_active_ago: opts.ago,
            presence: opts.presence || "offline",
        },
    };
    return opts.event ? new MatrixEvent(event) : event;
}

/**
 * Create an m.room.member event.
 * @param {Object} opts Values for the membership.
 * @param {string} opts.room The room ID for the event.
 * @param {string} opts.mship The content.membership for the event.
 * @param {string} opts.sender The sender user ID for the event.
 * @param {string} opts.skey The target user ID for the event if applicable
 * e.g. for invites/bans.
 * @param {string} opts.name The content.displayname for the event.
 * @param {string} opts.url The content.avatar_url for the event.
 * @param {boolean} opts.event True to make a MatrixEvent.
 * @return {Object|MatrixEvent} The event
 */
export function mkMembership(opts) {
    opts.type = "m.room.member";
    if (!opts.skey) {
        opts.skey = opts.sender || opts.user;
    }
    if (!opts.mship) {
        throw new Error("Missing .mship => " + JSON.stringify(opts));
    }
    opts.content = {
        membership: opts.mship,
    };
    if (opts.name) {
        opts.content.displayname = opts.name;
    }
    if (opts.url) {
        opts.content.avatar_url = opts.url;
    }
    return mkEvent(opts);
}

/**
 * Create an m.room.message event.
 * @param {Object} opts Values for the message
 * @param {string} opts.room The room ID for the event.
 * @param {string} opts.user The user ID for the event.
 * @param {string} opts.msg Optional. The content.body for the event.
 * @param {boolean} opts.event True to make a MatrixEvent.
 * @return {Object|MatrixEvent} The event
 */
export function mkMessage(opts) {
    opts.type = "m.room.message";
    if (!opts.msg) {
        opts.msg = "Random->" + Math.random();
    }
    if (!opts.room || !opts.user) {
        throw new Error("Missing .room or .user from %s", opts);
    }
    opts.content = {
        msgtype: "m.text",
        body: opts.msg,
    };
    return mkEvent(opts);
}


/**
 * A mock implementation of webstorage
 *
 * @constructor
 */
export function MockStorageApi() {
    this.data = {};
}
MockStorageApi.prototype = {
    get length() {
        return Object.keys(this.data).length;
    },
    key: function(i) {
        return Object.keys(this.data)[i];
    },
    setItem: function(k, v) {
        this.data[k] = v;
    },
    getItem: function(k) {
        return this.data[k] || null;
    },
    removeItem: function(k) {
        delete this.data[k];
    },
};


/**
 * If an event is being decrypted, wait for it to finish being decrypted.
 *
 * @param {MatrixEvent} event
 * @returns {Promise} promise which resolves (to `event`) when the event has been decrypted
 */
export function awaitDecryption(event) {
    if (!event.isBeingDecrypted()) {
        return Promise.resolve(event);
    }

    logger.log(`${Date.now()} event ${event.getId()} is being decrypted; waiting`);

    return new Promise((resolve, reject) => {
        event.once('Event.decrypted', (ev) => {
            logger.log(`${Date.now()} event ${event.getId()} now decrypted`);
            resolve(ev);
        });
    });
}


export function HttpResponse(
    httpLookups, acceptKeepalives, ignoreUnhandledSync,
) {
    this.httpLookups = httpLookups;
    this.acceptKeepalives = acceptKeepalives === undefined ? true : acceptKeepalives;
    this.ignoreUnhandledSync = ignoreUnhandledSync;
    this.pendingLookup = null;
}

HttpResponse.prototype.request = function(
    cb, method, path, qp, data, prefix,
) {
    if (path === HttpResponse.KEEP_ALIVE_PATH && this.acceptKeepalives) {
        return Promise.resolve();
    }
    const next = this.httpLookups.shift();
    const logLine = (
        "MatrixClient[UT] RECV " + method + " " + path + "  " +
            "EXPECT " + (next ? next.method : next) + " " + (next ? next.path : next)
    );
    logger.log(logLine);

    if (!next) { // no more things to return
        if (method === "GET" && path === "/sync" && this.ignoreUnhandledSync) {
            logger.log("MatrixClient[UT] Ignoring.");
            return new Promise(() => {});
        }
        if (this.pendingLookup) {
            if (this.pendingLookup.method === method
                && this.pendingLookup.path === path) {
                return this.pendingLookup.promise;
            }
            // >1 pending thing, and they are different, whine.
            expect(false).toBe(
                true, ">1 pending request. You should probably handle them. " +
                    "PENDING: " + JSON.stringify(this.pendingLookup) + " JUST GOT: " +
                    method + " " + path,
            );
        }
        this.pendingLookup = {
            promise: new Promise(() => {}),
            method: method,
            path: path,
        };
        return this.pendingLookup.promise;
    }
    if (next.path === path && next.method === method) {
        logger.log(
            "MatrixClient[UT] Matched. Returning " +
                (next.error ? "BAD" : "GOOD") + " response",
        );
        if (next.expectBody) {
            expect(next.expectBody).toEqual(data);
        }
        if (next.expectQueryParams) {
            Object.keys(next.expectQueryParams).forEach(function(k) {
                expect(qp[k]).toEqual(next.expectQueryParams[k]);
            });
        }

        if (next.thenCall) {
            process.nextTick(next.thenCall, 0); // next tick so we return first.
        }

        if (next.error) {
            return Promise.reject({
                errcode: next.error.errcode,
                httpStatus: next.error.httpStatus,
                name: next.error.errcode,
                message: "Expected testing error",
                data: next.error,
            });
        }
        return Promise.resolve(next.data);
    } else if (method === "GET" && path === "/sync" && this.ignoreUnhandledSync) {
        logger.log("MatrixClient[UT] Ignoring.");
        this.httpLookups.unshift(next);
        return new Promise(() => {});
    }
    expect(true).toBe(false, "Expected different request. " + logLine);
    return new Promise(() => {});
};

HttpResponse.KEEP_ALIVE_PATH = "/_matrix/client/versions";

HttpResponse.PUSH_RULES_RESPONSE = {
    method: "GET",
    path: "/pushrules/",
    data: {},
};

HttpResponse.USER_ID = "@alice:bar";

HttpResponse.filterResponse = function(userId) {
    const filterPath = "/user/" + encodeURIComponent(userId) + "/filter";
    return {
        method: "POST",
        path: filterPath,
        data: { filter_id: "f1lt3r" },
    };
};

HttpResponse.SYNC_DATA = {
    next_batch: "s_5_3",
    presence: { events: [] },
    rooms: {},
};

HttpResponse.SYNC_RESPONSE = {
    method: "GET",
    path: "/sync",
    data: HttpResponse.SYNC_DATA,
};

HttpResponse.defaultResponses = function(userId) {
    return [
        HttpResponse.PUSH_RULES_RESPONSE,
        HttpResponse.filterResponse(userId),
        HttpResponse.SYNC_RESPONSE,
    ];
};

export function setHttpResponses(
    client, responses, acceptKeepalives, ignoreUnhandledSyncs,
) {
    const httpResponseObj = new HttpResponse(
        responses, acceptKeepalives, ignoreUnhandledSyncs,
    );

    const httpReq = httpResponseObj.request.bind(httpResponseObj);
    client._http = [
        "authedRequest", "authedRequestWithPrefix", "getContentUri",
        "request", "requestWithPrefix", "uploadContent",
    ].reduce((r, k) => {r[k] = jest.fn(); return r;}, {});
    client._http.authedRequest.mockImplementation(httpReq);
    client._http.authedRequestWithPrefix.mockImplementation(httpReq);
    client._http.requestWithPrefix.mockImplementation(httpReq);
    client._http.request.mockImplementation(httpReq);
}
