import { DrizzlePostgreSQLAdapter } from "@lucia-auth/adapter-drizzle";
import { createId } from "@paralleldrive/cuid2";
import { expect, test, type BrowserContext, type Page } from "@playwright/test";
import "dotenv/config";
import { eq } from "drizzle-orm";
import { drizzle } from "drizzle-orm/postgres-js";
import { Lucia, type Cookie } from "lucia";
import createClient from "openapi-fetch";
import postgres from "postgres";
import * as schema from "~/drizzle/schema";
import type * as discord from "~/types/discord";
import type * as liljudd from "~/types/liljudd";

const unencoded = `${process.env.DISCORD_CLIENT_ID}:${process.env.DISCORD_CLIENT_SECRET}`;
const encoded = btoa(unencoded);

const queryClient = postgres(process.env.DATABASE_URL!);
const db = drizzle(queryClient, {
  schema,
});

const adapter = new DrizzlePostgreSQLAdapter(db, schema.sessions, schema.users);
export const lucia = new Lucia(adapter, {
  getUserAttributes: (attributes) => attributes,
});

let context: BrowserContext;
let page: Page;

let sessionCookie: Cookie | undefined;

let userId = createId();
let guildId: bigint;

test.describe.serial("User auth process", () => {
  test.beforeAll(() => {
    expect(
      [
        "DISCORD_CLIENT_ID",
        "DISCORD_CLIENT_SECRET",
        "DATABASE_URL",
        "DISCORD_BOT_TOKEN",
      ].filter((e) => typeof process.env[e] === "undefined").length,
      { message: "Please specify all env vars." },
    ).toBeFalsy();
  });

  test.beforeAll(async ({ browser }) => {
    context = await browser.newContext();
    page = await context.newPage();
  });

  test.beforeEach(async () => {
    if (!sessionCookie) return;

    const sameSiteProps = {
      lax: "Lax",
      strict: "Strict",
      none: "None",
    } as const;
    const expires = sessionCookie.attributes.expires
      ? sessionCookie.attributes.expires.getTime() / 1000
      : undefined;
    const sameSite = sessionCookie.attributes.sameSite
      ? sameSiteProps[sessionCookie.attributes.sameSite]
      : undefined;
    await context.addCookies([
      {
        name: sessionCookie.name,
        value: sessionCookie.value,
        ...sessionCookie.attributes,
        sameSite,
        expires,
        secure: false,
        domain: "localhost",
        path: "/",
      },
    ]);
  });

  test.afterAll("Delete DB entries", async () => {
    await db.delete(schema.users).where(eq(schema.users.id, userId)).execute();
    await db
      .delete(schema.guilds)
      .where(eq(schema.guilds.id, guildId))
      .execute();
  });

  test.afterAll(async () => {
    await context.close();
  });

  test("Landing page", async () => {
    await page.goto("/");
    await page.waitForLoadState("load");

    expect(await page.screenshot()).toMatchSnapshot("landing_page.png");
  });

  test("Unauthorized Access Redirect Test", async () => {
    await page.goto("/config");
    await page.waitForURL("/");
  });

  test("Generate auth session for further tests", async ({ browser }) => {
    const { GET } = createClient<discord.paths>({
      baseUrl: "https://discord.com/api/v10",
    });
    const discordUserResponse = await GET("/users/@me", {
      headers: {
        Authorization: `Bot ${process.env.DISCORD_BOT_TOKEN}`,
      },
    });
    if (discordUserResponse.error) throw discordUserResponse.error;
    const discordUser = discordUserResponse.data;

    const browserName = browser.browserType().name() as
      | "chromium"
      | "webkit"
      | "firefox";

    userId = discordUser.id + userId.slice(discordUser.id.length);
    userId = userId.slice(0, -browserName.length) + browserName;

    enum BrowserIds {
      chromium,
      webkit,
      firefox,
    }
    guildId = BigInt(discordUser.id) ^ BigInt(BrowserIds[browserName]);

    await db.insert(schema.users).values({
      id: userId,
      discord_id: discordUser.id,
      name: discordUser.global_name,
      image: discordUser.avatar,
    });
    const session = await lucia.createSession(
      userId,
      {},
      { sessionId: createId() },
    );
    sessionCookie = lucia.createSessionCookie(session.id);
    await db
      .insert(schema.discordTokens)
      .values({
        userId,
        accessToken: "tokens.accessToken",
        expiresAt: sessionCookie.attributes.expires ?? new Date(),
        refreshToken: "tokens.refreshToken",
      })
      .returning()
      .execute();
  });

  test("Landing page when logged in", async () => {
    await page.goto("/");
    await page.waitForLoadState("load");

    expect(await page.screenshot()).toMatchSnapshot(
      "landing_page_logged_in.png",
    );
  });

  test("Test Api", async () => {
    const { GET, POST, PUT } = createClient<liljudd.paths>({
      baseUrl: "http://localhost:3000/",
    });

    const createConfigResponse = await POST("/api/{guildId}/config", {
      params: {
        path: {
          guildId: guildId.toString(),
        },
      },
      headers: {
        Authorization: `Basic ${encoded}`,
        Origin: "http://localhost:3000",
      },
    });

    if (createConfigResponse.error)
      throw new Error(createConfigResponse.error.error);

    let getConfigResponse = await GET("/api/{guildId}/config", {
      params: {
        path: {
          guildId: guildId.toString(),
        },
      },
      headers: {
        Authorization: `Basic ${encoded}`,
        Origin: "http://localhost:3000",
      },
    });

    if (getConfigResponse.error) throw new Error(getConfigResponse.error.error);

    switch (getConfigResponse.data?.checksum) {
      case "209a644c31a5ef123c432c2885d231a2e0efc4de": // chromium
      case "aead21e132a94ab897ec28e0f0c337a66207bad3": // webkit
      case "c3e2ff2ce5a8936234552125a54c2fe1ce1a35da": // firefox
        break;

      default:
        throw new Error(
          "Before guild GET checksum didn't matched known ones: " +
            getConfigResponse.data?.checksum,
        );
    }

    const putTimePlanningResponse = await PUT("/api/{guildId}/timePlanning", {
      body: {
        enabled: true,
        channelId: "1234567890123456789",
        rolesEnabled: true,
        isAvailableRoleId: "1234567890123456789",
        wantsToBeNotifieRoledId: "1234567890123456789",
        messageIds: {
          "0": "1234567890123456789",
          "1": "1234567890123456789",
          "2": "1234567890123456789",
          "3": "1234567890123456789",
          "4": "1234567890123456789",
          "5": "1234567890123456789",
          "6": "1234567890123456789",
        },
      },
      params: {
        path: {
          guildId: guildId.toString(),
        },
      },
      headers: {
        Authorization: `Basic ${encoded}`,
        Origin: "http://localhost:3000",
      },
    });

    if (putTimePlanningResponse.error)
      throw new Error(putTimePlanningResponse.error.error);

    getConfigResponse = await GET("/api/{guildId}/config", {
      params: {
        path: {
          guildId: guildId.toString(),
        },
      },
      headers: {
        Authorization: `Basic ${encoded}`,
        Origin: "http://localhost:3000",
      },
    });

    if (getConfigResponse.error) throw new Error(getConfigResponse.error.error);

    switch (getConfigResponse.data?.checksum) {
      case "681c8324b21096255d942bb78bd6655da90d352e": // chromium
      case "a2fb3601b7d0949b1ceada3b3ac0ba408c6159bb": // webkit
      case "bf20daba95e8f3ddd17cc64e8a7ba184b68ad37b": // firefox
        break;

      default:
        throw new Error(
          "After guild GET checksum didn't matched known ones: " +
            getConfigResponse.data?.checksum,
        );
    }
  });
});
