/* eslint-disable @typescript-eslint/no-non-null-assertion */
import type { ResponseCookie } from "next/dist/compiled/@edge-runtime/cookies";
import type { ReadonlyRequestCookies } from "next/dist/server/web/spec-extension/adapters/request-cookies";
import { cookies } from "next/headers";
import type { Adapter, AdapterUser } from "@auth/core/adapters";
import type { Account } from "next-auth";
import type { JWT } from "next-auth/jwt";
import { describe, expect, it, test, vi } from "vitest";

import { groupMembers, groupPermissions, groups, users } from "@homarr/db/schema/sqlite";
import { createDb } from "@homarr/db/test";
import * as definitions from "@homarr/definitions";

import { createSessionCallback, createSignInCallback, getCurrentUserPermissionsAsync } from "../callbacks";

describe("getCurrentUserPermissions", () => {
  test("should return empty permissions when non existing user requested", async () => {
    const db = createDb();

    await db.insert(groups).values({
      id: "2",
      name: "test",
    });
    await db.insert(groupPermissions).values({
      groupId: "2",
      permission: "admin",
    });
    await db.insert(users).values({
      id: "2",
    });

    const userId = "1";
    const result = await getCurrentUserPermissionsAsync(db, userId);
    expect(result).toEqual([]);
  });

  test("should return empty permissions when user has no groups", async () => {
    const db = createDb();
    const userId = "1";

    await db.insert(groups).values({
      id: "2",
      name: "test",
    });
    await db.insert(groupPermissions).values({
      groupId: "2",
      permission: "admin",
    });
    await db.insert(users).values({
      id: userId,
    });

    const result = await getCurrentUserPermissionsAsync(db, userId);
    expect(result).toEqual([]);
  });

  test("should return permissions for user", async () => {
    const db = createDb();
    const getPermissionsWithChildrenMock = vi
      .spyOn(definitions, "getPermissionsWithChildren")
      .mockReturnValue(["board-create"]);
    const mockId = "1";

    await db.insert(users).values({
      id: mockId,
    });
    await db.insert(groups).values({
      id: mockId,
      name: "test",
    });
    await db.insert(groupMembers).values({
      userId: mockId,
      groupId: mockId,
    });
    await db.insert(groupPermissions).values({
      groupId: mockId,
      permission: "admin",
    });

    const result = await getCurrentUserPermissionsAsync(db, mockId);
    expect(result).toEqual(["board-create"]);
    expect(getPermissionsWithChildrenMock).toHaveBeenCalledWith(["admin"]);
  });
});

describe("session callback", () => {
  it("should add id and name to session user", async () => {
    const user: AdapterUser = {
      id: "id",
      name: "name",
      email: "email",
      emailVerified: new Date("2023-01-13"),
    };
    const token: JWT = {};
    const db = createDb();
    const callback = createSessionCallback(db);
    const result = await callback({
      session: {
        user: {
          id: "no-id",
          email: "no-email",
          emailVerified: new Date("2023-01-13"),
          permissions: [],
          colorScheme: "dark",
        },
        expires: "2023-01-13" as Date & string,
        sessionToken: "token",
        userId: "no-id",
      },
      user,
      token,
      trigger: "update",
      newSession: {},
    });
    expect(result.user).toBeDefined();
    expect(result.user!.id).toEqual(user.id);
    expect(result.user!.name).toEqual(user.name);
  });
});

type AdapterSessionInput = Parameters<Exclude<Adapter["createSession"], undefined>>[0];

const createAdapter = () => {
  const result = {
    createSession: (input: AdapterSessionInput) => input,
  };

  vi.spyOn(result, "createSession");
  return result;
};

// eslint-disable-next-line @typescript-eslint/consistent-type-imports
type SessionExport = typeof import("../session");
const mockSessionToken = "e9ef3010-6981-4a81-b9d6-8495d09cf3b5";
const mockSessionExpiry = new Date("2023-07-01");
vi.mock("../env.mjs", () => {
  return {
    env: {
      AUTH_SESSION_EXPIRY_TIME: 60 * 60 * 24 * 7,
    },
  };
});
vi.mock("../session", async (importOriginal) => {
  const mod = await importOriginal<SessionExport>();

  const generateSessionToken = (): typeof mockSessionToken => mockSessionToken;
  const expireDateAfter = (_seconds: number) => mockSessionExpiry;

  return {
    ...mod,
    generateSessionToken,
    expireDateAfter,
  } satisfies SessionExport;
});
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
type HeadersExport = typeof import("next/headers");
vi.mock("next/headers", async (importOriginal) => {
  const mod = await importOriginal<HeadersExport>();

  const result = {
    set: (name: string, value: string, options: Partial<ResponseCookie>) => options as ResponseCookie,
  } as unknown as ReadonlyRequestCookies;

  vi.spyOn(result, "set");

  const cookies = () => result;

  return { ...mod, cookies } satisfies HeadersExport;
});

describe("createSignInCallback", () => {
  it("should return true if not credentials request", async () => {
    const isCredentialsRequest = false;
    const signInCallback = createSignInCallback(createAdapter(), isCredentialsRequest);
    const result = await signInCallback({
      user: { id: "1", emailVerified: new Date("2023-01-13") },
      account: {} as Account,
    });
    expect(result).toBe(true);
  });

  it("should return false if no adapter.createSession", async () => {
    const isCredentialsRequest = true;
    const signInCallback = createSignInCallback(
      // https://github.com/nextauthjs/next-auth/issues/6106
      { createSession: undefined } as unknown as Adapter,
      isCredentialsRequest,
    );
    const result = await signInCallback({
      user: { id: "1", emailVerified: new Date("2023-01-13") },
      account: {} as Account,
    });
    expect(result).toBe(false);
  });

  test("should call adapter.createSession with correct input", async () => {
    const adapter = createAdapter();
    const isCredentialsRequest = true;
    const signInCallback = createSignInCallback(adapter, isCredentialsRequest);
    const user = { id: "1", emailVerified: new Date("2023-01-13") };
    const account = {} as Account;
    await signInCallback({ user, account });
    expect(adapter.createSession).toHaveBeenCalledWith({
      sessionToken: mockSessionToken,
      userId: user.id,
      expires: mockSessionExpiry,
    });
    expect(cookies().set).toHaveBeenCalledWith("next-auth.session-token", mockSessionToken, {
      path: "/",
      expires: mockSessionExpiry,
      httpOnly: true,
      sameSite: "lax",
      secure: true,
    });
  });
});
