#!/usr/bin/env node

import { execFileSync } from 'node:child_process';
import { mkdir, writeFile } from 'node:fs/promises';
import { dirname } from 'node:path';

const baseURL = (process.env.GATEWAY_BASE_URL || 'http://127.0.0.1:8088').replace(/\/+$/, '');
const authentication = resolveGatewayAPIKey();
const timeoutMs = Number(process.env.GATEWAY_E2E_TIMEOUT_MS || 180_000);

const scenarios = [
  {
    name: '火山 128K Chat 未传上限',
    platform: '火山引擎',
    model: process.env.GATEWAY_E2E_VOLCES_128K_MODEL || 'volces-openai:doubao-seed-2-0-pro-260215',
    path: '/v1/chat/completions',
    body: chatBody(),
    expected: { parameter: 'max_tokens', value: 43690, injected: true },
  },
  {
    name: '火山 32K Chat 未传上限',
    platform: '火山引擎',
    model: process.env.GATEWAY_E2E_VOLCES_32K_MODEL || 'volces-openai:doubao-seed-1-8-251228',
    path: '/v1/chat/completions',
    body: chatBody(),
    expected: { parameter: 'max_tokens', value: 10922, injected: true },
  },
  {
    name: '火山 Chat max_tokens=null',
    platform: '火山引擎',
    model: process.env.GATEWAY_E2E_VOLCES_128K_MODEL || 'volces-openai:doubao-seed-2-0-pro-260215',
    path: '/v1/chat/completions',
    body: chatBody({ max_tokens: null }),
    expected: { parameter: 'max_tokens', value: 43690, injected: true },
  },
  {
    name: '火山 Chat 显式 max_tokens',
    platform: '火山引擎',
    model: process.env.GATEWAY_E2E_VOLCES_128K_MODEL || 'volces-openai:doubao-seed-2-0-pro-260215',
    path: '/v1/chat/completions',
    body: chatBody({ max_tokens: 64 }),
    expected: { parameter: 'max_tokens', value: 64, injected: false },
  },
  {
    name: '火山 Chat 显式 max_completion_tokens',
    platform: '火山引擎',
    model: process.env.GATEWAY_E2E_VOLCES_128K_MODEL || 'volces-openai:doubao-seed-2-0-pro-260215',
    path: '/v1/chat/completions',
    body: chatBody({ max_completion_tokens: 64 }),
    expected: { parameter: 'max_tokens', absent: true, injected: false, preserved: { max_completion_tokens: 64 } },
  },
  {
    name: '火山 Responses 未传上限',
    platform: '火山引擎',
    model: process.env.GATEWAY_E2E_VOLCES_128K_MODEL || 'volces-openai:doubao-seed-2-0-pro-260215',
    path: '/v1/responses',
    body: responsesBody(),
    expected: { parameter: 'max_output_tokens', value: 43690, injected: true },
  },
  {
    name: '阿里百炼 Qwen Chat 未传上限',
    platform: '阿里云百炼',
    model: process.env.GATEWAY_E2E_QWEN_MODEL || 'aliyun-bailian-openai:qwen3.7-plus',
    path: '/v1/chat/completions',
    body: chatBody(),
    expected: { parameter: 'max_tokens', absent: true, injected: false },
  },
  {
    name: '阿里百炼 Qwen Responses 未传上限',
    platform: '阿里云百炼',
    model: process.env.GATEWAY_E2E_QWEN_MODEL || 'aliyun-bailian-openai:qwen3.7-plus',
    path: '/v1/responses',
    body: responsesBody(),
    expected: { parameter: 'max_output_tokens', absent: true, injected: false },
  },
  {
    name: 'DeepSeek 官网 Chat 未传上限',
    platform: 'DeepSeek 官网',
    model: process.env.GATEWAY_E2E_DEEPSEEK_MODEL || 'deepseek-openai:deepseek-v4-pro',
    path: '/v1/chat/completions',
    body: chatBody(),
    expected: { parameter: 'max_tokens', absent: true, injected: false },
  },
  {
    name: 'DeepSeek 官网 Responses 回退未传上限',
    platform: 'DeepSeek 官网',
    model: process.env.GATEWAY_E2E_DEEPSEEK_MODEL || 'deepseek-openai:deepseek-v4-pro',
    path: '/v1/responses',
    body: responsesBody(),
    expected: { parameter: 'max_output_tokens', absent: true, injected: false },
  },
  {
    name: '智谱 GLM-5.2 Chat 未传上限',
    platform: '智谱AI',
    model: process.env.GATEWAY_E2E_ZHIPU_MODEL || 'zhipu-openai:glm-5.2',
    path: '/v1/chat/completions',
    body: chatBody(),
    expected: { parameter: 'max_tokens', absent: true, injected: false },
  },
  {
    name: 'OpenAI GPT-4o Chat 未传上限',
    platform: 'OpenAI',
    model: process.env.GATEWAY_E2E_OPENAI_MODEL || 'openai:gpt-4o',
    path: '/v1/chat/completions',
    body: chatBody(),
    expected: { parameter: 'max_tokens', absent: true, injected: false },
  },
  {
    name: 'Google Gemini Chat 未传上限',
    platform: 'Google Gemini',
    model: process.env.GATEWAY_E2E_GEMINI_MODEL || 'gemini:gemini-2.5-pro',
    path: '/v1/chat/completions',
    body: chatBody(),
    expected: { parameter: 'max_tokens', absent: true, injected: false },
  },
];

function chatBody(extra = {}) {
  return {
    messages: [{ role: 'user', content: '这是输出上限链路验证。只回复 OK。' }],
    ...extra,
  };
}

function responsesBody(extra = {}) {
  return { input: '这是输出上限链路验证。只回复 OK。', ...extra };
}

function resolveGatewayAPIKey() {
  if (process.env.GATEWAY_E2E_API_KEY) {
    return { value: process.env.GATEWAY_E2E_API_KEY, source: 'environment', keyId: null };
  }
  const container = process.env.GATEWAY_E2E_DB_CONTAINER || 'postgres';
  const database = process.env.GATEWAY_E2E_DB_NAME || 'easyai_ai_gateway';
  const user = process.env.GATEWAY_E2E_DB_USER || 'easyai';
  const query = `SELECT key.id::text || E'\\t' || key.key_secret FROM gateway_api_keys key JOIN gateway_wallet_accounts wallet ON wallet.gateway_user_id = key.gateway_user_id AND wallet.status = 'active' WHERE key.deleted_at IS NULL AND key.status = 'active' AND key.key_secret IS NOT NULL AND key.key_secret <> '' AND key.scopes ? 'chat' AND (key.expires_at IS NULL OR key.expires_at > now()) AND (wallet.balance - wallet.frozen_balance) > 0 ORDER BY (wallet.balance - wallet.frozen_balance) DESC, key.created_at DESC LIMIT 1`;
  let output = '';
  try {
    output = execFileSync('docker', ['exec', container, 'psql', '-U', user, '-d', database, '-At', '-c', query], { encoding: 'utf8' }).trim();
  } catch {
    throw new Error('Unable to load an active chat-scoped Gateway API Key; set GATEWAY_E2E_API_KEY explicitly');
  }
  const separator = output.indexOf('\t');
  if (separator <= 0 || separator === output.length - 1) {
    throw new Error('No active chat-scoped Gateway API Key with a positive balance was found');
  }
  return { keyId: output.slice(0, separator), value: output.slice(separator + 1), source: 'database' };
}

async function fetchText(path, options = {}) {
  const response = await fetch(`${baseURL}${path}`, {
    ...options,
    signal: AbortSignal.timeout(timeoutMs),
    headers: { Authorization: `Bearer ${authentication.value}`, ...(options.headers || {}) },
  });
  const text = await response.text();
  let body = null;
  try {
    body = text ? JSON.parse(text) : null;
  } catch {
    body = { raw: text.slice(0, 2_000) };
  }
  return { response, body };
}

async function getTask(taskId) {
  const { response, body } = await fetchText(`/api/v1/tasks/${taskId}`);
  if (!response.ok) throw new Error(`GET task ${taskId} failed ${response.status}: ${JSON.stringify(body)}`);
  return body;
}

async function getPreprocessing(taskId) {
  const { response, body } = await fetchText(`/api/v1/tasks/${taskId}/param-preprocessing`);
  if (!response.ok) throw new Error(`GET preprocessing ${taskId} failed ${response.status}: ${JSON.stringify(body)}`);
  return body?.items || [];
}

function successfulAttempt(task) {
  const attempts = Array.isArray(task.attempts) ? task.attempts : [];
  return attempts.findLast((attempt) => attempt.status === 'succeeded') || attempts.at(-1);
}

function hasOwn(value, key) {
  return value != null && Object.prototype.hasOwnProperty.call(value, key);
}

function validateAudit(scenario, task, preprocessing) {
  const attempt = successfulAttempt(task);
  if (!attempt) throw new Error('task does not contain an upstream attempt');
  if (task.simulated || attempt.simulated) throw new Error('request was served by a simulation candidate');
  const platformName = String(attempt.metrics?.platformName || task.metrics?.platformName || '');
  if (!platformName.includes(scenario.platform)) {
    throw new Error(`unexpected platform ${platformName}; expected ${scenario.platform}`);
  }
  const snapshot = attempt.requestSnapshot || {};
  const expected = scenario.expected;
  if (expected.absent) {
    if (hasOwn(snapshot, expected.parameter)) {
      throw new Error(`${expected.parameter} was unexpectedly sent upstream as ${JSON.stringify(snapshot[expected.parameter])}`);
    }
  } else if (snapshot[expected.parameter] !== expected.value) {
    throw new Error(`${expected.parameter}=${JSON.stringify(snapshot[expected.parameter])}; expected ${expected.value}`);
  }
  for (const [key, value] of Object.entries(expected.preserved || {})) {
    if (snapshot[key] !== value) throw new Error(`${key} was not preserved in the upstream snapshot`);
  }
  const changes = preprocessing.flatMap((item) => item.changes || []);
  const outputLimitChanges = changes.filter((change) => change.processor === 'OutputTokenLimitProcessor');
  if (expected.injected) {
    const matched = outputLimitChanges.some((change) => change.action === 'set' && change.path === expected.parameter && change.after === expected.value);
    if (!matched) throw new Error(`missing OutputTokenLimitProcessor audit for ${expected.parameter}=${expected.value}`);
  } else if (outputLimitChanges.length > 0) {
    throw new Error(`unexpected OutputTokenLimitProcessor audit: ${JSON.stringify(outputLimitChanges)}`);
  }
  return {
    taskId: task.id,
    attemptId: attempt.id,
    platform: platformName,
    provider: attempt.metrics?.provider || task.metrics?.provider || null,
    platformModelId: attempt.metrics?.platformModelId || task.metrics?.platformModelId || null,
    upstreamProtocol: attempt.metrics?.upstreamProtocol || task.metrics?.upstreamProtocol || null,
    upstreamEndpoint: attempt.metrics?.upstreamEndpoint || task.metrics?.upstreamEndpoint || null,
    simulated: Boolean(task.simulated || attempt.simulated),
    requestParameters: {
      max_tokens: hasOwn(snapshot, 'max_tokens') ? snapshot.max_tokens : '(absent)',
      max_completion_tokens: hasOwn(snapshot, 'max_completion_tokens') ? snapshot.max_completion_tokens : '(absent)',
      max_output_tokens: hasOwn(snapshot, 'max_output_tokens') ? snapshot.max_output_tokens : '(absent)',
    },
    outputLimitAudit: outputLimitChanges,
    usage: task.usage || attempt.usage || null,
  };
}

const results = [];
for (const scenario of scenarios) {
  const startedAt = Date.now();
  let taskId = null;
  try {
    const requestBody = { model: scenario.model, ...scenario.body };
    const { response, body } = await fetchText(scenario.path, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(requestBody),
    });
    taskId = response.headers.get('x-gateway-task-id');
    if (!response.ok) throw new Error(`POST ${scenario.path} failed ${response.status}: ${JSON.stringify(body)}`);
    if (!taskId) throw new Error('response is missing X-Gateway-Task-Id');
    const [task, preprocessing] = await Promise.all([getTask(taskId), getPreprocessing(taskId)]);
    const audit = validateAudit(scenario, task, preprocessing);
    results.push({ name: scenario.name, model: scenario.model, path: scenario.path, passed: true, durationMs: Date.now() - startedAt, audit });
    process.stderr.write(`PASS ${scenario.name} (${Date.now() - startedAt} ms)\n`);
  } catch (error) {
    results.push({ name: scenario.name, model: scenario.model, path: scenario.path, passed: false, durationMs: Date.now() - startedAt, taskId, error: error instanceof Error ? error.message : String(error) });
    process.stderr.write(`FAIL ${scenario.name}: ${error instanceof Error ? error.message : String(error)}\n`);
  }
}

const report = {
  ok: results.every((result) => result.passed),
  generatedAt: new Date().toISOString(),
  baseURL,
  authentication: { type: 'gateway_api_key', source: authentication.source, keyId: authentication.keyId },
  summary: { total: results.length, passed: results.filter((result) => result.passed).length, failed: results.filter((result) => !result.passed).length },
  results,
};
const outputPath = process.env.GATEWAY_E2E_OUTPUT;
if (outputPath) {
  await mkdir(dirname(outputPath), { recursive: true });
  await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
}
console.log(JSON.stringify(report, null, 2));
if (!report.ok) process.exitCode = 1;
