import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import Database from 'better-sqlite3';
import { createRegistry } from './registry.js';
import { runMigrations } from '../db/migrate.js';

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

  beforeEach(() => {
    process.env.MCP_ENCRYPTION_KEY = validKey;
    db = new Database(':memory:');
    db.pragma('foreign_keys = ON');
    // runMigrations expects these tables to exist (it ALTERs them).
    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(db);
    // Seed users referenced as owner_id in tests
    for (const id of ['u1', 'alice', 'bob']) {
      db.prepare('INSERT INTO users(id) VALUES(?)').run(id);
    }
  });

  afterEach(() => {
    db.close();
  });

  it('rejects invalid server ids', () => {
    const r = createRegistry(db);
    expect(() =>
      r.upsert({
        id: 'Bad ID!',
        name: 'Canva',
        url: 'https://mcp.canva.com/mcp',
        authKind: 'oauth',
        ownerId: null,
        oauthClientId: 'cid',
        oauthClientSecret: 'cs',
        oauthScopes: null,
      }),
    ).toThrow(/id must match/);
  });

  it('stores and returns without secret in public view (oauth)', () => {
    const r = createRegistry(db);
    r.upsert({
      id: 'canva',
      name: 'Canva',
      url: 'https://mcp.canva.com/mcp',
      authKind: 'oauth',
      ownerId: null,
      oauthClientId: 'cid',
      oauthClientSecret: 'cs-secret',
      oauthScopes: 'read write',
    });
    const pub = r.listPublic();
    expect(pub).toHaveLength(1);
    expect(pub[0].id).toBe('canva');
    expect(pub[0].authKind).toBe('oauth');
    expect(pub[0].ownerId).toBeNull();
    // @ts-expect-error - public view must not have the secret
    expect(pub[0].oauthClientSecret).toBeUndefined();
    // @ts-expect-error - public view must not have staticToken
    expect(pub[0].staticToken).toBeUndefined();

    const full = r.getDecrypted('canva');
    expect(full?.oauthClientSecret).toBe('cs-secret');
    expect(full?.staticToken).toBeNull();
  });

  it('stores and retrieves api_key server', () => {
    const r = createRegistry(db);
    r.upsert({
      id: 'myapi',
      name: 'My API',
      url: 'https://api.example.com/mcp',
      authKind: 'api_key',
      ownerId: 'u1',
      staticToken: 'tok-secret-123',
    });
    const pub = r.listPublic();
    expect(pub).toHaveLength(1);
    expect(pub[0].authKind).toBe('api_key');
    expect(pub[0].ownerId).toBe('u1');
    // @ts-expect-error - public view must not have staticToken
    expect(pub[0].staticToken).toBeUndefined();

    const full = r.getDecrypted('myapi');
    expect(full?.staticToken).toBe('tok-secret-123');
    expect(full?.oauthClientSecret).toBe(''); // empty placeholder for NOT NULL column
  });

  it('rejects oauth server missing oauthClientId', () => {
    const r = createRegistry(db);
    expect(() =>
      r.upsert({
        id: 'bad',
        name: 'Bad',
        url: 'https://bad.example/mcp',
        authKind: 'oauth',
        ownerId: null,
        // missing oauthClientId / oauthClientSecret
      }),
    ).toThrow(/oauthClientId and oauthClientSecret/);
  });

  it('rejects api_key server missing staticToken', () => {
    const r = createRegistry(db);
    expect(() =>
      r.upsert({
        id: 'bad2',
        name: 'Bad2',
        url: 'https://bad2.example/mcp',
        authKind: 'api_key',
        ownerId: null,
        // missing staticToken
      }),
    ).toThrow(/staticToken/);
  });

  it('round-trips a custom auth header name for api_key servers', () => {
    const r = createRegistry(db);
    r.upsert({
      id: 'nocodb',
      name: 'NocoDB',
      url: 'https://nocodb.example/mcp/abc',
      authKind: 'api_key',
      ownerId: 'u1',
      staticToken: 'tok',
      authHeaderName: 'xc-mcp-token',
    });
    expect(r.listPublic()[0].authHeaderName).toBe('xc-mcp-token');
    expect(r.getDecrypted('nocodb')?.authHeaderName).toBe('xc-mcp-token');
  });

  it('defaults authHeaderName to null when omitted', () => {
    const r = createRegistry(db);
    r.upsert({
      id: 'plain',
      name: 'Plain',
      url: 'https://plain.example/mcp',
      authKind: 'api_key',
      ownerId: 'u1',
      staticToken: 'tok',
    });
    expect(r.getDecrypted('plain')?.authHeaderName).toBeNull();
  });

  it('rejects authHeaderName on oauth servers', () => {
    const r = createRegistry(db);
    expect(() =>
      r.upsert({
        id: 'badhdr',
        name: 'BadHdr',
        url: 'https://b.example/mcp',
        authKind: 'oauth',
        ownerId: null,
        oauthClientId: 'i',
        oauthClientSecret: 's',
        authHeaderName: 'xc-mcp-token',
      }),
    ).toThrow(/only valid for authKind 'api_key'/);
  });

  it('rejects an auth header name containing illegal characters', () => {
    const r = createRegistry(db);
    expect(() =>
      r.upsert({
        id: 'badhdr2',
        name: 'BadHdr2',
        url: 'https://b2.example/mcp',
        authKind: 'api_key',
        ownerId: 'u1',
        staticToken: 'tok',
        authHeaderName: 'bad header: value',
      }),
    ).toThrow(/valid HTTP header token/);
  });

  it('treats a blank authHeaderName as null (no error)', () => {
    const r = createRegistry(db);
    r.upsert({
      id: 'blankhdr',
      name: 'BlankHdr',
      url: 'https://bh.example/mcp',
      authKind: 'api_key',
      ownerId: 'u1',
      staticToken: 'tok',
      authHeaderName: '   ',
    });
    expect(r.getDecrypted('blankhdr')?.authHeaderName).toBeNull();
  });

  it('listEnabledForUser returns global and own servers only', () => {
    const r = createRegistry(db);
    r.upsert({ id: 'global1', name: 'Global', url: 'https://g.example/mcp', authKind: 'oauth', ownerId: null, oauthClientId: 'g', oauthClientSecret: 'gs' });
    r.upsert({ id: 'user1', name: 'User1', url: 'https://u1.example/mcp', authKind: 'api_key', ownerId: 'alice', staticToken: 'tok' });
    r.upsert({ id: 'user2', name: 'User2', url: 'https://u2.example/mcp', authKind: 'api_key', ownerId: 'bob', staticToken: 'tok' });

    const aliceServers = r.listEnabledForUser('alice');
    expect(aliceServers.map(s => s.id).sort()).toEqual(['global1', 'user1']);

    const bobServers = r.listEnabledForUser('bob');
    expect(bobServers.map(s => s.id).sort()).toEqual(['global1', 'user2']);

    const strangerServers = r.listEnabledForUser('stranger');
    expect(strangerServers.map(s => s.id)).toEqual(['global1']);
  });

  it('listEnabledForOwner returns only that user\'s servers', () => {
    const r = createRegistry(db);
    r.upsert({ id: 'global1', name: 'Global', url: 'https://g.example/mcp', authKind: 'oauth', ownerId: null, oauthClientId: 'g', oauthClientSecret: 'gs' });
    r.upsert({ id: 'user1', name: 'User1', url: 'https://u1.example/mcp', authKind: 'api_key', ownerId: 'alice', staticToken: 'tok' });

    const owned = r.listEnabledForOwner('alice');
    expect(owned.map(s => s.id)).toEqual(['user1']);
  });

  it('delete removes the row and cascades', () => {
    const r = createRegistry(db);
    r.upsert({
      id: 'x',
      name: 'X',
      url: 'https://a.example/mcp',
      authKind: 'oauth',
      ownerId: null,
      oauthClientId: 'i',
      oauthClientSecret: 's',
      oauthScopes: null,
    });
    r.delete('x');
    expect(r.listPublic()).toHaveLength(0);
  });

  it('updates discovery metadata', () => {
    const r = createRegistry(db);
    r.upsert({
      id: 'canva',
      name: 'Canva',
      url: 'https://mcp.canva.com/mcp',
      authKind: 'oauth',
      ownerId: null,
      oauthClientId: 'i',
      oauthClientSecret: 's',
      oauthScopes: null,
    });
    r.setDiscovery('canva', {
      issuer: 'https://mcp.canva.com',
      authorizationEndpoint: 'https://mcp.canva.com/authorize',
      tokenEndpoint: 'https://mcp.canva.com/token',
      fingerprint: 'abc123',
    });
    const got = r.getDecrypted('canva');
    expect(got?.authorizationEndpoint).toBe('https://mcp.canva.com/authorize');
    expect(got?.discoveryFingerprint).toBe('abc123');
  });
});
