'use strict';
const assert = require('assert');
const ref = require('ref-napi');
const Array = require('ref-array-di')(ref);
const Struct = require('ref-struct-di')(ref);
const ffi = require('../');
const bindings = require('bindings')({ module_root: __dirname, bindings: 'ffi_tests' });

describe('ForeignFunction', function () {
  afterEach(global.gc);

  // these structs are also defined in ffi_tests.cc
  const box = Struct({
    width: ref.types.int,
    height: ref.types.int
  });

  const arst = Struct({
    num: 'int',
    array: Array('double', 20)
  });

  it('should call the static "abs" bindings', function () {
    const _abs = bindings.abs;
    const abs = ffi.ForeignFunction(_abs, 'int', [ 'int' ]);
    assert.strictEqual('function', typeof abs);
    assert.strictEqual(1234, abs(-1234));
  })

  it('should throw an Error with a meaningful message when type\'s `set()` throws', function () {
    const _abs = bindings.abs;
    const abs = ffi.ForeignFunction(_abs, 'int', [ 'int' ]);
    assert.throws(function () {
      // Changed, because returning string is not failing because of this; https://github.com/iojs/io.js/issues/1161
      abs(11111111111111111111);
    }, /error setting argument 1/);
  });

  it('should call the static "atoi" bindings', function () {
    const _atoi = bindings.atoi;
    const atoi = ffi.ForeignFunction(_atoi, 'int', [ 'string' ]);
    assert.strictEqual('function', typeof atoi);
    assert.strictEqual(1234, atoi('1234'));
  });

  it('should call the static "double_box" bindings', function () {
    const double_box = ffi.ForeignFunction(bindings.double_box, box, [ box ]);
    const b = new box;
    assert(b instanceof box);
    b.width = 4;
    b.height = 5;
    const out = double_box(b);
    // double_box writes to its input "box" struct, so make sure that the one we
    // passed in remains unaffected (since we passed it in by value, not pointer)
    assert.strictEqual(4, b.width);
    assert.strictEqual(5, b.height);
    assert(out instanceof box);
    assert.strictEqual(8, out.width);
    assert.strictEqual(10, out.height);
    assert.notStrictEqual(b.ref().address(), out.ref().address());
  });

  it('should call the static "double_box_ptr" bindings', function () {
    const boxPtr = ref.refType(box);
    const double_box_ptr = ffi.ForeignFunction(bindings.double_box_ptr, box, [ boxPtr ]);
    const b = new box;
    b.width = 4;
    b.height = 5;
    const out = double_box_ptr(b.ref());
    // double_box_ptr writes to its input "box" struct, so make sure that the one
    // we passed in has it's values changed (since we passed it in by pointer)
    assert.strictEqual(8, b.width);
    assert.strictEqual(10, b.height);
    assert(out instanceof box);
    assert.strictEqual(8, out.width);
    assert.strictEqual(10, out.height);
    assert.notStrictEqual(b.ref().address(), out.ref().address());
  });

  it('should call the static "area_box" bindings', function () {
    const area_box = ffi.ForeignFunction(bindings.area_box, ref.types.int, [ box ]);
    const b = new box({ width: 5, height: 20 });
    const rtn = area_box(b);
    assert.strictEqual('number', typeof rtn);
    assert.strictEqual(100, rtn);
  });

  it('should call the static "area_box_ptr" bindings', function () {
    const boxPtr = ref.refType(box);
    const area_box = ffi.ForeignFunction(bindings.area_box_ptr, ref.types.int, [ boxPtr ]);
    const b = new box({ width: 5, height: 20 });
    const rtn = area_box(b.ref());
    assert.strictEqual('number', typeof rtn);
    assert.strictEqual(100, rtn);
  });

  it('should call the static "create_box" bindings', function () {
    const create_box = ffi.ForeignFunction(bindings.create_box, box, [ 'int', 'int' ]);
    const rtn = create_box(1, 2);
    assert(rtn instanceof box);
    assert.strictEqual(1, rtn.width);
    assert.strictEqual(2, rtn.height);
  });

  it('should call the static "add_boxes" bindings', function () {
    const count = 3;
    const boxes = new Buffer(box.size * count);
    box.set(boxes, box.size * 0, { width: 1, height: 10 });
    box.set(boxes, box.size * 1, { width: 2, height: 20 });
    box.set(boxes, box.size * 2, { width: 3, height: 30 });
    const boxPtr = ref.refType(box);
    const add_boxes = ffi.ForeignFunction(bindings.add_boxes, box, [ boxPtr, 'int' ]);
    const rtn = add_boxes(boxes, count);
    assert(rtn instanceof box);
    assert.strictEqual(6, rtn.width);
    assert.strictEqual(60, rtn.height);
  });

  it('should call the static "int_array" bindings', function () {
    const IntArray = Array('int');
    const int_array = ffi.ForeignFunction(bindings.int_array, IntArray, [ IntArray ]);
    const array = new IntArray([ 1, 2, 3, 4, 5, -1 ]);
    const out = int_array(array);
    out.length = array.length;
    assert.strictEqual(2, out[0]);
    assert.strictEqual(4, out[1]);
    assert.strictEqual(6, out[2]);
    assert.strictEqual(8, out[3]);
    assert.strictEqual(10, out[4]);
    assert.strictEqual(-1, out[5]);
  });

  it('should call the static "array_in_struct" bindings', function () {
    const array_in_struct = ffi.ForeignFunction(bindings.array_in_struct, arst, [ arst ]);
    const a = new arst;
    assert.strictEqual(20, a.array.length);
    a.num = 69;
    for (let i = 0; i < 20; i++) {
      a.array[i] = i / 3.14;
    }

    const b = array_in_struct(a);
    assert(b instanceof arst);
    assert.strictEqual(138, b.num);
    assert.strictEqual(20, b.array.length);
    for (let i = 0; i < 20; i++) {
      // Math.round() because of floating point rounding erros
      assert.strictEqual(i, Math.round(b.array[i]));
    }
  });

  // allow a Buffer backing store to be used as a "string" FFI argument
  // https://github.com/node-ffi/node-ffi/issues/169
  it('should call the static "test_169" bindings', function () {
    const test = ffi.ForeignFunction(bindings.test_169, 'int', [ 'string', 'int' ]);
    const b = new Buffer(20);
    const len = test(b, b.length);
    assert.strictEqual('sample str', b.toString('ascii', 0, len));
  });

  // testing `bool` ref type
  // https://github.com/TooTallNate/ref/issues/56
  it('should call the static "test_169" bindings', function () {
    const Obj56 = Struct({
      'traceMode': ref.types.bool
    });
    const t = new Obj56({ traceMode: true });
    const f = new Obj56({ traceMode: false });

    const test = ffi.ForeignFunction(bindings.test_ref_56, 'int', [ ref.refType(Obj56) ]);

    assert.strictEqual(1, test(t.ref()));
    assert.strictEqual(0, test(f.ref()));
  });

  it('should not call the "ref()" function of its arguments', function () {
    const void_ptr_arg = ffi.ForeignFunction(bindings.abs, 'void *', [ 'void *' ]);
    const b = new Buffer(0);
    b.ref = assert.bind(null, 0, '"ref()" should not be called');
    void_ptr_arg(b);
  });

  describe('async', function () {
    it('should call the static "abs" bindings asynchronously', function (done) {
      const _abs = bindings.abs;
      const abs = ffi.ForeignFunction(_abs, 'int', [ 'int' ]);
      assert.strictEqual('function', typeof abs.async);

      // invoke asynchronously
      abs.async(-1234, function (err, res) {
        assert.strictEqual(null, err);
        assert.strictEqual(1234, res);
        done();
      });
    });

    it('should invoke the callback with an Error with a meaningful message when type\'s `set()` throws', function (done) {
      const _abs = bindings.abs;
      const abs = ffi.ForeignFunction(_abs, 'int', [ 'int' ]);

      // Changed, because returning string is not failing because of this; https://github.com/iojs/io.js/issues/1161
      abs.async(1111111111111111111111, function (err, res) {
        try {
          assert(err);
          assert(/error setting argument 0/.test(err.message));
          assert.strictEqual('undefined', typeof res);
          done();
        }
        catch (e) {
          done(e);
        }
      });
    });
  });
});
