import { describe, it, expect, vi } from 'vitest';
import { AgentService } from '../src/services/agent.service.js';
import type { IAgentRepository } from '../src/repositories/agent.repository.js';
import type { LlmService } from '../src/services/llm.service.js';
import type { ProjectService } from '../src/services/project.service.js';
import type { Agent } from '@prisma/client';

function makeAgent(overrides: Partial<Agent> = {}): Agent {
  return {
    id: 'agent-1',
    name: 'reviewer',
    description: '',
    systemPrompt: '',
    llmId: 'llm-1',
    projectId: null,
    proxyModelName: null,
    defaultParams: {} as Agent['defaultParams'],
    extras: {} as Agent['extras'],
    ownerId: 'owner-1',
    version: 1,
    createdAt: new Date(),
    updatedAt: new Date(),
    ...overrides,
  };
}

function mockRepo(initial: Agent[] = []): IAgentRepository {
  const rows = new Map<string, Agent>(initial.map((r) => [r.id, r]));
  return {
    findAll: vi.fn(async () => [...rows.values()]),
    findById: vi.fn(async (id: string) => rows.get(id) ?? null),
    findByName: vi.fn(async (name: string) => {
      for (const r of rows.values()) if (r.name === name) return r;
      return null;
    }),
    findByProjectId: vi.fn(async (projectId: string) =>
      [...rows.values()].filter((r) => r.projectId === projectId)),
    create: vi.fn(async (data) => {
      const row = makeAgent({
        id: `agent-${String(rows.size + 1)}`,
        name: data.name,
        description: data.description ?? '',
        systemPrompt: data.systemPrompt ?? '',
        llmId: data.llmId,
        projectId: data.projectId ?? null,
        proxyModelName: data.proxyModelName ?? null,
        defaultParams: (data.defaultParams ?? {}) as Agent['defaultParams'],
        extras: (data.extras ?? {}) as Agent['extras'],
        ownerId: data.ownerId,
      });
      rows.set(row.id, row);
      return row;
    }),
    update: vi.fn(async (id, data) => {
      const existing = rows.get(id);
      if (!existing) throw new Error('not found');
      const next: Agent = {
        ...existing,
        ...(data.description !== undefined ? { description: data.description } : {}),
        ...(data.systemPrompt !== undefined ? { systemPrompt: data.systemPrompt } : {}),
        ...(data.llmId !== undefined ? { llmId: data.llmId } : {}),
        ...(data.projectId !== undefined ? { projectId: data.projectId } : {}),
        ...(data.proxyModelName !== undefined ? { proxyModelName: data.proxyModelName } : {}),
        ...(data.defaultParams !== undefined ? { defaultParams: data.defaultParams as Agent['defaultParams'] } : {}),
        ...(data.extras !== undefined ? { extras: data.extras as Agent['extras'] } : {}),
        version: existing.version + 1,
      };
      rows.set(id, next);
      return next;
    }),
    delete: vi.fn(async (id: string) => {
      rows.delete(id);
    }),
  };
}

function mockLlms(): LlmService {
  return {
    getById: vi.fn(async (id: string) => ({
      id, name: id === 'llm-1' ? 'qwen3-thinking' : 'other',
      type: 'openai', model: 'm', url: '', tier: 'fast',
      description: '', apiKeyRef: null, extraConfig: {},
      version: 1, createdAt: new Date(), updatedAt: new Date(),
    })),
    getByName: vi.fn(async (name: string) => ({
      id: name === 'qwen3-thinking' ? 'llm-1' : 'llm-other',
      name, type: 'openai', model: 'm', url: '', tier: 'fast',
      description: '', apiKeyRef: null, extraConfig: {},
      version: 1, createdAt: new Date(), updatedAt: new Date(),
    })),
  } as unknown as LlmService;
}

function mockProjects(): ProjectService {
  return {
    getById: vi.fn(async (id: string) => ({ id, name: id === 'proj-1' ? 'mcpctl-dev' : 'other' })),
    resolveAndGet: vi.fn(async (idOrName: string) => ({
      id: idOrName === 'mcpctl-dev' ? 'proj-1' : 'proj-other',
      name: idOrName,
    })),
  } as unknown as ProjectService;
}

describe('AgentService', () => {
  it('creates an agent resolving llm + project by name', async () => {
    const repo = mockRepo();
    const svc = new AgentService(repo, mockLlms(), mockProjects());
    const view = await svc.create({
      name: 'reviewer',
      description: 'I review security',
      systemPrompt: 'be terse',
      llm: { name: 'qwen3-thinking' },
      project: { name: 'mcpctl-dev' },
      defaultParams: { temperature: 0.2, max_tokens: 4096 },
    }, 'owner-1');
    expect(view.name).toBe('reviewer');
    expect(view.llm.name).toBe('qwen3-thinking');
    expect(view.project?.name).toBe('mcpctl-dev');
    expect(view.defaultParams.temperature).toBe(0.2);
    expect(repo.create).toHaveBeenCalledOnce();
  });

  it('creates an agent without a project (null projectId stays null)', async () => {
    const repo = mockRepo();
    const svc = new AgentService(repo, mockLlms(), mockProjects());
    const view = await svc.create({
      name: 'standalone',
      llm: { name: 'qwen3-thinking' },
    }, 'owner-1');
    expect(view.project).toBeNull();
  });

  it('rejects creating an agent with a duplicate name (Conflict)', async () => {
    const repo = mockRepo([makeAgent({ id: 'a1', name: 'dup' })]);
    const svc = new AgentService(repo, mockLlms(), mockProjects());
    await expect(svc.create({
      name: 'dup',
      llm: { name: 'qwen3-thinking' },
    }, 'owner-1')).rejects.toThrow(/already exists/);
  });

  it('updates llm reference by name', async () => {
    const repo = mockRepo([makeAgent({ id: 'a1', name: 'switcher', llmId: 'llm-1' })]);
    const svc = new AgentService(repo, mockLlms(), mockProjects());
    const updated = await svc.update('a1', { llm: { name: 'other' } });
    expect(updated.llm.id).toBe('llm-other');
  });

  it('detaches a project when project is set to null', async () => {
    const repo = mockRepo([makeAgent({ id: 'a1', name: 'attached', projectId: 'proj-1' })]);
    const svc = new AgentService(repo, mockLlms(), mockProjects());
    const updated = await svc.update('a1', { project: null });
    expect(updated.project).toBeNull();
  });

  it('listByProject returns only agents in the project', async () => {
    const repo = mockRepo([
      makeAgent({ id: 'a1', name: 'in-proj', projectId: 'proj-1' }),
      makeAgent({ id: 'a2', name: 'no-proj', projectId: null }),
      makeAgent({ id: 'a3', name: 'other-proj', projectId: 'proj-other' }),
    ]);
    const svc = new AgentService(repo, mockLlms(), mockProjects());
    const list = await svc.listByProject('mcpctl-dev');
    expect(list.map((a) => a.name)).toEqual(['in-proj']);
  });

  it('upsertByName creates if missing, updates if present', async () => {
    const repo = mockRepo();
    const svc = new AgentService(repo, mockLlms(), mockProjects());

    const created = await svc.upsertByName({
      name: 'roundtrip',
      description: 'first',
      systemPrompt: '',
      llm: { name: 'qwen3-thinking' },
      defaultParams: {},
      extras: {},
    }, 'owner-1');
    expect(created.description).toBe('first');

    const updated = await svc.upsertByName({
      name: 'roundtrip',
      description: 'second',
      systemPrompt: '',
      llm: { name: 'qwen3-thinking' },
      defaultParams: {},
      extras: {},
    }, 'owner-1');
    expect(updated.description).toBe('second');
    expect(updated.id).toBe(created.id);
  });
});
