import { createServer as createNetServer } from 'node:net';
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { HomeAssistantOsramlightifyIntegration, OsramlightifyClient, OsramlightifyConfigFlow, OsramlightifyIntegration, OsramlightifyMapper, createOsramlightifyDiscoveryDescriptor, osramlightifyProfile, type IOsramlightifySnapshot, type TOsramlightifyRawData } from '../../ts/integrations/osramlightify/index.js';

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

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

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

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

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

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

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

tap.test('reads and controls OSRAM Lightify gateway over local TCP', async () => {
  const requests: Buffer[] = [];
  const server = createNetServer((socketArg) => {
    let buffer = Buffer.alloc(0);
    socketArg.on('data', (chunkArg) => {
      buffer = Buffer.concat([buffer, Buffer.isBuffer(chunkArg) ? chunkArg : Buffer.from(chunkArg)]);
      while (buffer.length >= 2) {
        const length = buffer.readUInt16LE(0);
        if (buffer.length < 2 + length) {
          return;
        }
        const packet = buffer.subarray(0, 2 + length);
        requests.push(packet);
        socketArg.write(osramResponse(packet[3]));
        buffer = buffer.subarray(2 + length);
      }
    });
  });
  await listen(server);

  try {
    const address = server.address();
    const port = typeof address === 'object' && address ? address.port : 0;
    const client = new OsramlightifyClient({ host: '127.0.0.1', port, timeoutMs: 1000 });
    const snapshot = await client.getSnapshot(true);
    const command = await client.execute({ domain: 'light', service: 'turn_off', target: {}, data: { lightAddress: '1122334455667788' } });
    const runtime = await new OsramlightifyIntegration().setup({ host: '127.0.0.1', port, timeoutMs: 1000 }, {});
    const status = await runtime.callService!({ domain: 'osramlightify', service: 'status', target: {} });

    expect(snapshot.online).toBeTrue();
    expect(snapshot.source).toEqual('tcp');
    expect(snapshot.entities.find((entityArg) => entityArg.id === 'light_1122334455667788')?.state).toBeTrue();
    expect(snapshot.entities.find((entityArg) => entityArg.id === 'light_1122334455667788')?.attributes?.brightness).toEqual(127);
    expect(snapshot.entities.find((entityArg) => entityArg.id === 'group_1')?.attributes?.scenes).toEqual(['Evening']);
    expect(command.success).toBeTrue();
    expect(status.success).toBeTrue();
    expect(requests.some((packetArg) => packetArg[3] === 0x13)).toBeTrue();
    expect(requests.some((packetArg) => packetArg[3] === 0x1f)).toBeTrue();
    expect(requests.some((packetArg) => packetArg[3] === 0x1e)).toBeTrue();
    const offPacket = requests.find((packetArg) => packetArg[3] === 0x32 && packetArg[2] === 0x00);
    expect(offPacket?.[offPacket.length - 1]).toEqual(0);
    await runtime.destroy();
  } finally {
    await close(server);
  }
});

tap.test('exposes Osramlightify runtime, HA alias, and unsupported control without executor', async () => {
  const integration = new OsramlightifyIntegration();
  const alias = new HomeAssistantOsramlightifyIntegration();
  expect(alias instanceof OsramlightifyIntegration).toBeTrue();
  expect(alias.domain).toEqual("osramlightify");
  expect(integration.status).toEqual("control-runtime");
  expect(osramlightifyProfile.metadata.configFlow).toEqual(false);
  expect(osramlightifyProfile.metadata.requirements).toEqual([
    "lightify==1.0.7.3",
  ]);

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

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

  const command = await runtime.callService!({ domain: "osramlightify", service: osramlightifyProfile.controlServices?.[0] || 'turn_on', target: {} });
  expect(command.success).toBeFalse();
  expect(command.error!).toContain('TCP gateway');
  await runtime.destroy();
});

const osramResponse = (commandArg: number): Buffer => {
  if (commandArg === 0x13) {
    return frame(osramLightStatusPayload());
  }
  if (commandArg === 0x1e) {
    return frame(osramGroupPayload());
  }
  if (commandArg === 0x1f) {
    return frame(osramScenePayload());
  }
  return frame(Buffer.alloc(0));
};

const osramLightStatusPayload = (): Buffer => {
  const payload = Buffer.alloc(9 + 50);
  payload.writeUInt16LE(1, 7);
  const offset = 9;
  payload.writeBigUInt64LE(0x1122334455667788n, offset + 2);
  const stat = payload.subarray(offset + 10, offset + 26);
  stat[0] = 10;
  Buffer.from([0x01, 0x02, 0x03, 0x04]).copy(stat, 1);
  stat[5] = 1;
  stat.writeUInt16LE(1, 6);
  stat[8] = 1;
  stat[9] = 50;
  stat.writeUInt16LE(3000, 10);
  stat[12] = 10;
  stat[13] = 20;
  stat[14] = 30;
  stat[15] = 255;
  payload.write('Desk', offset + 26, 'utf8');
  payload.writeUInt32LE(1, offset + 42);
  return payload;
};

const osramGroupPayload = (): Buffer => {
  const payload = Buffer.alloc(9 + 18);
  payload.writeUInt16LE(1, 7);
  payload.writeUInt16LE(1, 9);
  payload.write('Office', 11, 'utf8');
  return payload;
};

const osramScenePayload = (): Buffer => {
  const payload = Buffer.alloc(9 + 20);
  payload.writeUInt16LE(1, 7);
  payload[9] = 3;
  payload.write('Evening', 11, 'utf8');
  payload.writeUInt16LE(1, 27);
  return payload;
};

const frame = (payloadArg: Buffer): Buffer => {
  const header = Buffer.alloc(2);
  header.writeUInt16LE(payloadArg.length, 0);
  return Buffer.concat([header, payloadArg]);
};

const listen = async (serverArg: ReturnType<typeof createNetServer>): Promise<void> => {
  await new Promise<void>((resolve, reject) => {
    serverArg.once('error', reject);
    serverArg.listen(0, '127.0.0.1', () => {
      serverArg.off('error', reject);
      resolve();
    });
  });
};

const close = async (serverArg: ReturnType<typeof createNetServer>): Promise<void> => {
  await new Promise<void>((resolve, reject) => serverArg.close((errorArg) => errorArg ? reject(errorArg) : resolve()));
};

export default tap.start();
