// @vitest-environment node

/**
 * End-to-end coverage for an AMR (vela) chat run driven through the real
 * tools-dev orchestrated daemon. Boots a namespaced daemon + web pair,
 * configures it to spawn a self-contained fake `vela` binary, pre-seeds
 * `~/.amr/config.json` as if the user had already approved CLI login,
 * then drives a complete /api/runs lifecycle for `agentId: 'amr'` and
 * asserts the assistant message picks up the fake's canned text.
 *
 * What this proves that lower-tier tests don't:
 *
 *   1. The chat-run path in `apps/daemon/src/server.ts` correctly routes
 *      `agentId: 'amr'` through `attachAcpSession` (not the legacy
 *      json-event-stream parser the old `incongruous-megaraptor` branch
 *      used).
 *   2. AMR preflight refreshes `vela models` and substitutes the synthetic
 *      `'default'` model id with the first live model (`glm-5`), so vela
 *      receives a real `session/set_model` before `session/prompt` — a
 *      regression here would manifest as
 *      `session/set_model must be called before session/prompt` on the
 *      real `vela` binary, but the fake here enforces the same gate
 *      so it surfaces locally without a vela install.
 *   3. The full ACP transport (`initialize` → `session/new` →
 *      `session/set_model` → `session/prompt` → `session/update*`) flows
 *      between the daemon and a spawned subprocess that respects vela's
 *      `~/.amr/config.json` resolution path.
 */

import { mkdir, writeFile, chmod } from 'node:fs/promises';
import { randomUUID } from 'node:crypto';
import { join } from 'node:path';

import { describe, expect, test } from 'vitest';

import { requestJson } from '@/vitest/http';
import { listMessages } from '@/vitest/messages';
import { startRun, waitForRunStatus } from '@/vitest/runs';
import { createSmokeSuite } from '@/vitest/smoke-suite';

type ProjectResponse = {
  conversationId: string;
  project: { id: string; metadata?: { kind?: string }; name: string };
};

// Inline fake `vela` binary. Handles the two argv shapes Open Design's
// daemon ever spawns:
//
//   `vela models`                       — legacy catalog probe compatibility.
//   `vela model preset --format json`   — print the fast preset catalog.
//   `vela model list --format json`     — print the live link model catalog.
//   `vela login`                        — write ~/.amr/config.json and exit 0.
//   `vela agent run --runtime opencode` — ACP stdio runtime (initialize →
//                                          session/new → session/set_model →
//                                          session/prompt → session/update*).
//
// Kept inline (not imported from apps/daemon/tests/fixtures/fake-vela.mjs)
// because cross-app private fixtures must not be reused — see
// e2e/AGENTS.md "tests must not borrow another app's private source".
const FAKE_VELA_SCRIPT = `#!/usr/bin/env node
import { mkdirSync, writeFileSync } from 'node:fs';
import { homedir } from 'node:os';
import { dirname, join } from 'node:path';
import { argv, stdin, stdout, env, exit } from 'node:process';

const ASSISTANT_TEXT = env.FAKE_VELA_TEXT || 'Hello from the e2e fake vela.';
const SESSION_ID = 'fake-amr-session-1';
const LIVE_MODEL_ID = 'glm-5';
const PRESET_MODELS_JSON = JSON.stringify({ source: 'preset', data: [{ id: LIVE_MODEL_ID }] });
const REMOTE_MODELS_JSON = JSON.stringify({ source: 'remote', data: [{ id: LIVE_MODEL_ID }] });

function writeMessage(obj) {
  stdout.write(JSON.stringify(obj) + '\\n');
}
function writeResult(id, result) {
  writeMessage({ jsonrpc: '2.0', id, result });
}
function writeNotification(method, params) {
  writeMessage({ jsonrpc: '2.0', method, params });
}

if (argv[2] === 'login') {
  const file = join(homedir(), '.amr', 'config.json');
  mkdirSync(dirname(file), { recursive: true });
  const profile = (env.VELA_PROFILE || 'local').trim() || 'local';
  writeFileSync(file, JSON.stringify({
    profiles: {
      [profile]: {
        runtimeKey: 'fake-runtime-key-0000000000000000000000',
        controlKey: 'fake-control-key-0000000000000000000000',
        apiUrl: 'http://localhost:18080',
        linkUrl: 'http://localhost:18081',
        user: { id: 'fake-user-id', email: 'e2e@example.com', plan: 'free' },
      },
    },
  }, null, 2), 'utf8');
  exit(0);
}

if (argv[2] === 'models') {
  stdout.write('public_model_glm_5    vela\\n');
  exit(0);
}

if (argv[2] === 'model' && argv[3] === 'preset' && argv[4] === '--format' && argv[5] === 'json') {
  stdout.write(PRESET_MODELS_JSON + '\\n');
  exit(0);
}

if (argv[2] === 'model' && argv[3] === 'list' && argv[4] === '--format' && argv[5] === 'json') {
  stdout.write(REMOTE_MODELS_JSON + '\\n');
  exit(0);
}

const sessionsWithModel = new Set();
let buffer = '';
stdin.setEncoding('utf8');
stdin.on('data', (chunk) => {
  buffer += chunk;
  const lines = buffer.split('\\n');
  buffer = lines.pop() || '';
  for (const raw of lines) {
    const line = raw.trim();
    if (!line) continue;
    let msg;
    try { msg = JSON.parse(line); } catch { continue; }
    handle(msg);
  }
});
stdin.on('end', () => { stdout.end(); exit(0); });

function handle(msg) {
  const { id, method, params } = msg;
  if (method === 'initialize') {
    writeResult(id, {
      protocolVersion: 1,
      agentCapabilities: { promptCapabilities: { text: true } },
      models: {
        currentModelId: LIVE_MODEL_ID,
        availableModels: [{ modelId: LIVE_MODEL_ID, name: LIVE_MODEL_ID }],
      },
    });
    return;
  }
  if (method === 'session/new') {
    writeResult(id, {
      sessionId: SESSION_ID,
      models: {
        currentModelId: LIVE_MODEL_ID,
        availableModels: [{ modelId: LIVE_MODEL_ID, name: LIVE_MODEL_ID }],
      },
    });
    return;
  }
  if (method === 'session/set_model' || method === 'session/set_config_option') {
    const sid = (params && params.sessionId) || SESSION_ID;
    sessionsWithModel.add(sid);
    writeResult(id, {});
    return;
  }
  if (method === 'session/prompt') {
    const sid = (params && params.sessionId) || SESSION_ID;
    if (!sessionsWithModel.has(sid)) {
      writeMessage({
        jsonrpc: '2.0',
        id,
        error: { code: -32602, message: 'session/set_model must be called before session/prompt' },
      });
      return;
    }
    writeNotification('session/update', {
      sessionId: sid,
      update: { sessionUpdate: 'agent_message_chunk', content: { type: 'text', text: ASSISTANT_TEXT } },
    });
    writeResult(id, {
      stopReason: 'end_turn',
      usage: { inputTokens: 7, outputTokens: 5, totalTokens: 12 },
    });
    return;
  }
  if (typeof id !== 'undefined') {
    writeMessage({ jsonrpc: '2.0', id, error: { code: -32601, message: 'unknown method ' + method } });
  }
}
`;

async function writeFakeVelaBin(root: string): Promise<string> {
  await mkdir(root, { recursive: true });
  const bin = join(root, 'vela');
  await writeFile(bin, FAKE_VELA_SCRIPT, 'utf8');
  await chmod(bin, 0o755);
  return bin;
}

const PROMPT = 'Reply: HELLO';
const ASSISTANT_TEXT = 'AMR-E2E-OK';

describe('AMR chat-run end-to-end', () => {
  test('drives /api/runs against vela ACP and the assistant message captures the fake stream', async () => {
    // tools-dev daemon boot + chat run lifecycle needs the same headroom
    // as the dialog/* smoke specs (~3 minutes for cold spawn + run).
    const suite = await createSmokeSuite('amr-turn');

    await suite.with.toolsDev(async ({ webUrl }) => {
      const velaBin = await writeFakeVelaBin(join(suite.scratchDir, 'fake-vela'));

      // Pre-seed `~/.amr/config.json` so `vela agent run` (the fake) does
      // not need to negotiate device-auth. Production AMR works the same
      // way: once login has happened once, the runtime reads the file.
      const velaConfigDir = join(suite.scratchDir, 'home', '.amr');
      await mkdir(velaConfigDir, { recursive: true });
      await writeFile(
        join(velaConfigDir, 'config.json'),
        JSON.stringify(
          {
            profiles: {
              local: {
                runtimeKey: 'fake-runtime-key',
                controlKey: 'fake-control-key',
                apiUrl: 'http://localhost:18080',
                linkUrl: 'http://localhost:18081',
                user: { id: 'fake-user-id', email: 'e2e@example.com', plan: 'free' },
              },
            },
          },
          null,
          2,
        ),
      );

      // Persist agentCliEnv so the daemon's runtime resolver picks up the
      // fake binary and the pre-run AMR status guard sees configured runtime
      // credentials without touching the developer's real ~/.amr config.
      await requestJson<{ config: Record<string, unknown> }>(webUrl, '/api/app-config', {
        body: {
          agentCliEnv: {
            amr: {
              VELA_BIN: velaBin,
              VELA_LINK_URL: 'http://localhost:18081',
              VELA_RUNTIME_KEY: 'fake-runtime-key',
            },
          },
          agentId: 'amr',
          agentModels: { amr: { model: 'default', reasoning: 'default' } },
          designSystemId: null,
          onboardingCompleted: true,
          skillId: null,
          telemetry: { artifactManifest: true, content: false, metrics: false },
        },
        method: 'PUT',
      });

      const project = await requestJson<ProjectResponse>(webUrl, '/api/projects', {
        body: {
          designSystemId: null,
          id: randomUUID(),
          metadata: { kind: 'prototype' },
          name: 'AMR turn e2e',
          pendingPrompt: null,
          skillId: null,
        },
      });
      const projectId = project.project.id;
      const conversationId = project.conversationId;

      const t0 = Date.now();
      const userMessageId = `user-${t0}`;
      const assistantMessageId = `assistant-${t0}`;

      const run = await startRun(webUrl, {
        agentId: 'amr',
        assistantMessageId,
        clientRequestId: `req-${t0}`,
        conversationId,
        designSystemId: null,
        message: PROMPT,
        // 'default' must be resolved through AMR's live `vela models`
        // preflight; if that helper regressed, the fake vela would reject
        // session/prompt with the `set_model must be called before
        // session/prompt` error encoded above.
        model: 'default',
        projectId,
        reasoning: 'default',
        skillId: null,
      });
      expect(run.runId).toMatch(/[a-z0-9-]/i);

      // Override the per-process FAKE_VELA_TEXT so the assertion below is
      // tied to a stable canned reply. The runtime spawn inherits the
      // daemon's process.env, so setting it after startRun would race with
      // spawn; instead we set it on `process.env` for the test process —
      // but tools-dev orchestrates a separate daemon child, so this only
      // takes effect when the fake script reads its own env. The fake
      // ships with a default 'Hello from the e2e fake vela.' literal, so
      // we assert on a substring instead of pinning the full text here.
      void ASSISTANT_TEXT;

      const finalStatus = await waitForRunStatus(webUrl, run.runId, 'succeeded', {
        timeoutMs: 30_000,
      });
      expect(finalStatus.status).toBe('succeeded');

      const messages = await listMessages(webUrl, projectId, conversationId);
      const assistantMessage = messages.find((m) => m.id === assistantMessageId);
      if (assistantMessage) {
        expect(assistantMessage.content).toContain('Hello from the e2e fake vela');
      } else {
        // Some chat flows save the assistant message under a daemon-assigned
        // id rather than the client-provided one. Fall back to checking any
        // assistant message captured the fake's text.
        const anyAssistant = messages.find(
          (m) => m.role === 'assistant' && m.content.includes('Hello from the e2e fake vela'),
        );
        expect(anyAssistant).toBeTruthy();
      }
    });
  }, 180_000);
});
