import { describe, it, expect, vi, beforeEach } from 'vitest';
import { EventEmitter } from 'node:events';
import type { Server } from 'http';
import type { SessionManager, BrowserSession } from '../engine/browser-session.js';

const proxyWs = vi.fn();
const proxyOn = vi.fn();
vi.mock('http-proxy', () => ({
  default: { createProxyServer: vi.fn(() => ({ ws: proxyWs, on: proxyOn })) },
}));

import {
  buildNovncPath,
  isNovncStaticInstalled,
  setupNovncWebSocketProxy,
} from './novnc-proxy.js';

const flush = () => new Promise((r) => setImmediate(r));

function makeSession(overrides: Partial<BrowserSession> = {}): BrowserSession {
  return {
    id: 'sess-1',
    novncPort: 6080,
    userId: 'owner-1',
    kind: 'task',
    taskId: 5,
    ...overrides,
  } as unknown as BrowserSession;
}

function makeUser(overrides: Partial<Express.User> = {}): Express.User {
  return { id: 'owner-1', role: 'user' } as Express.User;
}

function setup(opts: {
  session?: BrowserSession | null;
  manager?: boolean;
  authenticateUpgrade?: (req: unknown) => Promise<Express.User | null>;
  authorizeSession?: (s: BrowserSession, u: Express.User) => Promise<boolean>;
} = {}) {
  const server = new EventEmitter() as unknown as Server;
  const session = opts.session === undefined ? makeSession() : opts.session;
  const sm =
    opts.manager === false
      ? null
      : ({ getSession: vi.fn((id: string) => (id === 'sess-1' ? session : null)) } as unknown as SessionManager);
  setupNovncWebSocketProxy(
    server,
    () => sm,
    opts.authenticateUpgrade as never,
    opts.authorizeSession as never,
  );
  const socket = { destroy: vi.fn() };
  const emit = (url: string) =>
    (server as unknown as EventEmitter).emit('upgrade', { url }, socket, Buffer.alloc(0));
  return { emit, socket };
}

beforeEach(() => {
  proxyWs.mockClear();
});

describe('buildNovncPath', () => {
  it('builds an absolute websockify path with autoconnect', () => {
    expect(buildNovncPath('abc')).toBe(
      '/novnc/vnc.html?path=/novnc/abc/websockify&autoconnect=true&resize=scale',
    );
  });
});

describe('isNovncStaticInstalled', () => {
  it('returns a boolean without throwing', () => {
    expect(typeof isNovncStaticInstalled()).toBe('boolean');
  });
});

describe('setupNovncWebSocketProxy upgrade handling', () => {
  it('ignores non-novnc upgrade URLs', async () => {
    const { emit, socket } = setup();
    emit('/some/other/ws');
    await flush();
    expect(socket.destroy).not.toHaveBeenCalled();
    expect(proxyWs).not.toHaveBeenCalled();
  });

  it('rejects when the session manager is unavailable', async () => {
    const { emit, socket } = setup({ manager: false });
    emit('/novnc/sess-1/websockify');
    await flush();
    expect(socket.destroy).toHaveBeenCalled();
    expect(proxyWs).not.toHaveBeenCalled();
  });

  it('rejects unknown sessions', async () => {
    const { emit, socket } = setup();
    emit('/novnc/no-such-session/websockify');
    await flush();
    expect(socket.destroy).toHaveBeenCalled();
    expect(proxyWs).not.toHaveBeenCalled();
  });

  it('proxies without auth checks in no-auth mode', async () => {
    const { emit, socket } = setup();
    emit('/novnc/sess-1/websockify');
    await flush();
    expect(socket.destroy).not.toHaveBeenCalled();
    expect(proxyWs).toHaveBeenCalledWith(
      expect.anything(), expect.anything(), expect.anything(),
      { target: 'http://127.0.0.1:6080' },
    );
  });

  it('tolerates the legacy double-slash prefix (noVNC v1.5.x)', async () => {
    const { emit } = setup();
    emit('//novnc/sess-1/websockify');
    await flush();
    expect(proxyWs).toHaveBeenCalled();
  });

  it('rejects unauthenticated upgrades when auth is active', async () => {
    const { emit, socket } = setup({
      authenticateUpgrade: vi.fn().mockResolvedValue(null),
    });
    emit('/novnc/sess-1/websockify');
    await flush();
    expect(socket.destroy).toHaveBeenCalled();
    expect(proxyWs).not.toHaveBeenCalled();
  });

  it('consults authorizeSession and proxies on approval', async () => {
    const authorize = vi.fn().mockResolvedValue(true);
    const { emit, socket } = setup({
      authenticateUpgrade: vi.fn().mockResolvedValue(makeUser()),
      authorizeSession: authorize,
    });
    emit('/novnc/sess-1/websockify');
    await flush();
    expect(authorize).toHaveBeenCalled();
    expect(socket.destroy).not.toHaveBeenCalled();
    expect(proxyWs).toHaveBeenCalled();
  });

  it('rejects when authorizeSession denies', async () => {
    const { emit, socket } = setup({
      authenticateUpgrade: vi.fn().mockResolvedValue(makeUser()),
      authorizeSession: vi.fn().mockResolvedValue(false),
    });
    emit('/novnc/sess-1/websockify');
    await flush();
    expect(socket.destroy).toHaveBeenCalled();
    expect(proxyWs).not.toHaveBeenCalled();
  });

  it('rejects (fail-closed) when authorizeSession throws', async () => {
    const { emit, socket } = setup({
      authenticateUpgrade: vi.fn().mockResolvedValue(makeUser()),
      authorizeSession: vi.fn().mockRejectedValue(new Error('db down')),
    });
    emit('/novnc/sess-1/websockify');
    await flush();
    expect(socket.destroy).toHaveBeenCalled();
    expect(proxyWs).not.toHaveBeenCalled();
  });

  it('falls back to owner-or-admin without authorizeSession', async () => {
    // Owner passes.
    const owner = setup({ authenticateUpgrade: vi.fn().mockResolvedValue(makeUser()) });
    owner.emit('/novnc/sess-1/websockify');
    await flush();
    expect(proxyWs).toHaveBeenCalledTimes(1);

    // A stranger is rejected.
    const stranger = setup({
      authenticateUpgrade: vi.fn().mockResolvedValue({ id: 'other', role: 'user' } as Express.User),
    });
    stranger.emit('/novnc/sess-1/websockify');
    await flush();
    expect(stranger.socket.destroy).toHaveBeenCalled();
    expect(proxyWs).toHaveBeenCalledTimes(1);

    // An admin passes.
    const admin = setup({
      authenticateUpgrade: vi.fn().mockResolvedValue({ id: 'adm', role: 'admin' } as Express.User),
    });
    admin.emit('/novnc/sess-1/websockify');
    await flush();
    expect(proxyWs).toHaveBeenCalledTimes(2);
  });

  it('rejects when the auth check itself fails', async () => {
    const { emit, socket } = setup({
      authenticateUpgrade: vi.fn().mockRejectedValue(new Error('session store down')),
    });
    emit('/novnc/sess-1/websockify');
    await flush();
    expect(socket.destroy).toHaveBeenCalled();
  });
});
