/**
 * E2E tests: Role-based access control.
 *
 * Verifies that:
 * - Non-admin users (user, viewer) CAN access / and /profile
 * - Non-admin users CANNOT access admin-only pages
 * - Unauthenticated users are redirected to /login everywhere
 * - Admin users can access all pages
 *
 * Test setup:
 * - Creates "testuser" (role=user) and "testviewer" (role=viewer) in the database
 *   via `docker compose exec` + bun script inside the web container.
 * - Logs in as each role in separate browser contexts.
 */
import { test, expect, type BrowserContext } from '@playwright/test';
import { execFileSync } from 'node:child_process';

const COMPOSE_ARGS = [
  'compose',
  '-f', 'docker-compose.yml',
  '-f', 'tests/docker-compose.test.yml',
];

// Pages that require admin role (via requireAdmin in their own page.tsx)
const ADMIN_ONLY_PAGES = [
  '/proxy-hosts',
  '/l4-proxy-hosts',
  '/certificates',
  '/access-lists',
  '/analytics',
  '/waf',
  '/audit-log',
  '/settings',
  '/users',
  '/groups',
  '/api-docs',
];

// Pages accessible to any authenticated user
const USER_ACCESSIBLE_PAGES = [
  '/',
  '/profile',
];

// All dashboard pages (union of both sets)
const ALL_DASHBOARD_PAGES = [...USER_ACCESSIBLE_PAGES, ...ADMIN_ONLY_PAGES];

/**
 * Create a test user inside the running web container using bun.
 * Uses Bun's built-in Bun.password.hash (bcrypt) — no npm deps needed.
 */
function ensureTestUser(username: string, password: string, role: string) {
  const script = `
    import { Database } from "bun:sqlite";
    const db = new Database("./data/caddy-proxy-manager.db");
    const email = "${username}@localhost";
    const hash = await Bun.password.hash("${password}", { algorithm: "bcrypt", cost: 12 });
    const now = new Date().toISOString();
    const existing = db.query("SELECT id FROM users WHERE email = ?").get(email);
    if (existing) {
      db.run("UPDATE users SET passwordHash = ?, role = ?, status = 'active', updatedAt = ? WHERE email = ?",
        [hash, "${role}", now, email]);
      // Update or create credential account for Better Auth
      const acc = db.query("SELECT id FROM accounts WHERE userId = ? AND providerId = 'credential'").get(existing.id);
      if (acc) {
        db.run("UPDATE accounts SET password = ?, updatedAt = ? WHERE id = ?", [hash, now, acc.id]);
      } else {
        db.run("INSERT INTO accounts (userId, accountId, providerId, password, createdAt, updatedAt) VALUES (?, ?, 'credential', ?, ?, ?)",
          [existing.id, String(existing.id), hash, now, now]);
      }
    } else {
      db.run(
        "INSERT INTO users (email, name, passwordHash, role, provider, subject, username, status, createdAt, updatedAt) VALUES (?, ?, ?, ?, 'credentials', ?, ?, 'active', ?, ?)",
        [email, "${username}", hash, "${role}", "${username}", "${username}", now, now]
      );
      const user = db.query("SELECT id FROM users WHERE email = ?").get(email);
      // Create credential account for Better Auth
      db.run("INSERT INTO accounts (userId, accountId, providerId, password, createdAt, updatedAt) VALUES (?, ?, 'credential', ?, ?, ?)",
        [user.id, String(user.id), hash, now, now]);
    }
  `;
  execFileSync('docker', [...COMPOSE_ARGS, 'exec', '-T', 'web', 'bun', '-e', script], {
    cwd: process.cwd(),
    stdio: 'pipe',
  });
}

/**
 * Log in as the given user and return an authenticated browser context.
 */
async function loginAs(
  browser: import('@playwright/test').Browser,
  username: string,
  password: string
): Promise<BrowserContext> {
  const context = await browser.newContext();
  const page = await context.newPage();

  await page.goto('http://localhost:3000/login');
  await page.getByLabel('Username').fill(username);
  await page.getByLabel('Password').fill(password);
  await page.getByRole('button', { name: 'Sign in', exact: true }).click();

  // The login client does router.replace('/') on success — wait for that
  await page.waitForURL((url) => !url.pathname.includes('/login'), { timeout: 60_000 });
  await page.close();
  return context;
}

// ── Unauthenticated access ────────────────────────────────────────────────

test.describe('Unauthenticated access', () => {
  test.use({ storageState: { cookies: [], origins: [] } });

  for (const path of ALL_DASHBOARD_PAGES) {
    test(`${path} redirects to /login`, async ({ page }) => {
      await page.goto(path);
      await expect(page).toHaveURL(/\/login/, { timeout: 10_000 });
    });
  }
});

// ── Role-based access ─────────────────────────────────────────────────────

test.describe('Role-based access control', () => {
  test.use({ storageState: { cookies: [], origins: [] } });

  let userContext: BrowserContext;
  let viewerContext: BrowserContext;

  test.beforeAll(async ({ browser }) => {
    // Create test users with non-admin roles
    ensureTestUser('testuser', 'TestUserPass2026!', 'user');
    ensureTestUser('testviewer', 'TestViewerPass2026!', 'viewer');

    // Log in as each role
    userContext = await loginAs(browser, 'testuser', 'TestUserPass2026!');
    viewerContext = await loginAs(browser, 'testviewer', 'TestViewerPass2026!');
  });

  test.afterAll(async () => {
    await userContext?.close();
    await viewerContext?.close();
  });

  // ── "user" role — can access / and /profile ─────────────────────────

  test('user role: / loads with welcome message', async () => {
    const page = await userContext.newPage();
    try {
      await page.goto('/');
      await expect(page).not.toHaveURL(/\/login/, { timeout: 10_000 });
      await expect(page.getByText(/welcome back/i)).toBeVisible({ timeout: 5_000 });
    } finally {
      await page.close();
    }
  });

  test('user role: / does not show admin stat cards', async () => {
    const page = await userContext.newPage();
    try {
      await page.goto('/');
      await expect(page.getByText(/welcome back/i)).toBeVisible({ timeout: 5_000 });
      // Non-admin gets empty stats — no Proxy Hosts / Certificates / Access Lists cards
      await expect(page.getByRole('link', { name: /proxy hosts/i })).not.toBeVisible({ timeout: 3_000 });
    } finally {
      await page.close();
    }
  });

  test('user role: sidebar only shows Overview', async () => {
    const page = await userContext.newPage();
    try {
      await page.goto('/');
      await expect(page.getByText(/welcome back/i)).toBeVisible({ timeout: 5_000 });
      // Overview should be in the nav
      await expect(page.getByRole('link', { name: 'Overview' })).toBeVisible();
      // Admin-only nav items should not be visible
      await expect(page.getByRole('link', { name: 'Proxy Hosts' })).not.toBeVisible();
      await expect(page.getByRole('link', { name: 'Settings' })).not.toBeVisible();
      await expect(page.getByRole('link', { name: 'Users' })).not.toBeVisible();
    } finally {
      await page.close();
    }
  });

  test('user role: /profile loads successfully', async () => {
    const page = await userContext.newPage();
    try {
      await page.goto('/profile');
      await expect(page).not.toHaveURL(/\/login/, { timeout: 10_000 });
      await expect(page.getByText(/profile|password/i).first()).toBeVisible({ timeout: 5_000 });
    } finally {
      await page.close();
    }
  });

  // ── "viewer" role — can access / and /profile ───────────────────────

  test('viewer role: / loads with welcome message', async () => {
    const page = await viewerContext.newPage();
    try {
      await page.goto('/');
      await expect(page).not.toHaveURL(/\/login/, { timeout: 10_000 });
      await expect(page.getByText(/welcome back/i)).toBeVisible({ timeout: 5_000 });
    } finally {
      await page.close();
    }
  });

  test('viewer role: / does not show admin stat cards', async () => {
    const page = await viewerContext.newPage();
    try {
      await page.goto('/');
      await expect(page.getByText(/welcome back/i)).toBeVisible({ timeout: 5_000 });
      await expect(page.getByRole('link', { name: /proxy hosts/i })).not.toBeVisible({ timeout: 3_000 });
    } finally {
      await page.close();
    }
  });

  test('viewer role: sidebar only shows Overview', async () => {
    const page = await viewerContext.newPage();
    try {
      await page.goto('/');
      await expect(page.getByText(/welcome back/i)).toBeVisible({ timeout: 5_000 });
      await expect(page.getByRole('link', { name: 'Overview' })).toBeVisible();
      await expect(page.getByRole('link', { name: 'Proxy Hosts' })).not.toBeVisible();
      await expect(page.getByRole('link', { name: 'Settings' })).not.toBeVisible();
    } finally {
      await page.close();
    }
  });

  test('viewer role: /profile loads successfully', async () => {
    const page = await viewerContext.newPage();
    try {
      await page.goto('/profile');
      await expect(page).not.toHaveURL(/\/login/, { timeout: 10_000 });
      await expect(page.getByText(/profile|password/i).first()).toBeVisible({ timeout: 5_000 });
    } finally {
      await page.close();
    }
  });

  // ── "user" role — blocked from admin-only pages ─────────────────────

  for (const path of ADMIN_ONLY_PAGES) {
    test(`user role: ${path} is blocked`, async () => {
      const page = await userContext.newPage();
      try {
        const response = await page.goto(path);
        // requireAdmin() throws "Administrator privileges required".
        // Next.js renders the error boundary or returns 500.
        const status = response?.status() ?? 0;
        const url = page.url();

        const isBlocked =
          status >= 400 ||
          url.includes('/login') ||
          await page.getByText(/administrator privileges|error|forbidden|not authorized/i)
            .isVisible({ timeout: 3_000 }).catch(() => false);

        expect(isBlocked).toBe(true);
      } finally {
        await page.close();
      }
    });
  }

  // ── "viewer" role — blocked from admin-only pages ───────────────────

  for (const path of ADMIN_ONLY_PAGES) {
    test(`viewer role: ${path} is blocked`, async () => {
      const page = await viewerContext.newPage();
      try {
        const response = await page.goto(path);
        const status = response?.status() ?? 0;
        const url = page.url();

        const isBlocked =
          status >= 400 ||
          url.includes('/login') ||
          await page.getByText(/administrator privileges|error|forbidden|not authorized/i)
            .isVisible({ timeout: 3_000 }).catch(() => false);

        expect(isBlocked).toBe(true);
      } finally {
        await page.close();
      }
    });
  }

  // ── Admin user — can access all pages ───────────────────────────────

  test('admin role: all dashboard pages are accessible', async ({ browser }, testInfo) => {
    testInfo.setTimeout(90_000);
    // Use the pre-authenticated admin state from global-setup
    const adminContext = await browser.newContext({
      storageState: require('path').resolve(__dirname, '../.auth/admin.json'),
    });
    try {
      for (const path of ALL_DASHBOARD_PAGES) {
        const page = await adminContext.newPage();
        const response = await page.goto(path);
        const status = response?.status() ?? 0;
        expect(status).toBeLessThan(400);
        expect(page.url()).not.toContain('/login');
        await page.close();
      }
    } finally {
      await adminContext.close();
    }
  });

  test('admin role: sidebar shows all nav items', async ({ browser }) => {
    const adminContext = await browser.newContext({
      storageState: require('path').resolve(__dirname, '../.auth/admin.json'),
    });
    try {
      const page = await adminContext.newPage();
      await page.goto('/');
      await expect(page.getByRole('link', { name: 'Overview' })).toBeVisible();
      await expect(page.getByRole('link', { name: 'Proxy Hosts', exact: true })).toBeVisible();
      await expect(page.getByRole('link', { name: 'Settings' })).toBeVisible();
      await expect(page.getByRole('link', { name: 'Users' })).toBeVisible();
      await page.close();
    } finally {
      await adminContext.close();
    }
  });

  // ── API endpoints — non-admin should be blocked ───────────────────────

  test('user role: API v1 endpoints return 401/403', async () => {
    const page = await userContext.newPage();
    try {
      const response = await page.request.get('/api/v1/proxy-hosts');
      expect(response.status()).toBeGreaterThanOrEqual(400);
    } finally {
      await page.close();
    }
  });

  test('viewer role: API v1 endpoints return 401/403', async () => {
    const page = await viewerContext.newPage();
    try {
      const response = await page.request.get('/api/v1/proxy-hosts');
      expect(response.status()).toBeGreaterThanOrEqual(400);
    } finally {
      await page.close();
    }
  });
});
