import { CredentialsSignin } from "@auth/core/errors";
import { describe, expect, test, vi } from "vitest";

import type { Database } from "@homarr/db";
import { and, createId, eq } from "@homarr/db";
import { groups, users } from "@homarr/db/schema";
import { createDb } from "@homarr/db/test";

import { authorizeWithLdapCredentialsAsync } from "../credentials/authorization/ldap-authorization";
import * as ldapClient from "../credentials/ldap-client";

vi.mock("../../env", () => ({
  env: {
    AUTH_LDAP_BIND_DN: "bind_dn",
    AUTH_LDAP_BIND_PASSWORD: "bind_password",
    AUTH_LDAP_USER_MAIL_ATTRIBUTE: "mail",
    AUTH_LDAP_GROUP_CLASS: "group",
  },
}));

describe("authorizeWithLdapCredentials", () => {
  test("should fail when wrong ldap base credentials", async () => {
    // Arrange
    const spy = vi.spyOn(ldapClient, "LdapClient");
    spy.mockImplementation(
      () =>
        ({
          bindAsync: vi.fn(() => Promise.reject(new Error("bindAsync"))),
        }) as unknown as ldapClient.LdapClient,
    );

    // Act
    const act = () =>
      authorizeWithLdapCredentialsAsync(null as unknown as Database, {
        name: "test",
        password: "test",
      });

    // Assert
    await expect(act()).rejects.toThrow(CredentialsSignin);
  });

  test("should fail when user not found", async () => {
    // Arrange
    const spy = vi.spyOn(ldapClient, "LdapClient");
    spy.mockImplementation(
      () =>
        ({
          bindAsync: vi.fn(() => Promise.resolve()),
          searchAsync: vi.fn(() => Promise.resolve([])),
        }) as unknown as ldapClient.LdapClient,
    );

    // Act
    const act = () =>
      authorizeWithLdapCredentialsAsync(null as unknown as Database, {
        name: "test",
        password: "test",
      });

    // Assert
    await expect(act()).rejects.toThrow(CredentialsSignin);
  });

  test("should fail when user has invalid email", async () => {
    // Arrange
    const spy = vi.spyOn(ldapClient, "LdapClient");
    spy.mockImplementation(
      () =>
        ({
          bindAsync: vi.fn(() => Promise.resolve()),
          searchAsync: vi.fn(() =>
            Promise.resolve([
              {
                dn: "test",
                mail: "test",
              },
            ]),
          ),
        }) as unknown as ldapClient.LdapClient,
    );

    // Act
    const act = () =>
      authorizeWithLdapCredentialsAsync(null as unknown as Database, {
        name: "test",
        password: "test",
      });

    // Assert
    await expect(act()).rejects.toThrow(CredentialsSignin);
  });

  test("should fail when user password is incorrect", async () => {
    // Arrange
    const searchSpy = vi.fn(() =>
      Promise.resolve([
        {
          dn: "test",
          mail: "test@gmail.com",
        },
      ]),
    );
    const spy = vi.spyOn(ldapClient, "LdapClient");
    spy.mockImplementation(
      () =>
        ({
          bindAsync: vi.fn((props: ldapClient.BindOptions) =>
            props.distinguishedName === "test" ? Promise.reject(new Error("bindAsync")) : Promise.resolve(),
          ),
          searchAsync: searchSpy,
        }) as unknown as ldapClient.LdapClient,
    );

    // Act
    const act = () =>
      authorizeWithLdapCredentialsAsync(null as unknown as Database, {
        name: "test",
        password: "test",
      });

    // Assert
    await expect(act()).rejects.toThrow(CredentialsSignin);
    expect(searchSpy).toHaveBeenCalledTimes(1);
  });

  test("should authorize user with correct credentials and create user", async () => {
    // Arrange
    const db = createDb();
    const spy = vi.spyOn(ldapClient, "LdapClient");
    spy.mockImplementation(
      () =>
        ({
          bindAsync: vi.fn(() => Promise.resolve()),
          searchAsync: vi.fn(() =>
            Promise.resolve([
              {
                dn: "test",
                mail: "test@gmail.com",
              },
            ]),
          ),
          disconnectAsync: vi.fn(),
        }) as unknown as ldapClient.LdapClient,
    );

    // Act
    const result = await authorizeWithLdapCredentialsAsync(db, {
      name: "test",
      password: "test",
    });

    // Assert
    expect(result.name).toBe("test");
    expect(result.groups).toHaveLength(0); // Groups are needed in signIn events callback
    const dbUser = await db.query.users.findFirst({
      where: eq(users.name, "test"),
    });
    expect(dbUser).toBeDefined();
    expect(dbUser?.id).toBe(result.id);
    expect(dbUser?.email).toBe("test@gmail.com");
    expect(dbUser?.emailVerified).not.toBeNull();
    expect(dbUser?.provider).toBe("ldap");
  });

  test("should authorize user with correct credentials and create user with same email when credentials user already exists", async () => {
    // Arrange
    const db = createDb();
    const spy = vi.spyOn(ldapClient, "LdapClient");
    spy.mockImplementation(
      () =>
        ({
          bindAsync: vi.fn(() => Promise.resolve()),
          searchAsync: vi.fn(() =>
            Promise.resolve([
              {
                dn: "test",
                mail: "test@gmail.com",
              },
            ]),
          ),
          disconnectAsync: vi.fn(),
        }) as unknown as ldapClient.LdapClient,
    );
    await db.insert(users).values({
      id: createId(),
      name: "test",
      email: "test@gmail.com",
      provider: "credentials",
    });

    // Act
    const result = await authorizeWithLdapCredentialsAsync(db, {
      name: "test",
      password: "test",
    });

    // Assert
    expect(result.name).toBe("test");
    expect(result.groups).toHaveLength(0); // Groups are needed in signIn events callback
    const dbUser = await db.query.users.findFirst({
      where: and(eq(users.name, "test"), eq(users.provider, "ldap")),
    });
    expect(dbUser).toBeDefined();
    expect(dbUser?.id).toBe(result.id);
    expect(dbUser?.email).toBe("test@gmail.com");
    expect(dbUser?.emailVerified).not.toBeNull();
    expect(dbUser?.provider).toBe("ldap");

    const credentialsUser = await db.query.users.findFirst({
      where: and(eq(users.name, "test"), eq(users.provider, "credentials")),
    });

    expect(credentialsUser).toBeDefined();
    expect(credentialsUser?.id).not.toBe(result.id);
  });

  // The name update occurs in the signIn event callback
  test("should authorize user with correct credentials and return updated name", async () => {
    // Arrange
    const spy = vi.spyOn(ldapClient, "LdapClient");
    spy.mockImplementation(
      () =>
        ({
          bindAsync: vi.fn(() => Promise.resolve()),
          searchAsync: vi.fn(() =>
            Promise.resolve([
              {
                dn: "test55",
                mail: "test@gmail.com",
              },
            ]),
          ),
          disconnectAsync: vi.fn(),
        }) as unknown as ldapClient.LdapClient,
    );

    const userId = createId();
    const db = createDb();
    await db.insert(users).values({
      id: userId,
      name: "test-old",
      email: "test@gmail.com",
      provider: "ldap",
    });

    // Act
    const result = await authorizeWithLdapCredentialsAsync(db, {
      name: "test",
      password: "test",
    });

    // Assert
    expect(result).toEqual({ id: userId, name: "test", groups: [] });

    const dbUser = await db.query.users.findFirst({
      where: eq(users.id, userId),
    });

    expect(dbUser).toBeDefined();
    expect(dbUser?.id).toBe(userId);
    expect(dbUser?.name).toBe("test-old");
    expect(dbUser?.email).toBe("test@gmail.com");
    expect(dbUser?.provider).toBe("ldap");
  });

  test("should authorize user with correct credentials and return his groups", async () => {
    // Arrange
    const spy = vi.spyOn(ldapClient, "LdapClient");
    spy.mockImplementation(
      () =>
        ({
          bindAsync: vi.fn(() => Promise.resolve()),
          searchAsync: vi.fn((argument: { options: { filter: string } }) =>
            argument.options.filter.includes("group")
              ? Promise.resolve([
                  {
                    cn: "homarr_example",
                  },
                ])
              : Promise.resolve([
                  {
                    dn: "test55",
                    mail: "test@gmail.com",
                  },
                ]),
          ),
          disconnectAsync: vi.fn(),
        }) as unknown as ldapClient.LdapClient,
    );
    const db = createDb();
    const userId = createId();
    await db.insert(users).values({
      id: userId,
      name: "test",
      email: "test@gmail.com",
      provider: "ldap",
    });

    const groupId = createId();
    await db.insert(groups).values({
      id: groupId,
      name: "homarr_example",
      position: 1,
    });

    // Act
    const result = await authorizeWithLdapCredentialsAsync(db, {
      name: "test",
      password: "test",
    });

    // Assert
    expect(result).toEqual({ id: userId, name: "test", groups: ["homarr_example"] });
  });
});
