/**
 * Terminal close code tests for useJobStatusWebSocket.
 *
 * Verifies that codes 4001/4003/4004/4403 are treated as permanent failures
 * and do NOT trigger reconnect — preventing reconnect storms on auth/org failures.
 * Mod Comms incident 2026-03-18 and org-isolation MT-7.
 */
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { renderHook, act } from '@testing-library/react';

import { useJobStatusWebSocket } from '../useJobStatusWebSocket';

// ── Mocks ────────────────────────────────────────────────────────────────────

vi.mock('../../lib/auth', () => ({
  useAuthStore: () => ({ isAuthenticated: true }),
}));

vi.mock('../../lib/api', () => ({
  apiClient: { getAccessToken: () => 'test-token' },
}));

vi.mock('@tanstack/react-query', () => ({
  useQueryClient: () => ({
    invalidateQueries: vi.fn(),
  }),
}));

// Minimal WebSocket mock with static constants required by the hook's guard checks.
class MockWebSocket {
  static CONNECTING = 0;
  static OPEN = 1;
  static CLOSING = 2;
  static CLOSED = 3;

  readyState = MockWebSocket.CONNECTING;
  private handlers: Map<string, EventListener[]> = new Map();

  send = vi.fn();
  close = vi.fn(() => { this.readyState = MockWebSocket.CLOSED; });

  addEventListener(event: string, handler: EventListener) {
    if (!this.handlers.has(event)) this.handlers.set(event, []);
    this.handlers.get(event)!.push(handler);
  }

  removeEventListener(event: string, handler: EventListener) {
    const list = this.handlers.get(event) ?? [];
    this.handlers.set(event, list.filter(h => h !== handler));
  }

  fireOpen() {
    this.readyState = MockWebSocket.OPEN;
    this.handlers.get('open')?.forEach(h => h(new Event('open')));
  }

  fireClose(code: number, reason = '') {
    this.readyState = MockWebSocket.CLOSED;
    const event = new CloseEvent('close', { code, reason, wasClean: true });
    this.handlers.get('close')?.forEach(h => h(event));
  }
}

let mockWsInstance: MockWebSocket;

function makeWsConstructor() {
  const ctor = vi.fn(() => mockWsInstance) as unknown as typeof WebSocket;
  // Hook uses WebSocket.CONNECTING / WebSocket.OPEN / etc. as class statics
  Object.assign(ctor, {
    CONNECTING: 0,
    OPEN: 1,
    CLOSING: 2,
    CLOSED: 3,
  });
  return ctor;
}

beforeEach(() => {
  vi.useFakeTimers();
  mockWsInstance = new MockWebSocket();
  vi.stubGlobal('WebSocket', makeWsConstructor());
});

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

// ── Terminal close codes ──────────────────────────────────────────────────────

describe('terminal close codes — no reconnect', () => {
  const TERMINAL_CODES = [4001, 4003, 4004, 4403] as const;

  for (const code of TERMINAL_CODES) {
    it(`code ${code} does not schedule a reconnect`, () => {
      const setTimeoutSpy = vi.spyOn(globalThis, 'setTimeout');

      const { unmount } = renderHook(() =>
        useJobStatusWebSocket(undefined, { autoReconnect: true, maxReconnectAttempts: 15 })
      );

      // Let initial connection open
      act(() => { mockWsInstance.fireOpen(); });

      // Clear any setTimeout calls from connection setup
      setTimeoutSpy.mockClear();

      // Simulate terminal close
      act(() => { mockWsInstance.fireClose(code, 'Access denied'); });

      // Advance timers — reconnect would fire here if incorrectly scheduled
      act(() => { vi.advanceTimersByTime(30_000); });

      // Filter out short-lived timers unrelated to reconnect (< 500ms)
      const reconnectCalls = setTimeoutSpy.mock.calls.filter(
        ([, delay]) => typeof delay === 'number' && (delay as number) >= 500
      );
      expect(reconnectCalls).toHaveLength(0);

      unmount();
    });
  }

  it('code 1000 (normal close) does not reconnect', () => {
    const setTimeoutSpy = vi.spyOn(globalThis, 'setTimeout');

    const { unmount } = renderHook(() =>
      useJobStatusWebSocket(undefined, { autoReconnect: true })
    );

    act(() => { mockWsInstance.fireOpen(); });
    setTimeoutSpy.mockClear();

    act(() => { mockWsInstance.fireClose(1000, 'Normal closure'); });
    act(() => { vi.advanceTimersByTime(30_000); });

    const reconnectCalls = setTimeoutSpy.mock.calls.filter(
      ([, delay]) => typeof delay === 'number' && (delay as number) >= 500
    );
    expect(reconnectCalls).toHaveLength(0);

    unmount();
  });

  it('code 1006 (abnormal close) DOES trigger reconnect when autoReconnect=true', () => {
    const setTimeoutSpy = vi.spyOn(globalThis, 'setTimeout');

    const { unmount } = renderHook(() =>
      useJobStatusWebSocket(undefined, { autoReconnect: true, reconnectDelay: 1000 })
    );

    act(() => { mockWsInstance.fireOpen(); });
    setTimeoutSpy.mockClear();

    // 1006 = abnormal/unexpected disconnect — should reconnect
    act(() => { mockWsInstance.fireClose(1006, 'Connection lost'); });

    const reconnectCalls = setTimeoutSpy.mock.calls.filter(
      ([, delay]) => typeof delay === 'number' && (delay as number) >= 500
    );
    expect(reconnectCalls.length).toBeGreaterThan(0);

    unmount();
  });
});

// ── Status transitions ────────────────────────────────────────────────────────

describe('connection status transitions', () => {
  it('is "connecting" before open, "connected" after open', () => {
    const { result, unmount } = renderHook(() => useJobStatusWebSocket());

    // After mount and before fireOpen, hook is in 'connecting' state
    // (set synchronously in connect())
    expect(['connecting', 'disconnected']).toContain(result.current.connectionStatus);

    act(() => { mockWsInstance.fireOpen(); });
    expect(result.current.connectionStatus).toBe('connected');

    unmount();
  });

  it('sets status to disconnected after 4403 close', () => {
    const { result, unmount } = renderHook(() => useJobStatusWebSocket());

    act(() => { mockWsInstance.fireOpen(); });
    expect(result.current.connectionStatus).toBe('connected');

    act(() => { mockWsInstance.fireClose(4403, 'Org access denied'); });
    expect(result.current.connectionStatus).toBe('disconnected');

    unmount();
  });

  it('sets status to disconnected after 4001 close', () => {
    const { result, unmount } = renderHook(() => useJobStatusWebSocket());

    act(() => { mockWsInstance.fireOpen(); });
    act(() => { mockWsInstance.fireClose(4001, 'User not found'); });
    expect(result.current.connectionStatus).toBe('disconnected');

    unmount();
  });
});
