/**
 * Smoke tests: Security issues — end-to-end validation against live system.
 *
 * Tests for identified attack vectors:
 * 1. mcplocal has no authentication (all endpoints open to any local process)
 * 2. CORS origin:true on mcplocal (any website can make cross-origin requests)
 * 3. /inspect endpoint leaks all MCP traffic without auth
 * 4. /proxymodel/replay allows unauthenticated LLM token consumption
 * 5. /projects/:name/override PUT allows unauthenticated runtime config changes
 * 6. audit-events endpoint accessible without RBAC
 * 7. x-service-account header can be set by any authenticated user
 * 8. externalUrl SSRF — internal IPs accepted in server definitions
 *
 * Run with: pnpm test:smoke
 */
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import http from 'node:http';
import https from 'node:https';
import { isMcplocalRunning, getMcplocalUrl, loadMcpdAuth } from './mcp-client.js';

const MCPLOCAL_URL = getMcplocalUrl();

// URL from config.json, token from credentials (matches the CLI itself).
// See loadMcpdAuth() JSDoc for why credentials.mcpdUrl is intentionally ignored.
const MCPD_CREDS = loadMcpdAuth();
const MCPD_EFFECTIVE_URL = MCPD_CREDS.url;

/** Low-level HTTP request helper. */
function httpRequest(opts: {
  url: string;
  method: string;
  headers?: Record<string, string>;
  body?: string;
  timeout?: number;
}): Promise<{ status: number; headers: http.IncomingHttpHeaders; body: string }> {
  return new Promise((resolve, reject) => {
    const parsed = new URL(opts.url);
    const driver = parsed.protocol === 'https:' ? https : http;
    const req = driver.request(
      {
        hostname: parsed.hostname,
        port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
        path: parsed.pathname + parsed.search,
        method: opts.method,
        headers: opts.headers,
        timeout: opts.timeout ?? 10_000,
      },
      (res) => {
        const chunks: Buffer[] = [];
        res.on('data', (chunk: Buffer) => chunks.push(chunk));
        res.on('end', () => {
          resolve({
            status: res.statusCode ?? 0,
            headers: res.headers,
            body: Buffer.concat(chunks).toString('utf-8'),
          });
        });
      },
    );
    req.on('error', reject);
    req.on('timeout', () => {
      req.destroy();
      reject(new Error('Request timed out'));
    });
    if (opts.body) req.write(opts.body);
    req.end();
  });
}

describe('Smoke: Security — mcplocal unauthenticated endpoints', () => {
  let available = false;

  beforeAll(async () => {
    console.log('');
    console.log('  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
    console.log('  Smoke Test: Security Issues');
    console.log('  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');

    available = await isMcplocalRunning();
    if (!available) {
      console.log('\n  ✗ mcplocal not running — all tests will be skipped\n');
    }
  }, 10_000);

  afterAll(() => {
    console.log('\n  ━━━ Security smoke tests complete ━━━\n');
  });

  // ── § 1  mcplocal has no authentication ──

  it('/inspect SSE endpoint is accessible without authentication', async () => {
    if (!available) return;

    // /inspect streams ALL MCP traffic (tool calls, arguments, responses)
    // for ALL projects to any unauthenticated local client
    const res = await httpRequest({
      url: `${MCPLOCAL_URL}/inspect`,
      method: 'GET',
      headers: { 'Accept': 'text/event-stream' },
      timeout: 3_000,
    }).catch((err) => {
      // Timeout is expected (SSE keeps connection open) — still means endpoint is accessible
      if ((err as Error).message.includes('timed out')) {
        return { status: 200, headers: {} as http.IncomingHttpHeaders, body: '' };
      }
      throw err;
    });

    // Should be accessible without auth (documenting the vulnerability)
    expect(res.status).toBeLessThan(400);
    console.log(`    ⚠ /inspect accessible without auth (status ${res.status})`);
  }, 10_000);

  it('/health/detailed leaks system info without authentication', async () => {
    if (!available) return;

    const res = await httpRequest({
      url: `${MCPLOCAL_URL}/health/detailed`,
      method: 'GET',
    });

    // 503 = monitor not configured, 200 = monitor available — either way, no auth required
    expect([200, 503]).toContain(res.status);
    console.log(`    ⚠ /health/detailed accessible without auth (status ${res.status})`);
  });

  it('/llm/health leaks provider info without authentication', async () => {
    if (!available) return;

    const res = await httpRequest({
      url: `${MCPLOCAL_URL}/llm/health`,
      method: 'GET',
    });

    expect(res.status).toBe(200);
    const body = JSON.parse(res.body) as Record<string, unknown>;
    // Leaks: provider name, status, possibly error messages
    if (body['provider']) {
      console.log(`    ⚠ /llm/health leaks provider info: ${body['provider']} (status: ${body['status']})`);
    }
  });

  it('/llm/models lists available models without authentication', async () => {
    if (!available) return;

    const res = await httpRequest({
      url: `${MCPLOCAL_URL}/llm/models`,
      method: 'GET',
    });

    expect(res.status).toBe(200);
    const body = JSON.parse(res.body) as { models?: unknown[]; provider?: string };
    console.log(`    ⚠ /llm/models lists ${body.models?.length ?? 0} models from ${body.provider ?? 'none'} without auth`);
  });

  it('/llm/providers lists all providers without authentication', async () => {
    if (!available) return;

    const res = await httpRequest({
      url: `${MCPLOCAL_URL}/llm/providers`,
      method: 'GET',
    });

    expect(res.status).toBe(200);
    const body = JSON.parse(res.body) as { providers?: string[] };
    if (body.providers && body.providers.length > 0) {
      console.log(`    ⚠ /llm/providers leaks: ${body.providers.join(', ')}`);
    }
  });

  it('/proxymodels lists pipeline configurations without authentication', async () => {
    if (!available) return;

    const res = await httpRequest({
      url: `${MCPLOCAL_URL}/proxymodels`,
      method: 'GET',
    });

    expect(res.status).toBe(200);
    const body = JSON.parse(res.body) as { proxymodels?: unknown[] };
    console.log(`    ⚠ /proxymodels lists ${(body.proxymodels ?? (Array.isArray(body) ? body : [])).length} pipeline configs without auth`);
  });

  // ── § 2  CORS origin:true ──

  it('CORS allows any origin', async () => {
    if (!available) return;

    // Simulate a browser cross-origin request from a malicious website
    const res = await httpRequest({
      url: `${MCPLOCAL_URL}/health`,
      method: 'GET',
      headers: {
        'Origin': 'https://evil-website.example.com',
      },
    });

    expect(res.status).toBe(200);
    const corsHeader = res.headers['access-control-allow-origin'];
    // origin:true means the server reflects back any Origin header
    expect(corsHeader).toBe('https://evil-website.example.com');
    console.log(`    ⚠ CORS allows origin: ${corsHeader}`);
  });

  it('CORS preflight allows any origin', async () => {
    if (!available) return;

    const res = await httpRequest({
      url: `${MCPLOCAL_URL}/health`,
      method: 'OPTIONS',
      headers: {
        'Origin': 'https://evil-website.example.com',
        'Access-Control-Request-Method': 'POST',
        'Access-Control-Request-Headers': 'content-type',
      },
    });

    // Preflight should be accepted
    expect(res.status).toBeLessThan(400);
    const allowOrigin = res.headers['access-control-allow-origin'];
    expect(allowOrigin).toBe('https://evil-website.example.com');
    console.log(`    ⚠ CORS preflight allows origin: ${allowOrigin}`);
  });

  // ── § 3  /projects/:name/override allows unauthenticated config changes ──

  it('GET /projects/:name/override readable without auth', async () => {
    if (!available) return;

    // Try a known project name — smoke-data exists from other smoke tests
    const res = await httpRequest({
      url: `${MCPLOCAL_URL}/projects/smoke-data/override`,
      method: 'GET',
    });

    // Even if 404 (no override set), endpoint responds without auth
    console.log(`    ⚠ /projects/smoke-data/override GET returns ${res.status} without auth`);
    expect(res.status).toBeLessThan(500);
  });
});

// ─────────────────────────────────────────────────────────
// § 4  mcpd security — audit-events RBAC bypass
// ─────────────────────────────────────────────────────────

describe('Smoke: Security — mcpd audit-events RBAC bypass', () => {
  let available = false;

  beforeAll(async () => {
    available = await isMcplocalRunning();
    if (!available || !MCPD_CREDS.token) {
      if (!MCPD_CREDS.token) console.log('  ⏭ No mcpd credentials — skipping mcpd security tests');
      return;
    }
  });

  it('audit-events accessible with any valid auth token (no RBAC)', async () => {
    if (!available || !MCPD_CREDS.token) return;

    // Any authenticated user can query ALL audit events regardless of RBAC bindings.
    // This is because 'audit-events' is not in mapUrlToPermission's resourceMap.
    const res = await httpRequest({
      url: `${MCPD_EFFECTIVE_URL}/api/v1/audit/events?limit=1`,
      method: 'GET',
      headers: {
        'Authorization': `Bearer ${MCPD_CREDS.token}`,
        'Accept': 'application/json',
      },
    });

    expect(res.status).toBe(200);
    const body = JSON.parse(res.body) as { events?: unknown[]; total?: number };
    console.log(`    ⚠ audit-events accessible without RBAC check (${body.total ?? 0} total events)`);
  });

  it('audit-events batch insert accepts events from any authenticated user', async () => {
    if (!available || !MCPD_CREDS.token) return;

    // Any authenticated user can INSERT audit events for ANY project.
    // They can also set verified=true and source='mcpd' to fake server-verified events.
    const res = await httpRequest({
      url: `${MCPD_EFFECTIVE_URL}/api/v1/audit/events`,
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${MCPD_CREDS.token}`,
        'Content-Type': 'application/json',
        'Accept': 'application/json',
      },
      body: JSON.stringify([
        {
          timestamp: new Date().toISOString(),
          sessionId: 'security-test-probe',
          projectName: 'security-test-canary',
          eventKind: 'gate_decision',
          source: 'security-test',
          verified: false,
          payload: { test: true, note: 'security test probe — safe to delete' },
        },
      ]),
    });

    expect(res.status).toBe(201);
    console.log(`    ⚠ audit-events POST accepted without RBAC check (status ${res.status})`);

    // Verify we can read it back
    const readRes = await httpRequest({
      url: `${MCPD_EFFECTIVE_URL}/api/v1/audit/events?projectName=security-test-canary&limit=1`,
      method: 'GET',
      headers: {
        'Authorization': `Bearer ${MCPD_CREDS.token}`,
        'Accept': 'application/json',
      },
    });

    const readBody = JSON.parse(readRes.body) as { events: Array<Record<string, unknown>> };
    if (readBody.events?.length > 0) {
      expect(readBody.events[0]!['sessionId']).toBe('security-test-probe');
      console.log('    ⚠ Injected audit event readable back — audit trail can be polluted');
    }
  });
});

// ─────────────────────────────────────────────────────────
// § 5  mcpd security — x-service-account header impersonation
// ─────────────────────────────────────────────────────────

describe('Smoke: Security — x-service-account header', () => {
  let available = false;

  beforeAll(async () => {
    available = await isMcplocalRunning();
    if (!available || !MCPD_CREDS.token) return;
  });

  it('any authenticated user can send x-service-account header', async () => {
    if (!available || !MCPD_CREDS.token) return;

    // The x-service-account header is trusted without verification.
    // If the user's regular RBAC would deny access, adding this header
    // might grant additional permissions from a service account's bindings.
    const res = await httpRequest({
      url: `${MCPD_EFFECTIVE_URL}/api/v1/servers`,
      method: 'GET',
      headers: {
        'Authorization': `Bearer ${MCPD_CREDS.token}`,
        'X-Service-Account': 'project:admin',
        'Accept': 'application/json',
      },
    });

    // The request is processed — the header is not rejected
    // Whether it actually grants extra permissions depends on RBAC definitions
    expect(res.status).toBeLessThan(500);
    console.log(`    ⚠ x-service-account header accepted (status ${res.status})`);
  });
});

// ─────────────────────────────────────────────────────────
// § 6  MCP proxy — RBAC action mismatch
// ─────────────────────────────────────────────────────────

describe('Smoke: Security — MCP proxy RBAC action', () => {
  let available = false;

  beforeAll(async () => {
    available = await isMcplocalRunning();
    if (!available || !MCPD_CREDS.token) return;
  });

  it('MCP proxy uses POST (create action) not run action', async () => {
    if (!available || !MCPD_CREDS.token) return;

    // The MCP proxy endpoint is POST /api/v1/mcp/proxy which maps to
    // servers:create in RBAC. A user with 'create' permission on servers
    // can execute arbitrary MCP tool calls, even if they don't have 'run' permission.
    //
    // This test verifies the endpoint exists and accepts POST
    const res = await httpRequest({
      url: `${MCPD_EFFECTIVE_URL}/api/v1/mcp/proxy`,
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${MCPD_CREDS.token}`,
        'Content-Type': 'application/json',
        'Accept': 'application/json',
      },
      body: JSON.stringify({
        serverId: 'nonexistent-server-id',
        method: 'tools/list',
      }),
    });

    // Will get 404 (server not found) or 403 — either confirms the endpoint exists
    // and uses POST mapping (→ servers:create), not a dedicated 'run' action
    expect([200, 403, 404, 500]).toContain(res.status);
    console.log(`    MCP proxy POST returned ${res.status} (expected 403 or 404)`);
  });
});

// ─────────────────────────────────────────────────────────
// § 7  externalUrl SSRF — create server with internal URL
// ─────────────────────────────────────────────────────────

describe('Smoke: Security — externalUrl SSRF', () => {
  let available = false;

  beforeAll(async () => {
    available = await isMcplocalRunning();
    if (!available || !MCPD_CREDS.token) return;
  });

  it('server creation accepts internal IP as externalUrl', async () => {
    if (!available || !MCPD_CREDS.token) return;

    // Attempt to create a server pointing to an internal IP.
    // If accepted, the MCP proxy would send requests to this internal address.
    const res = await httpRequest({
      url: `${MCPD_EFFECTIVE_URL}/api/v1/servers`,
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${MCPD_CREDS.token}`,
        'Content-Type': 'application/json',
        'Accept': 'application/json',
      },
      body: JSON.stringify({
        name: 'security-test-ssrf-canary',
        description: 'Security test — SSRF canary (safe to delete)',
        externalUrl: 'http://169.254.169.254/latest/meta-data/',
        transport: 'STREAMABLE_HTTP',
        replicas: 0,
      }),
    });

    if (res.status === 201 || res.status === 200) {
      console.log('    ⚠ Server created with cloud metadata URL as externalUrl — SSRF possible');

      // Clean up: delete the canary server
      const body = JSON.parse(res.body) as { id?: string };
      if (body.id) {
        await httpRequest({
          url: `${MCPD_EFFECTIVE_URL}/api/v1/servers/${body.id}`,
          method: 'DELETE',
          headers: {
            'Authorization': `Bearer ${MCPD_CREDS.token}`,
            'Accept': 'application/json',
          },
        }).catch(() => {});
        console.log('    ✓ Canary server cleaned up');
      }
    } else if (res.status === 403) {
      console.log('    ⏭ No create permission — cannot test SSRF (this is fine)');
    } else if (res.status === 409) {
      console.log('    ⏭ Server name conflict — canary already exists');
      // Clean up by name
      await httpRequest({
        url: `${MCPD_EFFECTIVE_URL}/api/v1/servers/security-test-ssrf-canary`,
        method: 'DELETE',
        headers: {
          'Authorization': `Bearer ${MCPD_CREDS.token}`,
          'Accept': 'application/json',
        },
      }).catch(() => {});
    } else {
      console.log(`    Server creation returned ${res.status}: ${res.body.slice(0, 200)}`);
    }

    // The test passes regardless — we're documenting behavior, not blocking CI
    expect(true).toBe(true);
  });

  it('server creation accepts localhost as externalUrl (self-SSRF)', async () => {
    if (!available || !MCPD_CREDS.token) return;

    const res = await httpRequest({
      url: `${MCPD_EFFECTIVE_URL}/api/v1/servers`,
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${MCPD_CREDS.token}`,
        'Content-Type': 'application/json',
        'Accept': 'application/json',
      },
      body: JSON.stringify({
        name: 'security-test-ssrf-localhost',
        description: 'Security test — localhost SSRF (safe to delete)',
        externalUrl: 'http://127.0.0.1:3100/api/v1/servers',
        transport: 'STREAMABLE_HTTP',
        replicas: 0,
      }),
    });

    if (res.status === 201 || res.status === 200) {
      console.log('    ⚠ Server created with localhost URL — self-SSRF to mcpd possible');
      const body = JSON.parse(res.body) as { id?: string };
      if (body.id) {
        await httpRequest({
          url: `${MCPD_EFFECTIVE_URL}/api/v1/servers/${body.id}`,
          method: 'DELETE',
          headers: {
            'Authorization': `Bearer ${MCPD_CREDS.token}`,
            'Accept': 'application/json',
          },
        }).catch(() => {});
      }
    } else if (res.status === 403) {
      console.log('    ⏭ No create permission — cannot test SSRF');
    } else if (res.status === 409) {
      await httpRequest({
        url: `${MCPD_EFFECTIVE_URL}/api/v1/servers/security-test-ssrf-localhost`,
        method: 'DELETE',
        headers: {
          'Authorization': `Bearer ${MCPD_CREDS.token}`,
          'Accept': 'application/json',
        },
      }).catch(() => {});
    }

    expect(true).toBe(true);
  });
});
