import { describe, expect, it, vi } from 'vitest';
import type { ServerDefinition } from '../src/config.js';
import { stripAnsi } from './fixtures/ansi.js';
import { buildLinearDocumentsTool, cliModulePromise, linearDefinition } from './fixtures/cli-list-fixtures.js';

describe('CLI list formatting', () => {
  it('prints detailed usage for single server listings', async () => {
    const { handleList } = await cliModulePromise;
    const listToolsSpy = vi.fn((_name: string, options?: { includeSchema?: boolean }) =>
      Promise.resolve([
        {
          name: 'add',
          description: 'Add two numbers',
          inputSchema: options?.includeSchema
            ? {
                type: 'object',
                properties: {
                  a: { type: 'number', description: 'First operand' },
                  format: { type: 'string', enum: ['json', 'markdown'], description: 'Output serialization format' },
                  dueBefore: { type: 'string', format: 'date-time', description: 'ISO 8601 timestamp' },
                },
                required: ['a'],
              }
            : undefined,
          outputSchema: options?.includeSchema
            ? {
                type: 'object',
                properties: {
                  result: { type: 'array', description: 'List of calculation results' },
                  total: { type: 'number', description: 'Total results returned' },
                },
              }
            : undefined,
        },
      ])
    );
    const runtime = {
      getDefinition: (name: string) => ({
        name,
        description: 'Test integration server',
        command: { kind: 'http', url: new URL('https://example.com/mcp') },
      }),
      listTools: listToolsSpy,
    } as unknown as Awaited<ReturnType<(typeof import('../src/runtime.js'))['createRuntime']>>;

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

    await handleList(runtime, ['calculator']);

    const rawLines = logSpy.mock.calls.map((call) => call.join(' '));
    const lines = rawLines.map(stripAnsi);

    const headerLine = lines.find((line) => line.trim().startsWith('calculator -'));
    expect(headerLine).toBeDefined();
    const summaryLine = lines.find((line) => line.includes('HTTP https://example.com/mcp'));
    expect(summaryLine).toBeDefined();
    expect(summaryLine).toMatch(/1 tool/);
    expect(summaryLine).toMatch(/ms/);
    expect(summaryLine).toContain('HTTP https://example.com/mcp');
    expect(lines.some((line) => line.includes('/**'))).toBe(true);
    const paramLineIndex = lines.findIndex((line) => line.includes('@param a'));
    expect(paramLineIndex).toBeGreaterThan(1);
    expect(lines[paramLineIndex - 1]?.trim()).toBe('*');
    expect(lines.some((line) => line.includes('@param a') && line.includes('First operand'))).toBe(true);
    expect(lines.some((line) => line.includes('function add('))).toBe(true);
    expect(lines.some((line) => line.includes('format?: "json" | "markdown"'))).toBe(true);
    expect(lines.some((line) => line.includes('dueBefore?: string'))).toBe(true);
    expect(lines.some((line) => line.includes('Examples:'))).toBe(true);
    expect(lines.some((line) => line.includes('mcporter call calculator.add(a: 1'))).toBe(true);
    expect(
      lines.some((line) => line.includes('Optional parameters hidden; run with --all-parameters to view all fields'))
    ).toBe(false);
    expect(listToolsSpy).toHaveBeenCalledWith('calculator', expect.objectContaining({ includeSchema: true }));

    logSpy.mockRestore();
  });

  it('emits JSON summaries for multi-server listings when --json is provided', async () => {
    const { handleList } = await cliModulePromise;
    const originalCI = process.env.CI;
    process.env.CI = '1';
    const definitions: ServerDefinition[] = [
      linearDefinition,
      {
        name: 'github',
        command: { kind: 'http', url: new URL('https://example.com/mcp') },
      },
    ];
    const runtime = {
      getDefinitions: () => definitions,
      listTools: (name: string) => {
        if (name === 'linear') {
          return Promise.resolve([{ name: 'list_documents' }]);
        }
        return Promise.reject(new Error('HTTP error 500: upstream unavailable'));
      },
    } as unknown as Awaited<ReturnType<(typeof import('../src/runtime.js'))['createRuntime']>>;
    const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
    await handleList(runtime, ['--json']);
    const payload = JSON.parse(logSpy.mock.calls.at(-1)?.[0] ?? '{}');
    expect(payload.mode).toBe('list');
    expect(payload.servers).toHaveLength(2);
    const github = payload.servers.find((entry: { name: string }) => entry.name === 'github');
    expect(github.status).toBe('http');
    const linear = payload.servers.find((entry: { name: string }) => entry.name === 'linear');
    expect(linear.tools[0].name).toBe('list_documents');
    logSpy.mockRestore();
    process.env.CI = originalCI;
  });

  it('emits JSON payloads for single server listings when --json is provided', async () => {
    const { handleList } = await cliModulePromise;
    const toolCache = await import('../src/cli/tool-cache.js');
    const metadata = [
      {
        tool: {
          name: 'add',
          description: 'Add numbers',
          inputSchema: { type: 'object', properties: { a: { type: 'number' } }, required: ['a'] },
          outputSchema: { type: 'number' },
        },
        methodName: 'add',
        options: [],
      },
    ];
    const metadataSpy = vi.spyOn(toolCache, 'loadToolMetadata').mockResolvedValue(metadata as never);
    const definition: ServerDefinition = {
      name: 'linear',
      description: 'Hosted Linear MCP',
      command: { kind: 'http', url: new URL('https://example.com/mcp') },
    };
    const runtime = {
      getDefinitions: () => [definition],
      getDefinition: () => definition,
      registerDefinition: vi.fn(),
    } as unknown as Awaited<ReturnType<(typeof import('../src/runtime.js'))['createRuntime']>>;

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

    await handleList(runtime, ['--json', 'linear']);

    const payload = JSON.parse(logSpy.mock.calls.at(-1)?.[0] ?? '{}');
    expect(payload.mode).toBe('server');
    expect(payload.status).toBe('ok');
    expect(payload.tools[0].name).toBe('add');

    logSpy.mockRestore();
    metadataSpy.mockRestore();
  });

  it('emits JSON schemas for configured HTTP servers listed by name', async () => {
    const { handleList } = await cliModulePromise;
    const toolCache = await import('../src/cli/tool-cache.js');
    const metadata = [
      {
        tool: {
          name: 'check_login_status',
          description: 'Check login status',
          inputSchema: { type: 'object', properties: {} },
        },
        methodName: 'check_login_status',
        options: [],
      },
    ];
    const metadataSpy = vi.spyOn(toolCache, 'loadToolMetadata').mockResolvedValue(metadata as never);
    const definition: ServerDefinition = {
      name: 'xhs',
      command: { kind: 'http', url: new URL('http://127.0.0.1:18060/mcp') },
      source: { kind: 'local', path: '<test>' },
    };
    const registerDefinition = vi.fn();
    const runtime = {
      getDefinitions: () => [definition],
      getDefinition: () => definition,
      registerDefinition,
    } as unknown as Awaited<ReturnType<(typeof import('../src/runtime.js'))['createRuntime']>>;

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

    await handleList(runtime, ['xhs', '--schema', '--json']);

    const payload = JSON.parse(logSpy.mock.calls.at(-1)?.[0] ?? '{}');
    expect(payload).toMatchObject({
      mode: 'server',
      name: 'xhs',
      status: 'ok',
      tools: [{ name: 'check_login_status', inputSchema: { type: 'object', properties: {} } }],
    });
    expect(registerDefinition).not.toHaveBeenCalled();

    logSpy.mockRestore();
    metadataSpy.mockRestore();
  });

  it('surfaces initialize instructions in single server text and JSON output', async () => {
    const { handleList } = await cliModulePromise;
    const definition: ServerDefinition = {
      name: 'immich',
      description: 'Immich MCP',
      command: { kind: 'http', url: new URL('https://example.com/mcp') },
    };
    const runtime = {
      getDefinitions: () => [definition],
      getDefinition: () => definition,
      listTools: vi.fn().mockResolvedValue([{ name: 'search_assets' }]),
      getInstructions: vi.fn().mockResolvedValue('Use asset IDs from search results when calling mutation tools.'),
    } as unknown as Awaited<ReturnType<(typeof import('../src/runtime.js'))['createRuntime']>>;
    const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});

    await handleList(runtime, ['immich']);
    let lines = logSpy.mock.calls.map((call) => stripAnsi(call.join(' ')));
    expect(lines.some((line) => line.includes('Instructions: Use asset IDs from search results'))).toBe(true);

    logSpy.mockClear();
    await handleList(runtime, ['--json', 'immich']);
    const payload = JSON.parse(logSpy.mock.calls.at(-1)?.[0] ?? '{}');
    expect(payload.instructions).toBe('Use asset IDs from search results when calling mutation tools.');

    logSpy.mockRestore();
  });

  it('prints compact signatures for single server listings with --brief', async () => {
    const { handleList } = await cliModulePromise;
    const listToolsSpy = vi.fn((_name: string, options?: { includeSchema?: boolean }) =>
      Promise.resolve([buildLinearDocumentsTool(options?.includeSchema)])
    );
    const runtime = {
      getDefinitions: () => [linearDefinition],
      getDefinition: () => linearDefinition,
      listTools: listToolsSpy,
      getInstructions: vi.fn().mockResolvedValue('Use Linear IDs from list operations in mutation tools.'),
    } as unknown as Awaited<ReturnType<(typeof import('../src/runtime.js'))['createRuntime']>>;

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

    await handleList(runtime, ['linear', '--brief']);

    const lines = logSpy.mock.calls.map((call) => stripAnsi(call.join(' ')));
    expect(lines.some((line) => line.includes('Instructions: Use Linear IDs'))).toBe(true);
    expect(lines.some((line) => line.trim().startsWith('/**'))).toBe(false);
    expect(lines.some((line) => line.includes('Examples:'))).toBe(false);
    expect(lines.some((line) => line.includes('@param'))).toBe(false);
    expect(lines.some((line) => line.includes('function list_documents('))).toBe(true);
    expect(
      lines.some((line) => line.includes('// optional (4): projectId, initiativeId, creatorId, includeArchived'))
    ).toBe(true);
    expect(
      lines.some((line) => line.includes('Optional parameters hidden; run with --all-parameters to view all fields'))
    ).toBe(true);
    expect(listToolsSpy).toHaveBeenCalledWith('linear', expect.objectContaining({ includeSchema: true }));

    logSpy.mockRestore();
  });

  it('prints compact signatures for selected tools with --signatures', async () => {
    const { handleList } = await cliModulePromise;
    const listToolsSpy = vi.fn((_name: string, options?: { includeSchema?: boolean }) =>
      Promise.resolve([
        buildLinearDocumentsTool(options?.includeSchema),
        {
          name: 'create_comment',
          description: 'Create a comment on a specific Linear issue',
          inputSchema: options?.includeSchema
            ? {
                type: 'object',
                properties: {
                  issueId: { type: 'string', description: 'The issue ID' },
                  body: { type: 'string', description: 'Comment body as Markdown' },
                },
                required: ['issueId', 'body'],
              }
            : undefined,
        },
      ])
    );
    const runtime = {
      getDefinitions: () => [linearDefinition],
      getDefinition: () => linearDefinition,
      listTools: listToolsSpy,
    } as unknown as Awaited<ReturnType<(typeof import('../src/runtime.js'))['createRuntime']>>;

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

    await handleList(runtime, ['linear.create_comment', '--signatures']);

    const lines = logSpy.mock.calls.map((call) => stripAnsi(call.join(' ')));
    expect(lines.some((line) => line.includes('function create_comment('))).toBe(true);
    expect(lines.some((line) => line.includes('function list_documents('))).toBe(false);
    expect(lines.some((line) => line.includes('Examples:'))).toBe(false);
    expect(lines.find((line) => line.includes('HTTP https://example.com/mcp'))).toMatch(/1 tool/);

    logSpy.mockRestore();
  });

  it('prints only the selected tool when listing server.tool with schemas', async () => {
    const { handleList } = await cliModulePromise;
    const listToolsSpy = vi.fn((_name: string, options?: { includeSchema?: boolean }) =>
      Promise.resolve([
        {
          name: 'add',
          description: 'Add numbers',
          inputSchema: options?.includeSchema
            ? { type: 'object', properties: { a: { type: 'number' } }, required: ['a'] }
            : undefined,
          outputSchema: { type: 'number' },
        },
        {
          name: 'subtract',
          description: 'Subtract numbers',
          inputSchema: options?.includeSchema
            ? { type: 'object', properties: { a: { type: 'number' }, b: { type: 'number' } }, required: ['a', 'b'] }
            : undefined,
        },
      ])
    );
    const definition: ServerDefinition = {
      name: 'calculator',
      description: 'Math tools',
      command: { kind: 'http', url: new URL('https://example.com/mcp') },
    };
    const runtime = {
      getDefinitions: () => [definition],
      getDefinition: () => definition,
      listTools: listToolsSpy,
    } as unknown as Awaited<ReturnType<(typeof import('../src/runtime.js'))['createRuntime']>>;
    const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});

    await handleList(runtime, ['calculator.add', '--schema']);

    const lines = logSpy.mock.calls.map((call) => stripAnsi(call.join(' ')));
    expect(lines.some((line) => line.includes('function add('))).toBe(true);
    expect(lines.some((line) => line.includes('function subtract('))).toBe(false);
    expect(lines.some((line) => line.includes('"properties"'))).toBe(true);
    expect(lines.find((line) => line.includes('HTTP https://example.com/mcp'))).toMatch(/1 tool/);
    expect(listToolsSpy).toHaveBeenCalledWith('calculator', expect.objectContaining({ includeSchema: true }));

    logSpy.mockRestore();
  });

  it('summarizes hidden optional parameters and hints include flag', async () => {
    const { handleList } = await cliModulePromise;
    const listToolsSpy = vi.fn((_name: string, options?: { includeSchema?: boolean }) =>
      Promise.resolve([buildLinearDocumentsTool(options?.includeSchema)])
    );
    const runtime = {
      getDefinition: () => linearDefinition,
      listTools: listToolsSpy,
    } as unknown as Awaited<ReturnType<(typeof import('../src/runtime.js'))['createRuntime']>>;

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

    await handleList(runtime, ['linear']);

    const lines = logSpy.mock.calls.map((call) => stripAnsi(call.join(' ')));
    expect(lines.some((line) => line.includes('function list_documents('))).toBe(true);
    expect(
      lines.some((line) => line.includes('// optional (4): projectId, initiativeId, creatorId, includeArchived'))
    ).toBe(true);
    expect(
      lines.some((line) => line.includes('Optional parameters hidden; run with --all-parameters to view all fields'))
    ).toBe(true);
    expect(listToolsSpy).toHaveBeenCalledWith('linear', expect.objectContaining({ includeSchema: true }));

    logSpy.mockRestore();
  });

  it('truncates long examples for readability', async () => {
    const { handleList } = await cliModulePromise;
    const listToolsSpy = vi.fn((_name: string, options?: { includeSchema?: boolean }) =>
      Promise.resolve([buildLinearDocumentsTool(options?.includeSchema)])
    );
    const runtime = {
      getDefinition: () => linearDefinition,
      listTools: listToolsSpy,
    } as unknown as Awaited<ReturnType<(typeof import('../src/runtime.js'))['createRuntime']>>;

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

    await handleList(runtime, ['linear']);

    const lines = logSpy.mock.calls.map((call) => stripAnsi(call.join(' ')));
    const exampleLines = lines.filter((line) => line.includes('mcporter call linear.'));
    expect(exampleLines).toHaveLength(1);
    const exampleLine = exampleLines[0] as string;
    expect(exampleLine.length).toBeLessThanOrEqual(90);
    expect(exampleLine).toMatch(/, ...\)$/);

    logSpy.mockRestore();
  });

  it('indents multi-line parameter docs beneath the @param label', async () => {
    const { handleList } = await cliModulePromise;
    const listToolsSpy = vi.fn((_name: string, options?: { includeSchema?: boolean }) =>
      Promise.resolve([
        {
          name: 'list_projects',
          description: 'List Vercel projects',
          inputSchema: options?.includeSchema
            ? {
                type: 'object',
                properties: {
                  teamId: {
                    type: 'string',
                    description: `The team ID to target.\nTeam IDs start with "team_".\n- Read the file .vercel/project.json\n- Use the list_teams tool`,
                  },
                },
                required: ['teamId'],
              }
            : undefined,
        },
      ])
    );
    const runtime = {
      getDefinition: () => linearDefinition,
      listTools: listToolsSpy,
    } as unknown as Awaited<ReturnType<(typeof import('../src/runtime.js'))['createRuntime']>>;

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

    await handleList(runtime, ['linear']);

    const lines = logSpy.mock.calls.map((call) => stripAnsi(call.join(' ')));
    expect(lines.some((line) => line.includes('@param teamId'))).toBe(true);
    const continuationLine = lines.find((line) => line.includes('Team IDs start with "team_"'));
    expect(continuationLine).toBeDefined();
    expect(continuationLine?.includes('*               Team IDs start with "team_"')).toBe(true);

    logSpy.mockRestore();
  });

  it('includes optional parameters when --all-parameters is set', async () => {
    const { handleList } = await cliModulePromise;
    const listToolsSpy = vi.fn((_name: string, options?: { includeSchema?: boolean }) =>
      Promise.resolve([buildLinearDocumentsTool(options?.includeSchema)])
    );
    const runtime = {
      getDefinition: () => linearDefinition,
      listTools: listToolsSpy,
    } as unknown as Awaited<ReturnType<(typeof import('../src/runtime.js'))['createRuntime']>>;

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

    await handleList(runtime, ['--all-parameters', 'linear']);

    const lines = logSpy.mock.calls.map((call) => stripAnsi(call.join(' ')));

    const headerLine = lines.find((line) => line.trim().startsWith('linear -'));
    expect(headerLine).toBeDefined();
    const summaryLine = lines.find((line) => line.includes('HTTP https://example.com/mcp'));
    expect(summaryLine).toBeDefined();
    expect(summaryLine).toMatch(/1 tool/);
    expect(summaryLine).toMatch(/ms/);
    expect(summaryLine).toContain('HTTP https://example.com/mcp');
    expect(lines.some((line) => line.includes('/**'))).toBe(true);
    expect(lines.some((line) => line.includes('@param limit?') && line.includes('Maximum number of documents'))).toBe(
      true
    );
    expect(lines.some((line) => line.includes('function list_documents('))).toBe(true);
    expect(lines.some((line) => line.includes('limit?: number'))).toBe(true);
    expect(lines.some((line) => line.includes('orderBy?: "createdAt" | "updatedAt"'))).toBe(true);
    expect(lines.some((line) => line.includes('includeArchived?: boolean'))).toBe(true);
    expect(listToolsSpy).toHaveBeenCalledWith('linear', expect.objectContaining({ includeSchema: true }));

    logSpy.mockRestore();
  });

  it('matches the expected formatted snapshot for a complex server', async () => {
    const { handleList } = await cliModulePromise;
    const listToolsSpy = vi.fn((_name: string, options?: { includeSchema?: boolean }) =>
      Promise.resolve([
        buildLinearDocumentsTool(options?.includeSchema),
        {
          name: 'create_comment',
          description: 'Create a comment on a specific Linear issue',
          inputSchema: options?.includeSchema
            ? {
                type: 'object',
                properties: {
                  issueId: { type: 'string', description: 'The issue ID' },
                  parentId: { type: 'string', description: 'Optional parent comment ID' },
                  body: { type: 'string', description: 'Comment body as Markdown' },
                },
                required: ['issueId', 'body'],
              }
            : undefined,
          outputSchema: options?.includeSchema
            ? {
                title: 'Comment',
                type: 'object',
              }
            : undefined,
        },
      ])
    );
    const runtime = {
      getDefinition: () => linearDefinition,
      listTools: listToolsSpy,
    } as unknown as Awaited<ReturnType<(typeof import('../src/runtime.js'))['createRuntime']>>;

    const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
    const nowSpy = vi.spyOn(Date, 'now').mockReturnValue(1_700_000_000_000);

    await handleList(runtime, ['linear']);

    nowSpy.mockRestore();

    const lines = logSpy.mock.calls.map((call) => stripAnsi(call.join(' ')));
    expect(lines.join('\n')).toMatchInlineSnapshot(`
      "linear - Hosted Linear MCP

        /**
         * List documents in the user's Linear workspace
         *
         * @param query The search query
         * @param limit? Maximum number of documents to return
         * @param before? Cursor to page backwards
         * @param after? Cursor to page forwards
         * @param orderBy? Sort order for the documents
         * @param projectId? Filter by project
         * @param initiativeId? Filter by initiative
         * @param creatorId? Filter by creator
         * @param includeArchived? Whether to include archived documents
         */
        function list_documents(query: string, limit?: number, before?: string, after?: string, orderBy?: "createdAt" | "updatedAt"): DocumentConnection;
        // optional (4): projectId, initiativeId, creatorId, includeArchived

        /**
         * Create a comment on a specific Linear issue
         *
         * @param issueId The issue ID
         * @param parentId? Optional parent comment ID
         * @param body Comment body as Markdown
         */
        function create_comment(issueId: string, parentId?: string, body: string): Comment;

        Examples:
          mcporter call linear.list_documents(query: "value", limit: 1, orderBy: "cr, ...)

        Optional parameters hidden; run with --all-parameters to view all fields.

        2 tools · 0ms · HTTP https://example.com/mcp
      "
    `);

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