import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as plugins from '../../ts/plugins.js';
import { HomeAssistantPjlinkIntegration, PjlinkClient, PjlinkConfigFlow, PjlinkIntegration, PjlinkMapper, createPjlinkDiscoveryDescriptor, pjlinkProfile, type IPjlinkSnapshot, type TPjlinkRawData } from '../../ts/integrations/pjlink/index.js';

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

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

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

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

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

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

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

tap.test('polls and controls PJLink through native authenticated TCP', async () => {
  const server = await startPjlinkTestServer();
  try {
    const client = new PjlinkClient({ host: '127.0.0.1', port: server.port, password: 'secret', timeoutMs: 500 });
    const snapshot = await client.getSnapshot(true);
    const entity = snapshot.entities.find((entityArg) => entityArg.id === 'media_player');

    expect(snapshot.source).toEqual('tcp');
    expect(snapshot.device.manufacturer).toEqual('Epson');
    expect(snapshot.device.model).toEqual('PowerLite');
    expect(entity?.state).toEqual('on');
    expect(entity?.attributes?.isVolumeMuted).toBeTrue();
    expect(entity?.attributes?.source).toEqual('RGB 1');
    expect(server.authenticatedCount > 0).toBeTrue();

    const result = await client.execute({ domain: 'media_player', service: 'volume_mute', target: {}, data: { mute: false } });
    expect(result.success).toBeTrue();
    expect(server.received.includes('%1AVMT 20')).toBeTrue();
  } finally {
    await server.close();
  }
});

tap.test('exposes PJLink runtime, HA alias, and unsupported control without executor', async () => {
  const integration = new PjlinkIntegration();
  const alias = new HomeAssistantPjlinkIntegration();
  expect(alias instanceof PjlinkIntegration).toBeTrue();
  expect(alias.domain).toEqual("pjlink");
  expect(integration.status).toEqual("control-runtime");
  expect(pjlinkProfile.metadata.configFlow).toEqual(true);
  expect(pjlinkProfile.metadata.requirements).toEqual([
    "pypjlink2==1.2.1",
  ]);

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

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

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

interface IPjlinkTestServer {
  port: number;
  received: string[];
  authenticatedCount: number;
  close(): Promise<void>;
}

async function startPjlinkTestServer(): Promise<IPjlinkTestServer> {
  const challenge = 'abcdefgh';
  const password = 'secret';
  const expectedDigest = plugins.crypto.createHash('md5').update(`${challenge}${password}`, 'utf8').digest('hex');
  const received: string[] = [];
  let authenticatedCount = 0;
  const sockets = new Set<plugins.net.Socket>();
  const server = plugins.net.createServer((socket) => {
    sockets.add(socket);
    socket.once('close', () => sockets.delete(socket));
    socket.write(`PJLINK 1 ${challenge}\r`, 'utf8');
    let buffer = '';
    socket.on('data', (chunkArg) => {
      buffer += Buffer.from(chunkArg).toString('utf8');
      let index = buffer.indexOf('\r');
      while (index >= 0) {
        let command = buffer.slice(0, index).trim();
        buffer = buffer.slice(index + 1);
        if (command.startsWith(expectedDigest)) {
          authenticatedCount++;
          command = command.slice(expectedDigest.length);
        }
        if (command) {
          received.push(command);
          socket.write(`${pjlinkResponse(command)}\r`, 'utf8');
        }
        index = buffer.indexOf('\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('PJLink test server did not bind to a TCP port.');
  }

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

function pjlinkResponse(commandArg: string): string {
  const match = /^%1([A-Z0-9]{4}) (.*)$/.exec(commandArg);
  if (!match) {
    return 'PJLINK ERRA';
  }
  const body = match[1];
  const param = match[2];
  if (param !== '?') {
    return `%1${body}=OK`;
  }
  const responses: Record<string, string> = {
    POWR: '1',
    NAME: 'Conference Projector',
    INF1: 'Epson',
    INF2: 'PowerLite',
    INFO: 'Lab',
    INST: '11 31',
    AVMT: '21',
    INPT: '11',
  };
  return `%1${body}=${responses[body] || 'ERR1'}`;
}

export default tap.start();
