import { Command } from 'commander';
import { McpHttpSession, McpProtocolError, McpTransportError, deriveBaseUrl, mcpHealthCheck } from '@mcpctl/shared';

export interface TestMcpCommandDeps {
  log: (...args: unknown[]) => void;
  /**
   * Inject a session factory for testing. The default creates a real `McpHttpSession`.
   */
  createSession?: (url: string, opts: { bearer?: string; timeoutMs?: number }) => {
    initialize(): Promise<unknown>;
    listTools(): Promise<Array<{ name: string }>>;
    callTool(name: string, args: Record<string, unknown>): Promise<unknown>;
    close(): Promise<void>;
  };
  healthCheck?: (baseUrl: string) => Promise<boolean>;
}

export type TestMcpExitCode = 0 | 1 | 2;

export interface TestMcpReport {
  url: string;
  health: 'ok' | 'fail' | 'skipped';
  initialize: 'ok' | 'fail';
  tools: string[] | null;
  toolCall?: { name: string; result: unknown; isError?: boolean };
  missingTools?: string[];
  exitCode: TestMcpExitCode;
  error?: string;
}

export function createTestCommand(deps: TestMcpCommandDeps): Command {
  const { log } = deps;
  const createSession = deps.createSession ?? ((url, opts) => new McpHttpSession(url, opts));
  const healthCheck = deps.healthCheck ?? mcpHealthCheck;

  const test = new Command('test').description('Utilities for testing MCP endpoints and config');

  test
    .command('mcp')
    .description('Verify a Streamable-HTTP MCP endpoint: health, initialize, tools/list, optionally call a tool.')
    .argument('<url>', 'Full URL of the MCP endpoint (e.g. https://mcp.example.com/projects/foo/mcp)')
    .option('--token <bearer>', 'Bearer token (also reads $MCPCTL_TOKEN)')
    .option('--tool <name>', 'Invoke a specific tool after listing')
    .option('--args <json>', 'JSON-encoded arguments for --tool', '{}')
    .option('--expect-tools <list>', 'Comma-separated tool names that MUST appear; fails otherwise')
    .option('--timeout <seconds>', 'Per-request timeout in seconds', '10')
    .option('-o, --output <format>', 'Output format: text or json', 'text')
    .option('--no-health', 'Skip the /healthz preflight check')
    .action(async (url: string, opts: {
      token?: string;
      tool?: string;
      args: string;
      expectTools?: string;
      timeout: string;
      output: string;
      health: boolean;
    }) => {
      const bearer = opts.token ?? process.env.MCPCTL_TOKEN;
      const timeoutMs = Number(opts.timeout) * 1000;
      if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
        throw new Error(`--timeout must be a positive number of seconds (got '${opts.timeout}')`);
      }

      const report: TestMcpReport = {
        url,
        health: 'skipped',
        initialize: 'fail',
        tools: null,
        exitCode: 1,
      };

      // 1. Health preflight
      if (opts.health !== false) {
        const baseUrl = deriveBaseUrl(url);
        const ok = await healthCheck(baseUrl);
        report.health = ok ? 'ok' : 'fail';
        if (!ok) {
          report.error = `healthz preflight failed at ${baseUrl}/healthz`;
          return emit(report, opts.output, log);
        }
      }

      const sessionOpts: { bearer?: string; timeoutMs: number } = { timeoutMs };
      if (bearer !== undefined) sessionOpts.bearer = bearer;
      const session = createSession(url, sessionOpts);

      try {
        // 2. Initialize
        await session.initialize();
        report.initialize = 'ok';

        // 3. tools/list
        const tools = await session.listTools();
        report.tools = tools.map((t) => t.name);

        // 4. --expect-tools check
        if (opts.expectTools !== undefined && opts.expectTools.trim() !== '') {
          const expected = opts.expectTools.split(',').map((s) => s.trim()).filter(Boolean);
          const missing = expected.filter((name) => !report.tools!.includes(name));
          if (missing.length > 0) {
            report.missingTools = missing;
            report.exitCode = 2;
            report.error = `Missing tools: ${missing.join(', ')}`;
            return emit(report, opts.output, log);
          }
        }

        // 5. Optional --tool call
        if (opts.tool !== undefined) {
          let parsedArgs: Record<string, unknown> = {};
          try {
            parsedArgs = JSON.parse(opts.args) as Record<string, unknown>;
          } catch {
            throw new Error(`--args must be valid JSON (got '${opts.args}')`);
          }
          const result = await session.callTool(opts.tool, parsedArgs);
          const toolCall: TestMcpReport['toolCall'] = { name: opts.tool, result };
          if (typeof result === 'object' && result !== null && 'isError' in result) {
            toolCall.isError = Boolean((result as { isError?: boolean }).isError);
          }
          report.toolCall = toolCall;
          if (toolCall.isError) {
            report.exitCode = 2;
            report.error = `Tool '${opts.tool}' returned isError=true`;
            return emit(report, opts.output, log);
          }
        }

        report.exitCode = 0;
      } catch (err) {
        if (err instanceof McpProtocolError) {
          report.exitCode = 1;
          report.error = `protocol error ${err.code}: ${err.message}`;
        } else if (err instanceof McpTransportError) {
          report.exitCode = 1;
          report.error = `transport error (HTTP ${err.status}): ${err.message}`;
        } else {
          report.exitCode = 1;
          report.error = err instanceof Error ? err.message : String(err);
        }
      } finally {
        await session.close().catch(() => { /* best-effort */ });
      }

      return emit(report, opts.output, log);
    });

  return test;
}

function emit(report: TestMcpReport, output: string, log: (...args: unknown[]) => void): void {
  if (output === 'json') {
    log(JSON.stringify(report, null, 2));
  } else {
    log(`URL:        ${report.url}`);
    log(`Health:     ${report.health}`);
    log(`Initialize: ${report.initialize}`);
    if (report.tools !== null) {
      log(`Tools (${report.tools.length}): ${report.tools.slice(0, 10).join(', ')}${report.tools.length > 10 ? `, …(+${report.tools.length - 10})` : ''}`);
    }
    if (report.missingTools !== undefined) {
      log(`Missing:    ${report.missingTools.join(', ')}`);
    }
    if (report.toolCall !== undefined) {
      log(`Tool call:  ${report.toolCall.name} → ${report.toolCall.isError ? 'ERROR' : 'ok'}`);
    }
    if (report.error !== undefined) {
      log(`Error:      ${report.error}`);
    }
    log(`Result:     ${report.exitCode === 0 ? 'PASS' : report.exitCode === 2 ? 'CONTRACT FAIL' : 'TRANSPORT/AUTH FAIL'}`);
  }

  if (report.exitCode !== 0) {
    process.exitCode = report.exitCode;
  }
}
