import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import express from 'express';
import { createServer, type Server } from 'node:http';
import Database from 'better-sqlite3';
import { runMigrations } from '../db/migrate.js';
import { createRegistry } from './registry.js';
import { createTokenManager } from './token-manager.js';
import { createMcpOauthRouter } from './oauth-routes.js';

async function startMockOauthServer(): Promise<{
  origin: string;
  expectedCode: string;
  close: () => Promise<void>;
}> {
  const server = createServer((req, res) => {
    if (req.url?.startsWith('/.well-known/oauth-authorization-server')) {
      res.setHeader('content-type', 'application/json');
      res.end(
        JSON.stringify({
          issuer: 'http://127.0.0.1',
          authorization_endpoint: `http://127.0.0.1:${(server.address() as { port: number }).port}/authorize`,
          token_endpoint: `http://127.0.0.1:${(server.address() as { port: number }).port}/token`,
        }),
      );
    } else if (req.url?.startsWith('/token')) {
      const chunks: Buffer[] = [];
      req.on('data', (c) => chunks.push(c));
      req.on('end', () => {
        res.setHeader('content-type', 'application/json');
        res.end(
          JSON.stringify({
            access_token: 'AT-1',
            refresh_token: 'RT-1',
            expires_in: 3600,
            scope: 'read',
          }),
        );
      });
    } else {
      res.statusCode = 404;
      res.end();
    }
  });
  await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
  const origin = `http://127.0.0.1:${(server.address() as { port: number }).port}`;
  return {
    origin,
    expectedCode: 'auth-code-123',
    close: () => new Promise((r) => server.close(() => r())),
  };
}

describe('mcp oauth routes', () => {
  const validKey = 'a'.repeat(64);
  let db: Database.Database;
  let mockOauth: Awaited<ReturnType<typeof startMockOauthServer>>;
  let app: express.Express;
  let appServer: Server;
  let appOrigin: string;
  let resumedCalls: Array<[string, string]>;

  beforeEach(async () => {
    process.env.MCP_ENCRYPTION_KEY = validKey;
    db = new Database(':memory:');
    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');

    mockOauth = await startMockOauthServer();

    const reg = createRegistry(db);
    reg.upsert({
      id: 'canva',
      name: 'Canva',
      url: mockOauth.origin + '/mcp',
      authKind: 'oauth',
      ownerId: null,
      oauthClientId: 'client',
      oauthClientSecret: 'secret',
      oauthScopes: 'read',
    });

    const tm = createTokenManager(db, { doRefresh: async () => ({ access_token: 'x' }) });
    resumedCalls = [];

    app = express();
    app.use(
      '/auth/mcp',
      createMcpOauthRouter({
        db,
        registry: reg,
        tokenManager: tm,
        pendingTtlMinutes: 10,
        getCallbackBaseUrl: () => appOrigin,
        getAuthenticatedUserId: () => 'u1',
        resumeWaitingJobs: (uid, sid) => {
          resumedCalls.push([uid, sid]);
        },
        insecureLocalTestMode: true,
      }),
    );
    appServer = app.listen(0, '127.0.0.1');
    await new Promise<void>((resolve) => appServer.once('listening', resolve));
    appOrigin = `http://127.0.0.1:${(appServer.address() as { port: number }).port}`;
  });

  afterEach(async () => {
    await new Promise<void>((r) => appServer.close(() => r()));
    await mockOauth.close();
    db.close();
    delete process.env.MCP_ENCRYPTION_KEY;
  });

  it('start redirects to authorization_endpoint and records pending row', async () => {
    const res = await fetch(`${appOrigin}/auth/mcp/canva/start`, { redirect: 'manual' });
    expect(res.status).toBe(302);
    const loc = res.headers.get('location')!;
    expect(loc).toContain('/authorize');
    expect(loc).toMatch(/code_challenge_method=S256/);
    const pendingCount = db.prepare('SELECT COUNT(*) AS c FROM mcp_oauth_pending').get() as {
      c: number;
    };
    expect(pendingCount.c).toBe(1);
  });

  it('callback exchanges code and saves tokens, then calls resumeWaitingJobs', async () => {
    // First, run start so state is recorded
    await fetch(`${appOrigin}/auth/mcp/canva/start`, { redirect: 'manual' });
    const pending = db.prepare('SELECT state FROM mcp_oauth_pending').get() as { state: string };
    const callbackRes = await fetch(
      `${appOrigin}/auth/mcp/canva/callback?code=some-code&state=${pending.state}`,
      { redirect: 'manual' },
    );
    expect(callbackRes.status).toBe(302);
    const token = db
      .prepare('SELECT * FROM user_mcp_tokens WHERE user_id=? AND server_id=?')
      .get('u1', 'canva');
    expect(token).toBeTruthy();
    expect(resumedCalls).toEqual([['u1', 'canva']]);
  });

  it('callback rejects reused state (single-use)', async () => {
    await fetch(`${appOrigin}/auth/mcp/canva/start`, { redirect: 'manual' });
    const pending = db.prepare('SELECT state FROM mcp_oauth_pending').get() as { state: string };
    const first = await fetch(
      `${appOrigin}/auth/mcp/canva/callback?code=c&state=${pending.state}`,
      { redirect: 'manual' },
    );
    expect(first.status).toBe(302);
    const second = await fetch(
      `${appOrigin}/auth/mcp/canva/callback?code=c&state=${pending.state}`,
      { redirect: 'manual' },
    );
    expect(second.status).toBe(400);
  });
});
