import { render, screen, waitFor } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import { XtermSession } from '../../src/components/agent-workspace/xterm-session';

const { openSpy, wsInstances, fetchSpy } = vi.hoisted(() => ({
  openSpy: vi.fn(),
  wsInstances: [] as Array<{ url: string }>,
  fetchSpy: vi.fn(),
}));

class FakeTerminal {
  cols = 80;
  rows = 24;
  options: Record<string, unknown> = {};
  open = openSpy;
  loadAddon = vi.fn();
  onData = vi.fn();
  onResize = vi.fn();
  onSelectionChange = vi.fn();
  dispose = vi.fn();
  refresh = vi.fn();
  write = vi.fn();
  focus = vi.fn();
  constructor(opts: Record<string, unknown>) {
    this.options = opts ?? {};
  }
}

vi.mock('@xterm/xterm', () => ({ Terminal: FakeTerminal }));
vi.mock('@xterm/addon-fit', () => ({
  FitAddon: class {
    fit = vi.fn();
  },
}));
vi.mock('@xterm/addon-web-links', () => ({
  WebLinksAddon: class {},
}));
vi.mock('next-themes', () => ({
  useTheme: () => ({ resolvedTheme: 'dark' }),
}));

class FakeWebSocket {
  static OPEN = 1;
  url: string;
  readyState = 1;
  binaryType = '';
  onopen: (() => void) | null = null;
  onmessage: ((event: unknown) => void) | null = null;
  onclose: (() => void) | null = null;
  onerror: (() => void) | null = null;
  send = vi.fn();
  close = vi.fn();
  constructor(url: string) {
    this.url = url;
    wsInstances.push(this);
  }
}

beforeEach(() => {
  wsInstances.length = 0;
  openSpy.mockClear();
  fetchSpy.mockReset();
  fetchSpy.mockResolvedValue({
    ok: true,
    status: 200,
    json: async () => ({ url: 'ws://x' }),
    text: async () => '',
  });
  vi.stubGlobal('WebSocket', FakeWebSocket);
  vi.stubGlobal('fetch', fetchSpy);
  vi.stubGlobal(
    'ResizeObserver',
    class {
      observe() {}
      disconnect() {}
      unobserve() {}
    },
  );
  Object.defineProperty(document, 'fonts', {
    configurable: true,
    value: {
      ready: Promise.resolve(),
      load: () => Promise.resolve([]),
    },
  });
});

afterEach(() => {
  vi.unstubAllGlobals();
});

describe('XtermSession', () => {
  it('shows the waiting label and does not open a WebSocket while inactive', async () => {
    render(
      <XtermSession
        active={false}
        tokenUrl='/api/box/terminal-token'
        sessionKey='box-1'
        waitingLabel='Workspace is asleep.'
      />,
    );

    expect(screen.getByText('Workspace is asleep.')).toBeInTheDocument();
    // Give any (unexpected) async connect a chance to run.
    await Promise.resolve();
    expect(fetchSpy).not.toHaveBeenCalled();
    expect(wsInstances).toHaveLength(0);
  });

  it('fetches the token URL and opens a WebSocket to the returned url when active', async () => {
    render(
      <XtermSession
        active
        tokenUrl='/api/box/terminal-token'
        sessionKey='box-1'
      />,
    );

    await waitFor(() =>
      expect(fetchSpy).toHaveBeenCalledWith(
        '/api/box/terminal-token',
        expect.objectContaining({ signal: expect.anything() }),
      ),
    );
    await waitFor(() => expect(wsInstances).toHaveLength(1));
    expect(wsInstances[0]?.url).toBe('ws://x');
    expect(openSpy).toHaveBeenCalled();
  });
});
