// @ts-nocheck
/**
 * tests/unit/core/SocketHandler.test.js
 *
 * Story 1.3 — Full unit coverage for SocketHandler.
 * Uses vitest fake timers for timeout/retry/revert paths.
 */

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { SocketHandler, SOCKET_TIMEOUT_MS } from '../../../src/core/SocketHandler.js';
import { MAX_PAYLOAD_BYTES } from '../../../src/contracts/socket-message.js';

// ─── Stubs ────────────────────────────────────────────────────────────────────

function makeSocket() {
  return { emit: vi.fn(), on: vi.fn(), off: vi.fn() };
}

function makeHooks() {
  return { on: vi.fn(), once: vi.fn() };
}

function makeHandler() {
  return { onRevert: vi.fn() };
}

/**
 * A minimal PendingOp-like object.
 * @param {string} [opId]
 * @returns {object}
 */
function makePendingOp(opId = 'op-test-1') {
  return { opId, userId: 'user-1', targetState: 'hidden', baseRevision: 0, timeoutId: null };
}

// ─── Constructor ─────────────────────────────────────────────────────────────

describe('SocketHandler constructor', () => {
  it('creates instance in not-ready state', () => {
    const h = new SocketHandler(makeSocket(), makeHooks());
    expect(h._isReady).toBe(false);
    expect(h._pendingOps.size).toBe(0);
    expect(h._messageQueue).toHaveLength(0);
  });
});

// ─── setReady() ──────────────────────────────────────────────────────────────

describe('SocketHandler.setReady()', () => {
  it('marks handler as ready', () => {
    const h = new SocketHandler(makeSocket(), makeHooks());
    h.setReady(makeHandler());
    expect(h._isReady).toBe(true);
  });

  it('drains queued messages in order', () => {
    const socket = makeSocket();
    const h = new SocketHandler(socket, makeHooks());
    h.emit('event-a', { id: 1 });
    h.emit('event-b', { id: 2 });
    expect(socket.emit).not.toHaveBeenCalled();
    h.setReady(makeHandler());
    expect(socket.emit).toHaveBeenCalledTimes(2);
    expect(socket.emit.mock.calls[0]).toEqual(['event-a', { id: 1 }]);
    expect(socket.emit.mock.calls[1]).toEqual(['event-b', { id: 2 }]);
  });

  it('accepts null handler and logs warn', () => {
    const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
    const h = new SocketHandler(makeSocket(), makeHooks());
    h.setReady(null);
    expect(h._isReady).toBe(true);
    expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('[ScryingPool]'));
    warnSpy.mockRestore();
  });
});

// ─── emit() ──────────────────────────────────────────────────────────────────

describe('SocketHandler.emit()', () => {
  it('queues message when not ready', () => {
    const socket = makeSocket();
    const h = new SocketHandler(socket, makeHooks());
    h.emit('test-event', { a: 1 });
    expect(socket.emit).not.toHaveBeenCalled();
    expect(h._messageQueue).toHaveLength(1);
  });

  it('emits directly when ready', () => {
    const socket = makeSocket();
    const h = new SocketHandler(socket, makeHooks());
    h.setReady(makeHandler());
    h.emit('test-event', { a: 1 });
    expect(socket.emit).toHaveBeenCalledWith('test-event', { a: 1 });
  });

  it('throws when payload exceeds MAX_PAYLOAD_BYTES', () => {
    const h = new SocketHandler(makeSocket(), makeHooks());
    h.setReady(makeHandler());
    const bigPayload = { data: 'x'.repeat(MAX_PAYLOAD_BYTES) };
    expect(() => h.emit('test-event', bigPayload)).toThrow(Error);
  });

  it('does NOT throw for payload just under MAX_PAYLOAD_BYTES', () => {
    const h = new SocketHandler(makeSocket(), makeHooks());
    h.setReady(makeHandler());
    const payload = { data: 'x'.repeat(MAX_PAYLOAD_BYTES - 20) };
    expect(() => h.emit('test-event', payload)).not.toThrow();
  });
});

// ─── registerPendingOp() + confirmPendingOp() ─────────────────────────────────

describe('SocketHandler.registerPendingOp() and confirmPendingOp()', () => {
  beforeEach(() => { vi.useFakeTimers(); });
  afterEach(() => { vi.useRealTimers(); });

  it('registers a pending op', () => {
    const h = new SocketHandler(makeSocket(), makeHooks());
    const op = makePendingOp();
    h.registerPendingOp(op);
    expect(h._pendingOps.has('op-test-1')).toBe(true);
  });

  it('confirms a pending op — clears it and cancels timer', () => {
    const h = new SocketHandler(makeSocket(), makeHooks());
    const op = makePendingOp();
    h.registerPendingOp(op);
    h.confirmPendingOp('op-test-1');
    expect(h._pendingOps.has('op-test-1')).toBe(false);
  });

  it('confirmPendingOp with unknown opId is a no-op (stale echo)', () => {
    const h = new SocketHandler(makeSocket(), makeHooks());
    expect(() => h.confirmPendingOp('unknown-op-id')).not.toThrow();
  });

  it('confirms op before timeout — no timeout fires', () => {
    const socket = makeSocket();
    const h = new SocketHandler(socket, makeHooks());
    h.setReady(makeHandler());
    const op = makePendingOp();
    h.registerPendingOp(op);
    h.confirmPendingOp('op-test-1');
    vi.advanceTimersByTime(SOCKET_TIMEOUT_MS * 3);
    // No retry emit should have occurred
    expect(socket.emit).not.toHaveBeenCalled();
  });
});

// ─── Timeout → retry → revert ────────────────────────────────────────────────

describe('SocketHandler timeout / retry / revert', () => {
  beforeEach(() => { vi.useFakeTimers(); });
  afterEach(() => { vi.useRealTimers(); });

  it('retries once after first timeout (re-emits the op)', () => {
    const socket = makeSocket();
    const h = new SocketHandler(socket, makeHooks());
    h.setReady(makeHandler());
    const op = makePendingOp();
    h.registerPendingOp(op);

    vi.advanceTimersByTime(SOCKET_TIMEOUT_MS);
    expect(socket.emit).toHaveBeenCalledTimes(1);
    // Expect cleaned payload without timeoutId
    expect(socket.emit).toHaveBeenCalledWith('scrying-pool.visibility.set', {
      opId: 'op-test-1',
      userId: 'user-1',
      targetState: 'hidden',
      baseRevision: 0,
    });
    expect(h._pendingOps.has('op-test-1')).toBe(true); // not removed yet
  });

  it('calls handler.onRevert and warns after second timeout', () => {
    const socket = makeSocket();
    const handler = makeHandler();
    const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
    const h = new SocketHandler(socket, makeHooks());
    h.setReady(handler);
    const op = makePendingOp();
    h.registerPendingOp(op);

    vi.advanceTimersByTime(SOCKET_TIMEOUT_MS);     // first timeout → retry
    vi.advanceTimersByTime(SOCKET_TIMEOUT_MS);     // second timeout → revert
    expect(handler.onRevert).toHaveBeenCalledWith(op);
    expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('[ScryingPool]'), 'op-test-1');
    expect(h._pendingOps.has('op-test-1')).toBe(false);
    warnSpy.mockRestore();
  });

  it('does NOT call handler.onRevert when handler is null', () => {
    const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
    const h = new SocketHandler(makeSocket(), makeHooks());
    h.setReady(null); // no handler
    const op = makePendingOp();
    h.registerPendingOp(op);

    vi.advanceTimersByTime(SOCKET_TIMEOUT_MS * 2);
    // No error thrown is the assertion
    warnSpy.mockRestore();
  });

  it('ignores second timeout if op was confirmed between retry and second timeout', () => {
    const handler = makeHandler();
    const h = new SocketHandler(makeSocket(), makeHooks());
    h.setReady(handler);
    const op = makePendingOp();
    h.registerPendingOp(op);

    vi.advanceTimersByTime(SOCKET_TIMEOUT_MS); // first timeout → retry
    h.confirmPendingOp('op-test-1');           // confirmed during retry window
    vi.advanceTimersByTime(SOCKET_TIMEOUT_MS); // second timeout fires but op is gone
    expect(handler.onRevert).not.toHaveBeenCalled();
  });

  it('stale echo (confirmPendingOp after second revert) is a no-op', () => {
    const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
    const h = new SocketHandler(makeSocket(), makeHooks());
    h.setReady(makeHandler());
    const op = makePendingOp();
    h.registerPendingOp(op);

    vi.advanceTimersByTime(SOCKET_TIMEOUT_MS * 2); // full revert
    expect(() => h.confirmPendingOp('op-test-1')).not.toThrow();
    warnSpy.mockRestore();
  });
});

// ─── SOCKET_TIMEOUT_MS constant ──────────────────────────────────────────────

describe('SOCKET_TIMEOUT_MS', () => {
  it('is 3000ms', () => {
    expect(SOCKET_TIMEOUT_MS).toBe(3000);
  });
});
