// @vitest-environment jsdom
/**
 * Hook test for useConsoleSession(taskId, connectionId):
 * - connectionId=null never opens a socket (idle/no_session).
 * - connectionId='A' builds a WS URL with ?connection_id=A.
 * - changing connectionId 'A' -> 'B' closes the old socket, opens a new one
 *   with ?connection_id=B, and emits a terminal-reset byte sequence to
 *   output listeners so a switched tab doesn't show the previous buffer.
 */
import '../test/dom-setup';
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { useConsoleSession } from './useConsoleSession';

class FakeWebSocket {
  static instances: FakeWebSocket[] = [];
  static readonly CONNECTING = 0;
  static readonly OPEN = 1;
  static readonly CLOSING = 2;
  static readonly CLOSED = 3;
  readonly CONNECTING = 0;
  readonly OPEN = 1;
  readonly CLOSING = 2;
  readonly CLOSED = 3;

  url: string;
  readyState = 0;
  binaryType = '';
  onopen: ((ev: any) => void) | null = null;
  onclose: ((ev: any) => void) | null = null;
  onmessage: ((ev: any) => void) | null = null;
  onerror: ((ev: any) => void) | null = null;

  constructor(url: string) {
    this.url = url;
    FakeWebSocket.instances.push(this);
  }

  send(): void {
    // no-op for these tests
  }

  close(): void {
    if (this.readyState === this.CLOSED) return;
    this.readyState = this.CLOSED;
    this.onclose?.({});
  }
}

let realWebSocket: typeof WebSocket;

beforeEach(() => {
  FakeWebSocket.instances = [];
  realWebSocket = global.WebSocket;
  (global as any).WebSocket = FakeWebSocket;
});

afterEach(() => {
  (global as any).WebSocket = realWebSocket;
  vi.restoreAllMocks();
});

describe('useConsoleSession(taskId, connectionId)', () => {
  it('opens no socket when connectionId is null', () => {
    renderHook(() => useConsoleSession('task1', null));
    expect(FakeWebSocket.instances).toHaveLength(0);
  });

  it('builds a WS URL containing connection_id=A on mount', () => {
    renderHook(() => useConsoleSession('task1', 'A'));
    expect(FakeWebSocket.instances).toHaveLength(1);
    expect(FakeWebSocket.instances[0].url).toContain('connection_id=A');
  });

  it('closes the old socket and opens a new one when connectionId changes', () => {
    const { rerender } = renderHook(
      ({ connectionId }: { connectionId: string | null }) => useConsoleSession('task1', connectionId),
      { initialProps: { connectionId: 'A' as string | null } },
    );
    expect(FakeWebSocket.instances).toHaveLength(1);
    const first = FakeWebSocket.instances[0];
    expect(first.readyState).not.toBe(first.CLOSED);

    act(() => {
      rerender({ connectionId: 'B' });
    });

    expect(first.readyState).toBe(first.CLOSED);
    expect(FakeWebSocket.instances).toHaveLength(2);
    expect(FakeWebSocket.instances[1].url).toContain('connection_id=B');
  });

  it('emits a terminal-reset byte sequence to output listeners when switching to a new connectionId', () => {
    const { result, rerender } = renderHook(
      ({ connectionId }: { connectionId: string | null }) => useConsoleSession('task1', connectionId),
      { initialProps: { connectionId: 'A' as string | null } },
    );

    const received: Uint8Array[] = [];
    act(() => {
      result.current.onOutput((data) => received.push(data));
    });

    act(() => {
      rerender({ connectionId: 'B' });
    });

    expect(received).toHaveLength(1);
    const text = new TextDecoder().decode(received[0]);
    expect(text).toBe('\x1bc');
  });

  it('does not emit a reset on the very first connect (nothing to clear yet)', () => {
    const received: Uint8Array[] = [];
    const { result } = renderHook(() => useConsoleSession('task1', 'A'));
    act(() => {
      result.current.onOutput((data) => received.push(data));
    });
    expect(received).toHaveLength(0);
  });
});
