import {
  beforeEach,
  describe,
  expect,
  it,
  vi
} from 'vitest';
import { WebSocket } from 'ws';
import { ConnectedUser } from './types';
import { connectedUsers } from './state';

const pluginSupportMocks = vi.hoisted(() => {
  class MockPluginSupportError extends Error {
    constructor(
      readonly status: number,
      readonly code: string,
      message: string
    ) {
      super(message);
      this.name = 'PluginSupportError';
    }
  }

  return {
    getPluginRequirementsSnapshot: vi.fn(),
    PluginSupportError: MockPluginSupportError,
    validatePluginEventEnvelope: vi.fn()
  };
});

vi.mock('../services/server-access.service', () => ({
  authorizeWebSocketJoin: vi.fn(async () => ({ allowed: true as const }))
}));

vi.mock('../services/plugin-support.service', () => pluginSupportMocks);

import { handleWebSocketMessage } from './handler';

interface SentMessageStore {
  sentMessages: string[];
}

function createMockWs(): WebSocket & SentMessageStore {
  const sentMessages: string[] = [];
  const socket = {
    readyState: WebSocket.OPEN,
    send: (data: string) => {
      sentMessages.push(data);
    },
    close: () => {},
    sentMessages
  } as unknown as WebSocket & SentMessageStore;

  return socket;
}

function createConnectedUser(
  connectionId: string,
  oderId: string,
  overrides: Partial<ConnectedUser> = {}
): ConnectedUser {
  const user: ConnectedUser = {
    displayName: `User ${oderId}`,
    lastPong: Date.now(),
    oderId,
    authenticated: true,
    serverIds: new Set(),
    ws: createMockWs(),
    ...overrides
  };

  connectedUsers.set(connectionId, user);
  return user;
}

function readSentMessages(user: ConnectedUser): Record<string, unknown>[] {
  return (user.ws as unknown as SentMessageStore).sentMessages.map((messageText) => JSON.parse(messageText) as Record<string, unknown>);
}

describe('server websocket handler - plugin support', () => {
  beforeEach(() => {
    connectedUsers.clear();
    pluginSupportMocks.getPluginRequirementsSnapshot.mockReset();
    pluginSupportMocks.validatePluginEventEnvelope.mockReset();
    pluginSupportMocks.getPluginRequirementsSnapshot.mockResolvedValue({
      eventDefinitions: [],
      requirements: [],
      serverId: 'server-1',
      updatedAt: 0
    });

    pluginSupportMocks.validatePluginEventEnvelope.mockResolvedValue({ direction: 'serverRelay' });
  });

  it('sends plugin requirement snapshots after joining a server', async () => {
    const alice = createConnectedUser('conn-1', 'alice');

    pluginSupportMocks.getPluginRequirementsSnapshot.mockResolvedValue({
      eventDefinitions: [
        {
          direction: 'serverRelay',
          eventName: 'e2e:relay',
          maxPayloadBytes: 2048,
          pluginId: 'e2e.plugin-api',
          scope: 'server',
          updatedAt: 2
        }
      ],
      requirements: [
        {
          pluginId: 'e2e.plugin-api',
          status: 'required',
          updatedAt: 1
        }
      ],
      serverId: 'server-1',
      updatedAt: 2
    });

    await handleWebSocketMessage('conn-1', { type: 'join_server', serverId: 'server-1' });

    const messages = readSentMessages(alice);
    const pluginRequirements = messages.find((message) => message['type'] === 'plugin_requirements');

    expect(pluginRequirements?.['serverId']).toBe('server-1');
    expect(pluginRequirements?.['snapshot']).toEqual(expect.objectContaining({ updatedAt: 2 }));
  });

  it('validates and relays plugin events to other joined users', async () => {
    const alice = createConnectedUser('conn-1', 'alice', { viewedServerId: 'server-1' });
    const bob = createConnectedUser('conn-2', 'bob', { viewedServerId: 'server-1' });

    alice.serverIds.add('server-1');
    bob.serverIds.add('server-1');

    await handleWebSocketMessage('conn-1', {
      type: 'plugin_event',
      eventId: 'event-1',
      eventName: 'e2e:relay',
      payload: { ok: true },
      pluginId: 'e2e.plugin-api',
      serverId: 'server-1',
      sourcePluginUserId: 'fixture-user'
    });

    expect(pluginSupportMocks.validatePluginEventEnvelope).toHaveBeenCalledWith({
      type: 'plugin_event',
      eventId: 'event-1',
      eventName: 'e2e:relay',
      payload: { ok: true },
      pluginId: 'e2e.plugin-api',
      serverId: 'server-1',
      sourcePluginUserId: 'fixture-user'
    });

    const bobMessages = readSentMessages(bob);
    const relayedEvent = bobMessages.find((message) => message['type'] === 'plugin_event');

    expect(relayedEvent).toEqual(expect.objectContaining({
      eventId: 'event-1',
      eventName: 'e2e:relay',
      pluginId: 'e2e.plugin-api',
      serverId: 'server-1',
      sourcePluginUserId: 'fixture-user',
      sourceUserId: 'alice'
    }));

    expect(typeof relayedEvent?.['emittedAt']).toBe('number');
  });

  it('returns plugin errors for invalid plugin event messages', async () => {
    const alice = createConnectedUser('conn-1', 'alice');

    await handleWebSocketMessage('conn-1', {
      type: 'plugin_event',
      eventName: 'e2e:relay',
      pluginId: 'e2e.plugin-api',
      serverId: 'server-1'
    });

    const pluginError = readSentMessages(alice).find((message) => message['type'] === 'plugin_error');

    expect(pluginError).toEqual(expect.objectContaining({
      code: 'INVALID_PLUGIN_EVENT',
      eventName: 'e2e:relay',
      pluginId: 'e2e.plugin-api',
      serverId: 'server-1'
    }));

    expect(pluginSupportMocks.validatePluginEventEnvelope).not.toHaveBeenCalled();
  });

  it('forwards plugin support validation errors to the sending user', async () => {
    const alice = createConnectedUser('conn-1', 'alice', { viewedServerId: 'server-1' });

    alice.serverIds.add('server-1');
    pluginSupportMocks.validatePluginEventEnvelope.mockRejectedValue(new pluginSupportMocks.PluginSupportError(
      400,
      'PLUGIN_EVENT_NOT_RELAYABLE',
      'P2P plugin events must not be relayed by the signal server'
    ));

    await handleWebSocketMessage('conn-1', {
      type: 'plugin_event',
      eventId: 'event-p2p',
      eventName: 'e2e:p2p',
      payload: { hint: true },
      pluginId: 'e2e.plugin-api',
      serverId: 'server-1'
    });

    const pluginError = readSentMessages(alice).find((message) => message['type'] === 'plugin_error');

    expect(pluginError).toEqual(expect.objectContaining({
      code: 'PLUGIN_EVENT_NOT_RELAYABLE',
      eventId: 'event-p2p',
      eventName: 'e2e:p2p',
      pluginId: 'e2e.plugin-api',
      serverId: 'server-1'
    }));
  });
});
