import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import type { Kysely, Transaction } from "kysely";
import { TextField } from "data/fields";
import { em as $em, entity as $entity, text as $text } from "data/prototype";
import { Entity, EntityManager } from "data/entities";
import { ManyToOneRelation } from "data/relations";
import { RepositoryEvents } from "data/events";
import { getDummyConnection } from "../helper";
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";

type E = Kysely<any> | Transaction<any>;

const { dummyConnection, afterAllCleanup } = getDummyConnection();
beforeAll(() => disableConsoleLog());
afterAll(async () => (await afterAllCleanup()) && enableConsoleLog());

async function sleep(ms: number) {
   return new Promise((resolve) => {
      setTimeout(resolve, ms);
   });
}

describe("[Repository]", async () => {
   test("count & exists", async () => {
      const items = new Entity("items", [new TextField("label")]);
      const em = new EntityManager([items], dummyConnection);

      await em.connection.kysely.schema
         .createTable("items")
         .ifNotExists()
         .addColumn("id", "integer", (col) => col.primaryKey().autoIncrement().notNull())
         .addColumn("label", "text")
         .execute();

      // fill
      await em.connection.kysely
         .insertInto("items")
         .values([{ label: "a" }, { label: "b" }, { label: "c" }])
         .execute();

      // count all
      const res = await em.repository(items).count();
      expect(res.sql).toBe('select count(*) as "count" from "items"');
      expect(res.data.count).toBe(3);

      //
      {
         const res = await em.repository(items).findMany();
         expect(res.count).toBe(3);
      }

      {
         const res = await em
            .repository(items, {
               includeCounts: true,
            })
            .findMany();
         expect(res.count).toBe(3);
      }

      // count filtered
      const res2 = await em
         .repository(items, {
            includeCounts: true,
         })
         .count({ label: { $in: ["a", "b"] } });

      expect(res2.sql).toBe('select count(*) as "count" from "items" where "label" in (?, ?)');
      expect(res2.parameters).toEqual(["a", "b"]);
      expect(res2.data.count).toBe(2);

      // check exists
      const res3 = await em.repository(items).exists({ label: "a" });
      expect(res3.data.exists).toBe(true);

      const res4 = await em.repository(items).exists({ label: "d" });
      expect(res4.data.exists).toBe(false);

      // for now, allow empty filter
      const res5 = await em.repository(items).exists({});
      expect(res5.data.exists).toBe(true);
   });

   test("option: silent", async () => {
      const em = $em({
         items: $entity("items", {
            label: $text(),
         }),
      }).proto.withConnection(getDummyConnection().dummyConnection);

      // should throw because table doesn't exist
      expect(em.repo("items").findMany({})).rejects.toThrow(/no such table/);
      // should silently return empty result
      em.repo("items", { silent: true })
         .findMany({})
         .then((r) => r.data);
      expect(
         em
            .repo("items", { silent: true })
            .findMany({})
            .then((r) => r.data),
      ).resolves.toEqual([]);
   });

   test("option: includeCounts", async () => {
      const em = $em({
         items: $entity("items", {
            label: $text(),
         }),
      }).proto.withConnection(getDummyConnection().dummyConnection);
      await em.schema().sync({ force: true });

      expect(
         em
            .repo("items", { includeCounts: true })
            .findMany({})
            .then((r) => [r.count, r.total]),
      ).resolves.toEqual([0, 0]);

      expect(
         em
            .repo("items", { includeCounts: false })
            .findMany({})
            .then((r) => [r.count, r.total]),
      ).resolves.toEqual([undefined, undefined]);
   });

   test("auto join", async () => {
      const schema = $em(
         {
            posts: $entity("posts", {
               title: $text(),
               content: $text(),
            }),
            comments: $entity("comments", {
               content: $text(),
            }),
            another: $entity("another", {
               title: $text(),
            }),
         },
         ({ relation }, { posts, comments }) => {
            relation(comments).manyToOne(posts);
         },
      );
      const em = schema.proto.withConnection(getDummyConnection().dummyConnection);
      await em.schema().sync({ force: true });

      await em.mutator("posts").insertOne({ title: "post1", content: "content1" });
      await em
         .mutator("comments")
         .insertMany([{ content: "comment1", posts_id: 1 }, { content: "comment2" }] as any);

      const res = await em.repo("comments").findMany({
         where: {
            "posts.title": "post1",
         },
      });
      expect(res.data as any).toEqual([
         {
            id: 1,
            content: "comment1",
            posts_id: 1,
         },
      ]);

      {
         // manual join should still work
         const res = await em.repo("comments").findMany({
            join: ["posts"],
            where: {
               "posts.title": "post1",
            },
         });
         expect(res.data as any).toEqual([
            {
               id: 1,
               content: "comment1",
               posts_id: 1,
            },
         ]);
      }

      // inexistent should be detected and thrown
      expect(
         em.repo("comments").findMany({
            where: {
               "random.title": "post1",
            },
         }),
      ).rejects.toThrow(/Invalid where field/);

      // existing alias, but not a relation should throw
      expect(
         em.repo("comments").findMany({
            where: {
               "another.title": "post1",
            },
         }),
      ).rejects.toThrow(/Invalid where field/);
   });
});

describe("[data] Repository (Events)", async () => {
   const items = new Entity("items", [new TextField("label")]);
   const categories = new Entity("categories", [new TextField("label")]);
   const em = new EntityManager([items, categories], dummyConnection, [
      new ManyToOneRelation(categories, items),
   ]);
   await em.schema().sync({ force: true });
   const events = new Map<string, any>();

   em.repository(items).emgr.onAny((event) => {
      // @ts-ignore
      events.set(event.constructor.slug, event);
   });
   em.repository(categories).emgr.onAny((event) => {
      // @ts-ignore
      events.set(event.constructor.slug, event);
   });

   test("events were fired", async () => {
      const repo = em.repository(items);
      await repo.findId(1);
      await repo.emgr.executeAsyncs();
      expect(events.has(RepositoryEvents.RepositoryFindOneBefore.slug)).toBeTrue();
      expect(events.has(RepositoryEvents.RepositoryFindOneAfter.slug)).toBeTrue();
      events.clear();

      await repo.findOne({ id: 1 });
      await repo.emgr.executeAsyncs();
      expect(events.has(RepositoryEvents.RepositoryFindOneBefore.slug)).toBeTrue();
      expect(events.has(RepositoryEvents.RepositoryFindOneAfter.slug)).toBeTrue();
      events.clear();

      await repo.findMany({ where: { id: 1 } });
      await repo.emgr.executeAsyncs();
      expect(events.has(RepositoryEvents.RepositoryFindManyBefore.slug)).toBeTrue();
      expect(events.has(RepositoryEvents.RepositoryFindManyAfter.slug)).toBeTrue();
      events.clear();

      await repo.findManyByReference(1, "categories");
      await repo.emgr.executeAsyncs();
      expect(events.has(RepositoryEvents.RepositoryFindManyBefore.slug)).toBeTrue();
      expect(events.has(RepositoryEvents.RepositoryFindManyAfter.slug)).toBeTrue();
      events.clear();

      // check find one on findMany with limit 1
      await repo.findMany({ where: { id: 1 }, limit: 1 });
      await repo.emgr.executeAsyncs();
      expect(events.has(RepositoryEvents.RepositoryFindOneBefore.slug)).toBeTrue();
      expect(events.has(RepositoryEvents.RepositoryFindOneAfter.slug)).toBeTrue();
      events.clear();
   });
});
