import { describe, it, expect, vi, beforeEach } from 'vitest';
import type { ResultSetHeader } from 'mysql2';

const { mockExecute } = vi.hoisted(() => ({ mockExecute: vi.fn() }));

vi.mock('./db.js', () => ({
  getPool: vi.fn(() => ({ execute: mockExecute })),
  isPoolReady: vi.fn(() => true),
}));

import { ensureOAuthAppRegistered, isValidRedirectUri } from './oauth.js';

function affected(n: number): [ResultSetHeader, unknown[]] {
  return [{ affectedRows: n } as ResultSetHeader, []];
}

describe('ensureOAuthAppRegistered', () => {
  beforeEach(() => vi.clearAllMocks());

  it('executes an INSERT ... ON DUPLICATE KEY UPDATE', async () => {
    mockExecute.mockResolvedValue(affected(1));

    await ensureOAuthAppRegistered('client-1', 'secret-1', [
      'https://app.example.com/oauth/mcp-callback',
      'http://localhost:*',
      'claude-desktop://callback',
    ]);

    const [sql, params] = mockExecute.mock.calls[0] as [string, unknown[]];
    expect(sql).toContain('INSERT INTO oauth_clients');
    expect(sql).toContain('ON DUPLICATE KEY UPDATE');
    expect(params[0]).toBe('client-1');
    expect(params[1]).toBe('secret-1');
    const redirectUris = JSON.parse(params[3] as string);
    expect(redirectUris).toContain('http://localhost:*');
    expect(redirectUris).toContain('claude-desktop://callback');
  });

  it('tolerates DB errors gracefully (does not throw)', async () => {
    // Should propagate — not silently swallow — so callers can log
    mockExecute.mockRejectedValue(new Error('DB gone'));
    await expect(
      ensureOAuthAppRegistered('c', 's', [])
    ).rejects.toThrow('DB gone');
  });
});

describe('isValidRedirectUri — DCR client redirect URIs', () => {
  const registered = [
    'https://hermes.squaremcp.com/oauth/mcp-callback',
    'http://localhost:*',
    'claude-desktop://callback',
    'opencode://callback',
  ];

  it('allows the SquareMCP mcp-callback URI exactly', () => {
    expect(isValidRedirectUri('https://hermes.squaremcp.com/oauth/mcp-callback', registered)).toBe(true);
  });

  it('allows claude-desktop://callback exactly', () => {
    expect(isValidRedirectUri('claude-desktop://callback', registered)).toBe(true);
  });

  it('allows opencode://callback exactly', () => {
    expect(isValidRedirectUri('opencode://callback', registered)).toBe(true);
  });

  it('allows any localhost port via http://localhost:* wildcard', () => {
    expect(isValidRedirectUri('http://localhost:3000', registered)).toBe(true);
    expect(isValidRedirectUri('http://localhost:52481/callback', registered)).toBe(true);
  });

  it('rejects unregistered URIs', () => {
    expect(isValidRedirectUri('https://evil.com', registered)).toBe(false);
    expect(isValidRedirectUri('https://hermes.squaremcp.com/other', registered)).toBe(false);
  });

  it('rejects https://localhost (SSL loopback) — not in registered list', () => {
    expect(isValidRedirectUri('https://localhost:3000', registered)).toBe(false);
  });
});
