import { describe, it, expect, beforeEach, vi } from 'vitest';
import Database from 'better-sqlite3';
import { createTokenManager } from './token-manager.js';
import { createRegistry } from './registry.js';
import { runMigrations } from '../db/migrate.js';
import { encrypt, loadKeyFromEnv } from './crypto.js';

describe('McpTokenManager', () => {
  const validKey = 'a'.repeat(64);
  let db: Database.Database;

  beforeEach(() => {
    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('u1')`).run();
    const r = createRegistry(db);
    r.upsert({
      id: 'canva',
      name: 'Canva',
      url: 'https://mcp.canva.com/mcp',
      authKind: 'oauth',
      ownerId: null,
      oauthClientId: 'cid',
      oauthClientSecret: 'cs',
      oauthScopes: null,
    });
    r.setDiscovery('canva', {
      issuer: 'https://canva.example',
      authorizationEndpoint: 'https://canva.example/auth',
      tokenEndpoint: 'https://canva.example/token',
      fingerprint: 'x',
    });
  });

  it('saveTokens + hasToken + getValidToken happy path', async () => {
    const tm = createTokenManager(db, { doRefresh: async () => { throw new Error('should not refresh'); } });
    const future = new Date(Date.now() + 60 * 60_000).toISOString();
    tm.saveTokens({
      userId: 'u1',
      serverId: 'canva',
      accessToken: 'at-1',
      refreshToken: 'rt-1',
      expiresAt: future,
      scope: 'read',
    });
    expect(tm.hasToken('u1', 'canva')).toBe(true);
    expect(await tm.getValidToken('u1', 'canva')).toBe('at-1');
  });

  it('throws McpNotConnectedError when no row exists', async () => {
    const tm = createTokenManager(db, { doRefresh: async () => ({ access_token: 'x' }) });
    await expect(tm.getValidToken('u1', 'canva')).rejects.toMatchObject({ name: 'McpNotConnectedError' });
  });

  it('refreshes when expired', async () => {
    const doRefresh = vi.fn().mockResolvedValue({
      access_token: 'at-2',
      refresh_token: 'rt-2',
      expires_in: 3600,
    });
    const tm = createTokenManager(db, { doRefresh });
    tm.saveTokens({
      userId: 'u1',
      serverId: 'canva',
      accessToken: 'at-1',
      refreshToken: 'rt-1',
      expiresAt: new Date(Date.now() - 60_000).toISOString(), // already expired
      scope: null,
    });
    const token = await tm.getValidToken('u1', 'canva');
    expect(token).toBe('at-2');
    expect(doRefresh).toHaveBeenCalledOnce();
    // Second call hits cache (not expired anymore)
    const token2 = await tm.getValidToken('u1', 'canva');
    expect(token2).toBe('at-2');
    expect(doRefresh).toHaveBeenCalledOnce();
  });

  it('deletes token on invalid_grant error', async () => {
    const doRefresh = vi.fn().mockRejectedValue(Object.assign(new Error('invalid_grant'), { code: 'invalid_grant' }));
    const tm = createTokenManager(db, { doRefresh });
    tm.saveTokens({
      userId: 'u1',
      serverId: 'canva',
      accessToken: 'at-1',
      refreshToken: 'rt-1',
      expiresAt: new Date(Date.now() - 60_000).toISOString(),
      scope: null,
    });
    await expect(tm.getValidToken('u1', 'canva')).rejects.toThrow();
    expect(tm.hasToken('u1', 'canva')).toBe(false);
  });

  describe('api_key auth', () => {
    beforeEach(() => {
      const r = createRegistry(db);
      r.upsert({
        id: 'apiserver',
        name: 'API Server',
        url: 'https://api.example.com/mcp',
        authKind: 'api_key',
        ownerId: 'u1',
        staticToken: 'static-tok-abc',
      });
    });

    it('hasToken returns true for api_key server with static_token_enc', () => {
      const tm = createTokenManager(db, { doRefresh: async () => { throw new Error('no refresh'); } });
      expect(tm.hasToken('u1', 'apiserver')).toBe(true);
    });

    it('hasToken returns false for unknown server', () => {
      const tm = createTokenManager(db, { doRefresh: async () => { throw new Error('no refresh'); } });
      expect(tm.hasToken('u1', 'nonexistent')).toBe(false);
    });

    it('getValidToken returns decrypted static token without refresh', async () => {
      const doRefresh = vi.fn();
      const tm = createTokenManager(db, { doRefresh });
      const tok = await tm.getValidToken('u1', 'apiserver');
      expect(tok).toBe('static-tok-abc');
      expect(doRefresh).not.toHaveBeenCalled();
    });

    it('getValidToken for api_key is user-agnostic (any userId works)', async () => {
      const tm = createTokenManager(db, { doRefresh: async () => { throw new Error('no refresh'); } });
      // api_key servers don't gate on userId — any user can use the server's token
      const tok = await tm.getValidToken('different-user', 'apiserver');
      expect(tok).toBe('static-tok-abc');
    });
  });
});
