import { describe, expect, it, vi } from 'vitest';
import { resolveEphemeralServer } from '../src/cli/adhoc-server.js';
import type { ServerDefinition } from '../src/config.js';

process.env.MCPORTER_DISABLE_AUTORUN = '1';
const cliModulePromise = import('../src/cli.js');

describe('CLI call execution behavior', () => {
  it('auto-selects the sole tool when omitted', async () => {
    const toolName = 'list_issues';
    const { handleCall } = await cliModulePromise;
    const { runtime, callTool } = createRuntimeStub(
      {
        linear: [
          {
            name: toolName,
            description: 'List issues',
            inputSchema: {
              type: 'object',
              properties: {
                limit: { type: 'number' },
              },
              required: [],
            },
          },
        ],
      },
      {
        definitions: [
          {
            name: 'linear',
            command: { kind: 'stdio', command: 'linear', args: [], cwd: process.cwd() },
            source: { kind: 'local', path: '<test>' },
          },
        ],
      }
    );
    const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
    await handleCall(runtime, ['linear', 'limit=5']);
    expect(callTool).toHaveBeenCalledWith('linear', toolName, expect.objectContaining({ args: { limit: 5 } }));
    logSpy.mockRestore();
  });

  it('restores numeric-looking key=value args to schema-declared strings', async () => {
    const { handleCall } = await cliModulePromise;
    const { runtime, callTool, listTools } = createRuntimeStub({
      slack: [
        {
          name: 'conversations_replies',
          inputSchema: {
            type: 'object',
            properties: {
              channel_id: { type: 'string' },
              thread_ts: { type: 'string' },
              latest: { anyOf: [{ type: 'string' }, { type: 'null' }] },
              limit: { type: 'number' },
            },
            required: ['channel_id', 'thread_ts'],
          },
        },
      ],
    });
    const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});

    await handleCall(runtime, [
      'slack.conversations_replies',
      'channel_id=C1234567890',
      'thread_ts=1234567890.123456',
      'latest=1234567899.987654',
      'limit=1',
    ]);

    expect(callTool).toHaveBeenCalledWith(
      'slack',
      'conversations_replies',
      expect.objectContaining({
        args: {
          channel_id: 'C1234567890',
          thread_ts: '1234567890.123456',
          latest: '1234567899.987654',
          limit: 1,
        },
      })
    );
    expect(listTools).toHaveBeenCalledWith('slack', {
      autoAuthorize: true,
      includeSchema: true,
      allowCachedAuth: true,
      disableOAuth: undefined,
    });
    logSpy.mockRestore();
  });

  it('wraps bare long-flag strings when the schema declares an array', async () => {
    const { handleCall } = await cliModulePromise;
    const { runtime, callTool, listTools } = createRuntimeStub({
      email: [
        {
          name: 'send_email',
          inputSchema: {
            type: 'object',
            properties: {
              to: { type: 'array', items: { type: 'string' } },
              subject: { type: 'string' },
            },
            required: ['to', 'subject'],
          },
        },
      ],
    });
    const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});

    await handleCall(runtime, ['email.send_email', '--to', 'miguel@example.com', '--subject', 'Test']);

    expect(callTool).toHaveBeenCalledWith(
      'email',
      'send_email',
      expect.objectContaining({
        args: {
          to: ['miguel@example.com'],
          subject: 'Test',
        },
      })
    );
    expect(listTools).toHaveBeenCalledWith('email', {
      autoAuthorize: true,
      includeSchema: true,
      allowCachedAuth: true,
      disableOAuth: undefined,
    });
    logSpy.mockRestore();
  });

  it('does not load schemas for numeric values supplied via --args JSON', async () => {
    const { handleCall } = await cliModulePromise;
    const { runtime, callTool, listTools } = createRuntimeStub({
      linear: [
        {
          name: 'list_issues',
          inputSchema: {
            type: 'object',
            properties: {
              limit: { type: 'number' },
            },
          },
        },
      ],
    });
    const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});

    await handleCall(runtime, ['linear.list_issues', '--args', '{"limit":5}']);

    expect(callTool).toHaveBeenCalledWith('linear', 'list_issues', expect.objectContaining({ args: { limit: 5 } }));
    expect(listTools).not.toHaveBeenCalled();
    logSpy.mockRestore();
  });

  it('marks MCP isError tool results as process failures', async () => {
    const previousExitCode = process.exitCode;
    process.exitCode = undefined;
    try {
      const { handleCall } = await cliModulePromise;
      const { runtime, callTool } = createRuntimeStub({
        linear: [{ name: 'explode', inputSchema: { type: 'object', properties: {} } }],
      });
      callTool.mockResolvedValueOnce({ content: [{ type: 'text', text: 'Unknown resource' }], isError: true });
      const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});

      await handleCall(runtime, ['linear.explode']);

      expect(process.exitCode).toBe(1);
      logSpy.mockRestore();
    } finally {
      process.exitCode = previousExitCode;
    }
  });

  it('auto-corrects near-miss tool names returned as MCP isError content', async () => {
    const { handleCall } = await cliModulePromise;
    const callTool = vi
      .fn()
      .mockResolvedValueOnce({ content: [{ type: 'text', text: 'Unknown tool: read_wiki_structur' }], isError: true })
      .mockResolvedValueOnce({ ok: true });
    const listTools = vi.fn().mockResolvedValue([{ name: 'read_wiki_structure' }]);
    const runtime = {
      callTool,
      listTools,
      close: vi.fn().mockResolvedValue(undefined),
    } as unknown as Awaited<ReturnType<(typeof import('../src/runtime.js'))['createRuntime']>>;

    const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});

    await handleCall(runtime, ['deepwiki.read_wiki_structur']);

    const notes = logSpy.mock.calls.map((call) => call.join(' '));
    expect(notes.some((line) => line.includes('Auto-corrected tool call to deepwiki.read_wiki_structure'))).toBe(true);
    expect(callTool).toHaveBeenCalledTimes(2);
    expect(callTool).toHaveBeenNthCalledWith(
      1,
      'deepwiki',
      'read_wiki_structur',
      expect.objectContaining({ args: {} })
    );
    expect(callTool).toHaveBeenNthCalledWith(
      2,
      'deepwiki',
      'read_wiki_structure',
      expect.objectContaining({ args: {} })
    );

    logSpy.mockRestore();
  });

  it('keeps auto-correct diagnostics off stdout for JSON output', async () => {
    const { handleCall } = await cliModulePromise;
    const callTool = vi
      .fn()
      .mockResolvedValueOnce({ content: [{ type: 'text', text: 'Unknown tool: read_wiki_structur' }], isError: true })
      .mockResolvedValueOnce({ content: [{ type: 'text', text: '{"ok":true}' }] });
    const listTools = vi.fn().mockResolvedValue([{ name: 'read_wiki_structure' }]);
    const runtime = {
      callTool,
      listTools,
      close: vi.fn().mockResolvedValue(undefined),
    } as unknown as Awaited<ReturnType<(typeof import('../src/runtime.js'))['createRuntime']>>;

    const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
    const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});

    await handleCall(runtime, ['deepwiki.read_wiki_structur', '--output', 'json']);

    expect(errorSpy.mock.calls.map((call) => call.join(' ')).join('\n')).toContain(
      'Auto-corrected tool call to deepwiki.read_wiki_structure'
    );
    expect(logSpy).toHaveBeenCalledTimes(1);
    expect(JSON.parse(logSpy.mock.calls[0]?.[0]?.toString() ?? '{}')).toEqual({ ok: true });

    logSpy.mockRestore();
    errorSpy.mockRestore();
  });

  it('still requires an explicit tool when multiple are available', async () => {
    const { handleCall } = await cliModulePromise;
    const { runtime, callTool } = createRuntimeStub(
      {
        linear: [
          { name: 'list_issues', inputSchema: {} },
          { name: 'create_issue', inputSchema: {} },
        ],
      },
      {
        definitions: [
          {
            name: 'linear',
            command: { kind: 'stdio', command: 'linear', args: [], cwd: process.cwd() },
            source: { kind: 'local', path: '<test>' },
          },
        ],
      }
    );
    await expect(handleCall(runtime, ['linear'])).rejects.toThrow(
      'Missing tool name. Provide it via <server>.<tool> or --tool.'
    );
    expect(callTool).not.toHaveBeenCalled();
  });

  it('runs quoted stdio commands without --stdio and infers the tool automatically', async () => {
    const command = 'npx -y vercel-domains-mcp';
    const { name: adhocName } = resolveEphemeralServer({ stdioCommand: command });
    const { handleCall } = await cliModulePromise;
    const { runtime, callTool } = createRuntimeStub({
      [adhocName]: [
        {
          name: 'getDomainAvailability',
          inputSchema: {
            type: 'object',
            properties: { domain: { type: 'string' } },
            required: ['domain'],
          },
        },
      ],
    });
    const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
    await handleCall(runtime, [command, 'domain=answeroverflow.com']);
    expect(callTool).toHaveBeenCalledWith(
      adhocName,
      'getDomainAvailability',
      expect.objectContaining({
        args: { domain: 'answeroverflow.com' },
      })
    );
    logSpy.mockRestore();
  });

  it('calls configured HTTP servers by server.tool selector', async () => {
    const { handleCall } = await cliModulePromise;
    const definition: ServerDefinition = {
      name: 'xhs',
      command: { kind: 'http', url: new URL('http://127.0.0.1:18060/mcp') },
      source: { kind: 'local', path: '<test>' },
    };
    const { runtime, callTool, registerDefinition } = createRuntimeStub(
      {
        xhs: [{ name: 'check_login_status', inputSchema: { type: 'object', properties: {} } }],
      },
      { definitions: [definition] }
    );
    const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});

    await handleCall(runtime, ['xhs.check_login_status']);

    expect(callTool).toHaveBeenCalledWith(
      'xhs',
      'check_login_status',
      expect.objectContaining({
        args: {},
      })
    );
    expect(registerDefinition).not.toHaveBeenCalled();
    logSpy.mockRestore();
  });

  it('splits server.tool selectors before calling ad-hoc HTTP servers', async () => {
    const httpUrl = 'http://127.0.0.1:18060/mcp';
    const { handleCall } = await cliModulePromise;
    const { runtime, callTool, registerDefinition } = createRuntimeStub({});
    const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});

    await handleCall(runtime, ['xhs.check_login_status', '--http-url', httpUrl, '--allow-http']);

    expect(callTool).toHaveBeenCalledWith(
      'xhs',
      'check_login_status',
      expect.objectContaining({
        args: {},
      })
    );
    expect(registerDefinition).toHaveBeenCalledWith(
      expect.objectContaining({ name: 'xhs' }),
      expect.objectContaining({ overwrite: true })
    );
    logSpy.mockRestore();
  });

  it('splits server.tool selectors when ad-hoc HTTP servers also use --name', async () => {
    const httpUrl = 'http://127.0.0.1:18060/mcp';
    const { handleCall } = await cliModulePromise;
    const { runtime, callTool, registerDefinition } = createRuntimeStub({});
    const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});

    await handleCall(runtime, ['xhs.check_login_status', '--http-url', httpUrl, '--allow-http', '--name', 'xhs']);

    expect(callTool).toHaveBeenCalledWith(
      'xhs',
      'check_login_status',
      expect.objectContaining({
        args: {},
      })
    );
    expect(registerDefinition).toHaveBeenCalledWith(
      expect.objectContaining({ name: 'xhs' }),
      expect.objectContaining({ overwrite: true })
    );
    logSpy.mockRestore();
  });

  it('uses the server prefix as the ad-hoc name when --tool overrides a qualified selector', async () => {
    const httpUrl = 'http://127.0.0.1:18060/mcp';
    const { handleCall } = await cliModulePromise;
    const { runtime, callTool, registerDefinition } = createRuntimeStub({});
    const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});

    await handleCall(runtime, [
      'xhs.selector_tool',
      '--http-url',
      httpUrl,
      '--allow-http',
      '--tool',
      'check_login_status',
    ]);

    expect(callTool).toHaveBeenCalledWith(
      'xhs',
      'check_login_status',
      expect.objectContaining({
        args: {},
      })
    );
    expect(registerDefinition).toHaveBeenCalledWith(
      expect.objectContaining({ name: 'xhs' }),
      expect.objectContaining({ overwrite: true })
    );
    logSpy.mockRestore();
  });

  it('honors explicit literal dotted tool names for named ad-hoc HTTP servers', async () => {
    const httpUrl = 'http://127.0.0.1:18060/mcp';
    const { handleCall } = await cliModulePromise;
    const { runtime, callTool } = createRuntimeStub({});
    const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});

    await handleCall(runtime, [
      '--http-url',
      httpUrl,
      '--allow-http',
      '--name',
      'xhs',
      '--tool',
      'xhs.check_login_status',
    ]);

    expect(callTool).toHaveBeenCalledWith(
      'xhs',
      'xhs.check_login_status',
      expect.objectContaining({
        args: {},
      })
    );
    logSpy.mockRestore();
  });

  it('aborts long-running tools when the timeout elapses', async () => {
    vi.useFakeTimers();
    try {
      const { handleCall } = await cliModulePromise;
      const close = vi.fn().mockResolvedValue(undefined);
      const runtime = {
        callTool: () =>
          new Promise((resolve) => {
            setTimeout(() => resolve('done'), 1000);
          }),
        close,
      };
      const promise = handleCall(runtime as never, ['chrome-devtools.list_pages', '--timeout', '10']);
      const expectation = expect(promise).rejects.toThrow('Call to chrome-devtools.list_pages timed out after 10ms.');
      await vi.runOnlyPendingTimersAsync();
      await expectation;
      expect(close).toHaveBeenCalledWith('chrome-devtools');
    } finally {
      vi.useRealTimers();
    }
  });

  it('auto-corrects near-miss tool names', async () => {
    const { handleCall } = await cliModulePromise;
    const callTool = vi
      .fn()
      .mockRejectedValueOnce(new Error('MCP error -32602: Tool listIssues not found'))
      .mockResolvedValueOnce({ ok: true });
    const listTools = vi.fn().mockResolvedValue([{ name: 'list_issues' }]);
    const runtime = {
      callTool,
      listTools,
      close: vi.fn().mockResolvedValue(undefined),
    } as unknown as Awaited<ReturnType<(typeof import('../src/runtime.js'))['createRuntime']>>;

    const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});

    await handleCall(runtime, ['linear.listIssues']);

    const notes = logSpy.mock.calls.map((call) => call.join(' '));
    expect(notes.some((line) => line.includes('Auto-corrected tool call to linear.list_issues'))).toBe(true);
    expect(callTool).toHaveBeenCalledTimes(2);
    expect(callTool).toHaveBeenNthCalledWith(1, 'linear', 'listIssues', expect.objectContaining({ args: {} }));
    expect(callTool).toHaveBeenNthCalledWith(2, 'linear', 'list_issues', expect.objectContaining({ args: {} }));
    expect(listTools).toHaveBeenCalledWith('linear', {
      autoAuthorize: true,
      includeSchema: false,
      allowCachedAuth: true,
      disableOAuth: undefined,
    });

    logSpy.mockRestore();
  });

  it('suggests similar tool names when the match is uncertain', async () => {
    const { handleCall } = await cliModulePromise;
    const callTool = vi.fn().mockRejectedValue(new Error('MCP error -32602: Tool listIssues not found'));
    const listTools = vi.fn().mockResolvedValue([{ name: 'list_issue_statuses' }]);
    const runtime = {
      callTool,
      listTools,
      close: vi.fn().mockResolvedValue(undefined),
    } as unknown as Awaited<ReturnType<(typeof import('../src/runtime.js'))['createRuntime']>>;

    const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});

    await expect(handleCall(runtime, ['linear.listIssues'])).rejects.toThrow('listIssues not found');

    const messages = errorSpy.mock.calls.map((call) => call.join(' '));
    expect(messages.some((line) => line.includes('Did you mean linear.list_issue_statuses'))).toBe(true);

    errorSpy.mockRestore();
  });

  it("falls back to 'list' output when calling a missing help tool", async () => {
    const listModule = await import('../src/cli/list-command.js');
    const listSpy = vi.spyOn(listModule, 'handleList').mockResolvedValue(undefined);
    const { handleCall } = await cliModulePromise;
    const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
    const definition: ServerDefinition = {
      name: 'chrome-devtools',
      description: 'Chrome DevTools MCP server',
      command: { kind: 'stdio', command: 'chrome-devtools', args: [], cwd: process.cwd() },
      source: { kind: 'local', path: '<test>' },
    };
    const { runtime, callTool } = createRuntimeStub(
      {
        'chrome-devtools': [
          {
            name: 'take_snapshot',
            description: 'Takes a snapshot.',
            inputSchema: {
              type: 'object',
              properties: { url: { type: 'string' } },
              required: ['url'],
            },
          },
        ],
      },
      { definitions: [definition] }
    );

    try {
      await handleCall(runtime, ['chrome-devtools.help']);
      expect(listSpy).toHaveBeenNthCalledWith(1, runtime, ['chrome-devtools']);
      expect(
        logSpy.mock.calls.some((call) => call.some((line) => line.includes("does not expose a 'help' tool")))
      ).toBe(true);
      logSpy.mockClear();
      await handleCall(runtime, ['chrome-devtools.help', '--output', 'json']);
      expect(listSpy).toHaveBeenNthCalledWith(2, runtime, ['chrome-devtools', '--json']);
      expect(callTool).not.toHaveBeenCalled();
    } finally {
      listSpy.mockRestore();
      logSpy.mockRestore();
    }
  });

  it('treats list_tools selector as a shortcut for mcporter list', async () => {
    const listModule = await import('../src/cli/list-command.js');
    const listSpy = vi.spyOn(listModule, 'handleList').mockResolvedValue(undefined);
    const { handleCall } = await cliModulePromise;
    const definition: ServerDefinition = {
      name: 'chrome-devtools',
      description: 'Chrome DevTools MCP server',
      command: { kind: 'stdio', command: 'chrome-devtools', args: [], cwd: process.cwd() },
      source: { kind: 'local', path: '<test>' },
    };
    const { runtime, callTool } = createRuntimeStub({ 'chrome-devtools': [] }, { definitions: [definition] });

    try {
      await handleCall(runtime, ['chrome-devtools.list_tools']);
      await handleCall(runtime, ['chrome-devtools.list_tools', '--output', 'json']);
      expect(listSpy).toHaveBeenNthCalledWith(1, runtime, ['chrome-devtools']);
      expect(listSpy).toHaveBeenNthCalledWith(2, runtime, ['chrome-devtools', '--json']);
      expect(callTool).not.toHaveBeenCalled();
    } finally {
      listSpy.mockRestore();
    }
  });
});

function createRuntimeStub(
  toolCatalog: Record<
    string,
    Array<{
      name: string;
      description?: string;
      inputSchema?: unknown;
    }>
  >,
  options: { definitions?: ServerDefinition[] } = {}
): {
  runtime: Awaited<ReturnType<(typeof import('../src/runtime.js'))['createRuntime']>>;
  callTool: ReturnType<typeof vi.fn>;
  listTools: ReturnType<typeof vi.fn>;
  registerDefinition: ReturnType<typeof vi.fn>;
} {
  const definitions = new Map<string, ServerDefinition>();
  for (const entry of options.definitions ?? []) {
    definitions.set(entry.name, entry);
  }
  const callTool = vi.fn().mockResolvedValue({ ok: true });
  const listTools = vi.fn().mockImplementation(async (server: string) => {
    const tools = toolCatalog[server];
    if (!tools) {
      throw new Error(`Unknown MCP server '${server}'.`);
    }
    return tools;
  });
  const close = vi.fn().mockResolvedValue(undefined);
  const registerDefinition = vi.fn().mockImplementation((definition: ServerDefinition) => {
    definitions.set(definition.name, definition);
  });
  const runtime = {
    getDefinitions: () => [...definitions.values()],
    getDefinition: vi.fn().mockImplementation((name: string) => {
      const definition = definitions.get(name);
      if (!definition) {
        throw new Error(`Unknown MCP server '${name}'.`);
      }
      return definition;
    }),
    registerDefinition,
    listTools,
    callTool,
    close,
  } as unknown as Awaited<ReturnType<(typeof import('../src/runtime.js'))['createRuntime']>>;
  return { runtime, callTool, listTools, registerDefinition };
}
