import type { APIRequestContext, APIResponse } from '@playwright/test';
import WebSocket from 'ws';
import { expect, test } from '../../fixtures/multi-client';
import {
  authHeaders,
  registerTestUser,
  type AuthSession
} from '../../helpers/auth-api';
import {
  getPluginApiTestEvent,
  readPluginApiTestManifest,
  TEST_PLUGIN_ID,
  TEST_PLUGIN_P2P_EVENT,
  TEST_PLUGIN_RELAY_EVENT
} from '../../helpers/plugin-api-test-fixture';

interface CreatedServerResponse {
  id: string;
}

interface PluginRequirementResponse {
  requirement: {
    pluginId: string;
    reason?: string;
    status: string;
    versionRange?: string;
  };
}

interface PluginEventDefinitionResponse {
  eventDefinition: {
    direction: string;
    eventName: string;
    maxPayloadBytes: number;
    pluginId: string;
    scope: string;
  };
}

interface PluginSnapshotResponse {
  eventDefinitions: PluginEventDefinitionResponse['eventDefinition'][];
  requirements: PluginRequirementResponse['requirement'][];
  serverId: string;
}

interface SocketMessage {
  [key: string]: unknown;
  type?: string;
}

interface TestSocket {
  close: () => Promise<void>;
  messages: SocketMessage[];
  send: (message: SocketMessage) => void;
}

test.describe('Plugin support API', () => {
  test('covers plugin requirement, event, data, and websocket APIs with the fixture plugin', async ({ request, testServer }) => {
    const manifest = await readPluginApiTestManifest();
    const owner = await registerTestUser(
      request,
      testServer.url,
      `plugin-owner-${Date.now()}`,
      'TestPass123!',
      'Plugin Owner'
    );
    const peer = await registerTestUser(
      request,
      testServer.url,
      `plugin-peer-${Date.now()}`,
      'TestPass123!',
      'Plugin Peer'
    );
    const server = await createServer(request, testServer.url, owner, `Plugin API ${Date.now()}`);
    const relayEvent = getPluginApiTestEvent(manifest, TEST_PLUGIN_RELAY_EVENT);
    const p2pEvent = getPluginApiTestEvent(manifest, TEST_PLUGIN_P2P_EVENT);
    const pluginsApi = `${testServer.url}/api/servers/${encodeURIComponent(server.id)}/plugins`;
    const ownerHeaders = authHeaders(owner.token);

    await test.step('Initial snapshot is empty', async () => {
      const snapshot = await expectJson<PluginSnapshotResponse>(await request.get(pluginsApi));

      expect(snapshot).toEqual(expect.objectContaining({
        eventDefinitions: [],
        requirements: [],
        serverId: server.id
      }));
    });

    await test.step('Requirement API enforces server management permission', async () => {
      const response = await request.put(`${pluginsApi}/${TEST_PLUGIN_ID}/requirement`, {
        headers: authHeaders(peer.token),
        data: {
          status: 'required'
        }
      });
      const body = await expectJson<{ errorCode: string }>(response, 403);

      expect(body.errorCode).toBe('NOT_AUTHORIZED');
    });

    await test.step('Requirement and event definition APIs persist the test plugin contract', async () => {
      const requirement = await expectJson<PluginRequirementResponse>(await request.put(`${pluginsApi}/${TEST_PLUGIN_ID}/requirement`, {
        headers: ownerHeaders,
        data: {
          reason: manifest.description,
          status: 'required',
          versionRange: `^${manifest.version}`
        }
      }));

      expect(requirement.requirement).toEqual(expect.objectContaining({
        pluginId: TEST_PLUGIN_ID,
        reason: manifest.description,
        status: 'required',
        versionRange: `^${manifest.version}`
      }));

      const relayDefinition = await upsertEventDefinition(request, pluginsApi, ownerHeaders, relayEvent);
      const p2pDefinition = await upsertEventDefinition(request, pluginsApi, ownerHeaders, p2pEvent);

      expect(relayDefinition.eventDefinition).toEqual(expect.objectContaining({
        direction: 'serverRelay',
        eventName: TEST_PLUGIN_RELAY_EVENT,
        pluginId: TEST_PLUGIN_ID,
        scope: 'server'
      }));

      expect(p2pDefinition.eventDefinition).toEqual(expect.objectContaining({
        direction: 'p2pHint',
        eventName: TEST_PLUGIN_P2P_EVENT,
        pluginId: TEST_PLUGIN_ID,
        scope: 'user'
      }));

      const snapshot = await expectJson<PluginSnapshotResponse>(await request.get(pluginsApi));

      expect(snapshot.requirements.map((entry) => entry.pluginId)).toEqual([TEST_PLUGIN_ID]);
      expect(snapshot.eventDefinitions.map((entry) => entry.eventName).sort()).toEqual([TEST_PLUGIN_P2P_EVENT, TEST_PLUGIN_RELAY_EVENT]);
    });

    await test.step('Plugin data API refuses arbitrary server persistence', async () => {
      const stored = await expectJson<{ errorCode: string }>(await request.put(`${pluginsApi}/${TEST_PLUGIN_ID}/data/settings`, {
        headers: ownerHeaders,
        data: {
          schemaVersion: 1,
          scope: 'server',
          value: {
            enabled: true,
            pluginVersion: manifest.version
          }
        }
      }), 410);

      expect(stored.errorCode).toBe('PLUGIN_DATA_DISABLED');

      const listed = await expectJson<{ errorCode: string }>(await request.get(`${pluginsApi}/${TEST_PLUGIN_ID}/data`, {
        params: {
          key: 'settings',
          scope: 'server',
          userId: owner.id
        }
      }), 410);

      expect(listed.errorCode).toBe('PLUGIN_DATA_DISABLED');

      const afterDelete = await expectJson<{ errorCode: string }>(await request.delete(`${pluginsApi}/${TEST_PLUGIN_ID}/data/settings`, {
        headers: ownerHeaders,
        data: {
          scope: 'server'
        }
      }), 410);

      expect(afterDelete.errorCode).toBe('PLUGIN_DATA_DISABLED');
    });

    await test.step('WebSocket plugin API sends snapshots, relays server events, and rejects p2p relays', async () => {
      const alice = await openTestSocket(testServer.url);
      const bob = await openTestSocket(testServer.url);

      try {
        await identifySocket(alice, owner.token, 'Plugin Owner');
        await identifySocket(bob, peer.token, 'Plugin Peer');
        alice.send({ type: 'join_server', serverId: server.id });
        bob.send({ type: 'join_server', serverId: server.id });

        const aliceSnapshot = await waitForSocketMessage(alice, (message) => message.type === 'plugin_requirements');
        const bobSnapshot = await waitForSocketMessage(bob, (message) => message.type === 'plugin_requirements');
        const bobEventNames = (bobSnapshot['snapshot'] as PluginSnapshotResponse).eventDefinitions
          .map((entry) => entry.eventName)
          .sort();

        expect((aliceSnapshot['snapshot'] as PluginSnapshotResponse).requirements[0]?.pluginId).toBe(TEST_PLUGIN_ID);
        expect(bobEventNames).toEqual([TEST_PLUGIN_P2P_EVENT, TEST_PLUGIN_RELAY_EVENT]);

        alice.send({
          type: 'plugin_event',
          eventId: 'relay-event-1',
          eventName: TEST_PLUGIN_RELAY_EVENT,
          payload: { message: 'hello from fixture plugin' },
          pluginId: TEST_PLUGIN_ID,
          serverId: server.id,
          sourcePluginUserId: 'fixture-plugin-user'
        });

        const relayedEvent = await waitForSocketMessage(bob, (message) => message.type === 'plugin_event');

        expect(relayedEvent).toEqual(expect.objectContaining({
          eventId: 'relay-event-1',
          eventName: TEST_PLUGIN_RELAY_EVENT,
          pluginId: TEST_PLUGIN_ID,
          serverId: server.id,
          sourcePluginUserId: 'fixture-plugin-user',
          sourceUserId: owner.id
        }));

        expect(relayedEvent['payload']).toEqual({ message: 'hello from fixture plugin' });
        expect(typeof relayedEvent['emittedAt']).toBe('number');

        alice.send({
          type: 'plugin_event',
          eventId: 'p2p-event-1',
          eventName: TEST_PLUGIN_P2P_EVENT,
          payload: { hint: true },
          pluginId: TEST_PLUGIN_ID,
          serverId: server.id
        });

        const p2pError = await waitForSocketMessage(
          alice,
          (message) => message.type === 'plugin_error' && message['eventId'] === 'p2p-event-1'
        );

        expect(p2pError['code']).toBe('PLUGIN_EVENT_NOT_RELAYABLE');

        alice.send({
          type: 'plugin_event',
          eventId: 'missing-event-1',
          eventName: 'e2e:missing',
          payload: {},
          pluginId: TEST_PLUGIN_ID,
          serverId: server.id
        });

        const missingError = await waitForSocketMessage(
          alice,
          (message) => message.type === 'plugin_error' && message['eventId'] === 'missing-event-1'
        );

        expect(missingError['code']).toBe('PLUGIN_EVENT_NOT_REGISTERED');
      } finally {
        await Promise.all([alice.close(), bob.close()]);
      }
    });

    await test.step('Delete APIs remove event definitions and requirements', async () => {
      await expectJson<{ ok: boolean }>(await request.delete(`${pluginsApi}/${TEST_PLUGIN_ID}/events/${TEST_PLUGIN_RELAY_EVENT}`, {
        headers: ownerHeaders
      }));

      await expectJson<{ ok: boolean }>(await request.delete(`${pluginsApi}/${TEST_PLUGIN_ID}/events/${TEST_PLUGIN_P2P_EVENT}`, {
        headers: ownerHeaders
      }));

      await expectJson<{ ok: boolean }>(await request.delete(`${pluginsApi}/${TEST_PLUGIN_ID}/requirement`, {
        headers: ownerHeaders
      }));

      const snapshot = await expectJson<PluginSnapshotResponse>(await request.get(pluginsApi));

      expect(snapshot.eventDefinitions).toEqual([]);
      expect(snapshot.requirements).toEqual([]);
    });
  });
});

async function createServer(
  request: APIRequestContext,
  baseUrl: string,
  owner: AuthSession,
  serverName: string
): Promise<CreatedServerResponse> {
  const response = await request.post(`${baseUrl}/api/servers`, {
    headers: authHeaders(owner.token),
    data: {
      channels: [
        {
          id: 'general-text',
          name: 'general',
          position: 0,
          type: 'text'
        }
      ],
      description: 'Server for plugin API E2E coverage',
      id: `plugin-api-${Date.now()}`,
      isPrivate: false,
      name: serverName,
      ownerId: owner.id,
      ownerPublicKey: 'plugin-api-owner-public-key',
      tags: ['plugins']
    }
  });

  return await expectJson<CreatedServerResponse>(response, 201);
}

async function upsertEventDefinition(
  request: APIRequestContext,
  pluginsApi: string,
  headers: Record<string, string>,
  eventDefinition: ReturnType<typeof getPluginApiTestEvent>
): Promise<PluginEventDefinitionResponse> {
  return await expectJson<PluginEventDefinitionResponse>(await request.put(
    `${pluginsApi}/${TEST_PLUGIN_ID}/events/${encodeURIComponent(eventDefinition.eventName)}`,
    {
      headers,
      data: {
        direction: eventDefinition.direction,
        maxPayloadBytes: eventDefinition.maxPayloadBytes,
        schemaJson: '{"type":"object"}',
        scope: eventDefinition.scope
      }
    }
  ));
}

async function expectJson<T>(response: APIResponse, status = 200): Promise<T> {
  expect(response.status()).toBe(status);

  return await response.json() as T;
}

async function identifySocket(socket: TestSocket, token: string, displayName: string): Promise<void> {
  socket.send({ type: 'identify', token, displayName });

  await new Promise((resolve) => {
    setTimeout(resolve, 300);
  });

  const authError = socket.messages.find((message) => message.type === 'auth_error');

  if (authError) {
    throw new Error(`WebSocket identify failed: ${JSON.stringify(authError)}`);
  }
}

async function openTestSocket(baseUrl: string): Promise<TestSocket> {
  const socketUrl = baseUrl.replace(/^http/, 'ws');
  const socket = new WebSocket(socketUrl);
  const messages: SocketMessage[] = [];

  socket.on('message', (data) => {
    messages.push(JSON.parse(data.toString()) as SocketMessage);
  });

  await new Promise<void>((resolve, reject) => {
    socket.once('open', () => resolve());
    socket.once('error', reject);
  });

  await waitForSocketMessage({ messages, send: () => {}, close: async () => {} }, (message) => message.type === 'connected');

  return {
    close: async () => {
      if (socket.readyState === WebSocket.CLOSED) {
        return;
      }

      await new Promise<void>((resolve) => {
        socket.once('close', () => resolve());
        socket.close();
      });
    },
    messages,
    send: (message: SocketMessage) => {
      socket.send(JSON.stringify(message));
    }
  };
}

async function waitForSocketMessage(
  socket: Pick<TestSocket, 'messages'>,
  predicate: (message: SocketMessage) => boolean,
  timeoutMs = 10_000
): Promise<SocketMessage> {
  const startedAt = Date.now();

  return await new Promise((resolve, reject) => {
    const interval = setInterval(() => {
      const message = socket.messages.find(predicate);

      if (message) {
        clearInterval(interval);
        resolve(message);
        return;
      }

      if (Date.now() - startedAt > timeoutMs) {
        clearInterval(interval);
        reject(new Error('Timed out waiting for websocket message'));
      }
    }, 25);
  });
}
