import { before, describe, it } from 'node:test';

import { expect } from 'chai';

import { enableDevInstanceOf, instanceOf } from '../instanceOf.ts';

describe('instanceOf', () => {
  before(() => {
    enableDevInstanceOf();
  });

  it('do not throw on values without prototype', () => {
    const fooSymbol: unique symbol = Symbol('Foo');
    class Foo {
      readonly __kind: symbol = fooSymbol;
      get [Symbol.toStringTag]() {
        return 'Foo';
      }
    }

    expect(instanceOf(true, fooSymbol, Foo)).to.equal(false);
    expect(instanceOf(null, fooSymbol, Foo)).to.equal(false);
    expect(instanceOf(Object.create(null), fooSymbol, Foo)).to.equal(false);
  });

  it('detect name clashes with older versions of this lib', () => {
    function oldVersion() {
      class Foo {}
      return Foo;
    }

    function newVersion() {
      const fooSymbol: unique symbol = Symbol('Foo');
      class FooClass {
        readonly __kind: symbol = fooSymbol;
        get [Symbol.toStringTag]() {
          return 'Foo';
        }
      }
      return { fooSymbol, FooClass };
    }

    const { fooSymbol: newSymbol, FooClass: NewClass } = newVersion();
    const OldClass = oldVersion();
    expect(instanceOf(new NewClass(), newSymbol, NewClass)).to.equal(true);
    expect(() => instanceOf(new OldClass(), newSymbol, NewClass)).to.throw();
  });

  it('allows instances to have share the same constructor name', () => {
    function getMinifiedClass(tag: string) {
      const someSymbol: unique symbol = Symbol(tag);
      class SomeNameAfterMinification {
        readonly __kind: symbol = someSymbol;
        get [Symbol.toStringTag]() {
          return tag;
        }
      }
      return { someSymbol, SomeNameAfterMinification };
    }

    const { someSymbol: fooSymbol, SomeNameAfterMinification: Foo } =
      getMinifiedClass('Foo');
    const { someSymbol: barSymbol, SomeNameAfterMinification: Bar } =
      getMinifiedClass('Bar');
    expect(instanceOf(new Foo(), barSymbol, Bar)).to.equal(false);
    expect(instanceOf(new Bar(), fooSymbol, Foo)).to.equal(false);

    const {
      someSymbol: duplicateOfFooSymbol,
      SomeNameAfterMinification: DuplicateOfFoo,
    } = getMinifiedClass('Foo');
    expect(() => instanceOf(new DuplicateOfFoo(), fooSymbol, Foo)).to.throw();
    expect(() => instanceOf(new Foo(), duplicateOfFooSymbol, Foo)).to.throw();
  });

  it('fails with descriptive error message', () => {
    function getFoo() {
      const fooSymbol: unique symbol = Symbol('Foo');
      class Foo {
        get [Symbol.toStringTag]() {
          return 'Foo';
        }
      }
      return { fooSymbol, Foo };
    }
    const { fooSymbol: foo1Symbol, Foo: Foo1 } = getFoo();
    const { fooSymbol: foo2Symbol, Foo: Foo2 } = getFoo();

    expect(() => instanceOf(new Foo1(), foo2Symbol, Foo2)).to.throw(
      /^Cannot use Foo "{}" from another module or realm./m,
    );
    expect(() => instanceOf(new Foo2(), foo1Symbol, Foo1)).to.throw(
      /^Cannot use Foo "{}" from another module or realm./m,
    );
  });
});