import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { test as base, expect } from "@playwright/test";
import { LoginPage } from "../pages/auth/login";
import { createUser, type TestUser } from "../utils/user-factory";

export type Actor = "product-manager" | "viewer" | "user";

const ACTOR_PERMISSIONS: Record<Actor, string[]> = {
  "product-manager": [
    "product-management:product",
    "product-management:productCatalog",
    "user-management:user",
    "user-management:role",
    "user-management:roleAssignment",
    "user-management:profile",
  ],
  viewer: ["product-management:product", "product-management:productCatalog"],
  user: [
    "user-management:user",
    "user-management:role",
    "user-management:roleAssignment",
    "user-management:profile",
  ],
};

export { ACTOR_PERMISSIONS };

// `baseURL` is test-scoped, so the worker fixture below reads it from env instead.
function resolveBaseURL(): string {
  return process.env.PLAYWRIGHT_BASE_URL ?? process.env.E2E_BASE_URL ?? "http://localhost:5173";
}

interface TestFixtures {
  actor: Actor;
  user: TestUser;
}

interface WorkerFixtures {
  signInAs: (actor: Actor) => Promise<{ user: TestUser; storageState: string }>;
}

export const test = base.extend<TestFixtures, WorkerFixtures>({
  // One account + sign-in per (worker, actor), created lazily and reused across
  // the worker's tests. parallelIndex keeps the account unique per worker, so the
  // same account never logs in from two workers at once (Playwright's "one
  // account per parallel worker"). Tests in a worker run serially, so the shared
  // account is never used concurrently.
  signInAs: [
    async ({ browser }, use, workerInfo) => {
      const cache = new Map<Actor, { user: TestUser; storageState: string }>();
      const signIn = async (actor: Actor) => {
        const hit = cache.get(actor);
        if (hit) return hit;
        const user = await createUser({ permissions: ACTOR_PERMISSIONS[actor] });
        const storageState = path.join(
          os.tmpdir(),
          `e2e-auth-w${workerInfo.parallelIndex}-${actor}.json`,
        );
        // newContext inherits the project storageState option; start clean and
        // let LoginPage sign in, then persist the session for this worker.
        const context = await browser.newContext({
          baseURL: resolveBaseURL(),
          storageState: { cookies: [], origins: [] },
        });
        const page = await context.newPage();
        await new LoginPage(page).login(user.email, user.password);
        await context.storageState({ path: storageState });
        await context.close();
        const entry = { user, storageState };
        cache.set(actor, entry);
        return entry;
      };
      await use(signIn);
      for (const { storageState } of cache.values()) {
        fs.rmSync(storageState, { force: true });
      }
    },
    { scope: "worker" },
  ],

  // Default actor; override per describe with `useActor(test, actor)`.
  actor: ["user", { option: true }],

  // Restore this worker's session for the selected actor.
  storageState: async ({ actor, signInAs }, use) => {
    const { storageState } = await signInAs(actor);
    await use(storageState);
  },

  // The signed-in user — e.g. to assert one's own email.
  user: async ({ actor, signInAs }, use) => {
    const { user } = await signInAs(actor);
    await use(user);
  },

  // Start each test signed in; re-login (and refresh the saved state) if the
  // restored session was rejected.
  page: async ({ page, actor, signInAs }, use) => {
    await page.goto("/");
    const loggedIn = page.getByRole("link", { name: /products/i });
    const signInButton = page.getByRole("button", { name: "Sign in" });
    let needsLogin: boolean;
    try {
      await expect(loggedIn.or(signInButton)).toBeVisible({ timeout: 30_000 });
      needsLogin = await signInButton.isVisible();
    } catch {
      needsLogin = true;
    }
    if (needsLogin) {
      const { user, storageState } = await signInAs(actor);
      await new LoginPage(page).login(user.email, user.password);
      await page.context().storageState({ path: storageState });
    }
    await use(page);
  },
});

export { expect };

/** The base test or a per-spec extension of it. */
export type AppTest = typeof test;

/** Run the enclosing describe's tests signed in as `actor`. */
export function useActor(test: AppTest, actor: Actor): void {
  test.use({ actor });
}
