import test from 'node:test';
import assert from 'node:assert/strict';

import {
  AgentGatewayError,
  buildHermesEnvironment,
  createHermesAgentService,
  parseHermesCliOutput,
  resolveHermesAgentConfig,
  runHermesCliTurn,
} from '../src/hermes-agent-service.js';

const origin = 'http://127.0.0.1:4173';
const configured = () => resolveHermesAgentConfig({
  TIMMY_AGENT_ENABLED: 'true',
  TIMMY_AGENT_ACCESS_TOKEN: 'test-agent-access-code-2026',
  TIMMY_PUBLIC_ORIGIN: origin,
  TIMMY_AGENT_WORKDIR: '/tmp/timmy-agent-workspace',
  TIMMY_AGENT_TIMEOUT_MS: '45000',
});

async function rejectsStatus(fn, status) {
  await assert.rejects(fn, error => error instanceof AgentGatewayError && error.status === status);
}

test('Hermes agent is disabled by default and public status exposes no secrets', () => {
  const config = resolveHermesAgentConfig({});
  assert.equal(config.enabled, false);
  assert.equal(config.configured, false);
  assert.deepEqual(config.publicStatus(), { enabled: false, configured: false, authenticated: false, mode: 'local-fallback' });
  assert.doesNotMatch(JSON.stringify(config.publicStatus()), /token|workdir|command/i);
});

test('enabled agent requires a strong access token, exact origin, and absolute workdir', () => {
  for (const env of [
    { TIMMY_AGENT_ENABLED: 'true', TIMMY_PUBLIC_ORIGIN: origin, TIMMY_AGENT_WORKDIR: '/tmp/agent' },
    { TIMMY_AGENT_ENABLED: 'true', TIMMY_AGENT_ACCESS_TOKEN: 'short', TIMMY_PUBLIC_ORIGIN: origin, TIMMY_AGENT_WORKDIR: '/tmp/agent' },
    { TIMMY_AGENT_ENABLED: 'true', TIMMY_AGENT_ACCESS_TOKEN: 'test-agent-access-code-2026', TIMMY_AGENT_WORKDIR: '/tmp/agent' },
    { TIMMY_AGENT_ENABLED: 'true', TIMMY_AGENT_ACCESS_TOKEN: 'test-agent-access-code-2026', TIMMY_PUBLIC_ORIGIN: origin, TIMMY_AGENT_WORKDIR: 'relative' },
  ]) assert.equal(resolveHermesAgentConfig(env).configured, false);
  assert.equal(configured().configured, true);
});

test('unlock is exact-origin, rate bounded, and returns only an opaque browser credential', async () => {
  let serial = 0;
  const service = createHermesAgentService({ config: configured(), randomToken: () => `opaque-${++serial}` });
  await rejectsStatus(() => service.unlock({ origin: 'https://evil.example', accessCode: 'test-agent-access-code-2026' }), 403);
  await rejectsStatus(() => service.unlock({ origin, accessCode: 'wrong-access-code-value' }), 401);
  const result = await service.unlock({ origin, accessCode: 'test-agent-access-code-2026' });
  assert.equal(result.cookieToken, 'opaque-1');
  assert.deepEqual(result.public, { enabled: true, configured: true, authenticated: true, mode: 'hermes-agent' });
  assert.doesNotMatch(JSON.stringify(result), /test-agent-access|session_id|workdir/i);
});

test('unlock brute-force attempts are bounded before a correct code is accepted', async () => {
  const service = createHermesAgentService({ config: { ...configured(), maxUnlockAttemptsPerMinute: 3 }, randomToken: () => 'unused-cookie' });
  for (let attempt = 0; attempt < 3; attempt += 1) await rejectsStatus(() => service.unlock({ origin, accessCode: `wrong-access-code-${attempt}` }), 401);
  await rejectsStatus(() => service.unlock({ origin, accessCode: 'test-agent-access-code-2026' }), 429);
});

test('opaque browser credential generation never evicts an active conversation on collision', async () => {
  const tokens = ['collision-cookie', 'collision-cookie', 'fresh-cookie'];
  const service = createHermesAgentService({ config: configured(), randomToken: () => tokens.shift(), runTurn: async () => ({ reply: 'ok', sessionId: 'private-session' }) });
  const first = await service.unlock({ origin, accessCode: 'test-agent-access-code-2026' });
  await service.chat({ origin, cookieToken: first.cookieToken, payload: { message: 'first turn', ledger: [] } });
  const second = await service.unlock({ origin, accessCode: 'test-agent-access-code-2026' });
  assert.equal(first.cookieToken, 'collision-cookie');
  assert.equal(second.cookieToken, 'fresh-cookie');
  assert.equal(service.status(first.cookieToken).authenticated, true);
});

test('session capacity never evicts an active authenticated user', async () => {
  let clock = 0;
  const tokens = ['one-cookie', 'two-cookie', 'three-cookie'];
  const service = createHermesAgentService({ config: { ...configured(), maxSessions: 1 }, randomToken: () => tokens.shift(), now: () => clock });
  const first = await service.unlock({ origin, accessCode: 'test-agent-access-code-2026' });
  await rejectsStatus(() => service.unlock({ origin, accessCode: 'test-agent-access-code-2026' }), 429);
  assert.equal(service.status(first.cookieToken).authenticated, true);
  clock = 24 * 60 * 60 * 1000 + 1;
  const replacement = await service.unlock({ origin, accessCode: 'test-agent-access-code-2026' });
  assert.equal(replacement.cookieToken, 'two-cookie');
});

test('chat rejects unauthenticated, cross-origin, oversized, and browser-controlled agent policy', async () => {
  const service = createHermesAgentService({ config: configured(), randomToken: () => 'browser-cookie', runTurn: async () => ({ reply: 'ok', sessionId: 'private' }) });
  await service.unlock({ origin, accessCode: 'test-agent-access-code-2026' });
  await rejectsStatus(() => service.chat({ origin, cookieToken: '', payload: { message: 'hello', ledger: [] } }), 401);
  await rejectsStatus(() => service.chat({ origin: 'https://evil.example', cookieToken: 'browser-cookie', payload: { message: 'hello', ledger: [] } }), 403);
  await rejectsStatus(() => service.chat({ origin, cookieToken: 'browser-cookie', payload: { message: 'x'.repeat(4001), ledger: [] } }), 413);
  for (const forbidden of ['sessionId', 'toolsets', 'model', 'provider', 'systemPrompt']) {
    await rejectsStatus(() => service.chat({ origin, cookieToken: 'browser-cookie', payload: { message: 'hello', ledger: [], [forbidden]: 'attacker-controlled' } }), 400);
  }
});

test('chat keeps Hermes session IDs server-side, resumes continuity, and strips photos from ledger context', async () => {
  const calls = [];
  const service = createHermesAgentService({
    config: configured(),
    randomToken: () => 'browser-cookie',
    runTurn: async input => {
      calls.push(input);
      return calls.length === 1
        ? { reply: 'I see one confirmed log.', sessionId: 'hermes-private-session' }
        : { reply: 'We were discussing that confirmed log.', sessionId: 'hermes-private-session' };
    },
  });
  await service.unlock({ origin, accessCode: 'test-agent-access-code-2026' });
  const ledger = [{ id: 'one', occurredAt: '2026-08-20T12:00:00Z', bristolType: 4, color: 'brown', urgency: 1, discomfort: 0, note: 'normal', photoDataUrl: 'data:image/jpeg;base64,PRIVATE' }];
  const first = await service.chat({ origin, cookieToken: 'browser-cookie', payload: { message: 'What changed?', ledger } });
  const second = await service.chat({ origin, cookieToken: 'browser-cookie', payload: { message: 'What were we discussing?', ledger } });
  assert.equal(calls[0].hermesSessionId, null);
  assert.equal(calls[1].hermesSessionId, 'hermes-private-session');
  assert.match(calls[0].prompt, /observable patterns/i);
  assert.match(calls[0].prompt, /confirmed ledger context/i);
  assert.doesNotMatch(calls[0].prompt, /PRIVATE|photoDataUrl/);
  assert.deepEqual(first, { reply: 'I see one confirmed log.', connected: true });
  assert.deepEqual(second, { reply: 'We were discussing that confirmed log.', connected: true });
  assert.doesNotMatch(JSON.stringify([first, second]), /hermes-private-session/);
});

test('authoritative chat service intercepts urgent message and ledger symptoms before Hermes', async () => {
  const calls = [];
  const service = createHermesAgentService({
    config: configured(),
    randomToken: () => 'safety-cookie',
    runTurn: async input => { calls.push(input); return { reply: 'unsafe upstream reply', sessionId: 'unsafe-session' }; },
  });
  await service.unlock({ origin, accessCode: 'test-agent-access-code-2026' });

  const messageResult = await service.chat({ origin, cookieToken: 'safety-cookie', payload: { message: 'I have rectal bleeding', ledger: [] } });
  const ledgerResult = await service.chat({ origin, cookieToken: 'safety-cookie', payload: {
    message: 'What does my journal show?',
    ledger: [{ bristolType: 4, color: 'brown', note: '', symptoms: { cannotPassGas: true } }],
  } });
  const noteResult = await service.chat({ origin, cookieToken: 'safety-cookie', payload: {
    message: 'Summarize this entry',
    ledger: [{ bristolType: 4, color: 'brown', note: 'unable to pass gas', symptoms: {} }],
  } });

  assert.equal(calls.length, 0);
  for (const result of [messageResult, ledgerResult, noteResult]) {
    assert.equal(result.safetyOverride, true);
    assert.match(result.reply, /medical help/i);
    assert.doesNotMatch(result.reply, /unsafe upstream/i);
  }
});

test('Hermes CLI parser accepts quiet output, strips control sequences, and rejects leaked metadata', () => {
  const parsed = parseHermesCliOutput('\u001b[32msession_id: 20260820_abcd\u001b[0m\nA concise answer.');
  assert.deepEqual(parsed, { sessionId: '20260820_abcd', reply: 'A concise answer.' });
  assert.throws(() => parseHermesCliOutput('answer only'), /session metadata/i);
  assert.throws(() => parseHermesCliOutput('session_id: private\nsession_id: leaked'), /unsafe/i);
});

test('Hermes CLI adapter combines stderr session metadata with stdout answer', async () => {
  let captured;
  const result = await runHermesCliTurn({
    prompt: 'hello',
    hermesSessionId: null,
    config: configured(),
    execImpl: async (command, args, options) => {
      captured = { command, args, options };
      return { stdout: 'bounded answer\n', stderr: '\u001b[33mwarning\u001b[0m\nsession_id: 20260820_fixture\n' };
    },
  });
  assert.deepEqual(result, { sessionId: '20260820_fixture', reply: 'bounded answer' });
  assert.equal(captured.command, 'hermes');
  assert.equal(captured.args.includes('--in'), true);
  assert.equal(captured.options.cwd, '/tmp/timmy-agent-workspace');
});

test('Hermes child receives only an explicit non-secret environment allowlist', () => {
  const childEnv = buildHermesEnvironment({
    HOME: '/root',
    PATH: '/usr/bin',
    HERMES_HOME: '/root/.hermes/profiles/timmyapp',
    LANG: 'C.UTF-8',
    TIMMY_AGENT_ACCESS_TOKEN: 'test-must-not-reach-child',
    GITEA_TOKEN: 'test-must-not-reach-child',
    OPENAI_API_KEY: 'test-must-not-reach-child',
    RANDOM_PRIVATE_VALUE: 'test-must-not-reach-child',
  });
  assert.deepEqual(childEnv, {
    HOME: '/root',
    PATH: '/usr/bin',
    HERMES_HOME: '/root/.hermes/profiles/timmyapp',
    LANG: 'C.UTF-8',
    TIMMY_AGENT_BROWSER_REQUEST: '1',
  });
});

test('upstream failures and concurrency fail closed without leaking process detail', async () => {
  let release;
  const runTurn = () => new Promise(resolve => { release = resolve; });
  const service = createHermesAgentService({ config: configured(), randomToken: () => 'browser-cookie', runTurn });
  await service.unlock({ origin, accessCode: 'test-agent-access-code-2026' });
  const active = service.chat({ origin, cookieToken: 'browser-cookie', payload: { message: 'first', ledger: [] } });
  await new Promise(resolve => setImmediate(resolve));
  await rejectsStatus(() => service.chat({ origin, cookieToken: 'browser-cookie', payload: { message: 'second', ledger: [] } }), 429);
  release({ reply: 'done', sessionId: 'private-session' });
  await active;

  const broken = createHermesAgentService({ config: configured(), randomToken: () => 'broken-cookie', runTurn: async () => { throw new Error('spawn /root/.hermes/auth.json SECRET'); } });
  await broken.unlock({ origin, accessCode: 'test-agent-access-code-2026' });
  await assert.rejects(() => broken.chat({ origin, cookieToken: 'broken-cookie', payload: { message: 'hello', ledger: [] } }), error => {
    assert.equal(error.status, 503);
    assert.match(error.message, /temporarily unavailable/i);
    assert.doesNotMatch(error.message, /auth\.json|SECRET|spawn/);
    return true;
  });
});
