import { afterAll, beforeAll, beforeEach, describe, expect, spyOn, test } from "bun:test";
import { createApp } from "core/test/utils";
import { AuthController } from "../../src/auth/api/AuthController";
import { em, entity, make, text } from "data/prototype";
import { AppAuth, type ModuleBuildContext } from "modules";
import { makeCtx, moduleTestSuite } from "./module-test-suite";
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";

beforeAll(disableConsoleLog);
afterAll(enableConsoleLog);

describe("AppAuth", () => {
   moduleTestSuite(AppAuth);

   let ctx: ModuleBuildContext;

   beforeEach(() => {
      ctx = makeCtx();
   });

   test("secrets", async () => {
      // auth must be enabled, otherwise default config is returned
      const auth = new AppAuth({ enabled: true }, ctx);
      await auth.build();

      const config = auth.toJSON();
      expect(config.jwt).toBeUndefined();
      expect(config.strategies?.password?.config).toBeUndefined();
   });

   test("enabling auth: generate secret", async () => {
      const auth = new AppAuth(undefined, ctx);
      await auth.build();

      const oldConfig = auth.toJSON(true);
      await auth.schema().patch("enabled", true);
      await auth.build();
      const newConfig = auth.toJSON(true);
      expect(newConfig.jwt.secret).not.toBe(oldConfig.jwt.secret);
   });

   test("creates user on register", async () => {
      const auth = new AppAuth(
         {
            enabled: true,
            // @ts-ignore
            jwt: {
               secret: "123456",
            },
         },
         ctx,
      );

      await auth.build();
      await ctx.em.schema().sync({ force: true });

      // expect no users, but the query to pass
      const res = await ctx.em.repository("users").findMany();
      expect(res.data.length).toBe(0);

      const app = new AuthController(auth).getController();

      {
         const res = await app.request("/password/register", {
            method: "POST",
            headers: {
               "Content-Type": "application/json",
            },
            body: JSON.stringify({
               email: "some@body.com",
               password: "12345678",
            }),
         });
         expect(res.status).toBe(200);

         const { data: users } = await ctx.em.repository("users").findMany();
         expect(users.length).toBe(1);
         expect(users[0]?.email).toBe("some@body.com");
      }
   });

   test("creates user on register (bcrypt)", async () => {
      const auth = new AppAuth(
         {
            enabled: true,
            strategies: {
               password: {
                  type: "password",
                  config: {
                     hashing: "bcrypt",
                  },
               },
            },
            // @ts-ignore
            jwt: {
               secret: "123456",
            },
         },
         ctx,
      );

      await auth.build();
      await ctx.em.schema().sync({ force: true });

      // expect no users, but the query to pass
      const res = await ctx.em.repository("users").findMany();
      expect(res.data.length).toBe(0);

      const app = new AuthController(auth).getController();

      {
         const res = await app.request("/password/register", {
            method: "POST",
            headers: {
               "Content-Type": "application/json",
            },
            body: JSON.stringify({
               email: "some@body.com",
               password: "12345678",
            }),
         });
         expect(res.status).toBe(200);

         const { data: users } = await ctx.em.repository("users").findMany();
         expect(users.length).toBe(1);
         expect(users[0]?.email).toBe("some@body.com");
      }

      {
         // check user in database
         const rawUser = await ctx.connection.kysely
            .selectFrom("users")
            .selectAll()
            .executeTakeFirstOrThrow();
         expect(rawUser.strategy_value).toStartWith("$");
      }
   });

   test("registers auth middleware for bknd routes only", async () => {
      const app = createApp({
         config: {
            auth: {
               enabled: true,
               jwt: {
                  secret: "123456",
               },
            },
         },
      });

      await app.build();
      app.registerAdminController();
      const spy = spyOn(app.module.auth.authenticator, "requestCookieRefresh");

      // register custom route
      app.server.get("/test", async (c) => c.text("test"));

      // call a system api and then the custom route
      await app.server.request("/api/system/ping");
      await app.server.request("/test");

      expect(spy.mock.calls.length).toBe(0);

      // admin route
      await app.server.request("/");
      expect(spy.mock.calls.length).toBe(1);
   });

   test("should allow additional user fields", async () => {
      const app = createApp({
         config: {
            auth: {
               entity_name: "users",
               enabled: true,
            },
            data: em({
               users: entity("users", {
                  additional: text(),
               }),
            }).toJSON(),
         },
      });

      await app.build();

      const e = app.modules.em.entity("users");
      const fields = e.fields.map((f) => f.name);
      expect(e.type).toBe("system");
      expect(fields).toContain("additional");
      expect(fields).toEqual(["id", "additional", "email", "strategy", "strategy_value", "role"]);
   });

   test("ensure user field configs is always correct", async () => {
      const app = createApp({
         config: {
            auth: {
               enabled: true,
            },
            data: em({
               users: entity("users", {
                  strategy: text({
                     fillable: true,
                     hidden: false,
                  }),
                  strategy_value: text({
                     fillable: true,
                     hidden: false,
                  }),
               }),
            }).toJSON(),
         },
      });
      await app.build();

      const users = app.em.entity("users");
      const props = ["hidden", "fillable", "required"];

      for (const [name, _authFieldProto] of Object.entries(AppAuth.usersFields)) {
         const authField = make(name, _authFieldProto as any);
         const field = users.field(name)!;
         for (const prop of props) {
            expect(field.config[prop]).toEqual(authField.config[prop]);
         }
      }
   });

   test("default role for registration must be a valid role", async () => {
      const app = createApp({
         config: {
            auth: {
               enabled: true,
               jwt: {
                  secret: "123456",
               },
               allow_register: true,
               roles: {
                  guest: {
                     is_default: true,
                  },
               },
            },
         },
      });

      await app.build();

      const auth = app.module.auth;
      // doesn't allow invalid role
      expect(auth.schema().patch("default_role_register", "admin")).rejects.toThrow();
      // allows valid role
      await auth.schema().patch("default_role_register", "guest");
      expect(auth.toJSON().default_role_register).toBe("guest");
   });
});
