"use strict";

var fakeXhr = require("../fake-xhr");
var push = [].push;
var log = require("./log");
var configureLogError = require("../configure-logger");
var pathToRegexp = require("path-to-regexp");

var supportsArrayBuffer = typeof ArrayBuffer !== "undefined";

function responseArray(handler) {
    var response = handler;

    if (Object.prototype.toString.call(handler) !== "[object Array]") {
        response = [200, {}, handler];
    }

    if (typeof response[2] !== "string") {
        if (!supportsArrayBuffer) {
            throw new TypeError(
                `Fake server response body should be a string, but was ${typeof response[2]}`
            );
        } else if (!(response[2] instanceof ArrayBuffer)) {
            throw new TypeError(
                `Fake server response body should be a string or ArrayBuffer, but was ${typeof response[2]}`
            );
        }
    }

    return response;
}

function getDefaultWindowLocation() {
    var winloc = {
        hostname: "localhost",
        port: process.env.PORT || 80,
        protocol: "http:"
    };
    winloc.host =
        winloc.hostname +
        (String(winloc.port) === "80" ? "" : `:${winloc.port}`);
    return winloc;
}

function getWindowLocation() {
    if (typeof window === "undefined") {
        // Fallback
        return getDefaultWindowLocation();
    }

    if (typeof window.location !== "undefined") {
        // Browsers place location on window
        return window.location;
    }

    if (
        typeof window.window !== "undefined" &&
        typeof window.window.location !== "undefined"
    ) {
        // React Native on Android places location on window.window
        return window.window.location;
    }

    return getDefaultWindowLocation();
}

function matchOne(response, reqMethod, reqUrl) {
    var rmeth = response.method;
    var matchMethod = !rmeth || rmeth.toLowerCase() === reqMethod.toLowerCase();
    var url = response.url;
    var matchUrl =
        !url ||
        url === reqUrl ||
        (typeof url.test === "function" && url.test(reqUrl)) ||
        (typeof url === "function" && url(reqUrl) === true);

    return matchMethod && matchUrl;
}

function match(response, request) {
    var wloc = getWindowLocation();

    var rCurrLoc = new RegExp(`^${wloc.protocol}//${wloc.host}/`);

    var requestUrl = request.url;

    if (!/^https?:\/\//.test(requestUrl) || rCurrLoc.test(requestUrl)) {
        requestUrl = requestUrl.replace(rCurrLoc, "/");
    }

    if (matchOne(response, this.getHTTPMethod(request), requestUrl)) {
        if (typeof response.response === "function") {
            var ru = response.url;
            var args = [request].concat(
                ru && typeof ru.exec === "function"
                    ? ru.exec(requestUrl).slice(1)
                    : []
            );
            return response.response.apply(response, args);
        }

        return true;
    }

    return false;
}

function incrementRequestCount() {
    var count = ++this.requestCount;

    this.requested = true;

    this.requestedOnce = count === 1;
    this.requestedTwice = count === 2;
    this.requestedThrice = count === 3;

    this.firstRequest = this.getRequest(0);
    this.secondRequest = this.getRequest(1);
    this.thirdRequest = this.getRequest(2);

    this.lastRequest = this.getRequest(count - 1);
}

var fakeServer = {
    create: function(config) {
        var server = Object.create(this);
        server.configure(config);
        this.xhr = fakeXhr.useFakeXMLHttpRequest();
        server.requests = [];
        server.requestCount = 0;
        server.queue = [];
        server.responses = [];

        this.xhr.onCreate = function(xhrObj) {
            xhrObj.unsafeHeadersEnabled = function() {
                return !(server.unsafeHeadersEnabled === false);
            };
            server.addRequest(xhrObj);
        };

        return server;
    },

    configure: function(config) {
        var self = this;
        var allowlist = {
            autoRespond: true,
            autoRespondAfter: true,
            respondImmediately: true,
            fakeHTTPMethods: true,
            logger: true,
            unsafeHeadersEnabled: true
        };

        // eslint-disable-next-line no-param-reassign
        config = config || {};

        Object.keys(config).forEach(function(setting) {
            if (setting in allowlist) {
                self[setting] = config[setting];
            }
        });

        self.logError = configureLogError(config);
    },

    addRequest: function addRequest(xhrObj) {
        var server = this;
        push.call(this.requests, xhrObj);

        incrementRequestCount.call(this);

        xhrObj.onSend = function() {
            server.handleRequest(this);

            if (server.respondImmediately) {
                server.respond();
            } else if (server.autoRespond && !server.responding) {
                setTimeout(function() {
                    server.responding = false;
                    server.respond();
                }, server.autoRespondAfter || 10);

                server.responding = true;
            }
        };
    },

    getHTTPMethod: function getHTTPMethod(request) {
        if (this.fakeHTTPMethods && /post/i.test(request.method)) {
            var matches = (request.requestBody || "").match(
                /_method=([^\b;]+)/
            );
            return matches ? matches[1] : request.method;
        }

        return request.method;
    },

    handleRequest: function handleRequest(xhr) {
        if (xhr.async) {
            push.call(this.queue, xhr);
        } else {
            this.processRequest(xhr);
        }
    },

    logger: function() {
        // no-op; override via configure()
    },

    logError: configureLogError({}),

    log: log,

    respondWith: function respondWith(method, url, body) {
        if (arguments.length === 1 && typeof method !== "function") {
            this.response = responseArray(method);
            return;
        }

        if (arguments.length === 1) {
            // eslint-disable-next-line no-param-reassign
            body = method;
            // eslint-disable-next-line no-param-reassign
            url = method = null;
        }

        if (arguments.length === 2) {
            // eslint-disable-next-line no-param-reassign
            body = url;
            // eslint-disable-next-line no-param-reassign
            url = method;
            // eslint-disable-next-line no-param-reassign
            method = null;
        }

        // Escape port number to prevent "named" parameters in 'path-to-regexp' module
        if (typeof url === "string" && url !== "" && /:[0-9]+\//.test(url)) {
            var m = url.match(/^(https?:\/\/.*?):([0-9]+\/.*)$/);
            // eslint-disable-next-line no-param-reassign
            url = `${m[1]}\\:${m[2]}`;
        }

        push.call(this.responses, {
            method: method,
            url:
                typeof url === "string" && url !== "" ? pathToRegexp(url) : url,
            response: typeof body === "function" ? body : responseArray(body)
        });
    },

    respond: function respond() {
        if (arguments.length > 0) {
            this.respondWith.apply(this, arguments);
        }

        var queue = this.queue || [];
        var requests = queue.splice(0, queue.length);
        var self = this;

        requests.forEach(function(request) {
            self.processRequest(request);
        });
    },

    respondAll: function respondAll() {
        if (this.respondImmediately) {
            return;
        }

        this.queue = this.requests.slice(0);

        var request;
        while ((request = this.queue.shift())) {
            this.processRequest(request);
        }
    },

    processRequest: function processRequest(request) {
        try {
            if (request.aborted) {
                return;
            }

            var response = this.response || [404, {}, ""];

            if (this.responses) {
                for (var l = this.responses.length, i = l - 1; i >= 0; i--) {
                    if (match.call(this, this.responses[i], request)) {
                        response = this.responses[i].response;
                        break;
                    }
                }
            }

            if (request.readyState !== 4) {
                this.log(response, request);

                request.respond(response[0], response[1], response[2]);
            }
        } catch (e) {
            this.logError("Fake server request processing", e);
        }
    },

    restore: function restore() {
        return this.xhr.restore && this.xhr.restore.apply(this.xhr, arguments);
    },

    getRequest: function getRequest(index) {
        return this.requests[index] || null;
    },

    reset: function reset() {
        this.resetBehavior();
        this.resetHistory();
    },

    resetBehavior: function resetBehavior() {
        this.responses.length = this.queue.length = 0;
    },

    resetHistory: function resetHistory() {
        this.requests.length = this.requestCount = 0;

        this.requestedOnce = this.requestedTwice = this.requestedThrice = this.requested = false;

        this.firstRequest = this.secondRequest = this.thirdRequest = this.lastRequest = null;
    }
};

module.exports = fakeServer;
