#!/usr/bin/env node
import { createRequire } from 'node:module';
import { execFileSync } from 'node:child_process';
import { writeFileSync } from 'node:fs';

const require = createRequire(import.meta.url);
const { chromium } = require('playwright');

const now = new Date().toISOString().replace(/[-:.TZ]/g, '').slice(0, 14);
const username = process.env.BEAVER_VALIDATE_USERNAME || `fsauto${now}`;
const password = process.env.BEAVER_VALIDATE_PASSWORD || 'TestPass123!';
const email = process.env.BEAVER_VALIDATE_EMAIL || `${username}@example.test`;
const portalUrl = (process.env.BEAVER_VALIDATE_PORTAL_URL || 'http://127.0.0.1:3081').replace(/\/$/, '');
const executablePath = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH || undefined;
const hostResolverRules = process.env.BEAVER_VALIDATE_HOST_RESOLVER_RULES || process.env.BEAVER_SMOKE_HOST_RESOLVER_RULES || '';
const headless = process.env.BEAVER_VALIDATE_HEADLESS !== '0';
const verifyMinio = process.env.BEAVER_VALIDATE_VERIFY_MINIO === '1';
const cleanupEnabled = process.env.BEAVER_VALIDATE_CLEANUP !== '0';
const screenshotDir = process.env.BEAVER_VALIDATE_SCREENSHOT_DIR || '/tmp';
const reportPath = process.env.BEAVER_VALIDATE_REPORT || '';

const taskScope = `ui-delete-${Date.now()}`;
const deleteTargets = [
  { root: 'uploads', path: 'uploads/delete-me-uploads.txt', dir: 'uploads', name: 'delete-me-uploads.txt' },
  { root: 'outputs', path: 'outputs/delete-me-outputs.txt', dir: 'outputs', name: 'delete-me-outputs.txt' },
  { root: 'shared', path: 'shared/delete-me-shared.txt', dir: 'shared', name: 'delete-me-shared.txt' },
  { root: 'tasks', path: `tasks/${taskScope}/delete-me-tasks.txt`, dir: `tasks/${taskScope}`, name: 'delete-me-tasks.txt' },
];

const pageChecks = [
  { name: 'Files', path: '/files', marker: /文件管理|Files/ },
  { name: 'Tasks', path: '/tasks', marker: /任务|Tasks/ },
  { name: 'Tools/MCP', path: '/mcp', marker: /工具|Tools/ },
  { name: 'Logs', path: '/logs', marker: /运行日志|Runtime Logs/ },
  { name: 'Agents', path: '/agents', marker: /智能体|Agents/ },
  { name: 'Skills', path: '/skills', marker: /技能|Skills/ },
];

const launchArgs = [];
if (hostResolverRules) {
  launchArgs.push(`--host-resolver-rules=${hostResolverRules}`);
}

const failedResponses = [];
const failedRequests = [];
const consoleErrors = [];
const pageEvidence = [];

function remember(list, value) {
  list.push(value);
  if (list.length > 80) {
    list.shift();
  }
}

function sanitize(value) {
  return String(value)
    .replace(/(Authorization:\s*Bearer\s+)[^\s"']+/gi, '$1<redacted>')
    .replace(/(access[_-]?key["']?\s*[:=]\s*["']?)[^"',\s]+/gi, '$1<redacted>')
    .replace(/(secret[_-]?key["']?\s*[:=]\s*["']?)[^"',\s]+/gi, '$1<redacted>')
    .replace(/(password["']?\s*[:=]\s*["']?)[^"',\s]+/gi, '$1<redacted>');
}

async function clickFirstVisible(locators) {
  for (const locator of locators) {
    if (await locator.first().isVisible().catch(() => false)) {
      await locator.first().click();
      return true;
    }
  }
  return false;
}

async function registerAndHandoff(page) {
  const registerUrl = new URL('/register', portalUrl);
  registerUrl.searchParams.set('next', '/files');
  await page.goto(registerUrl.toString(), { waitUntil: 'domcontentloaded', timeout: 30000 });

  await page.locator('#username').fill(username);
  await page.locator('#email').fill(email);
  await page.locator('#password').fill(password);
  await page.locator('#confirmPassword').fill(password);
  await page.locator('form').first().locator('button[type="submit"]').click();
  await page.waitForResponse(
    (response) => response.url().includes('/api/runtime/register'),
    { timeout: 180000 },
  );

  await page.getByText(/配置模型|Model Setup/).waitFor({ timeout: 180000 });
  const skipped = await clickFirstVisible([
    page.getByRole('button', { name: /跳过|Skip/i }),
    page.locator('button.secondary-button'),
  ]);
  if (!skipped) {
    throw new Error('Model setup skip button was not found');
  }

  await page.waitForURL(/\/files$/, { timeout: 90000 });
  await page.waitForLoadState('domcontentloaded', { timeout: 30000 }).catch(() => {});
  await page.getByText(/文件管理|Files/).waitFor({ timeout: 30000 });

  const token = await page.evaluate(() => window.localStorage.getItem('beaver_access_token') || '');
  if (!token) {
    throw new Error('No Beaver access token found after app handoff');
  }
  const me = await apiFetch(page, token, '/api/auth/me');
  if (me.status !== 200 || me.body?.username !== username) {
    throw new Error(`App auth confirmation failed: ${JSON.stringify(me)}`);
  }
  assertAppHost(page.url());
  return token;
}

function assertAppHost(url) {
  const current = new URL(url);
  const portal = new URL(portalUrl);
  if (current.host === portal.host || current.pathname.startsWith('/login') || current.pathname.startsWith('/register')) {
    throw new Error(`Browser is not on the app instance after handoff: ${url}`);
  }
}

async function apiFetch(page, token, path, options = {}) {
  return page.evaluate(async ({ token, path, options }) => {
    const response = await fetch(path, {
      ...options,
      headers: {
        ...(options.headers || {}),
        Authorization: `Bearer ${token}`,
      },
    });
    const text = await response.text();
    let body = null;
    try {
      body = text ? JSON.parse(text) : null;
    } catch {
      body = text;
    }
    return { status: response.status, body };
  }, { token, path, options });
}

async function createDeleteTargets(page, token) {
  const result = await page.evaluate(async ({ token, deleteTargets, taskScope }) => {
    const headers = { Authorization: `Bearer ${token}` };
    const mkdir = await fetch(`/api/user-files/mkdir?path=${encodeURIComponent(`tasks/${taskScope}`)}`, {
      method: 'POST',
      headers,
    });
    if (!mkdir.ok && mkdir.status !== 409) {
      throw new Error(`mkdir failed: ${mkdir.status}`);
    }
    const uploaded = [];
    for (const target of deleteTargets) {
      const form = new FormData();
      form.set('path', target.dir);
      form.set('file', new File([`delete target ${target.path}`], target.name, { type: 'text/plain' }));
      const response = await fetch('/api/user-files/upload', {
        method: 'POST',
        headers,
        body: form,
      });
      const body = await response.json().catch(() => ({}));
      if (!response.ok || body.path !== target.path) {
        throw new Error(`upload failed for ${target.path}: ${response.status} ${JSON.stringify(body)}`);
      }
      uploaded.push(body.path);
    }
    return uploaded;
  }, { token, deleteTargets, taskScope });
  if (result.length !== deleteTargets.length) {
    throw new Error(`Unexpected upload target count: ${result.length}`);
  }
}

async function waitForRootEntries(page) {
  await page.getByText(/文件管理|Files/).waitFor({ timeout: 30000 });
  for (const root of ['uploads', 'outputs', 'shared', 'tasks']) {
    await page.locator('button').filter({ hasText: root }).first().waitFor({ state: 'visible', timeout: 30000 });
  }
}

async function navigateToRoot(page) {
  await page.goto(`${new URL(page.url()).origin}/files`, { waitUntil: 'domcontentloaded', timeout: 30000 });
  await waitForRootEntries(page);
}

async function openDirectory(page, name) {
  const row = page.locator('button').filter({ hasText: name }).first();
  await row.waitFor({ state: 'visible', timeout: 30000 });
  await row.click();
  await page.waitForLoadState('networkidle', { timeout: 8000 }).catch(() => {});
}

async function deleteFromFilesPage(page, target) {
  await navigateToRoot(page);
  await openDirectory(page, target.root);
  if (target.root === 'tasks') {
    await openDirectory(page, taskScope);
  }

  const row = page.locator('button').filter({ hasText: target.name }).first();
  await row.waitFor({ state: 'visible', timeout: 30000 });
  await row.hover();
  await row.locator('[title="Delete"], [title="删除"]').last().click({ force: true });
  await page.waitForFunction(
    (name) => !document.body.innerText.includes(name),
    target.name,
    { timeout: 30000 },
  );
}

async function verifyDeletedViaApi(page, token) {
  for (const target of deleteTargets) {
    const preview = await apiFetch(page, token, `/api/user-files/preview?path=${encodeURIComponent(target.path)}`);
    if (preview.status !== 404) {
      throw new Error(`Expected preview 404 after UI delete for ${target.path}, got ${preview.status}`);
    }
  }
  const roots = await apiFetch(page, token, '/api/user-files/browse');
  if (roots.status !== 200) {
    throw new Error(`Root browse failed after UI deletion: ${roots.status}`);
  }
  const names = new Set((roots.body?.items || []).map((item) => item.name));
  for (const root of ['uploads', 'outputs', 'shared', 'tasks']) {
    if (!names.has(root)) {
      throw new Error(`Virtual root disappeared after UI deletion: ${root}`);
    }
  }
}

function verifyDeletedInMinio() {
  if (!verifyMinio) {
    return false;
  }
  const endpoint = process.env.BEAVER_VALIDATE_MINIO_ENDPOINT || process.env.BEAVER_SMOKE_MINIO_ENDPOINT || 'http://127.0.0.1:9000';
  const accessKey = process.env.BEAVER_VALIDATE_MINIO_ACCESS_KEY || process.env.BEAVER_SMOKE_MINIO_ACCESS_KEY || process.env.BEAVER_MINIO_ROOT_USER || '';
  const secretKey = process.env.BEAVER_VALIDATE_MINIO_SECRET_KEY || process.env.BEAVER_SMOKE_MINIO_SECRET_KEY || process.env.BEAVER_MINIO_ROOT_PASSWORD || '';
  const bucket = process.env.BEAVER_VALIDATE_MINIO_BUCKET || process.env.BEAVER_SMOKE_MINIO_BUCKET || process.env.BEAVER_USER_FILES_BUCKET || 'beaver-user-files';
  const network = process.env.BEAVER_VALIDATE_MINIO_NETWORK || process.env.BEAVER_SMOKE_MINIO_NETWORK || '';
  if (!accessKey || !secretKey) {
    throw new Error('MinIO verification requested but access key or secret key is missing');
  }
  const checks = deleteTargets
    .map((target) => `if mc stat ${shellQuote(`beaver/${bucket}/users/${username}/${target.path}`)} >/dev/null 2>&1; then echo ${shellQuote(`object still exists: ${target.path}`)}; exit 1; fi`)
    .join(' && ');
  const containerArgs = ['run', '--rm', '--entrypoint', '/bin/sh'];
  if (network) {
    containerArgs.push('--network', network);
  }
  containerArgs.push(
    'minio/mc:latest',
    '-lc',
    [
      `mc alias set beaver ${shellQuote(endpoint)} ${shellQuote(accessKey)} ${shellQuote(secretKey)} >/dev/null`,
      checks,
    ].join(' && '),
  );
  execFileSync('docker', containerArgs, { stdio: 'pipe' });
  return true;
}

async function checkPage(page, appOrigin, check) {
  const beforeResponseCount = failedResponses.length;
  const beforeConsoleCount = consoleErrors.length;
  await page.goto(`${appOrigin}${check.path}`, { waitUntil: 'domcontentloaded', timeout: 30000 });
  await page.getByText(check.marker).first().waitFor({ timeout: 30000 });
  if (check.path === '/agents') {
    const subagentTab = page.getByRole('tab', { name: /Persistent sub-agents|Persistent Sub-Agents|Sub-Agent/i }).first();
    if (await subagentTab.isVisible().catch(() => false)) {
      await subagentTab.click();
      await page.getByText(/Sub-Agent|sub-agent|子智能体/i).first().waitFor({ timeout: 30000 });
    }
  }
  await page.waitForLoadState('networkidle', { timeout: 8000 }).catch(() => {});
  assertAppHost(page.url());
  const newResponses = failedResponses.slice(beforeResponseCount);
  const newConsoleErrors = consoleErrors.slice(beforeConsoleCount);
  const hardFailures = newResponses.filter((entry) => entry.status >= 500 || (entry.url.includes('/api/user-files/') && entry.status >= 400));
  if (hardFailures.length) {
    throw new Error(`Page ${check.name} had failing responses: ${hardFailures.map((item) => `${item.status} ${item.url}`).join('; ')}`);
  }
  const fileConsoleErrors = newConsoleErrors.filter((entry) => /user[-_ ]?files|filesystem|minio|storage/i.test(entry.text));
  if (fileConsoleErrors.length) {
    throw new Error(`Page ${check.name} had file-system-related console errors: ${fileConsoleErrors.map((item) => item.text).join('; ')}`);
  }
  pageEvidence.push({ name: check.name, path: check.path, finalUrl: page.url(), failedResponseCount: newResponses.length });
}

async function cleanupDisposableUser() {
  const token = process.env.BEAVER_VALIDATE_DEPLOY_TOKEN || process.env.DEPLOY_CONTROL_API_TOKEN || '';
  const deployUrl = (process.env.BEAVER_VALIDATE_DEPLOY_CONTROL_URL || process.env.DEPLOY_CONTROL_URL || 'http://127.0.0.1:8090').replace(/\/$/, '');
  if (!cleanupEnabled || !token) {
    return { attempted: false, reason: cleanupEnabled ? 'missing deploy-control token' : 'disabled' };
  }
  const response = await fetch(`${deployUrl}/api/instances/${encodeURIComponent(username)}`, {
    method: 'DELETE',
    headers: {
      Authorization: `Bearer ${token}`,
      Accept: 'application/json',
      'X-Purge-Data': '1',
      'X-Purge-User-Files': '1',
    },
  });
  const body = await response.json().catch(() => ({}));
  return { attempted: true, status: response.status, ok: response.ok, body };
}

function shellQuote(value) {
  return `'${String(value).replace(/'/g, "'\\''")}'`;
}

async function main() {
  const browser = await chromium.launch({ headless, executablePath, args: launchArgs });
  const page = await browser.newPage({ viewport: { width: 1365, height: 900 } });
  page.on('dialog', (dialog) => dialog.accept());
  page.on('response', (response) => {
    if (response.status() >= 400) {
      remember(failedResponses, { status: response.status(), url: response.url() });
    }
  });
  page.on('requestfailed', (request) => {
    remember(failedRequests, { url: request.url(), error: request.failure()?.errorText || '' });
  });
  page.on('console', (message) => {
    if (message.type() === 'error') {
      remember(consoleErrors, { text: sanitize(message.text()) });
    }
  });

  let cleanup = { attempted: false, reason: 'not reached' };
  try {
    const token = await registerAndHandoff(page);
    const appOrigin = new URL(page.url()).origin;
    await waitForRootEntries(page);
    await createDeleteTargets(page, token);

    for (const target of deleteTargets) {
      await deleteFromFilesPage(page, target);
    }

    await verifyDeletedViaApi(page, token);
    const minioVerified = verifyDeletedInMinio();

    for (const check of pageChecks) {
      await checkPage(page, appOrigin, check);
    }

    const finalScreenshot = `${screenshotDir}/beaver-filesystem-validation-${username}.png`;
    await page.screenshot({ path: finalScreenshot, fullPage: true }).catch(() => {});
    cleanup = await cleanupDisposableUser();
    const result = {
      ok: true,
      username,
      email,
      deletedRoots: deleteTargets.map((target) => target.root),
      deletedPaths: deleteTargets.map((target) => target.path),
      checkedPages: pageEvidence,
      minioVerified,
      cleanup,
      screenshot: finalScreenshot,
      failedResponses,
      failedRequests,
      consoleErrors,
    };
    if (reportPath) {
      writeFileSync(reportPath, `${JSON.stringify(result, null, 2)}\n`, 'utf8');
    }
    console.log(JSON.stringify(result, null, 2));
  } catch (error) {
    const errorScreenshot = `${screenshotDir}/beaver-filesystem-validation-${username}-error.png`;
    await page.screenshot({ path: errorScreenshot, fullPage: true }).catch(() => {});
    cleanup = await cleanupDisposableUser().catch((cleanupError) => ({ attempted: true, ok: false, error: sanitize(cleanupError) }));
    const result = {
      ok: false,
      username,
      email,
      currentUrl: page.url(),
      error: sanitize(error),
      cleanup,
      screenshot: errorScreenshot,
      failedResponses,
      failedRequests,
      consoleErrors,
    };
    if (reportPath) {
      writeFileSync(reportPath, `${JSON.stringify(result, null, 2)}\n`, 'utf8');
    }
    console.error(JSON.stringify(result, null, 2));
    process.exitCode = 1;
  } finally {
    await browser.close();
  }
}

await main();
