import assert from 'node:assert/strict';
import { spawn } from 'node:child_process';
import fs from 'node:fs/promises';
import net from 'node:net';
import path from 'node:path';
import pg from 'pg';

const DEFAULT_DATABASE_URL = 'postgresql://postgres:postgres@127.0.0.1:54322/postgres';
const databaseUrl = process.env.DATABASE_URL || DEFAULT_DATABASE_URL;
const outputDir = process.env.LAUNCH_SMOKE_OUTPUT_DIR || 'docs/refactor/launch-artifacts';
const startServer = boolEnv('LAUNCH_SMOKE_START_SERVER', !process.env.LAUNCH_SMOKE_API_BASE);
const fixedPort = Number(process.env.LAUNCH_SMOKE_API_PORT || 0) || 0;

const ids = {
  tenant: '00000000-0000-0000-0000-000000000001',
  partnerTenant: '00000000-0000-0000-0000-000000000901',
  student: '00000000-0000-0000-0000-000000000101',
  tenantAdmin: '00000000-0000-0000-0000-000000000102',
  platformAdmin: '00000000-0000-0000-0000-000000000999',
  region: '00000000-0000-0000-0000-000000000301',
  plan: '00000000-0000-0000-0000-000000000201',
};

let apiBase = process.env.LAUNCH_SMOKE_API_BASE || process.env.API_BASE || '';
let serverProcess = null;
let serverLogs = '';

function boolEnv(key, fallback) {
  const value = process.env[key];
  if (value === undefined || value === '') return fallback;
  return ['1', 'true', 'yes', 'on'].includes(value.toLowerCase());
}

function nowIso() {
  return new Date().toISOString();
}

function shanghaiStamp(date = new Date()) {
  const parts = Object.fromEntries(
    new Intl.DateTimeFormat('en-CA', {
      timeZone: 'Asia/Shanghai',
      year: 'numeric',
      month: '2-digit',
      day: '2-digit',
      hour: '2-digit',
      minute: '2-digit',
      second: '2-digit',
      hour12: false,
    }).formatToParts(date).map(part => [part.type, part.value]),
  );
  return `${parts.year}${parts.month}${parts.day}-${parts.hour}${parts.minute}${parts.second}`;
}

function getFreePort() {
  return new Promise((resolve, reject) => {
    const server = net.createServer();
    server.on('error', reject);
    server.listen(fixedPort, '127.0.0.1', () => {
      const address = server.address();
      server.close(() => resolve(address.port));
    });
  });
}

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function waitForHealth(timeoutMs = 20_000) {
  const started = Date.now();
  let lastError = null;
  while (Date.now() - started < timeoutMs) {
    try {
      const response = await fetch(new URL('/health', apiBase));
      const payload = await response.json().catch(() => ({}));
      if (response.ok && payload.ok) return;
    } catch (error) {
      lastError = error;
    }
    await sleep(250);
  }
  throw new Error(`API server did not become healthy. ${lastError?.message || ''}\n${serverLogs}`);
}

async function startServerIfNeeded() {
  if (!startServer) return;
  const port = await getFreePort();
  apiBase = `http://127.0.0.1:${port}`;
  serverProcess = spawn(process.execPath, ['apps/api/dist/apps/api/src/server.js'], {
    cwd: process.cwd(),
    env: {
      ...process.env,
      PORT: String(port),
      DATABASE_URL: databaseUrl,
      MAX_JSON_BODY_BYTES: process.env.MAX_JSON_BODY_BYTES || '1048576',
      MAX_IMPORT_JSON_BODY_BYTES: process.env.MAX_IMPORT_JSON_BODY_BYTES || '10485760',
    },
    stdio: ['ignore', 'pipe', 'pipe'],
    windowsHide: true,
  });
  serverProcess.stdout.on('data', chunk => {
    serverLogs += chunk.toString();
  });
  serverProcess.stderr.on('data', chunk => {
    serverLogs += chunk.toString();
  });
  await waitForHealth();
}

function stopServer() {
  if (serverProcess && !serverProcess.killed) serverProcess.kill();
}

function buildUrl(pathname, query = {}) {
  const target = new URL(pathname, apiBase);
  for (const [key, value] of Object.entries(query)) {
    if (value !== undefined && value !== null && value !== '') target.searchParams.set(key, String(value));
  }
  return target;
}

async function request(pathname, options = {}) {
  const response = await fetch(buildUrl(pathname, options.query), {
    method: options.method || 'GET',
    headers: {
      'content-type': 'application/json',
      ...(options.tenantId === false ? {} : { 'x-tenant-id': options.tenantId || ids.tenant }),
      ...(options.userId === false ? {} : { 'x-user-id': options.userId || ids.student }),
      ...(options.headers || {}),
    },
    body: options.body ? JSON.stringify(options.body) : undefined,
  });
  const payload = await response.json().catch(() => ({}));
  const expectedStatus = options.expectStatus || 200;
  assert.equal(response.status, expectedStatus, `${options.method || 'GET'} ${pathname} expected ${expectedStatus}, got ${response.status}: ${JSON.stringify(payload)}`);
  return payload;
}

async function one(pool, sql, params = []) {
  const result = await pool.query(sql, params);
  return result.rows[0] || null;
}

function pickAnswer(question) {
  const correctIndices = Array.isArray(question.correctOptionIndices) ? question.correctOptionIndices : [];
  if (correctIndices.length) return { selectedOptions: correctIndices.map(item => String(item)) };
  if (question.correctOptionIndex !== undefined && question.correctOptionIndex !== null) return { selectedOptions: [String(question.correctOptionIndex)] };
  if (Array.isArray(question.options) && question.options.length) return { selectedOptions: ['0'] };
  return { answerText: 'launch smoke answer', selfJudgedCorrect: true };
}

async function ensureSvipEntitlement(pool) {
  await pool.query(
    `
    insert into public.entitlements (
      tenant_id, user_id, entitlement_type, scope_type, scope_id,
      source_type, legacy_source_id, starts_at, expires_at, status, metadata
    )
    values (
      $1, $2, 'svip', 'tenant', null,
      'launch_persona_smoke', 'launch-persona-smoke', now() - interval '1 minute',
      now() + interval '1 day', 'active',
      '{"source":"launch_persona_smoke","temporary":true}'::jsonb
    )
    on conflict do nothing
    `,
    [ids.tenant, ids.student],
  );
}

async function discoverStudentPath(pool) {
  const entry = await one(
    pool,
    `
    select id, name
    from public.content_entries
    where tenant_id = $1 and is_active = true and entry_type = 'question_practice'
    order by sort_order asc, created_at asc
    limit 1
    `,
    [ids.tenant],
  );
  assert.ok(entry, 'launch smoke needs an active question_practice content entry');

  const collection = await one(
    pool,
    `
    select qc.id, qc.name, qc.node_id as content_node_id
    from public.question_collections qc
    where qc.tenant_id = $1
      and qc.status = 'active'
      and exists (
        select 1
        from public.question_collection_items qci
        join public.questions q on q.tenant_id = qci.tenant_id and q.id = qci.question_id
        where qci.tenant_id = qc.tenant_id
          and qci.collection_id = qc.id
          and q.status = 'published'
      )
    order by qc.created_at asc
    limit 1
    `,
    [ids.tenant],
  );
  assert.ok(collection, 'launch smoke needs an active question collection with questions');

  const blueprint = await one(
    pool,
    `
    select id, name, mode
    from public.practice_blueprints
    where tenant_id = $1 and collection_id = $2 and status = 'active'
    order by case mode when 'sequential' then 0 when 'random' then 1 else 2 end, created_at asc
    limit 1
    `,
    [ids.tenant, collection.id],
  );
  assert.ok(blueprint, 'launch smoke needs an active practice blueprint');

  return { entry, collection, blueprint };
}

async function studentJourney(pool) {
  await ensureSvipEntitlement(pool);
  const pathInfo = await discoverStudentPath(pool);

  const profile = await request('/api/profile/me');
  assert.equal(profile.item?.userId, ids.student, 'student profile should load current user');

  const entitlements = await request('/api/commerce/entitlements');
  assert.equal(entitlements.summary?.isSvip, true, 'student should be SVIP before practice journey');

  const entries = await request('/api/catalog/content-entries', { query: { entryType: 'question_practice' } });
  assert.ok(entries.items?.some(item => item.id === pathInfo.entry.id), 'student should see question practice entry');

  const collections = await request('/api/catalog/question-collections', { query: { nodeId: pathInfo.collection.content_node_id } });
  assert.ok(collections.items?.some(item => item.id === pathInfo.collection.id), 'student should see question collection');

  const session = await request('/api/learning/practice-sessions', {
    method: 'POST',
    body: {
      blueprintId: pathInfo.blueprint.id,
      collectionId: pathInfo.collection.id,
      questionLimit: 3,
      metadata: { source: 'launch_persona_smoke' },
    },
  });
  assert.ok(session.item?.id, 'student should create practice session');
  assert.notEqual(session.item?.accessMode, 'free', 'SVIP student practice should not be constrained by free quota');

  const detail = await request('/api/learning/practice-sessions/detail', {
    query: { practiceSessionId: session.item.id },
  });
  const questions = detail.item?.questions || [];
  assert.ok(questions.length > 0, 'practice detail should include questions');

  const firstQuestion = questions[0];
  await request('/api/learning/favorites/questions', {
    method: 'POST',
    body: { questionId: firstQuestion.id, favorite: true },
  });

  await request('/api/learning/answers', {
    method: 'POST',
    body: {
      practiceSessionId: session.item.id,
      questionId: firstQuestion.id,
      ...pickAnswer(firstQuestion),
    },
  });

  const report = await request('/api/learning/practice-sessions/submit', {
    method: 'POST',
    body: { practiceSessionId: session.item.id },
  });
  assert.equal(report.item?.practiceSessionId, session.item.id, 'student should submit practice and receive report');

  const favorites = await request('/api/learning/favorites/questions', { query: { limit: 20 } });
  assert.ok(favorites.items?.some(item => item.id === firstQuestion.id || item.questionId === firstQuestion.id), 'favorite list should include toggled question');

  const wrong = await request('/api/learning/wrong-questions', { query: { limit: 20 } });
  const wrongPlan = await request('/api/learning/wrong-questions/review-plan');
  const favoriteReview = await request('/api/learning/practice-sessions', {
    method: 'POST',
    body: { mode: 'favorite_review', questionLimit: 3, metadata: { source: 'launch_persona_smoke' } },
  });
  assert.equal(favoriteReview.item?.mode, 'favorite_review', 'student should create favorite review session');

  const wrongReview =
    wrong.items?.length > 0
      ? await request('/api/learning/practice-sessions', {
          method: 'POST',
          body: { mode: 'wrong_review', questionLimit: 3, metadata: { source: 'launch_persona_smoke' } },
        })
      : null;

  return {
    profile: { profileId: profile.item?.id, userId: profile.item?.userId, avatarPreset: profile.item?.avatarPreset || null },
    entitlement: { isSvip: entitlements.summary?.isSvip, count: entitlements.items?.length || 0 },
    practice: {
      sessionId: session.item.id,
      accessMode: session.item.accessMode,
      questionCount: questions.length,
      reportId: report.item?.id,
      correctCount: report.item?.correctCount,
      wrongCount: report.item?.wrongCount,
    },
    favorite: { questionId: firstQuestion.id, favoriteCount: favorites.items?.length || 0, reviewSessionId: favoriteReview.item?.id },
    wrongReview: {
      wrongCount: wrong.items?.length || 0,
      reviewPlanCount: wrongPlan.items?.length || 0,
      reviewSessionId: wrongReview?.item?.id || null,
      skippedReason: wrong.items?.length > 0 ? null : 'No wrong questions were present for this student after the sampled answer.',
    },
  };
}

async function tenantAdminJourney() {
  const overview = await request('/api/tenant-admin/overview', { userId: ids.tenantAdmin });
  const dashboard = await request('/api/tenant-admin/dashboard', {
    userId: ids.tenantAdmin,
    query: { timeRange: '7d', regionId: ids.region },
  });
  const theme = await request('/api/tenant-admin/theme', { userId: ids.tenantAdmin });
  const students = await request('/api/tenant-admin/students', {
    userId: ids.tenantAdmin,
    query: { limit: 10 },
  });
  const conversion = await request('/api/referral/conversion-report', {
    userId: ids.tenantAdmin,
    query: { limit: 10 },
  });
  const studentDenied = await request('/api/tenant-admin/dashboard', { expectStatus: 403 });
  const crossTenantDenied = await request('/api/tenant-admin/dashboard', {
    tenantId: ids.partnerTenant,
    userId: ids.tenantAdmin,
    expectStatus: 403,
  });

  return {
    overview: { tenantId: overview.item?.id, name: overview.item?.name },
    dashboard: {
      tenantId: dashboard.item?.scope?.tenantId,
      trendBuckets: dashboard.item?.trends?.length || 0,
      activeHourBuckets: dashboard.item?.activeHours?.length || 0,
      questionCount: dashboard.item?.cards?.content?.questions || 0,
    },
    theme: { status: theme.item?.status || null, activeTemplateCode: theme.item?.activeTemplateCode || null },
    students: { count: students.items?.length || 0 },
    referralConversion: {
      mode: conversion.item?.aggregation?.mode || 'realtime_or_legacy',
      leadCount: conversion.item?.summary?.leadCount || 0,
      paidSourceCount: conversion.item?.summary?.paidSourceCount || 0,
    },
    guards: {
      studentDashboardDenied: studentDenied.code,
      crossTenantDenied: crossTenantDenied.code,
    },
  };
}

async function platformAdminJourney() {
  const headers = { 'x-platform-admin-key': process.env.PLATFORM_ADMIN_API_KEY || 'local-platform-admin-key' };
  const overview = await request('/api/platform-admin/overview', {
    tenantId: false,
    userId: false,
    headers,
  });
  const tenants = await request('/api/platform-admin/tenants', {
    tenantId: false,
    userId: false,
    headers,
    query: { limit: 10 },
  });
  const plans = await request('/api/platform-admin/plans', {
    tenantId: false,
    userId: false,
    headers,
  });
  const auditAlerts = await request('/api/platform-admin/audit-alerts', {
    tenantId: false,
    userId: false,
    headers,
    query: { status: 'open', limit: 10 },
  });
  const studentDenied = await request('/api/platform-admin/overview', {
    tenantId: false,
    expectStatus: 403,
  });

  return {
    overview: {
      tenantTotal: overview.item?.tenants?.total || 0,
      activeTenantCount: overview.item?.tenants?.active || 0,
    },
    tenants: { count: tenants.items?.length || 0 },
    plans: { count: plans.items?.length || 0 },
    auditAlerts: { openCount: auditAlerts.items?.length || 0 },
    guards: { studentPlatformDenied: studentDenied.code },
  };
}

function stepCell(step, formatPassed) {
  if (step.status === 'pass') return formatPassed(step.result);
  return `失败：${step.error?.message || 'unknown error'}`;
}

async function writeReport(report) {
  await fs.mkdir(outputDir, { recursive: true });
  const jsonPath = path.join(outputDir, `launch-persona-smoke-${shanghaiStamp()}.json`);
  const mdPath = jsonPath.replace(/\.json$/, '.md');
  await fs.writeFile(jsonPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
  const lines = [
    '# 上线前角色旅程烟测报告',
    '',
    `生成时间：${new Date(report.finishedAt).toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' })}`,
    '',
    '| 角色 | 结果 | 关键验证 |',
    '| --- | --- | --- |',
    `| 普通学生 | ${report.student.status} | ${stepCell(report.student, result => `SVIP=${result.entitlement.isSvip}; session=${result.practice.sessionId}; favoriteReview=${result.favorite.reviewSessionId}; wrongReview=${result.wrongReview.reviewSessionId || result.wrongReview.skippedReason}`)} |`,
    `| 租户管理员 | ${report.tenantAdmin.status} | ${stepCell(report.tenantAdmin, result => `dashboardBuckets=${result.dashboard.trendBuckets}; studentDenied=${result.guards.studentDashboardDenied}; crossTenant=${result.guards.crossTenantDenied}`)} |`,
    `| 平台管理员 | ${report.platformAdmin.status} | ${stepCell(report.platformAdmin, result => `tenants=${result.overview.tenantTotal}; studentDenied=${result.guards.studentPlatformDenied}`)} |`,
    '',
    '说明：该脚本会写入少量 `launch_persona_smoke` 测试权益、练习 session、答题和收藏记录。生产环境仅建议在灰度/演练租户运行。',
    '',
  ];
  await fs.writeFile(mdPath, `${lines.join('\n')}\n`, 'utf8');
  return { jsonPath, mdPath };
}

async function runStep(name, fn) {
  const startedAt = nowIso();
  try {
    const result = await fn();
    return { name, status: 'pass', startedAt, finishedAt: nowIso(), result };
  } catch (error) {
    return {
      name,
      status: 'fail',
      startedAt,
      finishedAt: nowIso(),
      error: {
        message: error?.message || String(error),
        stack: error?.stack || '',
      },
    };
  }
}

async function main() {
  const pool = new pg.Pool({ connectionString: databaseUrl, max: 4 });
  try {
    await startServerIfNeeded();
    if (!apiBase) throw new Error('LAUNCH_SMOKE_API_BASE is required when LAUNCH_SMOKE_START_SERVER=false.');
    const report = {
      startedAt: nowIso(),
      finishedAt: '',
      apiBase,
      databaseUrl: databaseUrl.replace(/:\/\/([^:]+):([^@]+)@/, '://$1:[redacted]@'),
      student: await runStep('student', () => studentJourney(pool)),
      tenantAdmin: await runStep('tenant-admin', tenantAdminJourney),
      platformAdmin: await runStep('platform-admin', platformAdminJourney),
    };
    report.finishedAt = nowIso();
    report.status = [report.student, report.tenantAdmin, report.platformAdmin].every(item => item.status === 'pass') ? 'pass' : 'fail';
    const files = await writeReport(report);
    console.log(`[launch-smoke] status=${report.status}`);
    console.log(`[launch-smoke] wrote ${files.jsonPath}`);
    console.log(`[launch-smoke] wrote ${files.mdPath}`);
    if (report.status !== 'pass') process.exitCode = 1;
  } finally {
    await pool.end();
    stopServer();
  }
}

main().catch(error => {
  console.error(error);
  if (serverLogs) console.error(serverLogs);
  stopServer();
  process.exit(1);
});
