/**
 * Mock MCP server for integration tests (Issue #239 / Phase 7).
 *
 * Pure Node http.createServer instance. NOT an MCP Streamable HTTP SSE transport —
 * the real `@modelcontextprotocol/sdk` client cannot connect end-to-end through
 * this. Instead, this mock covers:
 *
 *   - OAuth discovery (`GET /.well-known/oauth-authorization-server`)
 *   - OAuth authorize (`GET /authorize` → 302 redirect with `?code=...&state=...`)
 *   - OAuth token (`POST /token`, grant=authorization_code or refresh_token)
 *   - Raw MCP JSON-RPC over HTTP (`POST /mcp` with `Authorization: Bearer ...`)
 *
 * Use cases:
 *   - OAuth dance integration tests: register server → start → callback → token roundtrip
 *   - api_key tests: validate that `tokenManager.getValidToken()` returns the right
 *     bearer and that the mock accepts it on `/mcp`
 *   - Refresh roundtrip tests: drive the /token endpoint with `grant_type=refresh_token`
 *
 * For full SDK transport tests, continue using fake `Client` objects (existing
 * pattern in `aggregator.test.ts` / `tool-executor.test.ts`).
 */
import { createServer, type IncomingMessage, type ServerResponse, type Server } from 'node:http';
import type { AddressInfo } from 'node:net';

/** Definition of a tool the mock advertises via `tools/list`. */
export interface MockTool {
  name: string;
  description?: string;
  inputSchema?: unknown;
}

export interface MockMcpServerOptions {
  /** OAuth client id accepted on the /token endpoint. Default 'test-client'. */
  clientId?: string;
  /** OAuth client secret accepted on the /token endpoint. Default 'test-secret'. */
  clientSecret?: string;
  /**
   * Static bearer token accepted on /mcp for api_key auth.
   * When set, /mcp accepts `Authorization: Bearer <staticToken>` without the
   * code → access_token dance.
   */
  staticToken?: string;
  /** Tools to advertise via tools/list. Default []. */
  tools?: MockTool[];
  /**
   * Custom tool-call handler. Receives the tool name and arguments,
   * returns the MCP `content[]` payload. Defaults to a stub that echoes
   * the tool name as a text content block.
   */
  callHandler?: (name: string, args: Record<string, unknown>) => { content: unknown[]; isError?: boolean };
  /**
   * When true, the next /token POST fails with 400 invalid_grant.
   * Test helper for refresh-failure paths. Auto-resets after consuming.
   */
  failNextTokenWith?: { status: number; body: unknown };
}

export interface MockMcpServer {
  /** http://127.0.0.1:<port> — pass to registry.upsert as the server origin. */
  origin: string;
  port: number;
  close(): Promise<void>;
  /** Issued auth-codes (set on /authorize, consumed on /token). */
  issuedCodes: Set<string>;
  /** access_token → { refresh, scope } for tokens issued by /token. */
  issuedTokens: Map<string, { refresh: string; scope: string | null }>;
  /** History of /mcp call payloads for assertions. */
  callLog: Array<{ method: string; params: unknown; authHeader: string | null }>;
  /** Set `failNextTokenWith` at runtime to inject an error on the next /token. */
  options: MockMcpServerOptions;
}

const DEFAULT_CALL_HANDLER: NonNullable<MockMcpServerOptions['callHandler']> = (name) => ({
  content: [{ type: 'text', text: `mock-called ${name}` }],
});

export async function startMockMcpServer(opts: MockMcpServerOptions = {}): Promise<MockMcpServer> {
  const options: MockMcpServerOptions = {
    clientId: opts.clientId ?? 'test-client',
    clientSecret: opts.clientSecret ?? 'test-secret',
    staticToken: opts.staticToken,
    tools: opts.tools ?? [],
    callHandler: opts.callHandler ?? DEFAULT_CALL_HANDLER,
    failNextTokenWith: opts.failNextTokenWith,
  };

  const issuedCodes = new Set<string>();
  const issuedTokens = new Map<string, { refresh: string; scope: string | null }>();
  const callLog: MockMcpServer['callLog'] = [];

  const server: Server = createServer((req, res) => {
    void handle(req, res).catch((err) => {
      res.statusCode = 500;
      res.end(JSON.stringify({ error: 'internal', detail: String(err) }));
    });
  });

  let port = 0;
  await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', () => resolve()));
  port = (server.address() as AddressInfo).port;
  const origin = `http://127.0.0.1:${port}`;

  async function readBody(req: IncomingMessage): Promise<string> {
    const chunks: Buffer[] = [];
    for await (const chunk of req) chunks.push(chunk as Buffer);
    return Buffer.concat(chunks).toString('utf-8');
  }

  function writeJson(res: ServerResponse, status: number, body: unknown): void {
    res.statusCode = status;
    res.setHeader('content-type', 'application/json');
    res.end(JSON.stringify(body));
  }

  async function handle(req: IncomingMessage, res: ServerResponse): Promise<void> {
    const url = new URL(req.url ?? '/', `http://${req.headers.host}`);

    // OAuth discovery
    if (url.pathname === '/.well-known/oauth-authorization-server' && req.method === 'GET') {
      writeJson(res, 200, {
        issuer: origin,
        authorization_endpoint: `${origin}/authorize`,
        token_endpoint: `${origin}/token`,
      });
      return;
    }

    // OAuth authorize: redirect with code+state
    if (url.pathname === '/authorize' && req.method === 'GET') {
      const code = `code-${Math.random().toString(36).slice(2)}`;
      issuedCodes.add(code);
      const state = url.searchParams.get('state') ?? '';
      const redirect = url.searchParams.get('redirect_uri') ?? '';
      res.statusCode = 302;
      res.setHeader('location', `${redirect}?code=${encodeURIComponent(code)}&state=${encodeURIComponent(state)}`);
      res.end();
      return;
    }

    // OAuth token endpoint (code → access; or refresh)
    if (url.pathname === '/token' && req.method === 'POST') {
      if (options.failNextTokenWith) {
        const fail = options.failNextTokenWith;
        options.failNextTokenWith = undefined;  // consume one-shot
        writeJson(res, fail.status, fail.body);
        return;
      }
      const body = await readBody(req);
      const form = new URLSearchParams(body);
      const grant = form.get('grant_type');

      if (grant === 'authorization_code') {
        const code = form.get('code') ?? '';
        if (!issuedCodes.has(code)) {
          writeJson(res, 400, { error: 'invalid_grant' });
          return;
        }
        issuedCodes.delete(code);
        const access = `at-${Math.random().toString(36).slice(2)}`;
        const refresh = `rt-${Math.random().toString(36).slice(2)}`;
        const scope = form.get('scope') ?? null;
        issuedTokens.set(access, { refresh, scope });
        writeJson(res, 200, {
          access_token: access,
          refresh_token: refresh,
          expires_in: 3600,
          scope,
          token_type: 'Bearer',
        });
        return;
      }

      if (grant === 'refresh_token') {
        const rt = form.get('refresh_token') ?? '';
        let matched: [string, { refresh: string; scope: string | null }] | undefined;
        for (const entry of issuedTokens.entries()) {
          if (entry[1].refresh === rt) { matched = entry; break; }
        }
        if (!matched) {
          writeJson(res, 400, { error: 'invalid_grant' });
          return;
        }
        const newAccess = `at-${Math.random().toString(36).slice(2)}`;
        issuedTokens.set(newAccess, matched[1]);
        // Old access token is invalidated (real OAuth servers vary; we mirror strict semantics).
        issuedTokens.delete(matched[0]);
        writeJson(res, 200, {
          access_token: newAccess,
          expires_in: 3600,
          token_type: 'Bearer',
        });
        return;
      }

      writeJson(res, 400, { error: 'unsupported_grant_type' });
      return;
    }

    // MCP JSON-RPC over HTTP
    if (url.pathname === '/mcp' && req.method === 'POST') {
      const authHeader = req.headers.authorization ?? null;
      const token = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : '';
      const isApiKey = options.staticToken && token === options.staticToken;
      const isOauth = issuedTokens.has(token);
      if (!token || (!isApiKey && !isOauth)) {
        writeJson(res, 401, { error: 'unauthorized' });
        return;
      }

      const body = await readBody(req);
      let payload: { method?: string; params?: { name?: string; arguments?: Record<string, unknown> } };
      try {
        payload = JSON.parse(body || '{}');
      } catch {
        writeJson(res, 400, { error: 'invalid_json' });
        return;
      }

      callLog.push({
        method: payload.method ?? '',
        params: payload.params ?? {},
        authHeader,
      });

      if (payload.method === 'tools/list') {
        writeJson(res, 200, { jsonrpc: '2.0', result: { tools: options.tools ?? [] } });
        return;
      }
      if (payload.method === 'tools/call') {
        const name = payload.params?.name ?? '';
        const args = payload.params?.arguments ?? {};
        const result = (options.callHandler ?? DEFAULT_CALL_HANDLER)(name, args);
        writeJson(res, 200, { jsonrpc: '2.0', result });
        return;
      }

      writeJson(res, 400, { error: 'bad method' });
      return;
    }

    res.statusCode = 404;
    res.end();
  }

  return {
    origin,
    port,
    issuedCodes,
    issuedTokens,
    callLog,
    options,
    close: () => new Promise<void>((resolve) => server.close(() => resolve())),
  };
}
