import { test as base, type Page, type BrowserContext } from '@playwright/test';
import * as crypto from 'node:crypto';
import type { FeatureFlags } from '@masterev/config-shared';
import { convexRun } from './helpers/convex.js';
import { customers, type CustomerConfig } from './config.js';
import type { AuthMode, CustomerRuntime } from './framework/runtime.js';

type UserRole = 'ADMIN' | 'POWERUSER' | 'USER';

interface CreatedUser {
  userId: string;
  username: string;
  password: string;
}

const SESSION_COOKIE_NAME = 'session';

function base64UrlEncode(buffer: Uint8Array | Buffer): string {
  return Buffer.from(buffer)
    .toString('base64')
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=+$/, '');
}

function newSessionId(): string {
  return base64UrlEncode(crypto.randomBytes(32));
}

async function hashSessionId(sessionId: string): Promise<string> {
  const data = new TextEncoder().encode(sessionId);
  const hashBuffer = await crypto.webcrypto.subtle.digest('SHA-256', data);
  return base64UrlEncode(new Uint8Array(hashBuffer));
}

async function passwordLogin(
  customerUrl: string,
  customerName: string,
  username: string,
  password: string
): Promise<string> {
  const loginUrl = new URL('/api/actions/auth/password/login', customerUrl);
  loginUrl.searchParams.set('redirect_uri', customerUrl);
  loginUrl.searchParams.set('customer', customerName);

  const response = await fetch(loginUrl.toString(), {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ username, password }),
    redirect: 'manual',
  });

  if (!response.ok) {
    const body = await response.text();
    throw new Error(
      `Login failed for ${username}@${customerName} (${response.status}): ${body}`
    );
  }

  const setCookie = response.headers.get('set-cookie');
  if (!setCookie) {
    throw new Error(`No Set-Cookie header for ${username}@${customerName}`);
  }
  const sessionMatch = /session=([^;]+)/.exec(setCookie);
  if (!sessionMatch || sessionMatch[1] === undefined) {
    throw new Error(`No session cookie for ${username}@${customerName}`);
  }
  return sessionMatch[1];
}

async function localBypassLogin(
  customerName: string,
  userId: string
): Promise<string> {
  const sessionId = newSessionId();
  const hash = await hashSessionId(sessionId);
  await convexRun('sessions:createSession', {
    hash,
    userId,
    customer: customerName,
    userAgent: 'e2e-local-bypass',
    ip: '127.0.0.1',
  });
  return sessionId;
}

async function loginUser(
  browser: { newContext: (opts: object) => Promise<BrowserContext> },
  runtime: CustomerRuntime,
  customerName: string,
  user: CreatedUser
): Promise<BrowserContext> {
  const sessionValue =
    runtime.authMode === 'local-bypass'
      ? await localBypassLogin(customerName, user.userId)
      : await passwordLogin(
          runtime.baseURL,
          customerName,
          user.username,
          user.password
        );

  const url = new URL(runtime.baseURL);
  const isHttps = url.protocol === 'https:';

  return browser.newContext({
    baseURL: runtime.baseURL,
    ignoreHTTPSErrors: true,
    storageState: {
      cookies: [
        {
          name: SESSION_COOKIE_NAME,
          value: sessionValue,
          domain: url.hostname,
          path: '/',
          httpOnly: true,
          secure: isHttps,
          sameSite: 'Lax' as const,
          expires: -1,
        },
      ],
      origins: [],
    },
  });
}

/**
 * Create a unique test user via Convex HTTP API and return credentials.
 */
async function createTestUser(
  customer: string,
  role: UserRole,
  testInfo: { titlePath: string[] },
  teamId?: string
): Promise<CreatedUser> {
  const specName = testInfo.titlePath[0]
    ?.replace(/[^a-zA-Z0-9]/g, '-')
    .toLowerCase()
    .slice(0, 30) ?? 'test';
  const timestamp = Date.now();
  const username = `e2e-${role.toLowerCase()}-${specName}-${timestamp}`;
  const password = `e2e-pass-${timestamp}`;

  const userId = await convexRun<string>('e2e:createUser', {
    customer,
    name: `E2E ${role} ${specName}`,
    username,
    role,
    password,
    teamIds: teamId ? [teamId] : [],
  });

  return { userId, username, password };
}

function getCustomerConfig(testInfo: { project: { metadata?: Record<string, unknown> } }): CustomerConfig {
  const customerName = testInfo.project.metadata?.customer as string;
  const config = customers.find((c) => c.name === customerName);
  if (!config) {
    throw new Error(`Customer config not found for project: ${customerName}`);
  }
  return config;
}

// Worker-scoped fixtures (shared across tests in one worker).
// We have none, but Playwright's `extend` generic expects each key
// in the worker map to be a fixture tuple, so the strictest "empty
// keys" form is Record<never, never>; Record<string, never> would
// claim every string key maps to never, which the generic rejects.
type E2EWorkerFixtures = Record<never, never>;

// Test-scoped fixtures
type E2ETestFixtures = {
  /** A page authenticated as a fresh ADMIN user (unique per test file). */
  adminPage: Page;
  /** A page authenticated as a fresh USER with a team assignment. */
  userPage: Page;
  /** Feature flags for the current customer. */
  features: FeatureFlags;
  /** Current customer name. */
  customerName: string;
  /** Current customer config. */
  customerConfig: CustomerConfig;
  /** Resolved runtime (target/baseURL/auth/convex). Same as `customerConfig.runtime`. */
  runtime: CustomerRuntime;
  /** Effective auth mode for this run. */
  authMode: AuthMode;
  /**
   * Create an authenticated page for a user with a specific role.
   * The user is created on-demand with a unique username.
   */
  createAuthenticatedUser: (opts: {
    role: UserRole;
    teamId?: string;
  }) => Promise<{ page: Page; user: CreatedUser }>;
};

export const test = base.extend<E2ETestFixtures, E2EWorkerFixtures>({
  customerConfig: async ({}, use, testInfo) => {
    await use(getCustomerConfig(testInfo));
  },

  features: async ({ customerConfig }, use) => {
    await use(customerConfig.features);
  },

  customerName: async ({ customerConfig }, use) => {
    await use(customerConfig.name);
  },

  runtime: async ({ customerConfig }, use) => {
    await use(customerConfig.runtime);
  },

  authMode: async ({ customerConfig }, use) => {
    await use(customerConfig.runtime.authMode);
  },

  createAuthenticatedUser: async ({ browser, customerConfig }, use, testInfo) => {
    const contexts: BrowserContext[] = [];

    const factory = async (opts: { role: UserRole; teamId?: string }) => {
      let { teamId } = opts;

      // USER role requires a team
      if (opts.role === 'USER' && !teamId) {
        const specName = testInfo.titlePath[0]
          ?.replace(/[^a-zA-Z0-9]/g, '-')
          .toLowerCase()
          .slice(0, 20) ?? 'test';
        const short = `t${Date.now().toString(36).slice(-4)}`;
        teamId = await convexRun<string>('e2e:createTeam', {
          customer: customerConfig.name,
          name: `E2E Team ${specName}`,
          short,
        });
      }

      const user = await createTestUser(
        customerConfig.name,
        opts.role,
        testInfo,
        teamId
      );

      const context = await loginUser(
        browser,
        customerConfig.runtime,
        customerConfig.name,
        user
      );
      contexts.push(context);

      const page = await context.newPage();
      return { page, user };
    };

    await use(factory);

    // Cleanup contexts (not users, those stay in DB)
    for (const ctx of contexts) {
      await ctx.close();
    }
  },

  adminPage: async ({ createAuthenticatedUser }, use) => {
    const { page } = await createAuthenticatedUser({ role: 'ADMIN' });
    await use(page);
  },

  userPage: async ({ createAuthenticatedUser }, use) => {
    const { page } = await createAuthenticatedUser({ role: 'USER' });
    await use(page);
  },
});

export { expect, type Page } from '@playwright/test';
