import { describe, it, expect, vi } from 'vitest';
import { ChatToolDispatcherImpl } from '../src/services/chat-tool-dispatcher.js';
import { TOOL_NAME_SEPARATOR } from '../src/services/chat.service.js';
import type { McpProxyService } from '../src/services/mcp-proxy-service.js';
import type { IProjectRepository, ProjectWithRelations } from '../src/repositories/project.repository.js';

const NOW = new Date();

function makeProject(overrides: Partial<ProjectWithRelations> = {}): ProjectWithRelations {
  return {
    id: 'proj-1',
    name: 'mcpctl-dev',
    description: '',
    prompt: '',
    proxyModel: '',
    gated: true,
    llmProvider: null,
    llmModel: null,
    serverOverrides: null,
    ownerId: 'owner-1',
    version: 1,
    createdAt: NOW,
    updatedAt: NOW,
    servers: [],
    ...overrides,
  };
}

function mockProjectRepo(p: ProjectWithRelations | null): IProjectRepository {
  return {
    findById: vi.fn(async () => p),
    findAll: vi.fn(),
    findByName: vi.fn(),
    create: vi.fn(),
    update: vi.fn(),
    delete: vi.fn(),
  } as unknown as IProjectRepository;
}

describe('ChatToolDispatcherImpl', () => {
  it('returns [] when project has no MCP servers', async () => {
    const proxy = { execute: vi.fn() } as unknown as McpProxyService;
    const dispatcher = new ChatToolDispatcherImpl({
      proxy,
      projects: mockProjectRepo(makeProject()),
    });
    const tools = await dispatcher.listTools('proj-1');
    expect(tools).toEqual([]);
    expect(proxy.execute).not.toHaveBeenCalled();
  });

  it('returns [] when projectId is null (unattached agent)', async () => {
    const proxy = { execute: vi.fn() } as unknown as McpProxyService;
    const dispatcher = new ChatToolDispatcherImpl({
      proxy,
      projects: mockProjectRepo(null),
    });
    expect(await dispatcher.listTools(null)).toEqual([]);
  });

  it('namespaces tools as `<server>__<tool>` and forwards inputSchema', async () => {
    const proxy = {
      execute: vi.fn(async () => ({
        jsonrpc: '2.0' as const,
        id: 1,
        result: {
          tools: [
            { name: 'query', description: 'do a query', inputSchema: { type: 'object', properties: { q: { type: 'string' } } } },
            { name: 'ping' },
          ],
        },
      })),
    } as unknown as McpProxyService;
    const dispatcher = new ChatToolDispatcherImpl({
      proxy,
      projects: mockProjectRepo(makeProject({
        servers: [{
          id: 'ps-1', projectId: 'proj-1', serverId: 'srv-grafana',
          server: { id: 'srv-grafana', name: 'grafana' },
        }],
      })),
    });
    const tools = await dispatcher.listTools('proj-1');
    expect(tools.map((t) => t.name)).toEqual([
      `grafana${TOOL_NAME_SEPARATOR}query`,
      `grafana${TOOL_NAME_SEPARATOR}ping`,
    ]);
    expect(tools[0]!.parameters).toEqual({ type: 'object', properties: { q: { type: 'string' } } });
    // The 'ping' tool with no inputSchema gets a permissive default.
    expect(tools[1]!.parameters).toEqual({ type: 'object', properties: {} });
  });

  it('skips servers whose tools/list errors out', async () => {
    const warn = vi.fn();
    const proxy = {
      execute: vi.fn(async ({ serverId }: { serverId: string }) => {
        if (serverId === 'srv-bad') {
          return { jsonrpc: '2.0' as const, id: 1, error: { code: -1, message: 'boom' } };
        }
        return {
          jsonrpc: '2.0' as const,
          id: 1,
          result: { tools: [{ name: 't1' }] },
        };
      }),
    } as unknown as McpProxyService;
    const dispatcher = new ChatToolDispatcherImpl({
      proxy,
      projects: mockProjectRepo(makeProject({
        servers: [
          { id: 'ps-1', projectId: 'proj-1', serverId: 'srv-bad', server: { id: 'srv-bad', name: 'bad' } },
          { id: 'ps-2', projectId: 'proj-1', serverId: 'srv-good', server: { id: 'srv-good', name: 'good' } },
        ],
      })),
      logger: { warn },
    });
    const tools = await dispatcher.listTools('proj-1');
    expect(tools.map((t) => t.name)).toEqual([`good${TOOL_NAME_SEPARATOR}t1`]);
    expect(warn).toHaveBeenCalledWith(
      expect.objectContaining({ serverId: 'srv-bad' }),
      'tools/list failed',
    );
  });

  it('callTool dispatches `tools/call` to the right serverId', async () => {
    const execute = vi.fn(async () => ({
      jsonrpc: '2.0' as const,
      id: 1,
      result: { content: [{ type: 'text', text: 'pong' }] },
    }));
    const dispatcher = new ChatToolDispatcherImpl({
      proxy: { execute } as unknown as McpProxyService,
      projects: mockProjectRepo(makeProject({
        servers: [{ id: 'ps-1', projectId: 'proj-1', serverId: 'srv-grafana', server: { id: 'srv-grafana', name: 'grafana' } }],
      })),
    });
    const result = await dispatcher.callTool({
      projectId: 'proj-1',
      serverName: 'grafana',
      toolName: 'ping',
      args: { q: 'cpu' },
    });
    expect(execute).toHaveBeenCalledWith({
      serverId: 'srv-grafana',
      method: 'tools/call',
      params: { name: 'ping', arguments: { q: 'cpu' } },
    });
    expect(result).toEqual({ content: [{ type: 'text', text: 'pong' }] });
  });

  it('callTool throws when the server is not attached to the project', async () => {
    const execute = vi.fn();
    const dispatcher = new ChatToolDispatcherImpl({
      proxy: { execute } as unknown as McpProxyService,
      projects: mockProjectRepo(makeProject({ servers: [] })),
    });
    await expect(dispatcher.callTool({
      projectId: 'proj-1',
      serverName: 'grafana',
      toolName: 'ping',
      args: {},
    })).rejects.toThrow(/not attached/);
    expect(execute).not.toHaveBeenCalled();
  });

  it('callTool surfaces JSON-RPC errors as exceptions', async () => {
    const execute = vi.fn(async () => ({
      jsonrpc: '2.0' as const,
      id: 1,
      error: { code: -1, message: 'tool blew up' },
    }));
    const dispatcher = new ChatToolDispatcherImpl({
      proxy: { execute } as unknown as McpProxyService,
      projects: mockProjectRepo(makeProject({
        servers: [{ id: 'ps-1', projectId: 'proj-1', serverId: 'srv-grafana', server: { id: 'srv-grafana', name: 'grafana' } }],
      })),
    });
    await expect(dispatcher.callTool({
      projectId: 'proj-1',
      serverName: 'grafana',
      toolName: 'ping',
      args: {},
    })).rejects.toThrow(/tool blew up/);
  });
});
