import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as plugins from '../../ts/plugins.js';
import { HomeAssistantPencomIntegration, PencomClient, PencomConfigFlow, PencomIntegration, PencomMapper, createPencomDiscoveryDescriptor, pencomProfile, type IPencomSnapshot, type TPencomRawData } from '../../ts/integrations/pencom/index.js';

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

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

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

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

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

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

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

tap.test('polls and controls Pencom relays through native TCP', async () => {
  const server = await startPencomTestServer();
  try {
    const config = {
      host: '127.0.0.1',
      port: server.port,
      timeoutMs: 500,
      boards: 1,
      relays: [
        { name: 'Pump', board: 0, addr: 0 },
        { name: 'Light', board: 0, addr: 1 },
      ],
    };
    const client = new PencomClient(config);
    const snapshot = await client.getSnapshot(true);
    const runtime = await new PencomIntegration().setup(config, {});
    const entities = await runtime.entities();

    expect(snapshot.source).toEqual('tcp');
    expect(snapshot.entities.find((entityArg) => entityArg.name === 'Pump')?.state).toBeTrue();
    expect(snapshot.entities.find((entityArg) => entityArg.name === 'Light')?.state).toBeFalse();
    expect(entities.some((entityArg) => entityArg.name === 'Pump' && entityArg.state === true)).toBeTrue();

    const result = await runtime.callService!({ domain: 'switch', service: 'turn_on', target: {}, data: { board: 0, addr: 1 } });
    expect(result.success).toBeTrue();
    expect(server.commands).toContain('AH2');
    expect(server.commands).toContain('AR0');
    expect(((result.data as IPencomSnapshot).entities.find((entityArg) => entityArg.name === 'Light')?.state)).toBeTrue();
    await runtime.destroy();
  } finally {
    await server.close();
  }
});

tap.test('exposes Pencom runtime, HA alias, and unsupported control without executor', async () => {
  const integration = new PencomIntegration();
  const alias = new HomeAssistantPencomIntegration();
  expect(alias instanceof PencomIntegration).toBeTrue();
  expect(alias.domain).toEqual("pencom");
  expect(integration.status).toEqual("control-runtime");
  expect(pencomProfile.metadata.configFlow).toEqual(false);
  expect(pencomProfile.metadata.requirements).toEqual([
  "pencompy==0.0.3",
  ]);
  expect(JSON.stringify(pencomProfile.metadata.localApi)).toContain('AR0');
  expect(JSON.stringify(pencomProfile.metadata.localApi)).toContain('AHn/ALn');

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

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

  const command = await runtime.callService!({ domain: "pencom", service: pencomProfile.controlServices?.[0] || 'turn_on', target: {} });
  expect(command.success).toBeFalse();
  expect(command.error!).toContain('requires an injected client.execute() or commandExecutor');
  await runtime.destroy();
});

interface IPencomTestServer {
  port: number;
  commands: string[];
  close(): Promise<void>;
}

async function startPencomTestServer(): Promise<IPencomTestServer> {
  const commands: string[] = [];
  const boardBits: Record<string, number> = { A: 1 };
  const sockets = new Set<plugins.net.Socket>();
  const server = plugins.net.createServer((socketArg) => {
    sockets.add(socketArg);
    socketArg.once('close', () => sockets.delete(socketArg));
    let buffer = '';
    socketArg.on('data', (chunkArg) => {
      buffer += Buffer.isBuffer(chunkArg) ? chunkArg.toString('utf8') : Buffer.from(chunkArg).toString('utf8');
      const parts = buffer.split('\r');
      buffer = parts.pop() || '';
      for (const command of parts.map((partArg) => partArg.trim()).filter(Boolean)) {
        commands.push(command);
        const board = command[0];
        const action = command[1];
        const relayNumber = Number(command.slice(2));
        if (action === 'H' || action === 'L') {
          const mask = 1 << (relayNumber - 1);
          boardBits[board] = action === 'H' ? (boardBits[board] || 0) | mask : (boardBits[board] || 0) & ~mask;
        }
        if (action === 'R') {
          socketArg.write(`${boardBits[board] || 0}\r`);
        }
      }
    });
  });

  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('Pencom test server did not bind to a TCP port.');
  }

  return {
    port: address.port,
    commands,
    close: async () => {
      for (const socket of sockets) {
        socket.destroy();
      }
      await new Promise<void>((resolve) => server.close(() => resolve()));
    },
  };
}

export default tap.start();
