import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as plugins from '../../ts/plugins.js';
import { HomeAssistantPilightIntegration, PilightClient, PilightConfigFlow, PilightIntegration, PilightMapper, createPilightDiscoveryDescriptor, pilightProfile, type IPilightSnapshot, type TPilightRawData } from '../../ts/integrations/pilight/index.js';

const rawData: TPilightRawData = {
  device: {
    id: 'pilight-device-1',
    name: "Pilight Device",
    manufacturer: "Pilight",
    model: "Pilight local integration",
    serialNumber: 'pilight-serial-1',
  },
  entities: [
    { id: 'status', name: 'Status', platform: "binary_sensor", state: true, attributes: { domain: "pilight" } },
  ],
  online: true,
  updatedAt: '2026-01-01T00:00:00.000Z',
  source: 'manual',
};

tap.test('matches manual Pilight candidates and creates config flow output', async () => {
  const descriptor = createPilightDiscoveryDescriptor();
  const matcher = descriptor.getMatchers().find((matcherArg) => matcherArg.id === 'pilight-manual-match');
  const result = await matcher!.matches({ source: 'manual', id: 'pilight-device-1', name: "Pilight Device", metadata: { rawData } }, {});

  expect(result.matched).toBeTrue();
  expect(result.candidate?.integrationDomain).toEqual("pilight");

  const validation = await descriptor.getValidators()[0].validate(result.candidate!, {});
  expect(validation.matched).toBeTrue();

  const done = await (await new PilightConfigFlow().start(result.candidate!, {})).submit!({});
  expect(done.kind).toEqual('done');
  expect(done.config?.uniqueId).toEqual('pilight-device-1');
  expect(done.config?.rawData).toEqual(rawData);
});

tap.test('maps Pilight raw snapshots to runtime devices and entities', async () => {
  const client = new PilightClient({ name: "Pilight Runtime", rawData });
  const snapshot = await client.getSnapshot();
  const mappedSnapshot = PilightMapper.toSnapshotFromRaw({ name: "Pilight Runtime" }, rawData);
  const devices = PilightMapper.toDevices(mappedSnapshot);
  const entities = PilightMapper.toEntities(mappedSnapshot);

  expect(snapshot.online).toBeTrue();
  expect(mappedSnapshot.source).toEqual('manual');
  expect(devices[0].integrationDomain).toEqual("pilight");
  expect(devices[0].manufacturer).toEqual("Pilight");
  expect(entities.some((entityArg) => entityArg.integrationDomain === "pilight" && entityArg.platform === "binary_sensor")).toBeTrue();
});

tap.test('probes, sends, and subscribes through native pilight TCP', async () => {
  const server = await startPilightTestServer();
  try {
    const client = new PilightClient({ host: '127.0.0.1', port: server.port, timeoutMs: 1000 });
    const snapshot = await client.getSnapshot(true);
    const send = await client.execute({ domain: 'pilight', service: 'send', target: {}, data: { protocol: ['daycom'], id: 1, off: 1 } });

    expect(snapshot.online).toBeTrue();
    expect(snapshot.source).toEqual('tcp');
    expect(send.success).toBeTrue();
    expect((server.sentCodes[0].protocol as string[])[0]).toEqual('daycom');

    const runtime = await new PilightIntegration().setup({ host: '127.0.0.1', port: server.port, timeoutMs: 1000 }, {});
    const events: Array<{ data?: unknown }> = [];
    const unsubscribe = await runtime.subscribe!((eventArg) => events.push(eventArg));
    server.broadcast({ origin: 'receiver', repeats: 1, uuid: 'uuid-1', message: { protocol: ['kaku_switch'], id: 7, unit: 0, on: 1 } });
    await waitFor(() => events.length > 0);

    const eventData = events[0].data as Record<string, unknown>;
    expect(eventData.uuid).toEqual('uuid-1');
    expect(eventData.id).toEqual(7);
    await unsubscribe();
    await runtime.destroy();
  } finally {
    await server.close();
  }
});

tap.test('exposes Pilight runtime, HA alias, and unsupported control without executor', async () => {
  const integration = new PilightIntegration();
  const alias = new HomeAssistantPilightIntegration();
  expect(alias instanceof PilightIntegration).toBeTrue();
  expect(alias.domain).toEqual("pilight");
  expect(integration.status).toEqual("control-runtime");
  expect(pilightProfile.metadata.configFlow).toEqual(false);
  expect(pilightProfile.metadata.requirements).toEqual([
    "pilight==0.1.1",
  ]);

  const runtime = await integration.setup({ name: "Pilight Runtime", rawData }, {});
  const statusResult = await runtime.callService!({ domain: "pilight", service: 'status', target: {} });
  const refresh = await runtime.callService!({ domain: "pilight", service: 'refresh', target: {} });
  const snapshot = statusResult.data as IPilightSnapshot;

  expect(statusResult.success).toBeTrue();
  expect(refresh.success).toBeTrue();
  expect(snapshot.online).toBeTrue();
  expect((await runtime.devices())[0].name).toEqual("Pilight Device");

  const command = await runtime.callService!({ domain: "pilight", service: pilightProfile.controlServices?.[0] || 'turn_on', target: {} });
  expect(command.success).toBeFalse();
  expect(command.error!).toContain('requires config.host/config.url');
  await runtime.destroy();
});

interface IPilightTestServer {
  port: number;
  sentCodes: Record<string, unknown>[];
  broadcast(messageArg: Record<string, unknown>): void;
  close(): Promise<void>;
}

async function startPilightTestServer(): Promise<IPilightTestServer> {
  const sentCodes: Record<string, unknown>[] = [];
  const sockets = new Set<plugins.net.Socket>();
  const receiverSockets = new Set<plugins.net.Socket>();
  const server = plugins.net.createServer((socket) => {
    sockets.add(socket);
    socket.once('close', () => {
      sockets.delete(socket);
      receiverSockets.delete(socket);
    });
    let buffer = '';
    socket.on('data', (chunkArg) => {
      buffer += Buffer.isBuffer(chunkArg) ? chunkArg.toString('utf8') : String(chunkArg);
      const parsed = parsePilightMessages(buffer);
      buffer = parsed.rest;
      for (const message of parsed.messages) {
        if (message.action === 'identify') {
          const options = message.options as Record<string, unknown> | undefined;
          if (options?.receiver === 1) {
            receiverSockets.add(socket);
          }
          socket.write(JSON.stringify({ status: 'success' }));
        } else if (message.action === 'send') {
          const code = message.code && typeof message.code === 'object' && !Array.isArray(message.code) ? message.code as Record<string, unknown> : {};
          sentCodes.push(code);
          socket.write(JSON.stringify({ status: code.protocol === 'unknown' ? 'failure' : 'success' }));
        }
      }
    });
  });

  await new Promise<void>((resolve, reject) => {
    server.once('error', reject);
    server.listen(0, '127.0.0.1', () => {
      server.off('error', reject);
      resolve();
    });
  });

  const address = server.address();
  if (!address || typeof address === 'string') {
    throw new Error('Pilight test server did not bind to a TCP port.');
  }

  return {
    port: address.port,
    sentCodes,
    broadcast: (messageArg) => {
      for (const socket of receiverSockets) {
        socket.write(`${JSON.stringify(messageArg)}\n`);
      }
    },
    close: async () => {
      for (const socket of sockets) {
        socket.destroy();
      }
      await new Promise<void>((resolve) => server.close(() => resolve()));
    },
  };
}

function parsePilightMessages(bufferArg: string): { messages: Record<string, unknown>[]; rest: string } {
  const messages: Record<string, unknown>[] = [];
  const lines = bufferArg.split(/\r?\n/g);
  if (lines.length > 1) {
    for (const line of lines.slice(0, -1)) {
      const parsed = parsePilightMessage(line);
      if (parsed) {
        messages.push(parsed);
      }
    }
    return { messages, rest: lines.at(-1) || '' };
  }
  const parsed = parsePilightMessage(bufferArg);
  if (parsed) {
    messages.push(parsed);
    return { messages, rest: '' };
  }
  return { messages, rest: bufferArg };
}

function parsePilightMessage(valueArg: string): Record<string, unknown> | undefined {
  const trimmed = valueArg.trim();
  if (!trimmed) {
    return undefined;
  }
  try {
    const parsed = JSON.parse(trimmed) as unknown;
    return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed as Record<string, unknown> : undefined;
  } catch {
    return undefined;
  }
}

async function waitFor(predicateArg: () => boolean, timeoutMsArg = 1000): Promise<void> {
  const started = Date.now();
  while (Date.now() - started < timeoutMsArg) {
    if (predicateArg()) {
      return;
    }
    await new Promise((resolve) => setTimeout(resolve, 10));
  }
  throw new Error('Timed out waiting for Pilight TCP test event.');
}

export default tap.start();
