#!/usr/bin/env node

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

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

const modelCases = [
  {
    model: process.env.GATEWAY_E2E_DOUBAO_MODEL || 'Doubao Seed 2.0 Pro',
    platform: '火山',
    protocol: 'openai_responses',
    converted: false,
  },
  {
    model: process.env.GATEWAY_E2E_QWEN_MODEL || 'Qwen3.7-Plus',
    platform: '百炼',
    protocol: 'openai_responses',
    converted: false,
  },
  {
    model: process.env.GATEWAY_E2E_DEEPSEEK_MODEL || 'deepseek-openai:deepseek-v4-pro',
    platform: 'DeepSeek 官网',
    protocol: 'openai_chat_completions',
    converted: true,
  },
];

function assert(condition, message) {
  if (!condition) throw new Error(message);
}

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 from the database; set GATEWAY_E2E_API_KEY explicitly');
  }
  const separator = output.indexOf('\t');
  if (separator <= 0 || separator === output.length - 1) {
    throw new Error('The database does not contain an active chat-scoped Gateway API Key with a stored secret');
  }
  return { keyId: output.slice(0, separator), value: output.slice(separator + 1), source: 'database' };
}

async function requestJSON(token, path, body) {
  const response = await fetch(`${baseURL}${path}`, {
    method: 'POST',
    headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  });
  const text = await response.text();
  if (!response.ok) throw new Error(`POST ${path} failed ${response.status}: ${text}`);
  return { body: JSON.parse(text), taskId: response.headers.get('x-gateway-task-id') };
}

async function requestStream(token, path, body) {
  const response = await fetch(`${baseURL}${path}`, {
    method: 'POST',
    headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
    body: JSON.stringify({ ...body, stream: true }),
  });
  const text = await response.text();
  if (!response.ok) throw new Error(`POST ${path} failed ${response.status}: ${text}`);
  const events = parseSSE(text);
  const errorEvent = events.find((event) => event.event === 'error');
  if (errorEvent) throw new Error(`Responses stream failed: ${JSON.stringify(errorEvent.data)}`);
  const completed = events.findLast((event) => event.event === 'response.completed');
  assert(completed?.data?.response, `stream is missing response.completed: ${text}`);
  validateEventOrder(events);
  return { events, body: completed.data.response, taskId: response.headers.get('x-gateway-task-id') };
}

function parseSSE(text) {
  return text.split(/\r?\n\r?\n/).flatMap((block) => {
    if (!block.trim()) return [];
    let event = 'message';
    const data = [];
    for (const line of block.split(/\r?\n/)) {
      if (line.startsWith('event:')) event = line.slice(6).trim();
      if (line.startsWith('data:')) data.push(line.slice(5).trim());
    }
    if (!data.length) return [];
    return [{ event, data: JSON.parse(data.join('\n')) }];
  });
}

function validateEventOrder(events) {
  const names = events.map((event) => event.event);
  const created = names.indexOf('response.created');
  const inProgress = names.indexOf('response.in_progress');
  const completed = names.lastIndexOf('response.completed');
  assert(created >= 0, `missing response.created: ${names.join(', ')}`);
  assert(inProgress > created, `response.in_progress is out of order: ${names.join(', ')}`);
  assert(completed > inProgress, `response.completed is out of order: ${names.join(', ')}`);
  assert(completed === names.length - 1, `response.completed must be the final event: ${names.join(', ')}`);
  assert(!names.includes('[DONE]'), 'Responses stream must not contain a Chat [DONE] event');
}

function responseText(response) {
  if (typeof response.output_text === 'string') return response.output_text;
  return (response.output || [])
    .flatMap((item) => item.type === 'message' ? item.content || [] : [])
    .filter((item) => item.type === 'output_text')
    .map((item) => item.text || '')
    .join('');
}

function functionCall(response) {
  return (response.output || []).find((item) => item.type === 'function_call');
}

async function getTask(token, taskId) {
  assert(taskId, 'response is missing X-Gateway-Task-Id');
  const response = await fetch(`${baseURL}/api/v1/tasks/${taskId}`, {
    headers: { Authorization: `Bearer ${token}` },
  });
  const text = await response.text();
  if (!response.ok) throw new Error(`GET task failed ${response.status}: ${text}`);
  return JSON.parse(text);
}

function auditSummary(task, expected) {
  const metrics = task.metrics || {};
  assert(String(metrics.platformName || '').includes(expected.platform), `${expected.model} used unexpected platform: ${metrics.platformName}`);
  assert(metrics.upstreamProtocol === expected.protocol, `${expected.model} used ${metrics.upstreamProtocol}, expected ${expected.protocol}`);
  assert(Boolean(metrics.responseConverted) === expected.converted, `${expected.model} responseConverted mismatch`);
  assert(metrics.upstreamEndpoint === (expected.converted ? '/chat/completions' : '/responses'), `${expected.model} upstream endpoint mismatch`);
  if (expected.converted) {
    assert(metrics.publicResponseId === `resp_${String(task.id).replaceAll('-', '')}`, `${expected.model} Chat fallback id is not derived from the task id`);
  } else {
    assert(metrics.publicResponseId === metrics.upstreamResponseId, `${expected.model} native Responses id was not preserved`);
  }
  return {
    taskId: task.id,
    publicResponseId: metrics.publicResponseId,
    upstreamResponseId: metrics.upstreamResponseId,
    platform: metrics.platformName,
    platformModelId: metrics.platformModelId,
    protocol: metrics.upstreamProtocol,
    upstreamEndpoint: metrics.upstreamEndpoint,
    responseConverted: metrics.responseConverted,
    parentResponseId: metrics.parentResponseId || null,
    chainDepth: metrics.responseChainDepth,
    usage: task.usage,
    finalChargeAmount: task.finalChargeAmount,
    billingSummary: task.billingSummary,
    billings: task.billings,
  };
}

const token = authentication.value;
const results = [];

for (const expected of modelCases) {
  const nonce = `E2E-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 9)}`;
  const first = await requestJSON(token, '/v1/responses', {
    model: expected.model,
    input: `请记住校验码 ${nonce}。只回复“已记住”。`,
    store: true,
  });
  assert(/^resp_[A-Za-z0-9_-]+$/.test(first.body.id), `${expected.model} returned invalid response id`);
  const second = await requestJSON(token, '/v1/responses', {
    model: expected.model,
    input: '我刚才让你记住的校验码是什么？只回复校验码。',
    previous_response_id: first.body.id,
    store: true,
  });
  assert(second.body.previous_response_id === first.body.id, `${expected.model} response did not preserve previous_response_id`);
  assert(responseText(second.body).includes(nonce), `${expected.model} did not preserve ordinary conversation continuity`);

  const toolOutput = `TOOL-${nonce}`;
  const normalizedModel = expected.model.toLowerCase();
  const requiresNonThinkingToolMode = normalizedModel.startsWith('qwen3.7-') || normalizedModel.includes('deepseek-v4-pro');
	const toolReasoning = requiresNonThinkingToolMode ? { reasoning: { effort: 'none' } } : {};
	const forcedToolChoice = 'required';
  const toolFirst = await requestStream(token, '/v1/responses', {
    model: expected.model,
    input: '调用 lookup_verification_code 获取校验结果。',
    tools: [{
      type: 'function',
      name: 'lookup_verification_code',
      description: '返回测试校验结果',
      parameters: { type: 'object', properties: { scope: { type: 'string' } }, required: ['scope'], additionalProperties: false },
      strict: true,
    }],
	tool_choice: forcedToolChoice,
    parallel_tool_calls: true,
	...toolReasoning,
    store: true,
  });
  const call = functionCall(toolFirst.body);
  assert(call?.call_id, `${expected.model} tool response is missing call_id`);
  assert(toolFirst.events.some((event) => event.event === 'response.function_call_arguments.delta'), `${expected.model} stream is missing function arguments delta`);

  const toolSecond = await requestStream(token, '/v1/responses', {
    model: expected.model,
    previous_response_id: toolFirst.body.id,
    input: [{ type: 'function_call_output', call_id: call.call_id, output: JSON.stringify({ verification: toolOutput }) }],
	...toolReasoning,
    store: true,
  });
  assert(responseText(toolSecond.body).includes(toolOutput), `${expected.model} did not consume function_call_output`);

  const auditedTasks = await Promise.all([
    getTask(token, first.taskId),
    getTask(token, second.taskId),
    getTask(token, toolFirst.taskId),
    getTask(token, toolSecond.taskId),
  ]);
  const ordinaryFirstAudit = auditSummary(auditedTasks[0], expected);
  const ordinarySecondAudit = auditSummary(auditedTasks[1], expected);
  const toolFirstAudit = auditSummary(auditedTasks[2], expected);
  const toolSecondAudit = auditSummary(auditedTasks[3], expected);
  assert(ordinarySecondAudit.parentResponseId === first.body.id, `${expected.model} ordinary parent response id mismatch`);
  assert(ordinarySecondAudit.chainDepth === 1, `${expected.model} ordinary chain depth mismatch`);
  assert(toolSecondAudit.parentResponseId === toolFirst.body.id, `${expected.model} tool parent response id mismatch`);
  assert(toolSecondAudit.chainDepth === 1, `${expected.model} tool chain depth mismatch`);
  results.push({
    model: expected.model,
    ordinaryConversation: {
      passed: true,
      first: ordinaryFirstAudit,
      second: ordinarySecondAudit,
    },
    toolCalling: {
      passed: true,
      callId: call.call_id,
      first: toolFirstAudit,
      second: toolSecondAudit,
	  firstEventTypes: toolFirst.events.map((event) => event.event),
	  secondEventTypes: toolSecond.events.map((event) => event.event),
    },
  });
}

const report = {
  ok: true,
  generatedAt: new Date().toISOString(),
  baseURL,
  authentication: { type: 'gateway_api_key', source: authentication.source, keyId: authentication.keyId },
  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));
