/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @format
 */

'use strict';

const Enum = require('../index');

describe('Enum', () => {
  const E = Enum({A: 1, B: 2});

  test('access members', () => {
    expect(E.A).toBe(1);
    expect(E.B).toBe(2);
  });

  test('only two own properties', () => {
    expect(Object.getOwnPropertyNames(E).length).toBe(2);
  });

  test('no enumerable properties', () => {
    let count = 0;
    for (const _ in E) {
      count++;
    }
    expect(count).toBe(0);
  });

  test('not extensible', () => {
    expect(Object.isExtensible(E)).toBe(false);
    expect(() => {
      E.C = 3;
    }).toThrow();
    expect(E.C).toBe(undefined);
    expect(Object.getOwnPropertyNames(E).length).toBe(2);
  });

  test('not writable', () => {
    expect(() => {
      E.A = 66;
    }).toThrow();
    expect(() => {
      E.B = 66;
    }).toThrow();
    expect(E.A).toBe(1);
    expect(E.B).toBe(2);
  });

  test('not deletable', () => {
    expect(() => {
      delete E.A;
    }).toThrow();
    expect(() => {
      delete E.B;
    }).toThrow();
    expect(E.A).toBe(1);
    expect(E.B).toBe(2);
  });

  test('not configurable', () => {
    expect(() => {
      Object.defineProperty(E, 'A', {
        value: 66,
        writable: true,
        enumerable: true,
        configurable: true,
      });
    }).toThrow('Cannot redefine property: A');
  });

  test('not castable to primitive', () => {
    expect(() => {
      '' + E;
    }).toThrow('Cannot convert object to primitive value');
  });

  test('prototype frozen', () => {
    const F = Enum({A: 1});
    expect(() => {
      Object.getPrototypeOf(F).isValid = () => true;
    }).toThrow();
    expect(F.isValid(1)).toBe(true);
  });

  describe('prototype methods', () => {
    test('isValid', () => {
      expect(E.isValid(1)).toBe(true);
      expect(E.isValid(2)).toBe(true);
      expect(E.isValid(3)).toBe(false);
      expect(E.isValid(null)).toBe(false);
      expect(E.isValid(undefined)).toBe(false);
    });

    test('cast', () => {
      expect(E.cast(1)).toBe(E.A);
      expect(E.cast(2)).toBe(E.B);
      expect(E.cast(3)).toBe(undefined);
    });

    test('members iterable', () => {
      expect(Array.from(E.members())).toEqual([1, 2]);
      let i = 1;
      for (const x of E.members()) {
        expect(x).toEqual(i);
        i++;
      }
    });

    test('members iterator', () => {
      const iter = E.members();
      expect(iter.next()).toEqual({value: 1, done: false});
      expect(iter.next()).toEqual({value: 2, done: false});
      expect(iter.next()).toEqual({value: undefined, done: true});
    });

    test('getName', () => {
      expect(E.getName(E.A)).toBe('A');
      expect(E.getName(E.B)).toBe('B');
    });
  });
});

describe('Enum.Mirrored', () => {
  const E = Enum.Mirrored(['A', 'B']);

  test('access members', () => {
    expect(E.A).toBe('A');
    expect(E.B).toBe('B');
  });

  test('only two own properties', () => {
    expect(Object.getOwnPropertyNames(E).length).toBe(2);
  });

  test('no enumerable properties', () => {
    let count = 0;
    for (const _ in E) {
      count++;
    }
    expect(count).toBe(0);
  });

  test('not extensible', () => {
    expect(Object.isExtensible(E)).toBe(false);
    expect(() => {
      E.C = 'C';
    }).toThrow();
    expect(E.C).toBe(undefined);
    expect(Object.getOwnPropertyNames(E).length).toBe(2);
  });

  test('not writable', () => {
    expect(() => {
      E.A = 'foo';
    }).toThrow();
    expect(() => {
      E.B = 'bar';
    }).toThrow();
    expect(E.A).toBe('A');
    expect(E.B).toBe('B');
  });

  test('not deletable', () => {
    expect(() => {
      delete E.A;
    }).toThrow();
    expect(() => {
      delete E.B;
    }).toThrow();
    expect(E.A).toBe('A');
    expect(E.B).toBe('B');
  });

  test('not configurable', () => {
    expect(() => {
      Object.defineProperty(E, 'A', {
        value: 'x',
        writable: true,
        enumerable: true,
        configurable: true,
      });
    }).toThrow('Cannot redefine property: A');
  });

  test('not castable to primitive', () => {
    expect(() => {
      '' + E;
    }).toThrow('Cannot convert object to primitive value');
  });

  test('prototype frozen', () => {
    const F = Enum.Mirrored(['A']);
    expect(() => {
      Object.getPrototypeOf(F).isValid = () => true;
    }).toThrow();
    expect(F.isValid('A')).toBe(true);
  });

  describe('prototype methods', () => {
    test('isValid', () => {
      expect(E.isValid('A')).toBe(true);
      expect(E.isValid('B')).toBe(true);
      expect(E.isValid('C')).toBe(false);
      expect(E.isValid(null)).toBe(false);
      expect(E.isValid(undefined)).toBe(false);

      const s = {
        toString() {
          return 'A';
        },
      };
      expect(E.isValid(s)).toBe(false);
    });

    test('cast', () => {
      expect(E.cast('A')).toBe(E.A);
      expect(E.cast('B')).toBe(E.B);
      expect(E.cast('C')).toBe(undefined);
    });

    test('members iterable', () => {
      const expected = ['A', 'B'];
      expect(Array.from(E.members())).toEqual(expected);
      let i = 0;
      for (const x of E.members()) {
        expect(x).toEqual(expected[i]);
        i++;
      }
    });

    test('members iterator', () => {
      const iter = E.members();
      expect(iter.next()).toEqual({value: 'A', done: false});
      expect(iter.next()).toEqual({value: 'B', done: false});
      expect(iter.next()).toEqual({value: undefined, done: true});
    });

    test('getName', () => {
      expect(E.getName(E.A)).toBe('A');
      expect(E.getName(E.B)).toBe('B');
    });
  });
});
