import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { createTestCommand } from '../../src/commands/test-mcp.js';

function makeSession(overrides: Partial<{
  initialize: () => Promise<unknown>;
  listTools: () => Promise<Array<{ name: string }>>;
  callTool: (name: string, args: Record<string, unknown>) => Promise<unknown>;
  close: () => Promise<void>;
}> = {}) {
  return {
    initialize: overrides.initialize ?? vi.fn(async () => ({ protocolVersion: '2024-11-05' })),
    listTools: overrides.listTools ?? vi.fn(async () => [{ name: 'echo' }, { name: 'search' }]),
    callTool: overrides.callTool ?? vi.fn(async () => ({ content: [{ type: 'text', text: 'hi' }] })),
    close: overrides.close ?? vi.fn(async () => { /* no-op */ }),
  };
}

describe('mcpctl test mcp', () => {
  const output: string[] = [];
  const log = (...args: unknown[]) => {
    output.push(args.map(String).join(' '));
  };

  beforeEach(() => {
    output.length = 0;
    process.exitCode = 0;
  });

  afterEach(() => {
    process.exitCode = 0;
  });

  it('exits 0 on happy path (health + initialize + tools/list)', async () => {
    const session = makeSession();
    const cmd = createTestCommand({
      log,
      createSession: () => session,
      healthCheck: async () => true,
    });
    await cmd.parseAsync(['mcp', 'https://mcp.example.com/projects/foo/mcp'], { from: 'user' });
    expect(process.exitCode).toBe(0);
    expect(session.initialize).toHaveBeenCalled();
    expect(session.listTools).toHaveBeenCalled();
    expect(output.join('\n')).toContain('Result:     PASS');
  });

  it('exits 1 when the /healthz preflight fails', async () => {
    const cmd = createTestCommand({
      log,
      createSession: () => makeSession(),
      healthCheck: async () => false,
    });
    await cmd.parseAsync(['mcp', 'https://mcp.example.com/projects/foo/mcp'], { from: 'user' });
    expect(process.exitCode).toBe(1);
    expect(output.join('\n')).toContain('healthz preflight failed');
  });

  it('exits 2 (contract fail) when --expect-tools are missing', async () => {
    const cmd = createTestCommand({
      log,
      createSession: () => makeSession({
        listTools: async () => [{ name: 'echo' }],
      }),
      healthCheck: async () => true,
    });
    await cmd.parseAsync(
      ['mcp', 'https://mcp.example.com/projects/foo/mcp', '--expect-tools', 'echo,search'],
      { from: 'user' },
    );
    expect(process.exitCode).toBe(2);
    expect(output.join('\n')).toContain('Missing:    search');
    expect(output.join('\n')).toContain('CONTRACT FAIL');
  });

  it('exits 0 when --expect-tools all match', async () => {
    const cmd = createTestCommand({
      log,
      createSession: () => makeSession({
        listTools: async () => [{ name: 'echo' }, { name: 'search' }, { name: 'x' }],
      }),
      healthCheck: async () => true,
    });
    await cmd.parseAsync(
      ['mcp', 'https://mcp.example.com/projects/foo/mcp', '--expect-tools', 'echo,search'],
      { from: 'user' },
    );
    expect(process.exitCode).toBe(0);
  });

  it('exits 1 on transport/auth failure (initialize throws)', async () => {
    const cmd = createTestCommand({
      log,
      createSession: () => makeSession({
        initialize: async () => { throw new Error('HTTP 401: unauthorized'); },
      }),
      healthCheck: async () => true,
    });
    await cmd.parseAsync(['mcp', 'https://mcp.example.com/projects/foo/mcp'], { from: 'user' });
    expect(process.exitCode).toBe(1);
    expect(output.join('\n')).toContain('Error:');
    expect(output.join('\n')).toContain('TRANSPORT/AUTH FAIL');
  });

  it('invokes --tool with --args and reports isError', async () => {
    const callTool = vi.fn(async () => ({ content: [{ type: 'text', text: 'oops' }], isError: true }));
    const cmd = createTestCommand({
      log,
      createSession: () => makeSession({ callTool }),
      healthCheck: async () => true,
    });
    await cmd.parseAsync(
      ['mcp', 'https://mcp.example.com/projects/foo/mcp', '--tool', 'echo', '--args', '{"msg":"hi"}'],
      { from: 'user' },
    );
    expect(callTool).toHaveBeenCalledWith('echo', { msg: 'hi' });
    expect(process.exitCode).toBe(2);
  });

  it('outputs a JSON report with -o json', async () => {
    const cmd = createTestCommand({
      log,
      createSession: () => makeSession(),
      healthCheck: async () => true,
    });
    await cmd.parseAsync(
      ['mcp', 'https://mcp.example.com/projects/foo/mcp', '-o', 'json'],
      { from: 'user' },
    );
    const parsed = JSON.parse(output.join('\n')) as { exitCode: number; tools: string[] };
    expect(parsed.exitCode).toBe(0);
    expect(parsed.tools).toEqual(['echo', 'search']);
  });

  it('reads $MCPCTL_TOKEN when --token is not given', async () => {
    let observedBearer: string | undefined;
    const cmd = createTestCommand({
      log,
      createSession: (_url, opts) => {
        observedBearer = opts.bearer;
        return makeSession();
      },
      healthCheck: async () => true,
    });
    const prev = process.env.MCPCTL_TOKEN;
    process.env.MCPCTL_TOKEN = 'mcpctl_pat_fromenv';
    try {
      await cmd.parseAsync(['mcp', 'https://mcp.example.com/projects/foo/mcp'], { from: 'user' });
    } finally {
      if (prev === undefined) delete process.env.MCPCTL_TOKEN;
      else process.env.MCPCTL_TOKEN = prev;
    }
    expect(observedBearer).toBe('mcpctl_pat_fromenv');
  });

  it('rejects invalid --args as JSON', async () => {
    const cmd = createTestCommand({
      log,
      createSession: () => makeSession(),
      healthCheck: async () => true,
    });
    await cmd.parseAsync(
      ['mcp', 'https://mcp.example.com/projects/foo/mcp', '--tool', 'echo', '--args', 'not-json'],
      { from: 'user' },
    );
    expect(process.exitCode).toBe(1);
    expect(output.join('\n')).toContain('must be valid JSON');
  });
});
