// Commons functions

var _ = require('underscore');

var log4js = require('log4js');

var promiseFinally = require('promise.prototype.finally');
promiseFinally.shim(); // will be a no-op if not needed

var readFile = require('fs').readFile;
var httpRequest = require('request');
var util = require('util');

var f = require('../../lib/functional');
var ispn = require('../../lib/infinispan');
var u = require('../../lib/utils');
var protocols = require('../../lib/protocols');

exports.serverDirName = "infinispan-server";
exports.local = {port: 11222, host: '127.0.0.1'};

exports.cluster1 = {port: 11322, host: '127.0.0.1'};
exports.cluster2 = {port: 11332, host: '127.0.0.1'};
exports.cluster3 = {port: 11342, host: '127.0.0.1'};
exports.cluster = [exports.cluster1, exports.cluster2, exports.cluster3];

exports.failover1 = {port: 11422, host: '127.0.0.1'};
exports.failover2 = {port: 11432, host: '127.0.0.1'};
exports.failover3 = {port: 11442, host: '127.0.0.1'};
exports.failoverConfig = 'infinispan-clustered.xml';
exports.failoverMCastAddr= '234.99.64.14';

// All ssl invocations needs to be directed to localhost instead of 127.0.0.1
// because Node.js uses `localhost` as default server name if none provided.
exports.sslTrust = {port: 11232, host: 'localhost'};
exports.sslAuth = {port: 11232, host: 'localhost'};
exports.sslSni = {port: 11232, host: 'localhost'};

exports.xsiteCacheName = 'xsiteCache';
exports.earth1 = {port: 11522, host: '127.0.0.1'};
exports.moon1 = {port: 11532, host: '127.0.0.1'};
exports.earth1Config = 'infinispan-xsite-EARTH.xml';
exports.moon1Config = 'infinispan-xsite-MOON.xml';
exports.earth1MCastAddr = '234.99.74.14';
exports.moon1MCastAddr = '234.99.74.24';

exports.json = {
  dataFormat: {
    keyType: 'application/json'
    , valueType: 'application/json'
  }
};

var CLUSTER_NODES = ['server-one', 'server-two', 'server-three'];

var MAX_WAIT = 7500;

var logger = u.logger('testing');

exports.configureLogging = function() {
  try {
    log4js.configure('spec/utils/test-log4js.json');
  } catch (error) {
    console.log(error);
    // In case running specs from IDEs
    log4js.configure('utils/test-log4js.json');
  }
};

exports.client = function(args, opts) {
  this.configureLogging();

  if (!f.existy(opts) || !f.existy(opts.version)) {
    var version = exports.getHotrodProtocolVersion();
    if (!f.existy(opts))
      opts = {};

    if (f.existy(version)) {
      opts.version = version;
    }
  }

  return ispn.client(args, opts);
};

exports.protocol25 = function() { return protocols.version25(); };

exports.protocol29 = function(clientOpts) { return protocols.version29(clientOpts); };

exports.put = function(k, v, opts) {
  return function(client) { return client.put(k, v, opts); }
};

exports.get = function(k) {
  return function(client) { return client.get(k); }
};

exports.getM = function(k) {
  return function(client) { return client.getWithMetadata(k); }
};

exports.putIfAbsent = function(k, v, opts) {
  return function(client) { return client.putIfAbsent(k, v, opts); }
};

exports.replace = function(k, v, opts) {
  return function(client) { return client.replace(k, v, opts); }
};

exports.remove = function(k, opts) {
  return function(client) { return client.remove(k, opts);  }
};

exports.containsKey = function(k) {
  return function(client) { return client.containsKey(k); }
};

exports.conditional = function(writeFun, getFun, k, old, v, opts) {
  return function(client) {
    return getFun(k)(client).then(function(versioned) {
      expect(versioned.value).toEqual(old);
      expect(versioned.version).toBeDefined();
      return writeFun(k, versioned.version, v, opts)(client);
    });
  }
};

exports.replaceV = function(k, version, v, opts) {
  return function(client) {
    return client.replaceWithVersion(k, v, version, opts);
  }
};

exports.putAll = function(pairs, opts) {
  return function(client) { return client.putAll(pairs, opts); }
};

exports.size = function() {
  return function(client) { return client.size(); }
};

exports.stats = function() {
  return function(client) { return client.stats(); }
};

exports.clear = function() {
  return function(client) {
    return client.clear();
  }
};

exports.ping = function() {
  return function(client) {
    return client.ping();
  }
};

exports.disconnect = function() {
  return function(client) {
    return client.disconnect();
  }
};

exports.on = function(event, listener, opts) {
  return function(client) {
    return client.addListener(event, listener(client), opts)
      .then(function() { return client; });
  }
};

exports.onMany = function(eventListeners) {
  return function(client) {
    var head = _.head(eventListeners);
    var tail = _.tail(eventListeners);
    return client.addListener(head.event, head.listener).then(function(listenerId) {
      var promises = _.map(tail, function(e) {
        return client.addListener(e.event, e.listener(client), {'listenerId' : listenerId });
      });

      return Promise.all(promises)
        .then(function(listenerIds) {
          _.map(listenerIds, function(lid) { expect(lid).toBe(listenerId); });
          return client;
        })
    });
  };
};

exports.exec = function(scriptName, params) {
  return function(client) {
    return client.execute(scriptName, params);
  }
};

exports.loadAndExec = function(path, name) {
  return function(client) {
    return Promise.all([client, readFileAsync(path)])
      .then(function(vals) {
        var c = vals[0];
        var scriptName = f.existy(name) ? name : path.split('/').pop();
        return c.addScript(scriptName, vals[1].toString())
          .then(function() { return c; } );
      })
  }
};

exports.getTopologyId = function() {
  return function(client) {
    return Promise.resolve(client.getTopologyInfo().getTopologyId());
  }
};

exports.getMembers = function() {
  return function(client) {
    return Promise.resolve(
        _.sortBy(client.getTopologyInfo().getMembers(), 'port'));
  }
};

exports.assert = function(fun, expectFun) {
  if (f.existy(expectFun)) {
    return function(client) {
      return fun(client).then(function(value) {
        expectFun(value);
        return client;
      });
    }
  }
  return function(client) {
    return fun(client).then(function() { return client; });
  }
};

exports.assertStats = function(fun, statsFun) {
  return function(client) {
    var before = client.stats().then(function(before) {return before});
    var put = before.then(function() { return fun(client); });
    var after = put.then(function() { return client.stats(); });
    return Promise.all([before, after]).then(function(stats) {
      statsFun(stats[0], stats[1]);
      return client;
    });
  }
};

exports.resetStats = function(client) {
  var resets = _.map(CLUSTER_NODES, function(nodeName) {
    var op = {
      operation: 'reset-statistics',
      address: [
        { host : 'master'},
        { server : nodeName},
        { subsystem : 'datagrid-infinispan'},
        { 'cache-container' : 'clustered' },
        { 'distributed-cache' : 'default' }
      ]
    };

    return invokeDmrHttp(op);
  });
  return Promise.all(resets).then(function() { return client; });
};

exports.clusterSize = function() { return exports.cluster.length; };

exports.toBe = function(value) {
  return function(actual) { expect(actual).toBe(value); }
};

exports.toEqual = function(value) {
  return function(actual) {
    var laundered = JSON.stringify({value: 'native-value'});
    var relaundered = JSON.parse(laundered);

    // logger.tracef('Match actual=%s and expected=%s? %s'
    //     , u.str(relaundered)
    //     , u.str({value: 'native-value'})
    //     , _.isMatch(relaundered, {value: 'native-value'})
    // );

    var match = _.isMatch(actual, value);
    logger.tracef('Match actual=%s and expected=%s? %s'
        , u.str(actual), u.str(value), match
    );
    expect(actual).toEqual(value);
  }
};

exports.toContain = function(value) {
  return function(actual) {
    if (_.isObject(value)) expect(actual).toEqual(jasmine.objectContaining(value));
    else expect(actual).toContain(value);
  }
};

exports.toBeUndefined = function(actual) { expect(actual).toBeUndefined(); };
exports.toBeTruthy = function(actual) { expect(actual).toBeTruthy(); };
exports.toBeFalsy = function(actual) { expect(actual).toBeFalsy(); };

exports.toBeUndefinedVersioned = function(actual) {
  expect(actual.value).toBeUndefined();
  expectToBeBuffer(actual.version, new Buffer());
};

exports.vNumSize = function(num) {
  var limits = [7,14,21,28,35,42,49,53];
  for (var i = 0; i < limits.length; i++) {
    var limit = limits[i];
    if (num < Math.pow(2, limit)) return Math.ceil(limit / 7);
  }
};

exports.newByteBuf = function(size) {
  return {buf: Buffer.alloc(f.existy(size) ? size : 128), offset: 0};
};
exports.assertEncode = function(bytebuf, encode, expectedBytes) {
  expect(encode(bytebuf)).toBe(expectedBytes);
  expect(bytebuf.buf.length).toBe(expectedBytes);
  return bytebuf;
};

exports.expectToBeBuffer = expectToBeBuffer;
function expectToBeBuffer(actual, expected) {
  expect(JSON.stringify(actual)).toBe(JSON.stringify(expected));
}

exports.expectEvent = function(key, done, removeAfterEvent, value) {
  return function(client) {
    if (f.existy(value)) {
      return function(eventKey, eventVersion, listenerId) {
        expect(eventKey).toEqual(key);
        assertListenerVersioned(key, value, eventVersion)(client)
          .then(function() {
            removeListener(client, listenerId, removeAfterEvent, done);
          })
          .catch(function(err) {
            client.removeListener(listenerId)
              .finally(exports.failed(done)(err));
          });
      }
    } else {
      return function(eventKey, listenerId) {
        expect(eventKey).toEqual(key);
        removeListener(client, listenerId, removeAfterEvent, done);
      }
    }
  }
};

exports.expectEventKeyOnly = function(key, done) {
  return function(client) {
    if (f.existy(done)) {
      return function (eventKey, listenerId) {
        expect(eventKey).toBe(key);
        removeListener(client, listenerId, true, done);
      }
    } else {
      return function(eventKey) {
        expect(eventKey).toBe(key);
      }
    }
  }
};

exports.expectCustomEvent = function(custom, done) {
  return function(client) {
    return function(eventCustom, listenerId) {
      expect(eventCustom).toEqual(custom);
      removeListener(client, listenerId, true, done);
    }
  }
};

function removeListener(client, listenerId, removeAfterEvent, done) {
  if (removeAfterEvent)
    return client.removeListener(listenerId)
      .then(function() { done(); })
      .catch(function(err) { done(err); });
  else
    return client.removeListener(listenerId);
}

function assertListenerVersioned(key, value, version) {
  return function(client) {
    return client.getWithMetadata(key)
      .then(function(getM) {
        expect(getM.value).toEqual(value);
        expectToBeBuffer(getM.version, version);
      })
  }
}

exports.expectEvents = function(keys, done, disconnect) {
  logger.debugf("Expect events for keys: %s", keys);
  return function(client) {
    var remain = keys;
    return function(eventKey, eventVersion, listenerId) {
      var match = _.filter(remain, function(k) {
        return _.isEqual(k, eventKey);
      });
      expect(match.length).toBe(1);
      remain = _.without(remain, eventKey);
      logger.debugf("Received event key=%s, still remaining [%s]", eventKey, remain);
      if (_.isEmpty(remain)) {
        var removed = removeListener(client, listenerId, true, done);
        if (disconnect) {
          removed.then(function() {
            return client.disconnect();
          });
        }
      }
    }
  }
};

exports.failed = function(done) {
  return function(error) {
    done(error);
  };
};

exports.randomStr = function(size) {
  var text = "";
  var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";

  for (var i = 0; i < size; i++)
    text += possible.charAt(Math.floor(Math.random() * possible.length));

  return text;
};

exports.findKeyForServers = function(client, addrs) {
  var attempts = 10000;
  var key;
  do {
    key = exports.randomStr(8);
    var owners = client.getTopologyInfo().findOwners(key);
    attempts--;
  } while (!_.isEqual(addrs, owners) && attempts >= 0);

  if (attempts < 0)
    throw new Error("Could not find any key owned by: " + u.showArrayAddress(addrs));

  logger.debugf("Generated key=%s hashing to %s", key, u.showArrayAddress(addrs));
  return key;
};

exports.getAll = function(keys) {
  return function(client) {
    return client.getAll(keys);
  }
};

exports.invalidVersion = function() {
  return Buffer.from([48, 49, 50, 51, 52, 53, 54, 55]);
};

exports.notReplaceWithVersion = function(k, opts) {
  return function(client) {
    return client.replaceWithVersion(k, 'ignore', exports.invalidVersion(), opts);
  }
};

exports.removeWithVersion = function(k, version, opts) {
  return function(client) {
    return client.removeWithVersion(k, version, opts);
  }
};

exports.notRemoveWithVersion = function(k, opts) {
  return function(client) {
    return client.removeWithVersion(k, exports.invalidVersion(), opts);
  }
};

exports.expectIteratorDone = function(it) {
  return function() {
    return it.next().then(function(entry) {
      expect(entry.done).toBeTruthy();
    })
  }
};

exports.seqIterator = function(sortByF, batchSize, expected, opts) {
  return function(client) {
    return client.iterator(batchSize, opts).then(function(it) {
      var p = _.foldl(_.range(expected.length),
          function(p) {
            return p.then(function(array) {
              return it.next().then(function(entry) {
                array.push(entry);
                return array;
              })
            });
          }, Promise.resolve([]));

      return p
          .then(function(array) { exports.toContainAllEntries(expected)(sortByF, array); })
          .then(function() { return it.close(); }) // Close iterator
          .then(function() { return client; });
    })
  }
};

exports.toEqualPairs = function(sortByF, value) {
  return function(actual) {
    expect(_.sortBy(actual, sortByF)).toEqual(value);
  }
};

exports.toContainAllEntries = function(expected) {
  return function(sortByF, actual) {
    var sorted = _.sortBy(actual, sortByF);
    return exports.toContainAll(expected)(sorted);
  }
};

exports.toContainAll = function(expected) {
  return function(actual) {
    var zipped = _.zip(actual, expected);
    _.map(zipped, function(e) {
      var actualEntry = e[0];
      var expectedEntry = e[1];
      exports.toContain(expectedEntry)(actualEntry);
    });
  }
};

exports.toBeStatIncr = function(stat) {
  return function(before, after) {
    expect(after[stat]).toBe(before[stat] + 1);
  }
};

exports.prev = function() {
  return { previous: true };
};

exports.expectToThrow = function(func, errorMessage, done) {
  expect(func).toThrow(errorMessage);
  if (f.existy(done)) done();
};

exports.getHotrodProtocolVersion = function() {
  var version;
  if (f.existy(process.env.protocol)) {
     version = process.env.protocol;
  }

  return version;
};

exports.stopClusterNode = function(port, waitStop) {
  return function() {
    var opUrl = "/server?action=stop";

    if (waitStop) {
      return invokeDmrHttpGet(opUrl, port).then(function() {
        return waitUntilStopped(port);
      });
    }

    return invokeDmrHttpGet(opUrl, port);
  }
};

function waitUntilStopped(port) {
  return waitUntil(
    function(resp) { expect(resp).toEqual(0); },
    function(resp) { return _.isEqual(resp, 0) },
    getServerStatus(port)
  );
}

function getServerStatus(port) {
  return function() {
    return new Promise(function(fulfil, reject) {
      var spawn = require('child_process').spawn;
      var child = spawn('fuser', [port + '/tcp']);
      var count = 0;

      child.stdout.on('data', function(chunk) {
          chunk.toString().split('\n').forEach(function(line) {
              count++;
          });
      });

      child.stdout.on('end', function(chunk) {
        fulfil(count);
      });
    })
  };
}

exports.stopAndWaitView = function(nodeStop, expectNumMembers, nodeView) {
  return function() {
    return exports.stopClusterNode(nodeStop, false)()
      .then(function() { return exports.waitUntilView(expectNumMembers, nodeView); });
  }
};

exports.waitUntilView = function(expectNumMembers, port) {
  logger.debugf("Wait until view of %d nodes on '%s'", expectNumMembers, port);
  return waitUntil(
    function(members) {
      logger.debugf(
          "Final check, check the expect %d nodes and got %d",
          expectNumMembers, members
      );
      expect(members).toEqual(expectNumMembers);
      logger.debugf("Check passed");
    },
    function(members) {
      var success = _.isEqual(expectNumMembers, members);
      logger.debugf(
          "Expected %d nodes, got %d, success=%s",
          expectNumMembers, members, success
      );
      return success;
    },
    getClusterMembers(port)
  );
};
exports.launchClusterNodeAndWaitView = function(nodeName, config, port, mcastAddr, expectNumMembers, client) {
    return new Promise(function (fulfill, reject) {
        var spawn = require('child_process').spawn;
        var path = require('path');

        var serverPath = path.resolve('server/' + exports.serverDirName + "/");
        var standaloneShPath = serverPath + '/bin/server.sh';
        var cmd = spawn(
            standaloneShPath,
            ['-c', config, '-p', port, '-s',serverPath + "/" + nodeName,
                '-Djgroups.mcast_addr=' + mcastAddr,
                '-Dinfinispan.node.name=' + nodeName,
                '-Djgroups.join_timeout=1000'
            ]);

        cmd.stderr.on('data', function (data) {
            logger.debugf('Stderr [%s]: %s', nodeName, data);
            reject(data);
        });

        cmd.on('exit', function (code) {
            logger.debugf('Child process exited with code %d', code);
        });

        fulfill();
    }).then(function() {
        return exports.waitUntilView(expectNumMembers, port);
    }).then(function() {
        return client;
    });
};

function waitUntil(expectF, cond, op) {
  var now = new Date().getTime();

  function done(actual) {
    var expired = new Date().getTime() < now + MAX_WAIT;
    logger.debugf(
        "Expired waiting for condition? %s", expired
    );
    return cond(actual) && !expired;
  }

  function loop(promise) {
    exports.sleepFor(1000); // brief sleep

    // Simple recursive loop until condition has been met
    return promise
      .then(function(response) {
        var isDone = done(response);
        logger.debugf("Is waiting done for condition? %s", isDone);

        return !isDone
          ? loop(op())
          : response;
      })
      .catch(function() {
        return loop(op());
      });
  }

  return loop(op())
    .then(function(actual) {
      expectF(actual);
    });
}

exports.sleepFor = function(sleepDuration) {
  var now = new Date().getTime();
  while(new Date().getTime() < now + sleepDuration){ /* do nothing */ }
};

function getClusterMembers(port) {
  return function() {
    var opUrl ="/cache-managers/clustered/";

    return invokeDmrHttpGet(opUrl, port)
      .then(function(response) {
        logger.debugf("Server '%s' replied with cluster members: %s", port, response.cluster_size);
        var members = response.cluster_size;
        logger.debugf("Members are: %s", members);
        return members;
      });
  }
}

function invokeDmrHttp(op, port) {
  return new Promise(function(fulfil, reject) {
    httpRequest({
      method: 'POST',
      url: 'http://localhost:' + port + '/rest/v2',
      auth: {
        user: 'admin',
        pass: 'mypassword',
        sendImmediately: false
      },
      headers: {
        'Content-Type' : 'application/json'
      },
      body: JSON.stringify(op)
    }, function(error, response, body) {
      if (!error && response.statusCode == 200) {
        fulfil(JSON.parse(body));
      } else {
        reject(util.format('Error (%s), body (%s), response(%s)',
          error, body, JSON.stringify(response)));
      }
    });
  });
}

function invokeDmrHttpGet(opUrl, port) {
    return new Promise(function(fulfil, reject) {
        httpRequest({
            method: 'GET',
            url: 'http://localhost:' + port + '/rest/v2' + opUrl,
            auth: {
                user: 'admin',
                pass: 'mypassword',
                sendImmediately: false
            },
            headers: {
                'Content-Type' : 'application/json'
            }
        }, function(error, response, body) {
            if (!error && response.statusCode == 200) {
              var resp = "";
              if (body) {
                resp = JSON.parse(body);
              }
              fulfil(resp);
            } else {
                reject(util.format('Error (%s), body (%s), response(%s)',
                    error, body, JSON.stringify(response)));
            }
        });
    });
}

function readFileAsync(path) {
  return new Promise(function(resolve,reject){
    readFile(path, function(err, data){
      if(err !== null) return reject(err);
      resolve(data);
    });
  });
}