import { describe, it, expect } from 'vitest';
import { parseEndpoint, buildWorkerEntry, ALL_ROLES, renderConfigYaml, renderDotenv, parseAnswersFromEnv, probeModels } from './setup-lib.mjs';
import { parse as parseYaml } from 'yaml';

describe('parseEndpoint', () => {
  it('keeps a full http URL and derives the /v1-stripped base', () => {
    expect(parseEndpoint('http://localhost:11434/v1')).toEqual({
      endpoint: 'http://localhost:11434/v1',
      base: 'http://localhost:11434',
    });
  });
  it('adds http:// when scheme is missing', () => {
    expect(parseEndpoint('localhost:11434/v1').endpoint).toBe('http://localhost:11434/v1');
  });
  it('accepts https and IPv6 literals', () => {
    const r = parseEndpoint('https://[::1]:9876/v1');
    expect(r.endpoint).toBe('https://[::1]:9876/v1');
    expect(r.base).toBe('https://[::1]:9876');
  });
  it('strips a trailing slash', () => {
    expect(parseEndpoint('http://h:11434/v1/').endpoint).toBe('http://h:11434/v1');
  });
  it('base equals endpoint when there is no /v1 suffix', () => {
    const r = parseEndpoint('http://h:11434');
    expect(r.endpoint).toBe('http://h:11434');
    expect(r.base).toBe('http://h:11434');
  });
  it('rejects empty input', () => {
    expect(parseEndpoint('  ').error).toMatch(/required/);
  });
  it('rejects a non-http scheme', () => {
    expect(parseEndpoint('ftp://h/v1').error).toMatch(/scheme/);
  });
  it('rejects unparseable input', () => {
    expect(parseEndpoint('http://').error).toBeTruthy();
  });
});

describe('buildWorkerEntry', () => {
  it('builds a direct worker with all roles and no api_key', () => {
    const w = buildWorkerEntry({ connectionType: 'direct', endpoint: 'http://h/v1', model: 'qwen3:32b' });
    expect(w).toEqual({
      id: 'local-llm', connection_type: 'direct', endpoint: 'http://h/v1', model: 'qwen3:32b',
      roles: ALL_ROLES, max_concurrency: 1, enabled: true, vlm: false,
    });
    expect('api_key' in w).toBe(false);
  });
  it('builds a gateway worker with api_key and id "gateway"', () => {
    const w = buildWorkerEntry({ connectionType: 'aao_gateway', endpoint: 'http://g/v1', model: 'm', apiKey: 'sk-aao-x' });
    expect(w.id).toBe('gateway');
    expect(w.connection_type).toBe('aao_gateway');
    expect(w.api_key).toBe('sk-aao-x');
  });
  it('returns a fresh roles array (not a shared reference)', () => {
    const a = buildWorkerEntry({ connectionType: 'direct', endpoint: 'e', model: 'm' });
    a.roles.push('mutated');
    const b = buildWorkerEntry({ connectionType: 'direct', endpoint: 'e', model: 'm' });
    expect(b.roles).toEqual(ALL_ROLES);
  });
});

describe('renderConfigYaml', () => {
  it('emits config_version + a single llm.workers entry that re-parses correctly', () => {
    const w = buildWorkerEntry({ connectionType: 'direct', endpoint: 'http://localhost:11434/v1', model: 'qwen3:32b' });
    const text = renderConfigYaml(w);
    const parsed = parseYaml(text);
    expect(parsed.config_version).toBe(2);
    expect(parsed.llm.workers).toHaveLength(1);
    expect(parsed.llm.workers[0]).toMatchObject({
      id: 'local-llm', connection_type: 'direct',
      endpoint: 'http://localhost:11434/v1', model: 'qwen3:32b',
      roles: ['auto', 'fast', 'quality', 'title', 'reflection'],
      max_concurrency: 1, enabled: true, vlm: false,
    });
  });
  it('quotes values containing a colon (model, endpoint) so they do not become maps', () => {
    const w = buildWorkerEntry({ connectionType: 'direct', endpoint: 'http://h:11434/v1', model: 'qwen3:32b' });
    const parsed = parseYaml(renderConfigYaml(w));
    expect(parsed.llm.workers[0].model).toBe('qwen3:32b');
    expect(parsed.llm.workers[0].endpoint).toBe('http://h:11434/v1');
  });
  it('includes api_key for gateway and round-trips it', () => {
    const w = buildWorkerEntry({ connectionType: 'aao_gateway', endpoint: 'http://g:9876/v1', model: 'm', apiKey: 'sk-aao-abc123' });
    const parsed = parseYaml(renderConfigYaml(w));
    expect(parsed.llm.workers[0].api_key).toBe('sk-aao-abc123');
  });
  it('does not include example comment noise (minimal output)', () => {
    const w = buildWorkerEntry({ connectionType: 'direct', endpoint: 'e', model: 'm' });
    expect(renderConfigYaml(w)).not.toMatch(/AAO Gateway Server|backends:|metrics:/);
  });
  it('quotes and escapes a value containing a newline so YAML stays valid', () => {
    const w = buildWorkerEntry({ connectionType: 'direct', endpoint: 'http://h/v1', model: 'a\nb' });
    const parsed = parseYaml(renderConfigYaml(w));
    expect(parsed.llm.workers[0].model).toBe('a\nb');
  });
});

describe('renderDotenv', () => {
  it('creates PORT line from empty input', () => {
    expect(renderDotenv('', { port: 9876 })).toBe('PORT=9876\n');
  });
  it('upserts an existing PORT line and preserves other lines', () => {
    const out = renderDotenv('FOO=bar\nPORT=1000\nBAZ=qux\n', { port: 9876 });
    expect(out).toContain('FOO=bar');
    expect(out).toContain('BAZ=qux');
    expect(out).toContain('PORT=9876');
    expect(out).not.toContain('PORT=1000');
  });
  it('appends PORT when absent and ends with a newline', () => {
    const out = renderDotenv('FOO=bar\n', { port: 9876 });
    expect(out).toBe('FOO=bar\nPORT=9876\n');
  });
  it('collapses multiple existing PORT lines into exactly one', () => {
    const out = renderDotenv('PORT=1\nFOO=bar\nPORT=2\n', { port: 9876 });
    expect(out.match(/^PORT=/gm)).toHaveLength(1);
    expect(out).toContain('PORT=9876');
    expect(out).toContain('FOO=bar');
  });
});

describe('parseAnswersFromEnv', () => {
  it('resolves a direct setup from env', () => {
    const r = parseAnswersFromEnv({ SETUP_LLM_ENDPOINT: 'http://localhost:11434/v1', SETUP_MODEL: 'm' });
    expect(r.error).toBeUndefined();
    expect(r.answers).toMatchObject({ connectionType: 'direct', endpoint: 'http://localhost:11434/v1', base: 'http://localhost:11434', model: 'm', port: 9876 });
  });
  it('requires SETUP_LLM_API_KEY for aao_gateway', () => {
    const r = parseAnswersFromEnv({ SETUP_CONNECTION_TYPE: 'aao_gateway', SETUP_LLM_ENDPOINT: 'http://g/v1', SETUP_MODEL: 'm' });
    expect(r.error).toMatch(/SETUP_LLM_API_KEY/);
  });
  it('requires SETUP_MODEL', () => {
    expect(parseAnswersFromEnv({ SETUP_LLM_ENDPOINT: 'http://h/v1' }).error).toMatch(/SETUP_MODEL/);
  });
  it('rejects a bad endpoint and a bad port', () => {
    expect(parseAnswersFromEnv({ SETUP_LLM_ENDPOINT: 'ftp://h', SETUP_MODEL: 'm' }).error).toMatch(/SETUP_LLM_ENDPOINT/);
    expect(parseAnswersFromEnv({ SETUP_LLM_ENDPOINT: 'http://h/v1', SETUP_MODEL: 'm', PORT: '70000' }).error).toMatch(/PORT/);
  });
  it('rejects an unknown connection type', () => {
    expect(parseAnswersFromEnv({ SETUP_CONNECTION_TYPE: 'weird', SETUP_LLM_ENDPOINT: 'http://h/v1', SETUP_MODEL: 'm' }).error).toMatch(/SETUP_CONNECTION_TYPE/);
  });
});

function res(status, body, { json = true } = {}) {
  return {
    ok: status >= 200 && status < 300,
    status,
    json: async () => { if (!json) throw new Error('not json'); return body; },
  };
}

describe('probeModels', () => {
  it('uses Ollama /api/tags first and parses {models:[{name}]}', async () => {
    const calls = [];
    const fetchImpl = async (url) => { calls.push(url); return res(200, { models: [{ name: 'qwen3:32b' }] }); };
    const r = await probeModels({ endpoint: 'http://h:11434/v1', base: 'http://h:11434', fetchImpl });
    expect(r).toEqual({ ok: true, models: ['qwen3:32b'] });
    expect(calls[0]).toBe('http://h:11434/api/tags');
  });
  it('falls back to {endpoint}/models and parses {data:[{id}]}', async () => {
    const calls = [];
    const fetchImpl = async (url) => { calls.push(url); return url.endsWith('/api/tags') ? res(404, {}) : res(200, { data: [{ id: 'gpt-x' }] }); };
    const r = await probeModels({ endpoint: 'http://g:9876/v1', base: 'http://g:9876', fetchImpl });
    expect(r).toEqual({ ok: true, models: ['gpt-x'] });
    expect(calls).toEqual(['http://g:9876/api/tags', 'http://g:9876/v1/models']);
  });
  it('forwards the Bearer header when apiKey is set', async () => {
    let seen;
    const fetchImpl = async (url, init) => { seen = init?.headers?.Authorization; return res(404, {}); };
    await probeModels({ endpoint: 'http://g/v1', base: 'http://g', apiKey: 'sk-aao-x', fetchImpl });
    expect(seen).toBe('Bearer sk-aao-x');
  });
  it('returns ok:false on non-2xx from both probes', async () => {
    const fetchImpl = async () => res(500, {});
    const r = await probeModels({ endpoint: 'http://h/v1', base: 'http://h', fetchImpl });
    expect(r.ok).toBe(false);
    expect(r.models).toEqual([]);
    expect(r.error).toBeTruthy();
  });
  it('treats non-JSON (HTML error) as failure', async () => {
    const fetchImpl = async () => res(200, null, { json: false });
    const r = await probeModels({ endpoint: 'http://h/v1', base: 'http://h', fetchImpl });
    expect(r.ok).toBe(false);
    expect(r.error).toMatch(/non-JSON/);
  });
  it('reports a timeout via AbortError', async () => {
    const fetchImpl = async (_url, init) => new Promise((_resolve, reject) => {
      init.signal.addEventListener('abort', () => reject(Object.assign(new Error('aborted'), { name: 'AbortError' })));
    });
    const r = await probeModels({ endpoint: 'http://h/v1', base: 'http://h', fetchImpl, timeoutMs: 10 });
    expect(r.ok).toBe(false);
    expect(r.error).toMatch(/timeout/);
  });
});
