/**
 * Smoke tests: McpToken + HTTP-mode mcplocal end-to-end.
 *
 * Exercises the full public CLI contract:
 *   1. `mcpctl create project` + `mcpctl create mcptoken`
 *   2. `mcpctl test mcp <url> --token $TOK --expect-tools …` → exit 0
 *   3. Same token against a different project → exit 1 (403)
 *   4. Revoke the token, retry → exit 1 (401) within the negative-cache window
 *   5. --expect-tools <nonexistent> → exit 2 (contract failure)
 *
 * Target endpoint: $MCPGW_URL (default https://mcp.ad.itaz.eu). The containerized
 * mcplocal must be deployed and reachable. If the /healthz preflight fails we
 * skip the whole suite with a clear message.
 *
 * Run with: pnpm test:smoke
 */
import { describe, it, expect, beforeAll } from 'vitest';
import http from 'node:http';
import https from 'node:https';
import { execSync } from 'node:child_process';

const MCPGW_URL = process.env.MCPGW_URL ?? 'https://mcp.ad.itaz.eu';
const PROJECT_NAME = `smoke-mcptoken-${Date.now().toString(36)}`;
const TOKEN_NAME = 'smoketok';
const OTHER_PROJECT = 'smoke-mcptoken-other';

// The revocation assertion is only meaningful against the HTTP-mode `serve.ts`
// entry, which has the token-introspection cache (5s negative TTL). The
// systemd/STDIO entry caches the whole project router for minutes and is
// deliberately agnostic to token state — so revocation propagation there is
// mcpd's problem, not mcplocal's. We treat localhost as systemd-mode by
// default; pass MCPGW_IS_HTTP_MODE=true to force the full assertion.
const IS_HTTP_MODE = process.env.MCPGW_IS_HTTP_MODE === 'true'
  || (!/^(http|https):\/\/(localhost|127\.|0\.0\.0\.0)/i.test(MCPGW_URL));

interface CliResult { code: number; stdout: string; stderr: string }

function run(args: string): CliResult {
  try {
    const stdout = execSync(`mcpctl ${args}`, {
      encoding: 'utf-8',
      timeout: 30_000,
      stdio: ['ignore', 'pipe', 'pipe'],
    });
    return { code: 0, stdout: stdout.trim(), stderr: '' };
  } catch (err) {
    const e = err as { status?: number; stdout?: Buffer | string; stderr?: Buffer | string };
    return {
      code: e.status ?? 1,
      stdout: e.stdout ? (typeof e.stdout === 'string' ? e.stdout : e.stdout.toString('utf-8')) : '',
      stderr: e.stderr ? (typeof e.stderr === 'string' ? e.stderr : e.stderr.toString('utf-8')) : '',
    };
  }
}

function healthz(url: string, timeoutMs = 5000): Promise<boolean> {
  return new Promise((resolve) => {
    const parsed = new URL(`${url.replace(/\/$/, '')}/healthz`);
    const driver = parsed.protocol === 'https:' ? https : http;
    const req = driver.get(
      {
        hostname: parsed.hostname,
        port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
        path: parsed.pathname,
        timeout: timeoutMs,
      },
      (res) => {
        resolve((res.statusCode ?? 500) < 500);
        res.resume();
      },
    );
    req.on('error', () => resolve(false));
    req.on('timeout', () => { req.destroy(); resolve(false); });
  });
}

let gatewayUp = false;
let rawToken = '';
let knownToolName: string | undefined;

describe('mcptoken smoke', () => {
  beforeAll(async () => {
    gatewayUp = await healthz(MCPGW_URL);
    if (!gatewayUp) {
      // eslint-disable-next-line no-console
      console.warn(`\n  ○ mcptoken smoke: skipped — ${MCPGW_URL}/healthz unreachable. Set MCPGW_URL to override.\n`);
    }
  }, 20_000);

  it('creates the project and a project-scoped mcptoken', () => {
    if (!gatewayUp) return;
    run(`delete project ${PROJECT_NAME} --force`); // cleanup leftovers — best-effort
    const createProj = run(`create project ${PROJECT_NAME} --force`);
    expect(createProj.code).toBe(0);

    const createTok = run(`create mcptoken ${TOKEN_NAME} --project ${PROJECT_NAME} --rbac clone`);
    expect(createTok.code).toBe(0);
    const match = createTok.stdout.match(/mcpctl_pat_[A-Za-z0-9]+/);
    expect(match, 'raw token was printed to stdout').not.toBeNull();
    rawToken = match![0];
  });

  it('passes `mcpctl test mcp` against the token\'s project endpoint', () => {
    if (!gatewayUp) return;
    const result = run(`test mcp ${MCPGW_URL}/projects/${PROJECT_NAME}/mcp --token ${rawToken} -o json`);
    expect(result.code, result.stderr || result.stdout).toBe(0);
    const report = JSON.parse(result.stdout.slice(result.stdout.indexOf('{'))) as {
      exitCode: number;
      tools: string[] | null;
      initialize: string;
    };
    expect(report.exitCode).toBe(0);
    expect(report.initialize).toBe('ok');
    expect(Array.isArray(report.tools)).toBe(true);
    knownToolName = report.tools?.[0];
  });

  it('fails `mcpctl test mcp` against a different project with 403', () => {
    if (!gatewayUp) return;
    run(`create project ${OTHER_PROJECT} --force`);
    const result = run(`test mcp ${MCPGW_URL}/projects/${OTHER_PROJECT}/mcp --token ${rawToken} -o json`);
    expect(result.code).toBe(1);
    const report = JSON.parse(result.stdout.slice(result.stdout.indexOf('{'))) as { error?: string };
    expect(report.error ?? '').toMatch(/403|not valid for|project|Invalid/i);
  });

  it('exits 2 (contract failure) when --expect-tools names a nonexistent tool', () => {
    if (!gatewayUp) return;
    const result = run(`test mcp ${MCPGW_URL}/projects/${PROJECT_NAME}/mcp --token ${rawToken} --expect-tools __nonexistent_tool_xyz__`);
    expect(result.code).toBe(2);
  });

  it('returns 401 after the token is revoked (within the negative-cache window)', async () => {
    if (!gatewayUp) return;
    if (!IS_HTTP_MODE) {
      // eslint-disable-next-line no-console
      console.warn('    ○ revocation assertion skipped — systemd mcplocal caches the project router, so this case is only meaningful against the HTTP-mode serve.ts entry. Set MCPGW_IS_HTTP_MODE=true to force it.');
      // Still delete the token so cleanup runs the same way.
      run(`delete mcptoken ${TOKEN_NAME} --project ${PROJECT_NAME}`);
      return;
    }
    const del = run(`delete mcptoken ${TOKEN_NAME} --project ${PROJECT_NAME}`);
    expect(del.code).toBe(0);
    // Introspection negative TTL defaults to 5s — wait 7s to be safe.
    await new Promise((r) => setTimeout(r, 7_000));
    const result = run(`test mcp ${MCPGW_URL}/projects/${PROJECT_NAME}/mcp --token ${rawToken} -o json`);
    expect(result.code).toBe(1);
    const report = JSON.parse(result.stdout.slice(result.stdout.indexOf('{'))) as { error?: string };
    expect(report.error ?? '').toMatch(/401|revoked|Invalid token/i);
  }, 20_000);

  it('cleans up test fixtures', () => {
    if (!gatewayUp) return;
    run(`delete project ${PROJECT_NAME} --force`);
    run(`delete project ${OTHER_PROJECT} --force`);
    expect(knownToolName === undefined || typeof knownToolName === 'string').toBe(true);
  });
});
