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

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

vi.mock('../services/session-auth.service', () => ({
  consumeSessionToken: vi.fn(async (token: string) => {
    if (token !== 'test-token') {
      return null;
    }

    return {
      token,
      user: {
        id: 'user-1',
        username: 'alice',
        displayName: 'Alice',
        passwordHash: 'hash',
        createdAt: Date.now()
      },
      issuedAt: Date.now(),
      expiresAt: Date.now() + 60_000
    };
  })
}));

vi.mock('../services/plugin-support.service', async (importOriginal) => {
  const actual = await importOriginal<typeof import('../services/plugin-support.service')>();

  return {
    ...actual,
    getPluginRequirementsSnapshot: vi.fn(async () => ({
      requirements: [],
      eventDefinitions: []
    }))
  };
});

function createMockWs(): WebSocket & { sentMessages: string[]; closeCalled: boolean } {
  const sent: string[] = [];
  const ws = {
    readyState: WebSocket.OPEN,
    send: (data: string) => { sent.push(data); },
    close: () => { ws.closeCalled = true; },
    terminate: () => { ws.closeCalled = true; },
    closeCalled: false,
    sentMessages: sent
  } as unknown as WebSocket & { sentMessages: string[]; closeCalled: boolean };

  return ws;
}

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

  connectedUsers.set(connectionId, user);

  return user;
}

function getSentMessages(user: ConnectedUser): string[] {
  return (user.ws as WebSocket & { sentMessages: string[] }).sentMessages;
}

describe('server websocket handler - multi-client sessions', () => {
  beforeEach(() => {
    connectedUsers.clear();
    vi.clearAllMocks();
  });

  it('relays voice_state to other connections for the same user', async () => {
    const sender = createConnectedUser('conn-a1', {
      authenticated: true,
      oderId: 'user-1',
      serverIds: new Set(['server-1']),
      clientInstanceId: 'device-a'
    });
    const passive = createConnectedUser('conn-a2', {
      authenticated: true,
      oderId: 'user-1',
      serverIds: new Set(['server-1']),
      clientInstanceId: 'device-b'
    });

    getSentMessages(passive).length = 0;

    await handleWebSocketMessage('conn-a1', {
      type: 'voice_state',
      serverId: 'server-1',
      voiceState: {
        isConnected: true,
        roomId: 'voice-1',
        serverId: 'server-1',
        clientInstanceId: 'device-a'
      }
    });

    const messages = getSentMessages(passive).map((raw) => JSON.parse(raw) as { type: string });
    const voiceState = messages.find((message) => message.type === 'voice_state');

    expect(voiceState).toBeDefined();
    expect(connectedUsers.get('conn-a1')?.voiceActive).toBe(true);
    expect(connectedUsers.get('conn-a2')?.voiceActive).toBeFalsy();
  });

  it('forwards RTC offers to the voice-active connection for the target user', async () => {
    const sender = createConnectedUser('conn-sender', {
      authenticated: true,
      oderId: 'user-2',
      serverIds: new Set(['server-1'])
    });

    createConnectedUser('conn-passive', {
      authenticated: true,
      oderId: 'user-1',
      serverIds: new Set(['server-1']),
      clientInstanceId: 'device-passive'
    });

    const active = createConnectedUser('conn-active', {
      authenticated: true,
      oderId: 'user-1',
      serverIds: new Set(['server-1']),
      voiceActive: true,
      clientInstanceId: 'device-active'
    });

    getSentMessages(active).length = 0;

    await handleWebSocketMessage('conn-sender', {
      type: 'offer',
      targetUserId: 'user-1',
      serverId: 'server-1',
      payload: { sdp: { type: 'offer', sdp: 'v=0' } }
    });

    const messages = getSentMessages(active).map((raw) => JSON.parse(raw) as { type: string });

    expect(messages.some((message) => message.type === 'offer')).toBe(true);
  });

  it('relays voice_client_takeover to other connections for the same user', async () => {
    createConnectedUser('conn-requester', {
      authenticated: true,
      oderId: 'user-1',
      serverIds: new Set(['server-1']),
      clientInstanceId: 'device-b'
    });

    const active = createConnectedUser('conn-active', {
      authenticated: true,
      oderId: 'user-1',
      serverIds: new Set(['server-1']),
      voiceActive: true,
      clientInstanceId: 'device-a'
    });

    getSentMessages(active).length = 0;

    await handleWebSocketMessage('conn-requester', {
      type: 'voice_client_takeover',
      clientInstanceId: 'device-b'
    });

    const messages = getSentMessages(active).map((raw) => JSON.parse(raw) as { type: string; clientInstanceId?: string });
    const takeover = messages.find((message) => message.type === 'voice_client_takeover');

    expect(takeover?.clientInstanceId).toBe('device-b');
  });

  it('evicts a stale connection with the same identity scope and client instance', async () => {
    const stale = createConnectedUser('conn-stale', {
      authenticated: true,
      oderId: 'user-1',
      connectionScope: 'ws://localhost:3001',
      clientInstanceId: 'device-a'
    });

    createConnectedUser('conn-new', {
      authenticated: false,
      connectionScope: 'ws://localhost:3001',
      clientInstanceId: 'device-a'
    });

    await handleWebSocketMessage('conn-new', {
      type: 'identify',
      token: 'test-token',
      oderId: 'user-1',
      displayName: 'Alice',
      connectionScope: 'ws://localhost:3001',
      clientInstanceId: 'device-a'
    });

    expect(connectedUsers.has('conn-stale')).toBe(false);
    expect((stale.ws as WebSocket & { closeCalled: boolean }).closeCalled).toBe(true);
    expect(connectedUsers.get('conn-new')?.authenticated).toBe(true);
  });
});
