/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

const { Trait } = require('traits');

exports['test:simple compose'] = function(test) {
  let List = Trait.compose({
    _list: null,
    constructor: function List() {
      this._list = [];
    },
    list: function list() this._list.slice(0),
    add: function add(item) this._list.push(item),
    remove: function remove(item) {
      let list = this._list;
      let index = list.indexOf(item);
      if (0 <= index) list.slice(index, 1);
    }
  });

  test.assertNotEqual(undefined, List, 'should not be undefined');
  test.assertEqual('function', typeof List, 'type should be function');
  test.assertEqual(
    Trait.compose,
    List.compose,
    'should inherit static compose'
  );
  test.assertEqual(
    Trait.override,
    List.override,
    'should inherit static override'
  );
  test.assertEqual(
    Trait.required,
    List.required,
    'should inherit static required'
  );
  test.assertEqual(
    Trait.resolve,
    List.resolve,
    'should inherit static resolve'
  );

  test.assert(
    !('_list' in List.prototype),
    'should not expose private API'
  );
}
exports['test: compose trait instance and create instance'] = function(test) {
  let List = Trait.compose({
    constructor: function List(options) {
      this._list = [];
      this._public.publicMember = options.publicMember;
    },
    _privateMember: true,
    get privateMember() this._privateMember,
    get list() this._list.slice(0),
    add: function add(item) this._list.push(item),
    remove: function remove(item) {
      let list = this._list
      let index = list.indexOf(item)
      if (0 <= index) list.slice(index, 1)
    }
  });
  let list = List({ publicMember: true });

  test.assertEqual('object', typeof list, 'should return an object')
  test.assertEqual(
    true,
    list instanceof List,
    'should be instance of a List'
  );

  test.assertEqual(
    undefined,
    list._privateMember,
    'instance should not expose private API'
  );

  test.assertEqual(
    true,
    list.privateMember,
    'privates are accessible by  public API'
  );

  list._privateMember = false;

  test.assertEqual(
    true,
    list.privateMember,
    'property changes on instance must not affect privates'
  );

  test.assert(
    !('_list' in list),
    'instance should not expose private members'
  );

  test.assertEqual(
    true,
    list.publicMember,
    'public members are exposed'
  )
  test.assertEqual(
    'function',
    typeof list.add,
    'should be function'
  )
  test.assertEqual(
    'function',
    typeof list.remove,
    'should be function'
  );

  list.add(1);
  test.assertEqual(
    1,
    list.list[0],
    'exposed public API should be able of modifying privates'
  )
};


exports['test:instances must not be hackable'] = function(test) {
  let SECRET = 'There is no secret!',
      secret = null;

  let Class = Trait.compose({
    _secret: null,
    protect: function(data) this._secret = data
  });

  let i1 = Class();
  i1.protect(SECRET);

  test.assertEqual(
    undefined,
    (function() this._secret).call(i1),
    'call / apply can\'t access private state'
  );

  let proto = Object.getPrototypeOf(i1);
  try {
    proto.reveal = function() this._secret;
    secret = i1.reveal();
  } catch(e) {}
  test.assertNotEqual(
    SECRET,
    secret,
    'public __proto__ changes should not affect privates'
  );
  secret = null;

  let Class2 = Trait.compose({
    _secret: null,
    protect: function(data) this._secret = data
  });
  let i2 = Class2();
  i2.protect(SECRET);
  try {
    Object.prototype.reveal = function() this._secret;
    secret = i2.reveal();
  } catch(e) {}
  test.assertNotEqual(
    SECRET,
    secret,
    'Object.prototype changes must not affect instances'
  );
}

exports['test:instanceof'] = function(test) {
  const List = Trait.compose({
    // private API:
    _list: null,
    // public API
    constructor: function List() {
      this._list = []
    },
    get length() this._list.length,
    add: function add(item) this._list.push(item),
    remove: function remove(item) {
      let list = this._list;
      let index = list.indexOf(item);
      if (0 <= index) list.slice(index, 1);
    }
  });

  test.assert(List() instanceof List, 'Must be instance of List');
  test.assert(new List() instanceof List, 'Must be instance of List');
};

exports['test:privates are unaccessible'] = function(test) {
  const List = Trait.compose({
    // private API:
    _list: null,
    // public API
    constructor: function List() {
      this._list = [];
    },
    get length() this._list.length,
    add: function add(item) this._list.push(item),
    remove: function remove(item) {
      let list = this._list;
      let index = list.indexOf(item);
      if (0 <= index) list.slice(index, 1);
    }
  });

  let list = List();
  test.assert(!('_list' in list), 'no privates on instance');
  test.assert(
    !('_list' in List.prototype),
    'no privates on prototype'
  );
};

exports['test:public API can access private API'] = function(test) {
  const List = Trait.compose({
    // private API:
    _list: null,
    // public API
    constructor: function List() {
      this._list = [];
    },
    get length() this._list.length,
    add: function add(item) this._list.push(item),
    remove: function remove(item) {
      let list = this._list;
      let index = list.indexOf(item);
      if (0 <= index) list.slice(index, 1);
    }
  });
  let list = List();

  list.add('test');

  test.assertEqual(
    1,
    list.length,
    'should be able to add element and access it from public getter'
  );
};

exports['test:required'] = function(test) {
  const Enumerable = Trait.compose({
    list: Trait.required,
    forEach: function forEach(consumer) {
      return this.list.forEach(consumer);
    }
  });

  try {
    let i = Enumerable();
    test.fail('should throw when creating instance with required properties');
  } catch(e) {
    test.assertEqual(
      'Error: Missing required property: list',
      e.toString(),
      'required prop error'
    );
  }
};

exports['test:compose with required'] = function(test) {
  const List = Trait.compose({
    // private API:
    _list: null,
    // public API
    constructor: function List() {
      this._list = [];
    },
    get length() this._list.length,
    add: function add(item) this._list.push(item),
    remove: function remove(item) {
      let list = this._list;
      let index = list.indexOf(item);
      if (0 <= index) list.slice(index, 1);
    }
  });

  const Enumerable = Trait.compose({
    list: Trait.required,
    forEach: function forEach(consumer) {
      return this.list.forEach(consumer);
    }
  });

  const EnumerableList = Enumerable.compose({
    get list() this._list.slice(0)
  }, List);

  let array = [1,2, 'ab']
  let l = EnumerableList(array);
  array.forEach(function(element) l.add(element));
  let number = 0;
  l.forEach(function(element, index) {
    number ++;
    test.assertEqual(array[index], element, 'should mach array element')
  });
  test.assertEqual(
    array.length,
    number,
    'should perform as many asserts as elements in array'
  );
};

exports['test:resolve'] = function(test) {
  const List = Trait.compose({
    // private API:
    _list: null,
    // public API
    constructor: function List() {
      this._list = [];
    },
    get length() this._list.length,
    add: function add(item) this._list.push(item),
    remove: function remove(item) {
      let list = this._list;
      let index = list.indexOf(item);
      if (0 <= index) list.slice(index, 1);
    }
  });

  const Range = List.resolve({
    constructor: null,
    add: '_add',
  }).compose({
    min: null,
    max: null,
    get list() this._list.slice(0),
    constructor: function Range(min, max) {
      this.min = min;
      this.max = max;
      this._list = [];
    },
    add: function(item) {
      if (item <= this.max && item >= this.min)
        this._add(item)
    }
  });

  let r = Range(0, 10);

  test.assertEqual(
    0,
    r.min,
    'constructor must have set min'
  );
  test.assertEqual(
    10,
    r.max,
    'constructor must have set max'
  );

  test.assertEqual(
    0,
    r.length,
    'should not contain any elements'
  );

  r.add(5);

  test.assertEqual(
    1,
    r.length,
    'should add `5` to list'
  );

  r.add(12);

  test.assertEqual(
    1,
    r.length,
    'should not add `12` to list'
  );
};

exports['test:custom iterator'] = function(test) {
  let Sub = Trait.compose({
    foo: "foo",
    bar: "bar",
    baz: "baz",
    __iterator__: function() {
      yield 1;
      yield 2;
      yield 3;
    }
  });

  let (i = 0, sub = Sub()) {
    for (let item in sub)
    test.assertEqual(++i, item, "iterated item has the right value");
  };
};

