import { describe, test, expect } from 'vitest';

import { stableJSONStringify, testStringOut, testStringSrc } from '.';
import { stableJSONStringify_fast } from './fast.js';
import { stableJSONStringify_portable } from './portable.js';

// an object with keys that are intentionally out of order
const obj1 = {
  // some alphanumeric keys
  b: 0,
  a: 1,

  // some keys that could be interpreted as indices
  '1': 2,
  '10': 3,
  '7': 4,

  // some keys that could be interpreted as numbers but not as indices
  '2.0': 5,
  '10.0': 6,
  '+0': 7,
  '-0': 8,

  // a key that sorts before the numeric keys, but should appear after the indices
  $c: 9,

  // Symbol keys should be ignored
  [Symbol('f')]: 10
};

const expectedObj1String = '{"1":2,"7":4,"10":3,"$c":9,"+0":7,"-0":8,"10.0":6,"2.0":5,"a":1,"b":0}';

// a class with a toJSON method that returns an object
class ClassWithToJSON {
  toJSON() {
    return obj1;
  }
}

// an object with a property that is an instance of a class with a toJSON method
const obj2 = {
  a: new ClassWithToJSON()
};

const stableJSONStringify_implementations: Record<string, typeof stableJSONStringify> = {
  stableJSONStringify_fast,
  stableJSONStringify_portable
};

// an object with keys that should be ignored because of their value type
const obj3 = {
  a: undefined,
  b: () => {
    /* do nothing */
  }
};

const complexObj = {
  // deliberately out of order
  obj3,
  obj1,
  obj2,
  booleanKey: true
};

describe('stableJSONStringify', () => {
  describe('object keys should be returned in canonical order', () => {
    test.each(Object.entries(stableJSONStringify_implementations))('stableJSONStringify implementation: %s', (_name, stringify) => {
      const str1 = stringify(obj1);
      expect(str1).toEqual(expectedObj1String);

      // verify that the canonicalization is applied recursively to objects returned by toJSON methods
      const str2 = stringify(obj2);
      expect(str2).toEqual('{"a":' + expectedObj1String + '}');
    });
  });

  describe('some object properties should be skipped', () => {
    test.each(Object.entries(stableJSONStringify_implementations))('stableJSONStringify implementation: %s', (_name, stringify) => {
      expect(stringify(obj3)).toEqual('{}');
    });
  });

  describe('skipped values are treated as null in arrays', () => {
    test.each(Object.entries(stableJSONStringify_implementations))('stableJSONStringify implementation: %s', (_name, stringify) => {
      const values = Object.values(obj3);
      const expected = '[' + new Array(values.length).fill('null').join(',') + ']';
      expect(stringify(values)).toEqual(expected);
    });
  });

  describe('escaping of characters in strings is consistent', () => {
    test.each(Object.entries(stableJSONStringify_implementations))('stableJSONStringify implementation: %s', (_name, stringify) => {
      expect(stringify(testStringSrc)).toEqual(testStringOut);
    });
  });

  test('the two implementations agree on a complex object', () => {
    expect(stableJSONStringify_fast(complexObj)).toEqual(stableJSONStringify_portable(complexObj));
  });
});
