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

import type { Config } from '../../config/schema.js';
import {
  extractAvatarFromIdentityMarkdown,
  listGatewayAgents,
  prepareCreateAgent,
  prepareCreateAgentsBatch,
  prepareDeleteAgent,
  prepareUpdateAgent,
  readAgentProfileFile,
} from '../agents-admin.js';

function minimalConfig(overrides: Partial<Config> = {}): Config {
  return {
    gateway: { port: 18790, corsOrigins: [] },
    agents: {
      defaults: {
        workspace: '/tmp/ws-default',
        model: 'anthropic/claude-sonnet-4-5',
        maxTokens: 8192,
        temperature: 0.7,
        maxToolIterations: 20,
        maxRequestsPerTurn: 50,
        maxToolFailuresPerTurn: 3,
        thinkingDefault: 'medium',
        reasoningDefault: 'stream',
        verboseDefault: 'full',
      },
      list: [],
    },
    channels: {},
    ...overrides,
  } as Config;
}

describe('agents-admin', () => {
  it('listGatewayAgents includes default agent when list is empty', async () => {
    const cfg = minimalConfig();
    const { defaultId, agents, builtinToolIds } = await listGatewayAgents(cfg);
    expect(defaultId).toBe('main');
    expect(agents).toHaveLength(1);
    expect(agents[0]?.id).toBe('main');
    expect(agents[0]?.isDefault).toBe(true);
    expect(Array.isArray(builtinToolIds)).toBe(true);
    expect(builtinToolIds.length).toBeGreaterThan(0);
    expect(agents[0]?.tools.effectiveDisable).toEqual([]);
    expect(agents[0]?.typedModels.defaults).toEqual([]);
    expect(agents[0]?.typedModels.effective).toEqual([]);
  });

  it('listGatewayAgents merges typed models defaults and entry', async () => {
    const cfg = minimalConfig({
      agents: {
        ...minimalConfig().agents,
        defaults: {
          ...minimalConfig().agents!.defaults!,
          models: [{ id: 'small', model: 'deepseek/flash' }],
        },
        list: [{ id: 'coder', enabled: true, workspace: '/tmp/c', models: [{ id: 'small', model: 'openai/mini' }] }],
      },
    } as Partial<Config>);
    const { agents } = await listGatewayAgents(cfg);
    const coder = agents.find((a) => a.id === 'coder');
    expect(coder?.typedModels.defaults).toEqual([{ id: 'small', model: 'deepseek/flash' }]);
    expect(coder?.typedModels.entry).toEqual([{ id: 'small', model: 'openai/mini' }]);
    expect(coder?.typedModels.effective).toEqual([{ id: 'small', model: 'openai/mini' }]);
  });

  it('prepareUpdateAgent sets and clears typed models on list entry', () => {
    const cfg = minimalConfig({
      agents: {
        ...minimalConfig().agents,
        list: [{ id: 'coder', enabled: true, workspace: '/tmp/c' }],
      },
    } as Partial<Config>);
    const set = prepareUpdateAgent(cfg, 'coder', {
      models: [{ id: 'small', model: 'openai/mini', description: 'Fast' }],
    });
    expect(set.ok).toBe(true);
    if (!set.ok) return;
    const e = set.data.nextConfig.agents?.list?.find((x) => x.id === 'coder');
    expect(e?.models).toEqual([{ id: 'small', model: 'openai/mini', description: 'Fast' }]);

    const cleared = prepareUpdateAgent(set.data.nextConfig, 'coder', { models: null });
    expect(cleared.ok).toBe(true);
    if (!cleared.ok) return;
    const e2 = cleared.data.nextConfig.agents?.list?.find((x) => x.id === 'coder');
    expect(e2?.models).toBeUndefined();
  });

  it('extractAvatarFromIdentityMarkdown reads Avatar line', () => {
    const md = '# IDENTITY\n\n- **Avatar:** xopc:dicebear:pixel-art:Test\n';
    expect(extractAvatarFromIdentityMarkdown(md)).toBe('xopc:dicebear:pixel-art:Test');
    expect(extractAvatarFromIdentityMarkdown('')).toBeUndefined();
    expect(extractAvatarFromIdentityMarkdown('- **Avatar:**  \n')).toBeUndefined();
  });
  it('prepareUpdateAgent sets description and can clear it', () => {
    const cfg = minimalConfig({
      agents: {
        ...minimalConfig().agents,
        list: [{ id: 'coder', enabled: true, workspace: '/tmp/c', description: 'old' }],
      },
    } as Partial<Config>);
    const set = prepareUpdateAgent(cfg, 'coder', { description: 'Builds features' });
    expect(set.ok).toBe(true);
    if (!set.ok) return;
    const e = set.data.nextConfig.agents?.list?.find((x) => x.id === 'coder');
    expect(e?.description).toBe('Builds features');

    const cleared = prepareUpdateAgent(set.data.nextConfig, 'coder', { description: null });
    expect(cleared.ok).toBe(true);
    if (!cleared.ok) return;
    const e2 = cleared.data.nextConfig.agents?.list?.find((x) => x.id === 'coder');
    expect(e2?.description).toBeUndefined();
  });

  it('prepareUpdateAgent sets skills and tools.disable on list entry', () => {
    const cfg = minimalConfig({
      agents: {
        ...minimalConfig().agents,
        list: [{ id: 'coder', enabled: true, workspace: '/tmp/c' }],
      },
    } as Partial<Config>);
    const r = prepareUpdateAgent(cfg, 'coder', {
      skills: ['note'],
      toolsDisable: ['shell'],
    });
    expect(r.ok).toBe(true);
    if (!r.ok) return;
    const list = r.data.nextConfig.agents?.list ?? [];
    const e = list.find((x) => x.id === 'coder');
    expect(e?.skills).toEqual(['note']);
    expect(e?.tools?.disable).toEqual(['shell']);
  });
  it('prepareCreateAgent uses explicit id seed for agent id and keeps display name', () => {
    const cfg = minimalConfig();
    const r = prepareCreateAgent(cfg, {
      name: 'Nice Label',
      id: 'nice-bot',
      workspace: '/tmp/ws-nice',
    });
    expect(r.ok).toBe(true);
    if (!r.ok) return;
    const list = r.data.nextConfig.agents?.list ?? [];
    const e = list.find((x) => x.id === 'nice-bot');
    expect(e?.name).toBe('Nice Label');
  });

  it('prepareCreateAgent rejects invalid explicit agent id', () => {
    const cfg = minimalConfig();
    const r = prepareCreateAgent(cfg, { name: 'Label', id: 'bad id', workspace: '/tmp/w' });
    expect(r.ok).toBe(false);
    if (!r.ok) {
      expect(r.status).toBe(400);
    }
  });

  it('prepareCreateAgent rejects Windows reserved explicit id', () => {
    const cfg = minimalConfig();
    const r = prepareCreateAgent(cfg, { name: 'Label', id: 'con', workspace: '/tmp/w' });
    expect(r.ok).toBe(false);
    if (!r.ok) {
      expect(r.status).toBe(400);
    }
  });

  it('prepareCreateAgent rejects display name that cannot yield a folder-safe id', () => {
    const cfg = minimalConfig();
    const r = prepareCreateAgent(cfg, { name: '!!!', workspace: '/tmp/w' });
    expect(r.ok).toBe(false);
    if (!r.ok) {
      expect(r.status).toBe(400);
    }
  });

  it('prepareCreateAgent rejects duplicate id', () => {
    const cfg = minimalConfig({
      agents: {
        ...minimalConfig().agents,
        list: [{ id: 'coder', enabled: true, workspace: '/tmp/c' }],
      },
    } as Partial<Config>);
    const r = prepareCreateAgent(cfg, { name: 'coder', workspace: '/tmp/x' });
    expect(r.ok).toBe(false);
    if (!r.ok) {
      expect(r.status).toBe(409);
    }
  });

  it('prepareCreateAgent sets toolsDisable on new entry', () => {
    const cfg = minimalConfig();
    const r = prepareCreateAgent(cfg, {
      name: 'Coder',
      id: 'coder',
      workspace: '/tmp/c',
      toolsDisable: ['shell', 'image_generate'],
    });
    expect(r.ok).toBe(true);
    if (!r.ok) return;
    const e = r.data.nextConfig.agents?.list?.find((x) => x.id === 'coder');
    expect(e?.tools?.disable).toEqual(['shell', 'image_generate']);
  });
  it('prepareCreateAgent rejects unsupported profileFiles name', () => {
    const cfg = minimalConfig();
    const r = prepareCreateAgent(cfg, {
      name: 'Coder',
      id: 'coder',
      workspace: '/tmp/c',
      profileFiles: { '../../../etc/passwd': 'x' },
    });
    expect(r.ok).toBe(false);
    if (!r.ok) {
      expect(r.status).toBe(400);
    }
  });

  it('prepareCreateAgent rejects non-string profileFiles content', () => {
    const cfg = minimalConfig();
    const r = prepareCreateAgent(cfg, {
      name: 'Coder',
      id: 'coder',
      workspace: '/tmp/c',
      profileFiles: { 'SOUL.md': 42 as unknown as string },
    });
    expect(r.ok).toBe(false);
    if (!r.ok) {
      expect(r.status).toBe(400);
    }
  });

  it('prepareCreateAgentsBatch adds multiple agents in one config pass', () => {
    const cfg = minimalConfig();
    const r = prepareCreateAgentsBatch(cfg, [
      { name: 'Coder', id: 'coder', workspace: '/tmp/coder' },
      { name: 'Writer', id: 'writer', workspace: '/tmp/writer', toolsDisable: ['shell'] },
    ]);
    expect(r.ok).toBe(true);
    if (!r.ok) return;
    const list = r.data.nextConfig.agents?.list ?? [];
    expect(list.some((e) => e.id === 'coder')).toBe(true);
    expect(list.some((e) => e.id === 'writer')).toBe(true);
    expect(r.data.created.map((c) => c.agentId).sort()).toEqual(['coder', 'writer']);
    const writer = list.find((e) => e.id === 'writer');
    expect(writer?.tools?.disable).toEqual(['shell']);
  });

  it('prepareCreateAgentsBatch rejects empty array', () => {
    const cfg = minimalConfig();
    const r = prepareCreateAgentsBatch(cfg, []);
    expect(r.ok).toBe(false);
    if (!r.ok) {
      expect(r.status).toBe(400);
    }
  });

  it('prepareCreateAgentsBatch fails fast on duplicate id in batch', () => {
    const cfg = minimalConfig();
    const r = prepareCreateAgentsBatch(cfg, [
      { name: 'A', id: 'dup', workspace: '/tmp/a' },
      { name: 'B', id: 'dup', workspace: '/tmp/b' },
    ]);
    expect(r.ok).toBe(false);
    if (!r.ok) {
      expect(r.status).toBe(409);
    }
  });

  it('prepareDeleteAgent refuses main', () => {
    const cfg = minimalConfig({
      agents: {
        ...minimalConfig().agents,
        list: [{ id: 'main', enabled: true }],
      },
    } as Partial<Config>);
    const r = prepareDeleteAgent(cfg, 'main');
    expect(r.ok).toBe(false);
  });

  it('readAgentProfileFile rejects unsupported filename', async () => {
    const cfg = minimalConfig();
    const r = await readAgentProfileFile(cfg, 'main', '../../../etc/passwd');
    expect(r.ok).toBe(false);
  });
});
