// amerc platform test suite. Spins up the API on a temp DB and exercises every surface.
// Run: node --experimental-sqlite server/platform-tests.mjs
import { spawn } from 'node:child_process';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';

const PORT = 5310, BASE = `http://127.0.0.1:${PORT}/api`;
const dir = mkdtempSync(join(tmpdir(), 'amerc-test-'));
const srv = spawn(process.execPath, ['--experimental-sqlite', new URL('./amerc-api.mjs', import.meta.url).pathname], {
  env: { ...process.env, PORT: String(PORT), AMERC_DB: join(dir, 't.db'), AMERC_FILES: join(dir, 'disk'), AMERC_SECRET: 'test-secret' },
  stdio: 'ignore',
});
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

let pass = 0, fail = 0; const fails = [];
function ok(cond, name) { if (cond) { pass++; } else { fail++; fails.push(name); console.log('  ✗ ' + name); } }
function eq(a, b, name) { ok(JSON.stringify(a) === JSON.stringify(b), `${name} (got ${JSON.stringify(a)}, want ${JSON.stringify(b)})`); }

let cookies = {};
function setCookie(res) { const sc = res.headers.get('set-cookie'); if (sc) { const [kv] = sc.split(';'); const [k, v] = kv.split('='); cookies[k] = v; } }
const cookieHdr = () => Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join('; ');
async function req(path, { method = 'GET', body, cookie = true, bearer, raw } = {}) {
  const headers = {};
  if (body) headers['Content-Type'] = 'application/json';
  if (cookie && cookieHdr()) headers.Cookie = cookieHdr();
  if (bearer) headers.Authorization = `Bearer ${bearer}`;
  const res = await fetch(BASE + path, { method, headers, body: body ? JSON.stringify(body) : undefined });
  setCookie(res);
  if (raw) return res;
  let j = {}; try { j = await res.json(); } catch {}
  return { status: res.status, ...j };
}

async function main() {
  // wait for server
  for (let i = 0; i < 30; i++) { try { const r = await fetch(BASE + '/health'); if (r.ok) break; } catch {} await sleep(150); }

  console.log('\n== auth + SSO ==');
  let r = await req('/health'); ok(r.ok && r.version === 2, 'health v2');
  r = await req('/auth/signup', { method: 'POST', body: { email: 'Admin@x.io', password: 'secret123', handle: 'admin' } });
  ok(r.ok && r.user.role === 'admin', 'first signup -> admin'); eq(r.user.email, 'admin@x.io', 'email lowercased');
  r = await req('/auth/me'); ok(r.user && r.user.handle === 'admin', 'me via cookie');
  r = await req('/auth/verify', { raw: true }); ok(r.status === 200 && r.headers.get('x-amerc-user') === 'admin', 'SSO verify header');
  r = await req('/auth/signup', { method: 'POST', body: { email: 'm@x.io', password: 'short' } }); ok(r.status === 400, 'reject short password');
  const adminCookies = { ...cookies };
  // member
  cookies = {}; r = await req('/auth/signup', { method: 'POST', body: { email: 'mercA@x.io', password: 'hunter22', handle: 'mercA' } });
  ok(r.user.role === 'member', 'second signup -> member');
  r = await req('/admin/users'); ok(r.status === 403, 'member blocked from admin');
  r = await req('/auth/password', { method: 'POST', body: { currentPassword: 'WRONG', newPassword: 'newpass1' } }); ok(r.status === 401, 'pw change wrong current');
  r = await req('/auth/password', { method: 'POST', body: { currentPassword: 'hunter22', newPassword: 'newpass1' } }); ok(r.ok, 'pw change ok');
  r = await req('/auth/login', { method: 'POST', body: { email: 'mercA@x.io', password: 'newpass1' } }); ok(r.ok, 'login with new pw');
  const memberCookies = { ...cookies };

  console.log('== admin + agent keys ==');
  cookies = adminCookies;
  r = await req('/admin/users'); ok(r.ok && r.users.length === 2, 'admin lists users');
  r = await req('/admin/keys', { method: 'POST', body: { name: 'fleet' } }); const agentKey = r.key; ok(r.ok && agentKey?.startsWith('amk_'), 'mint agent key');
  cookies = {}; r = await req('/auth/me', { bearer: agentKey }); ok(r.agent === 'fleet', 'agent identity via Bearer');

  console.log('== docs / netdisk / boards (agent + user) ==');
  cookies = {}; r = await req('/docs', { method: 'POST', bearer: agentKey, body: { title: 'Agent Doc', body: '# hi' } }); const docId = r.id; ok(r.ok, 'agent writes doc');
  cookies = adminCookies; r = await req(`/docs/${docId}`); ok(r.doc.updated_by === 'agent:fleet', 'doc authored by agent');
  r = await req('/files', { method: 'POST', body: { name: 'n.md', kind: 'text', text: 'hello', mime: 'text/markdown' } }); const fileId = r.id; ok(r.ok, 'create text file');
  r = await req(`/files/${fileId}/raw`, { raw: true }); ok((await r.text()) === 'hello', 'download text file');
  r = await req('/boards', { method: 'POST', body: { name: 'B', data: '{}', links: [{ fileId }] } }); ok(r.ok, 'create board');
  cookies = {}; r = await req('/docs'); ok(r.status === 401, 'docs need auth');

  console.log('== agent platform: classes ==');
  cookies = adminCookies;
  r = await req('/agents/classes', { method: 'POST', body: { name: 'Backend Coder', tags: ['backend', 'model'], description: 'api', visibility: 'public' } });
  const classId = r.id; ok(r.ok && r.slug === 'backend-coder', 'publish class + slug');
  await req('/agents/classes', { method: 'POST', body: { name: 'Frontend Pixie', tags: ['frontend'] } });
  cookies = memberCookies; await req('/agents/classes', { method: 'POST', body: { name: 'Secret Bot', tags: ['x'], visibility: 'private' } });
  cookies = {}; r = await req('/agents/classes'); ok(r.classes.length === 2, 'browse: only public classes anon');
  r = await req('/agents/classes?tag=backend'); eq(r.classes.map((c) => c.name), ['Backend Coder'], 'filter by tag');
  cookies = memberCookies; r = await req('/agents/classes?mine=1'); ok(r.classes.length === 1 && r.classes[0].name === 'Secret Bot', 'mine shows own private');
  cookies = adminCookies; r = await req('/agents/classes'); ok(r.classes.length === 3, 'admin sees private too');
  r = await req(`/agents/classes/${classId}/subscribe`, { method: 'POST' }); ok(r.token?.startsWith('sub_'), 'subscription token');

  console.log('== agent platform: instances + lifecycle ==');
  cookies = adminCookies;
  r = await req('/agents/instances', { method: 'POST', body: { classId, visibility: 'public' } });
  const instId = r.id, accesskey = r.accesskey; ok(r.ok && accesskey?.startsWith('ik_'), 'create instance + accesskey');
  ok(r.relayWs.includes(accesskey), 'instance returns relay WS');
  r = await req(`/agents/instances/${instId}`); eq(r.instance.status, 'pending', 'instance starts pending');
  r = await req(`/agents/instances/${instId}/heartbeat`, { method: 'POST', cookie: false, body: { accesskey, broker: 'kebab', tui: 'booting' } }); eq(r.status, 'online', 'heartbeat -> online');
  r = await req(`/agents/instances/${instId}`); eq(r.instance.status, 'online', 'instance online');
  r = await req(`/agents/instances/${instId}/heartbeat`, { method: 'POST', cookie: false, body: { accesskey: 'WRONG' } }); ok(r.status === 401, 'bad accesskey rejected');
  // stats reflect it
  r = await req('/agents/stats'); ok(r.classes === 2 && r.instances === 1 && r.online === 1, 'stats classes/instances/online');
  // session
  r = await req(`/agents/instances/${instId}/sessions`, { method: 'POST', body: { connector: 'chatbox' } });
  ok(r.ok && r.relayToken === accesskey && r.chatboxJs.includes(accesskey), 'start session -> relay token + chatbox js');
  r = await req('/agents/stats'); ok(r.sessions === 1, 'stats sessions');
  // private instance needs accesskey for a non-owner
  cookies = adminCookies; r = await req('/agents/instances', { method: 'POST', body: { classId, visibility: 'private' } });
  const privId = r.id, privKey = r.accesskey;
  cookies = memberCookies; r = await req(`/agents/instances/${privId}`); ok(r.status === 403, 'private instance hidden from non-owner');
  r = await req(`/agents/instances/${privId}?accesskey=${privKey}`); ok(r.ok, 'private instance visible with accesskey');
  // shutdown
  cookies = adminCookies; r = await req(`/agents/instances/${instId}`, { method: 'DELETE', body: { accesskey } }); ok(r.ok, 'shutdown instance');
  r = await req(`/agents/instances/${instId}`); eq(r.instance.status, 'down', 'instance down after shutdown');

  console.log('== webagent broker login ==');
  cookies = {};
  r = await req('/webagent/login', { method: 'POST', body: { username: 'admin@x.io', password: 'secret123' } }); ok(r.ok && r.token?.startsWith('wa_'), 'webagent login (amerc user)');
  r = await req('/webagent/login', { method: 'POST', body: { username: 'admin@x.io', password: 'nope' } }); ok(r.status === 401, 'webagent login bad pw');

  console.log(`\n${fail ? '❌' : '✅'} ${pass} passed, ${fail} failed`);
  if (fail) console.log('FAILED: ' + fails.join(' | '));
  srv.kill(); rmSync(dir, { recursive: true, force: true });
  process.exit(fail ? 1 : 0);
}
main().catch((e) => { console.error(e); srv.kill(); process.exit(1); });
