import { describe, it, expect, afterEach } from 'vitest';
import express from 'express';
import Database from 'better-sqlite3';
import { runMigrations } from '../db/migrate.js';
import { createRegistry } from '../mcp/registry.js';
import { createTokenManager } from '../mcp/token-manager.js';
import { createToolCache } from '../mcp/tool-cache.js';
import { createAdminRouter, createUserRouter, createUserServersRouter } from './mcp-api.js';
import request from 'supertest';

const openDbs: Database.Database[] = [];

function makeApp(opts: { currentRole: 'admin' | 'user' | 'anon'; userId?: string }) {
  const validKey = 'a'.repeat(64);
  process.env.MCP_ENCRYPTION_KEY = validKey;
  const db = new Database(':memory:');
  openDbs.push(db);
  db.exec(`CREATE TABLE users (id TEXT PRIMARY KEY);`);
  db.exec(`CREATE TABLE jobs (id TEXT PRIMARY KEY, wait_reason TEXT);`);
  db.exec(`CREATE TABLE local_tasks (id INTEGER PRIMARY KEY AUTOINCREMENT);`); // runMigrations needs this
  runMigrations(db);
  db.prepare('INSERT INTO users(id) VALUES(?)').run('u1');
  db.prepare('INSERT INTO users(id) VALUES(?)').run('u2');

  const reg = createRegistry(db);
  const tm = createTokenManager(db, { doRefresh: async () => ({ access_token: 'x' }) });
  const cache = createToolCache(db, 600);

  const userId = opts.userId ?? 'u1';

  const requireAdmin: express.RequestHandler = (_req, res, next) => {
    if (opts.currentRole === 'admin') next();
    else res.status(403).json({ error: 'admin required' });
  };
  const requireAuth: express.RequestHandler = (_req, res, next) => {
    if (opts.currentRole !== 'anon') next();
    else res.status(401).json({ error: 'unauth' });
  };

  const app = express();
  app.use(express.json());
  app.use(
    '/api/mcp/servers',
    createAdminRouter({
      db,
      registry: reg,
      tokenManager: tm,
      toolCache: cache,
      requireAdmin,
      requireAuth,
      getUserId: () => userId,
      insecureLocalTestMode: true,
    }),
  );
  app.use(
    '/api/mcp/connections',
    createUserRouter({
      db,
      registry: reg,
      tokenManager: tm,
      toolCache: cache,
      requireAdmin,
      requireAuth,
      getUserId: () => userId,
      insecureLocalTestMode: true,
    }),
  );
  app.use(
    '/api/mcp/user-servers',
    createUserServersRouter({
      db,
      registry: reg,
      tokenManager: tm,
      toolCache: cache,
      requireAdmin,
      requireAuth,
      getUserId: () => userId,
      insecureLocalTestMode: true,
    }),
  );
  return { app, db, reg, tm };
}

describe('mcp-api', () => {
  afterEach(() => {
    while (openDbs.length) {
      const db = openDbs.pop();
      try {
        db?.close();
      } catch {
        /* ignore */
      }
    }
    delete process.env.MCP_ENCRYPTION_KEY;
  });

  it('non-admin cannot POST /api/mcp/servers', async () => {
    const { app } = makeApp({ currentRole: 'user' });
    const res = await request(app).post('/api/mcp/servers').send({ id: 'canva' });
    expect(res.status).toBe(403);
  });

  it('admin can upsert (oauth) + list + delete', async () => {
    const { app } = makeApp({ currentRole: 'admin' });
    const post = await request(app).post('/api/mcp/servers').send({
      id: 'canva',
      name: 'Canva',
      url: 'http://127.0.0.1:1/mcp',
      authKind: 'oauth',
      oauthClientId: 'cid',
      oauthClientSecret: 'secret',
    });
    expect(post.status).toBe(200);
    const list = await request(app).get('/api/mcp/servers');
    expect(list.body.servers).toHaveLength(1);
    // authKind and ownerId included in response
    expect(list.body.servers[0].authKind).toBe('oauth');
    expect(list.body.servers[0].ownerId).toBeNull();
    // Secret must not leak
    expect(JSON.stringify(list.body.servers)).not.toContain('secret');

    const del = await request(app).delete('/api/mcp/servers/canva');
    expect(del.status).toBe(200);
  });

  it('admin can upsert api_key server', async () => {
    const { app } = makeApp({ currentRole: 'admin' });
    const post = await request(app).post('/api/mcp/servers').send({
      id: 'myapi',
      name: 'My API',
      url: 'http://127.0.0.1:1/mcp',
      authKind: 'api_key',
      staticToken: 'sk-test-admin',
    });
    expect(post.status).toBe(200);
    const list = await request(app).get('/api/mcp/servers');
    expect(list.body.servers[0].authKind).toBe('api_key');
    // Static token must not leak
    expect(JSON.stringify(list.body.servers)).not.toContain('sk-test-admin');
  });

  it('admin POST api_key fails without staticToken', async () => {
    const { app } = makeApp({ currentRole: 'admin' });
    const post = await request(app).post('/api/mcp/servers').send({
      id: 'myapi',
      name: 'My API',
      url: 'http://127.0.0.1:1/mcp',
      authKind: 'api_key',
    });
    expect(post.status).toBe(400);
  });

  it('user sees connection state with authKind + ownerId', async () => {
    const { app, reg, tm } = makeApp({ currentRole: 'user' });
    reg.upsert({
      id: 'canva',
      name: 'Canva',
      url: 'http://127.0.0.1:1/mcp',
      authKind: 'oauth',
      ownerId: null,
      oauthClientId: 'i',
      oauthClientSecret: 's',
      oauthScopes: null,
    });
    const accessTokenLiteral = 'access-token-do-not-leak-xyz';
    const refreshTokenLiteral = 'refresh-token-do-not-leak-xyz';
    tm.saveTokens({
      userId: 'u1',
      serverId: 'canva',
      accessToken: accessTokenLiteral,
      refreshToken: refreshTokenLiteral,
      expiresAt: new Date(Date.now() + 3600_000).toISOString(),
      scope: null,
    });

    const res = await request(app).get('/api/mcp/connections');
    expect(res.body.connections).toHaveLength(1);
    expect(res.body.connections[0]).toMatchObject({
      serverId: 'canva',
      serverName: 'Canva',
      connected: true,
      authKind: 'oauth',
      ownerId: null,
    });
    const serialized = JSON.stringify(res.body);
    expect(serialized).not.toContain(accessTokenLiteral);
    expect(serialized).not.toContain(refreshTokenLiteral);
  });

  it('connections GET uses listEnabledForUser (includes user-owned servers)', async () => {
    const { app, reg } = makeApp({ currentRole: 'user', userId: 'u1' });
    // Global server
    reg.upsert({
      id: 'global-server',
      name: 'Global',
      url: 'http://127.0.0.1:1/mcp',
      authKind: 'oauth',
      ownerId: null,
      oauthClientId: 'i',
      oauthClientSecret: 's',
      oauthScopes: null,
    });
    // User-owned server
    reg.upsert({
      id: 'u1-server',
      name: 'U1 Server',
      url: 'http://127.0.0.1:2/mcp',
      authKind: 'api_key',
      ownerId: 'u1',
      staticToken: 'sk-u1',
    });

    const res = await request(app).get('/api/mcp/connections');
    expect(res.status).toBe(200);
    const ids = res.body.connections.map((c: { serverId: string }) => c.serverId);
    expect(ids).toContain('global-server');
    expect(ids).toContain('u1-server');
  });

  it('user can POST /api/mcp/user-servers with api_key', async () => {
    const { app, reg } = makeApp({ currentRole: 'user', userId: 'u1' });
    const post = await request(app).post('/api/mcp/user-servers').send({
      id: 'my-tool',
      name: 'My Tool',
      url: 'http://127.0.0.1:9/mcp',
      authKind: 'api_key',
      staticToken: 'sk-test',
    });
    expect(post.status).toBe(200);

    // Server should appear in listEnabledForUser
    const servers = reg.listEnabledForUser('u1');
    expect(servers.find((s) => s.id === 'my-tool')).toBeTruthy();
    expect(servers.find((s) => s.id === 'my-tool')?.ownerId).toBe('u1');
  });

  it('user cannot DELETE another user\'s server (403)', async () => {
    // u2 creates a server, u1 tries to delete it
    const { db, reg } = makeApp({ currentRole: 'user', userId: 'u2' });
    reg.upsert({
      id: 'u2-tool',
      name: 'U2 Tool',
      url: 'http://127.0.0.1:9/mcp',
      authKind: 'api_key',
      ownerId: 'u2',
      staticToken: 'sk-u2',
    });

    // Now make an app as u1
    const validKey = 'a'.repeat(64);
    process.env.MCP_ENCRYPTION_KEY = validKey;
    const reg2 = createRegistry(db);
    const tm2 = createTokenManager(db, { doRefresh: async () => ({ access_token: 'x' }) });
    const cache2 = createToolCache(db, 600);
    const app2 = express();
    app2.use(express.json());
    app2.use('/api/mcp/user-servers', createUserServersRouter({
      db,
      registry: reg2,
      tokenManager: tm2,
      toolCache: cache2,
      requireAdmin: (_req, _res, next) => next(),
      requireAuth: (_req, _res, next) => next(),
      getUserId: () => 'u1',
      insecureLocalTestMode: true,
    }));

    const del = await request(app2).delete('/api/mcp/user-servers/u2-tool');
    expect(del.status).toBe(403);
  });

  it('user cannot DELETE a global server via user-servers route', async () => {
    const { app, reg } = makeApp({ currentRole: 'user', userId: 'u1' });
    reg.upsert({
      id: 'global-tool',
      name: 'Global Tool',
      url: 'http://127.0.0.1:9/mcp',
      authKind: 'oauth',
      ownerId: null,
      oauthClientId: 'cid',
      oauthClientSecret: 'csec',
      oauthScopes: null,
    });

    const del = await request(app).delete('/api/mcp/user-servers/global-tool');
    expect(del.status).toBe(403);
  });

  it('id collision: POST user-servers fails 409 if id already exists', async () => {
    const { app, reg } = makeApp({ currentRole: 'user', userId: 'u1' });
    // Pre-create a global server with same id
    reg.upsert({
      id: 'existing',
      name: 'Existing',
      url: 'http://127.0.0.1:9/mcp',
      authKind: 'oauth',
      ownerId: null,
      oauthClientId: 'cid',
      oauthClientSecret: 'csec',
      oauthScopes: null,
    });

    const post = await request(app).post('/api/mcp/user-servers').send({
      id: 'existing',
      name: 'My Tool',
      url: 'http://127.0.0.1:9/mcp',
      authKind: 'api_key',
      staticToken: 'sk-test',
    });
    expect(post.status).toBe(409);
  });

  it('user can update their own server (edit); blank token preserves the existing one', async () => {
    const { app, reg } = makeApp({ currentRole: 'user', userId: 'u1' });
    const create = await request(app).post('/api/mcp/user-servers').send({
      id: 'mine', name: 'Mine', url: 'http://127.0.0.1:9/mcp',
      authKind: 'api_key', staticToken: 'sk-original',
    });
    expect(create.status).toBe(200);

    // Edit: rename + add a custom header, leave the token blank (= keep existing).
    const edit = await request(app).post('/api/mcp/user-servers').send({
      id: 'mine', name: 'Mine Renamed', url: 'http://127.0.0.1:9/mcp',
      authKind: 'api_key', staticToken: '', authHeaderName: 'xc-mcp-token',
    });
    expect(edit.status).toBe(200);

    const full = reg.getDecrypted('mine');
    expect(full?.staticToken).toBe('sk-original');     // preserved, not wiped
    expect(full?.authHeaderName).toBe('xc-mcp-token');  // updated
    expect(reg.listEnabledForUser('u1').find((s) => s.id === 'mine')?.name).toBe('Mine Renamed');
  });

  it('admin edit of a global api_key server preserves the token when left blank', async () => {
    const { app, reg } = makeApp({ currentRole: 'admin' });
    await request(app).post('/api/mcp/servers').send({
      id: 'g', name: 'G', url: 'http://127.0.0.1:1/mcp',
      authKind: 'api_key', staticToken: 'sk-keep',
    });
    const edit = await request(app).post('/api/mcp/servers').send({
      id: 'g', name: 'G2', url: 'http://127.0.0.1:1/mcp',
      authKind: 'api_key', staticToken: '',
    });
    expect(edit.status).toBe(200);
    expect(reg.getDecrypted('g')?.staticToken).toBe('sk-keep');
  });

  it('DELETE /api/mcp/connections returns 400 for api_key global server', async () => {
    const { app, reg } = makeApp({ currentRole: 'user', userId: 'u1' });
    reg.upsert({
      id: 'apikey-global',
      name: 'API Key Global',
      url: 'http://127.0.0.1:9/mcp',
      authKind: 'api_key',
      ownerId: null,
      staticToken: 'sk-global',
    });

    const del = await request(app).delete('/api/mcp/connections/apikey-global');
    expect(del.status).toBe(400);
  });
});
