import Map from '..';
import MapWithDefault from '../with-default';
import OrderedSet, { __OrderedSet__ } from '../lib/ordered-set';
import { moduleFor, AbstractTestCase } from 'internal-test-helpers';

let object, number, string, map, variety;
const varieties = [['Map', Map], ['MapWithDefault', MapWithDefault]];

function testMap(nameAndFunc) {
  variety = nameAndFunc[0];

  moduleFor(
    'Ember.' + variety + ' (forEach and get are implicitly tested)',
    class extends AbstractTestCase {
      beforeEach() {
        object = {};
        number = 42;
        string = 'foo';

        expectDeprecation(
          () => {
            map = nameAndFunc[1].create();
          },
          'Use of @ember/Map is deprecated. Please use native `Map` instead',
          { id: 'ember-map-deprecation', until: '3.5.0' }
        );
      }

      ['@test set'](assert) {
        map.set(object, 'winning');
        map.set(number, 'winning');
        map.set(string, 'winning');

        mapHasEntries(assert, [[object, 'winning'], [number, 'winning'], [string, 'winning']]);

        map.set(object, 'losing');
        map.set(number, 'losing');
        map.set(string, 'losing');

        mapHasEntries(assert, [[object, 'losing'], [number, 'losing'], [string, 'losing']]);

        assert.equal(map.has('nope'), false, 'expected the key `nope` to not be present');
        assert.equal(map.has({}), false, 'expected they key `{}` to not be present');
      }

      ['@test set chaining'](assert) {
        map
          .set(object, 'winning')
          .set(number, 'winning')
          .set(string, 'winning');

        mapHasEntries(assert, [[object, 'winning'], [number, 'winning'], [string, 'winning']]);

        map
          .set(object, 'losing')
          .set(number, 'losing')
          .set(string, 'losing');

        mapHasEntries(assert, [[object, 'losing'], [number, 'losing'], [string, 'losing']]);

        assert.equal(map.has('nope'), false, 'expected the key `nope` to not be present');
        assert.equal(map.has({}), false, 'expected they key `{}` to not be present');
      }

      ['@test with key with undefined value'](assert) {
        map.set('foo', undefined);

        map.forEach(function(value, key) {
          assert.equal(value, undefined);
          assert.equal(key, 'foo');
        });

        assert.ok(map.has('foo'), 'has key foo, even with undefined value');

        assert.equal(map.size, 1);
      }

      ['@test arity of forEach is 1 – es6 23.1.3.5'](assert) {
        assert.equal(map.forEach.length, 1, 'expected arity for map.forEach is 1');
      }

      ['@test forEach throws without a callback as the first argument'](assert) {
        assert.equal(map.forEach.length, 1, 'expected arity for map.forEach is 1');
      }

      ['@test has empty collection'](assert) {
        assert.equal(map.has('foo'), false);
        assert.equal(map.has(), false);
      }

      ['@test delete'](assert) {
        expectNoDeprecation();

        map.set(object, 'winning');
        map.set(number, 'winning');
        map.set(string, 'winning');

        map.delete(object);
        map.delete(number);
        map.delete(string);

        // doesn't explode
        map.delete({});

        mapHasEntries(assert, []);
      }

      ['@test copy and then update'](assert) {
        map.set(object, 'winning');
        map.set(number, 'winning');
        map.set(string, 'winning');

        let map2;
        expectDeprecation(
          () => {
            map2 = map.copy();
          },
          'Use of @ember/Map is deprecated. Please use native `Map` instead',
          { id: 'ember-map-deprecation', until: '3.5.0' }
        );

        map2.set(object, 'losing');
        map2.set(number, 'losing');
        map2.set(string, 'losing');

        mapHasEntries(assert, [[object, 'winning'], [number, 'winning'], [string, 'winning']]);

        mapHasEntries(assert, [[object, 'losing'], [number, 'losing'], [string, 'losing']], map2);
      }

      ['@test copy and then delete'](assert) {
        map.set(object, 'winning');
        map.set(number, 'winning');
        map.set(string, 'winning');

        let map2;
        expectDeprecation(
          () => {
            map2 = map.copy();
          },
          'Use of @ember/Map is deprecated. Please use native `Map` instead',
          { id: 'ember-map-deprecation', until: '3.5.0' }
        );

        map2.delete(object);
        map2.delete(number);
        map2.delete(string);

        mapHasEntries(assert, [[object, 'winning'], [number, 'winning'], [string, 'winning']]);

        mapHasEntries(assert, [], map2);
      }

      ['@test size'](assert) {
        //Add a key twice
        assert.equal(map.size, 0);
        map.set(string, 'a string');
        assert.equal(map.size, 1);
        map.set(string, 'the same string');
        assert.equal(map.size, 1);

        //Add another
        map.set(number, 'a number');
        assert.equal(map.size, 2);

        //Remove one that doesn't exist
        map.delete('does not exist');
        assert.equal(map.size, 2);

        //Check copy
        expectDeprecation(
          () => {
            let copy = map.copy();
            assert.equal(copy.size, 2);
          },
          'Use of @ember/Map is deprecated. Please use native `Map` instead',
          { id: 'ember-map-deprecation', until: '3.5.0' }
        );

        //Remove a key twice
        map.delete(number);
        assert.equal(map.size, 1);
        map.delete(number);
        assert.equal(map.size, 1);

        //Remove the last key
        map.delete(string);
        assert.equal(map.size, 0);
        map.delete(string);
        assert.equal(map.size, 0);
      }

      ['@test forEach without proper callback'](assert) {
        expectAssertion(function() {
          map.forEach();
        }, '[object Undefined] is not a function');

        expectAssertion(function() {
          map.forEach(undefined);
        }, '[object Undefined] is not a function');

        expectAssertion(function() {
          map.forEach(1);
        }, '[object Number] is not a function');

        expectAssertion(function() {
          map.forEach({});
        }, '[object Object] is not a function');

        map.forEach(function(value, key) {
          map.delete(key);
        });
        // ensure the error happens even if no data is present
        assert.equal(map.size, 0);
        expectAssertion(function() {
          map.forEach({});
        }, '[object Object] is not a function');
      }

      ['@test forEach basic'](assert) {
        map.set('a', 1);
        map.set('b', 2);
        map.set('c', 3);

        let iteration = 0;

        let expectations = [
          { value: 1, key: 'a', context: unboundThis },
          { value: 2, key: 'b', context: unboundThis },
          { value: 3, key: 'c', context: unboundThis },
        ];

        map.forEach(function(value, key, theMap) {
          let expectation = expectations[iteration];

          assert.equal(value, expectation.value, 'value should be correct');
          assert.equal(key, expectation.key, 'key should be correct');
          assert.equal(this, expectation.context, 'context should be as if it was unbound');
          assert.equal(map, theMap, 'map being iterated over should be passed in');

          iteration++;
        });

        assert.equal(iteration, 3, 'expected 3 iterations');
      }

      ['@test forEach basic /w context'](assert) {
        map.set('a', 1);
        map.set('b', 2);
        map.set('c', 3);

        let iteration = 0;
        let context = {};
        let expectations = [
          { value: 1, key: 'a', context: context },
          { value: 2, key: 'b', context: context },
          { value: 3, key: 'c', context: context },
        ];

        map.forEach(function(value, key, theMap) {
          let expectation = expectations[iteration];

          assert.equal(value, expectation.value, 'value should be correct');
          assert.equal(key, expectation.key, 'key should be correct');
          assert.equal(this, expectation.context, 'context should be as if it was unbound');
          assert.equal(map, theMap, 'map being iterated over should be passed in');

          iteration++;
        }, context);

        assert.equal(iteration, 3, 'expected 3 iterations');
      }

      ['@test forEach basic /w deletion while enumerating'](assert) {
        map.set('a', 1);
        map.set('b', 2);
        map.set('c', 3);

        let iteration = 0;

        let expectations = [
          { value: 1, key: 'a', context: unboundThis },
          { value: 2, key: 'b', context: unboundThis },
        ];

        map.forEach(function(value, key, theMap) {
          if (iteration === 0) {
            map.delete('c');
          }

          let expectation = expectations[iteration];

          assert.equal(value, expectation.value, 'value should be correct');
          assert.equal(key, expectation.key, 'key should be correct');
          assert.equal(this, expectation.context, 'context should be as if it was unbound');
          assert.equal(map, theMap, 'map being iterated over should be passed in');

          iteration++;
        });

        assert.equal(iteration, 2, 'expected 3 iterations');
      }

      ['@test forEach basic /w addition while enumerating'](assert) {
        map.set('a', 1);
        map.set('b', 2);
        map.set('c', 3);

        let iteration = 0;

        let expectations = [
          { value: 1, key: 'a', context: unboundThis },
          { value: 2, key: 'b', context: unboundThis },
          { value: 3, key: 'c', context: unboundThis },
          { value: 4, key: 'd', context: unboundThis },
        ];

        map.forEach(function(value, key, theMap) {
          if (iteration === 0) {
            map.set('d', 4);
          }

          let expectation = expectations[iteration];

          assert.equal(value, expectation.value, 'value should be correct');
          assert.equal(key, expectation.key, 'key should be correct');
          assert.equal(this, expectation.context, 'context should be as if it was unbound');
          assert.equal(map, theMap, 'map being iterated over should be passed in');

          iteration++;
        });

        assert.equal(iteration, 4, 'expected 3 iterations');
      }

      ['@test clear'](assert) {
        let iterations = 0;

        map.set('a', 1);
        map.set('b', 2);
        map.set('c', 3);
        map.set('d', 4);

        assert.equal(map.size, 4);

        map.forEach(function() {
          iterations++;
        });
        assert.equal(iterations, 4);

        map.clear();
        assert.equal(map.size, 0);
        iterations = 0;
        map.forEach(function() {
          iterations++;
        });
        assert.equal(iterations, 0);
      }

      ['@skip -0'](assert) {
        assert.equal(map.has(-0), false);
        assert.equal(map.has(0), false);

        map.set(-0, 'zero');

        assert.equal(map.has(-0), true);
        assert.equal(map.has(0), true);

        assert.equal(map.get(0), 'zero');
        assert.equal(map.get(-0), 'zero');

        map.forEach(function(value, key) {
          assert.equal(1 / key, Infinity, 'spec says key should be positive zero');
        });
      }

      ['@test NaN'](assert) {
        assert.equal(map.has(NaN), false);

        map.set(NaN, 'not-a-number');

        assert.equal(map.has(NaN), true);

        assert.equal(map.get(NaN), 'not-a-number');
      }

      ['@test NaN Boxed'](assert) {
        //jshint -W053
        let boxed = new Number(NaN);
        assert.equal(map.has(boxed), false);

        map.set(boxed, 'not-a-number');

        assert.equal(map.has(boxed), true);
        assert.equal(map.has(NaN), false);

        assert.equal(map.get(NaN), undefined);
        assert.equal(map.get(boxed), 'not-a-number');
      }

      ['@test 0 value'](assert) {
        let obj = {};
        assert.equal(map.has(obj), false);

        assert.equal(map.size, 0);
        map.set(obj, 0);
        assert.equal(map.size, 1);

        assert.equal(map.has(obj), true);
        assert.equal(map.get(obj), 0);

        map.delete(obj);
        assert.equal(map.has(obj), false);
        assert.equal(map.get(obj), undefined);
        assert.equal(map.size, 0);
      }
    }
  );

  let mapHasLength = function(assert, expected, theMap) {
    theMap = theMap || map;

    let length = 0;
    theMap.forEach(function() {
      length++;
    });

    assert.equal(length, expected, 'map should contain ' + expected + ' items');
  };

  let mapHasEntries = function(assert, entries, theMap) {
    theMap = theMap || map;

    for (let i = 0; i < entries.length; i++) {
      assert.equal(theMap.get(entries[i][0]), entries[i][1]);
      assert.equal(theMap.has(entries[i][0]), true);
    }

    mapHasLength(assert, entries.length, theMap);
  };

  let unboundThis;

  (function() {
    unboundThis = this;
  })();
}

for (let i = 0; i < varieties.length; i++) {
  testMap(varieties[i]);
}

moduleFor(
  'MapWithDefault - default values',
  class extends AbstractTestCase {
    ['@test Retrieving a value that has not been set returns and sets a default value'](assert) {
      let map;
      expectDeprecation(
        () => {
          map = MapWithDefault.create({
            defaultValue(key) {
              return [key];
            },
          });
        },
        'Use of @ember/Map is deprecated. Please use native `Map` instead',
        { id: 'ember-map-deprecation', until: '3.5.0' }
      );

      let value = map.get('ohai');
      assert.deepEqual(value, ['ohai']);

      assert.strictEqual(value, map.get('ohai'));
    }

    ['@test Map.prototype.constructor'](assert) {
      expectDeprecation(
        () => {
          let map = new Map();
          assert.equal(map.constructor, Map);
        },
        'Use of @ember/Map is deprecated. Please use native `Map` instead',
        { id: 'ember-map-deprecation', until: '3.5.0' }
      );
    }

    ['@test MapWithDefault.prototype.constructor'](assert) {
      expectDeprecation(
        () => {
          let map = new MapWithDefault({
            defaultValue(key) {
              return key;
            },
          });
          assert.equal(map.constructor, MapWithDefault);
        },
        'Use of @ember/Map is deprecated. Please use native `Map` instead',
        { id: 'ember-map-deprecation', until: '3.5.0' }
      );
    }

    ['@test Copying a MapWithDefault copies the default value'](assert) {
      let map;
      expectDeprecation(
        () => {
          map = MapWithDefault.create({
            defaultValue(key) {
              return [key];
            },
          });
        },
        'Use of @ember/Map is deprecated. Please use native `Map` instead',
        { id: 'ember-map-deprecation', until: '3.5.0' }
      );

      map.set('ohai', 1);
      map.get('bai');

      let map2;
      expectDeprecation(
        () => {
          map2 = map.copy();
        },
        'Use of @ember/Map is deprecated. Please use native `Map` instead',
        { id: 'ember-map-deprecation', until: '3.5.0' }
      );

      assert.equal(map2.get('ohai'), 1);
      assert.deepEqual(map2.get('bai'), ['bai']);

      map2.set('kthx', 3);

      assert.deepEqual(map.get('kthx'), ['kthx']);
      assert.equal(map2.get('kthx'), 3);

      assert.deepEqual(map2.get('default'), ['default']);

      map2.defaultValue = key => ['tom is on', key];

      assert.deepEqual(map2.get('drugs'), ['tom is on', 'drugs']);
    }
  }
);

moduleFor(
  'OrderedSet',
  class extends AbstractTestCase {
    beforeEach() {
      object = {};
      number = 42;
      string = 'foo';

      expectDeprecation(
        () => {
          map = OrderedSet.create();
        },
        'Use of @ember/OrderedSet is deprecated. Please use native `Map` instead',
        { id: 'ember-map-deprecation', until: '3.5.0' }
      );
    }

    ['@test add returns the set'](assert) {
      let obj = {};
      assert.equal(map.add(obj), map);
      assert.equal(map.add(obj), map, 'when it is already in the set');
    }
  }
);

moduleFor(
  '__OrderedSet__',
  class extends AbstractTestCase {
    ['@test private __OrderedSet__ can be created without deprecation']() {
      expectNoDeprecation();
      __OrderedSet__.create();
    }
  }
);