"use strict";

var commons = require("@sinonjs/commons");
var referee = require("../referee");
var sinon = require("sinon");
var slice = require("@sinonjs/commons").prototypes.array.slice;
var assert = require("assert");
var allOff = require("event-emitter/all-off");

var testHelper = {
  setUp: function () {
    sinon.spy(referee, "fail");
    testHelper.okListener = sinon.spy();
    referee.on("pass", testHelper.okListener);
    testHelper.failListener = sinon.spy();
    referee.on("failure", testHelper.failListener);
    sinon.replace(commons.deprecated, "wrap", function (f) {
      return f;
    });
  },

  tearDown: function () {
    sinon.restore();
    delete testHelper.okListener;
    delete testHelper.failListener;
    allOff(referee);
    delete referee.throwOnFailure;
  },

  // eslint-disable-next-line no-use-before-define
  assertionTests: assertionTests,
};

function assertFailureEvent(callback) {
  var fails = this.failListener.callCount;
  var passes = this.okListener.callCount;
  referee.throwOnFailure = false;

  try {
    callback();
  } catch (e) {
    assert.fail("Assertion threw when it should not: " + e.message);
  }

  assert.equal(
    this.failListener.callCount,
    fails + 1,
    "Fail listener was not called once: " + this.failListener.callCount - fails
  );
  assert.equal(
    this.okListener.callCount,
    passes,
    "Pass listener was unexpectedly called"
  );
}

function passingAssertionTest(type, assertion, args) {
  return function () {
    var argsAsStrings = args.map(function (value) {
      if (value && value.toString) {
        return value.toString();
      }

      return value;
    });
    var callStr = type + "." + assertion + "(" + argsAsStrings.join(", ") + ")";

    try {
      referee[type][assertion].apply(referee, args);
      // eslint-disable-next-line no-empty
    } catch (e) {}

    assert.equal(
      testHelper.okListener.callCount,
      1,
      "Expected referee to emit the pass event once for " + callStr
    );
    sinon.assert.calledWith(testHelper.okListener, type + "." + assertion);
    sinon.assert.notCalled(referee.fail);
    sinon.assert.notCalled(testHelper.failListener);
  };
}

function failingAssertionTest(type, assertion, args) {
  return function () {
    var callStr = type + "." + assertion + "(" + args.join(", ") + ")";

    try {
      referee[type][assertion].apply(referee, args);

      // eslint-disable-next-line no-console
      console.log("Unexpectedly passed: " + callStr);
      // eslint-disable-next-line no-empty
    } catch (e) {}

    assert.equal(
      referee.fail.callCount,
      1,
      "Expected referee.fail to be called once for " +
        callStr +
        ", was called " +
        referee.fail.callCount +
        " times"
    );

    sinon.assert.notCalled(testHelper.okListener);
    sinon.assert.calledOnce(testHelper.failListener);

    assertFailureEvent.call(testHelper, function () {
      referee[type][assertion].apply(referee, args);
    });
  };
}

function assertionMessageTest(type, assertion, message, args) {
  var test = function () {
    var msg;

    try {
      referee[type][assertion].apply(referee, args);
      throw new Error(type + "." + assertion + " expected to fail");
    } catch (e) {
      assert.equal(e.name, "AssertionError", e.name + ": " + e.message);
      assert.equal(
        e.message,
        message,
        "Message was " + e.message + ", expected " + message
      );
      msg = e.message;
    }

    assert.equal(testHelper.failListener.args[0][0].name, "AssertionError");
    assert.equal(testHelper.failListener.args[0][0].message, msg);
  };

  return test;
}

function failingErrorPropertiesTest(type, assertion, properties, args) {
  return function () {
    try {
      referee[type][assertion].apply(referee, args);
      throw new Error(type + "." + assertion + " expected to fail");
    } catch (e) {
      assert.equal(e.name, "AssertionError", e.name + ": " + e.message);
      Object.keys(properties).forEach(function (key) {
        assert.deepEqual(
          e[key],
          properties[key],
          "AssertionError." + key + " " + e[key] + " == " + properties[key]
        );
      });
      Object.keys(e).forEach(function (key) {
        if (!properties.hasOwnProperty(key) && key !== "name") {
          throw new Error(
            type +
              "." +
              assertion +
              " defined unexpected property '" +
              key +
              "'' on error"
          );
        }
      });
    }
  };
}

function assertionTests(type, assertion, callback) {
  var tests = {
    setUp: testHelper.setUp,
    tearDown: testHelper.tearDown,
  };

  function pass(name) {
    var label = "should pass " + name;
    it(label, passingAssertionTest(type, assertion, slice(arguments, 1), name));
  }

  function fail(name) {
    var label = "should fail " + name;
    it(label, failingAssertionTest(type, assertion, slice(arguments, 1)));
  }

  function msg(name, message) {
    var label = "should fail " + name;
    var test = assertionMessageTest(
      type,
      assertion,
      message,
      slice(arguments, 2)
    );
    it(label, test);
    return test;
  }

  function error(name, properties) {
    var label = "should fail " + name + " with correct error properties";
    var test = failingErrorPropertiesTest(
      type,
      assertion,
      properties,
      slice(arguments, 2)
    );
    it(label, test);
  }

  describe(type + "." + assertion, function () {
    beforeEach(testHelper.setUp);
    afterEach(testHelper.tearDown);

    return callback.call(tests, pass, fail, msg, error);
  });

  return;
}

module.exports = testHelper;
