import { Embeddable, Embedded, Entity, Enum, PrimaryKey, Property, SerializedPrimaryKey, wrap } from '@mikro-orm/core';
import { MikroORM } from '@mikro-orm/mongodb';
import { ObjectId } from 'bson';
import { mockLogger } from '../../helpers';

enum AnimalType {
  CAT,
  DOG,
}

@Embeddable({ abstract: true, discriminatorColumn: 'type' })
abstract class Animal {

  @Enum(() => AnimalType)
  type!: AnimalType;

  @Property()
  name!: string;

}

@Embeddable({ discriminatorValue: AnimalType.CAT })
class Cat extends Animal {

  @Property({ nullable: true })
  canMeow?: boolean = true;

  constructor(name: string) {
    super();
    this.type = AnimalType.CAT;
    this.name = name;
  }

}

@Embeddable({ discriminatorValue: AnimalType.DOG })
class Dog extends Animal {

  @Property({ nullable: true })
  canBark?: boolean = true;

  constructor(name: string) {
    super();
    this.type = AnimalType.DOG;
    this.name = name;
  }

}

@Entity()
class Owner {

  @PrimaryKey()
  _id!: ObjectId;

  @SerializedPrimaryKey()
  id!: string;

  @Property()
  name!: string;

  @Embedded(() => [Cat, Dog])
  pet!: Cat | Dog;

  @Embedded(() => [Cat, Dog], { object: true })
  pet2!: Cat | Dog;

}

describe('polymorphic embeddables in mongo', () => {

  let orm: MikroORM;

  beforeAll(async () => {
    orm = await MikroORM.init({
      entities: [Owner],
      clientUrl: 'mongodb://localhost:27017/mikro-orm-test-poly-embeddables',
      validate: true,
      ensureIndexes: true,
    });
  });

  afterAll(async () => {
    await orm.schema.dropSchema();
    await orm.close();
  });

  beforeEach(async () => {
    await orm.em.nativeDelete(Owner, {});
    orm.em.clear();
  });

  test(`working with polymorphic embeddables`, async () => {
    const ent1 = new Owner();
    ent1._id = new ObjectId('600000000000000000000001');
    ent1.name = 'o1';
    ent1.pet = new Dog('d1');
    ent1.pet2 = new Cat('c3');
    expect(ent1.pet).toBeInstanceOf(Dog);
    expect((ent1.pet as Dog).canBark).toBe(true);
    expect(ent1.pet2).toBeInstanceOf(Cat);
    expect((ent1.pet2 as Cat).canMeow).toBe(true);
    const ent2 = orm.em.create(Owner, {
      id: '600000000000000000000002',
      name: 'o2',
      pet: { type: AnimalType.CAT, name: 'c1' },
      pet2: { type: AnimalType.DOG, name: 'd4' },
    });
    expect(ent2.pet).toBeInstanceOf(Cat);
    expect((ent2.pet as Cat).canMeow).toBe(true);
    expect(ent2.pet2).toBeInstanceOf(Dog);
    expect((ent2.pet2 as Dog).canBark).toBe(true);
    const ent3 = orm.em.create(Owner, {
      id: '600000000000000000000003',
      name: 'o3',
      pet: { type: AnimalType.DOG, name: 'd2' },
      pet2: { type: AnimalType.CAT, name: 'c4' },
    });
    expect(ent3.pet).toBeInstanceOf(Dog);
    expect((ent3.pet as Dog).canBark).toBe(true);
    expect(ent3.pet2).toBeInstanceOf(Cat);
    expect((ent3.pet2 as Cat).canMeow).toBe(true);

    const mock = mockLogger(orm, ['query']);
    await orm.em.persistAndFlush([ent1, ent2, ent3]);
    expect(mock.mock.calls[0][0]).toMatch(`db.getCollection('owner').insertMany([ { _id: ObjectId('600000000000000000000002'), name: 'o2', pet_canMeow: true, pet_type: 0, pet_name: 'c1', pet2: { type: 1, name: 'd4', canBark: true } }, { _id: ObjectId('600000000000000000000003'), name: 'o3', pet_type: 1, pet_name: 'd2', pet_canBark: true, pet2: { canMeow: true, type: 0, name: 'c4' } }, { _id: ObjectId('600000000000000000000001'), name: 'o1', pet_type: 1, pet_name: 'd1', pet_canBark: true, pet2: { canMeow: true, type: 0, name: 'c3' } } ], {});`);
    orm.em.clear();

    const owners = await orm.em.find(Owner, {}, { orderBy: { name: 1 } });
    expect(mock.mock.calls[1][0]).toMatch(`db.getCollection('owner').find({}, {}).sort([ [ 'name', 1 ] ]).toArray();`);
    expect(owners[0].name).toBe('o1');
    expect(owners[0].pet).toBeInstanceOf(Dog);
    expect(owners[0].pet.name).toBe('d1');
    expect(owners[0].pet.type).toBe(AnimalType.DOG);
    expect((owners[0].pet as Cat).canMeow).toBeUndefined();
    expect((owners[0].pet as Dog).canBark).toBe(true);
    expect(owners[1].pet).toBeInstanceOf(Cat);
    expect(owners[1].pet.name).toBe('c1');
    expect(owners[1].pet.type).toBe(AnimalType.CAT);
    expect((owners[1].pet as Cat).canMeow).toBe(true);
    expect((owners[1].pet as Dog).canBark).toBeUndefined();
    expect(owners[2].pet).toBeInstanceOf(Dog);
    expect(owners[2].pet.name).toBe('d2');
    expect(owners[2].pet.type).toBe(AnimalType.DOG);

    expect(mock.mock.calls).toHaveLength(2);
    await orm.em.flush();
    expect(mock.mock.calls).toHaveLength(2);

    owners[0].pet = new Cat('c2');
    owners[1].pet = new Dog('d3');
    owners[2].pet.name = 'old dog';
    mock.mock.calls.length = 0;
    await orm.em.flush();
    expect(mock.mock.calls).toHaveLength(1);
    expect(mock.mock.calls[0][0]).toMatch(`bulk = db.getCollection('owner').initializeUnorderedBulkOp({});bulk.find({ _id: ObjectId('600000000000000000000001') }).update({ '$set': { pet_canMeow: true, pet_type: 0, pet_name: 'c2' }, '$unset': { pet_canBark: '' } });bulk.find({ _id: ObjectId('600000000000000000000002') }).update({ '$set': { pet_type: 1, pet_name: 'd3', pet_canBark: true }, '$unset': { pet_canMeow: '' } });bulk.find({ _id: ObjectId('600000000000000000000003') }).update({ '$set': { pet_name: 'old dog' } });bulk.execute()`);
    orm.em.clear();

    const owners2 = await orm.em.find(Owner, {}, { orderBy: { name: 1 } });
    expect(mock.mock.calls[1][0]).toMatch(`db.getCollection('owner').find({}, {}).sort([ [ 'name', 1 ] ]).toArray();`);

    expect(owners2[0].name).toBe('o1');
    expect(owners2[0].pet).toBeInstanceOf(Cat);
    expect(owners2[0].pet.name).toBe('c2');
    expect(owners2[0].pet.type).toBe(AnimalType.CAT);
    expect((owners2[0].pet as Dog).canBark).toBeUndefined();
    expect((owners2[0].pet as Cat).canMeow).toBe(true);

    expect(owners2[1].pet).toBeInstanceOf(Dog);
    expect(owners2[1].pet.name).toBe('d3');
    expect(owners2[1].pet.type).toBe(AnimalType.DOG);
    expect((owners2[1].pet as Dog).canBark).toBe(true);
    expect((owners2[1].pet as Cat).canMeow).toBeUndefined();

    expect(owners2[2].pet).toBeInstanceOf(Dog);
    expect(owners2[2].pet.name).toBe('old dog');
    expect(owners2[2].pet.type).toBe(AnimalType.DOG);
    expect((owners2[2].pet as Dog).canBark).toBe(true);
    expect((owners2[2].pet as Cat).canMeow).toBeUndefined();
    orm.em.clear();

    mock.mock.calls.length = 0;
    const dogOwners = await orm.em.find(Owner, { pet: { name: ['d3', 'old dog'] } }, { orderBy: { name: 1 } });
    const dogOwners2 = await orm.em.find(Owner, { pet2: { name: ['d4', 'c4'] } }, { orderBy: { name: 1 } });
    const dogOwners3 = await orm.em.find(Owner, { $or: [
      { pet: { name: ['d3', 'old dog'] } },
      { pet2: { name: ['d4', 'c4'] } },
    ] }, { orderBy: { name: 1 } });

    const check = (items: Owner[]) => {
      expect(items).toHaveLength(2);
      expect(items[0].pet).toBeInstanceOf(Dog);
      expect(items[0].pet.name).toBe('d3');
      expect(items[0].pet2).toBeInstanceOf(Dog);
      expect(items[0].pet2.name).toBe('d4');
      expect(items[1].pet).toBeInstanceOf(Dog);
      expect(items[1].pet.name).toBe('old dog');
      expect(items[1].pet2).toBeInstanceOf(Cat);
      expect(items[1].pet2.name).toBe('c4');
    };
    check(dogOwners);
    check(dogOwners2);
    check(dogOwners3);

    expect(mock.mock.calls[0][0]).toMatch(`db.getCollection('owner').find({ pet_name: { '$in': [ 'd3', 'old dog' ] } }, {}).sort([ [ 'name', 1 ] ]).toArray();`);
    expect(mock.mock.calls[1][0]).toMatch(`db.getCollection('owner').find({ 'pet2.name': { '$in': [ 'd4', 'c4' ] } }, {}).sort([ [ 'name', 1 ] ]).toArray();`);
    expect(mock.mock.calls[2][0]).toMatch(`db.getCollection('owner').find({ '$or': [ { pet_name: { '$in': [ 'd3', 'old dog' ] } }, { 'pet2.name': { '$in': [ 'd4', 'c4' ] } } ] }, {}).sort([ [ 'name', 1 ] ]).toArray();`);
  });

  test('assign and serialization', async () => {
    const owner = new Owner();
    owner._id = new ObjectId('600000000000000000000004');
    owner.name = 'o1';
    orm.em.assign(owner, {
      pet: { name: 'cat', type: AnimalType.CAT },
      pet2: { name: 'dog', type: AnimalType.DOG },
    });
    expect(owner.pet).toMatchObject({ name: 'cat', type: AnimalType.CAT });
    expect(owner.pet).toBeInstanceOf(Cat);
    expect(owner.pet2).toMatchObject({ name: 'dog', type: AnimalType.DOG });
    expect(owner.pet2).toBeInstanceOf(Dog);

    const mock = mockLogger(orm, ['query']);
    await orm.em.persistAndFlush(owner);
    expect(mock.mock.calls[0][0]).toMatch(`db.getCollection('owner').insertMany([ { _id: ObjectId('600000000000000000000004'), name: 'o1', pet_canMeow: true, pet_type: 0, pet_name: 'cat', pet2: { type: 1, name: 'dog', canBark: true } } ], {});`);

    orm.em.assign(owner, {
      pet: { name: 'cat name' },
      pet2: { name: 'dog name' },
    });
    await orm.em.persistAndFlush(owner);
    expect(mock.mock.calls[1][0]).toMatch(`db.getCollection('owner').updateMany({ _id: ObjectId('600000000000000000000004') }, { '$set': { pet_name: 'cat name', pet2: { type: 1, name: 'dog name', canBark: true } } }, {});`);

    expect(wrap(owner).toObject()).toEqual({
      id: owner.id,
      name: 'o1',
      pet: {
        canMeow: true,
        name: 'cat name',
        type: 0,
      },
      pet2: {
        canBark: true,
        name: 'dog name',
        type: 1,
      },
    });
  });

});
