#!/usr/bin/env node
import mqtt from "mqtt";
import "dotenv/config";

const brokerUrl = process.env.MQTT_BROKER_URL;
const deviceA = {
  username: process.env.MQTT_TEST_DEVICE_A_USERNAME,
  password: process.env.MQTT_TEST_DEVICE_A_PASSWORD
};
const deviceB = {
  username: process.env.MQTT_TEST_DEVICE_B_USERNAME,
  password: process.env.MQTT_TEST_DEVICE_B_PASSWORD || process.env.MQTT_TEST_DEVICE_A_PASSWORD
};
const timeoutMs = Number(process.env.MQTT_ACL_SMOKE_TIMEOUT_MS || 8000);

function usage() {
  console.log(`Usage:
  MQTT_BROKER_URL=mqtts://broker:8883 \\
  MQTT_TEST_DEVICE_A_USERNAME=<device-a-id> \\
  MQTT_TEST_DEVICE_A_PASSWORD=<secret> \\
  MQTT_TEST_DEVICE_B_USERNAME=<device-b-id> \\
  npm run smoke:mqtt-acl

This validates Mosquitto ACL behavior from a device credential perspective:
  - device A can subscribe/read devices/A/downlink/#
  - device A can publish/write devices/A/uplink/#
  - device A is denied subscribe/read to devices/B/downlink/#
`);
}

if (!brokerUrl || !deviceA.username || !deviceA.password || !deviceB.username) {
  usage();
  process.exit(1);
}

function connectDevice(device) {
  const client = mqtt.connect(brokerUrl, {
    username: device.username,
    password: device.password,
    clientId: `qris-acl-smoke-${device.username}-${Date.now()}`,
    reconnectPeriod: 0,
    connectTimeout: timeoutMs
  });

  return new Promise((resolve, reject) => {
    const timer = setTimeout(() => {
      client.end(true);
      reject(new Error(`connect timeout for ${device.username}`));
    }, timeoutMs);
    client.once("connect", () => {
      clearTimeout(timer);
      resolve(client);
    });
    client.once("error", (error) => {
      clearTimeout(timer);
      client.end(true);
      reject(error);
    });
  });
}

function subscribe(client, topic) {
  return new Promise((resolve, reject) => {
    client.subscribe(topic, { qos: 1 }, (error, grants) => {
      if (error) {
        reject(error);
        return;
      }
      resolve(grants || []);
    });
  });
}

function publish(client, topic, payload) {
  return new Promise((resolve, reject) => {
    client.publish(topic, payload, { qos: 1 }, (error) => {
      if (error) {
        reject(error);
        return;
      }
      resolve();
    });
  });
}

function granted(grants) {
  return grants.some((grant) => Number(grant.qos) !== 128);
}

const client = await connectDevice(deviceA);
try {
  const ownDownlink = `devices/${deviceA.username}/downlink/#`;
  const ownUplink = `devices/${deviceA.username}/uplink/acl-smoke`;
  const foreignDownlink = `devices/${deviceB.username}/downlink/#`;

  const ownGrants = await subscribe(client, ownDownlink);
  if (!granted(ownGrants)) {
    throw new Error(`device ${deviceA.username} could not subscribe own downlink topic`);
  }

  await publish(
    client,
    ownUplink,
    JSON.stringify({ type: "acl_smoke", device_id: deviceA.username, timestamp: new Date().toISOString() })
  );

  const foreignGrants = await subscribe(client, foreignDownlink);
  if (granted(foreignGrants)) {
    throw new Error(`ACL allows ${deviceA.username} to subscribe foreign downlink ${foreignDownlink}`);
  }

  console.log(
    JSON.stringify(
      {
        ok: true,
        broker_url: brokerUrl,
        device: deviceA.username,
        own_downlink: ownDownlink,
        own_uplink: ownUplink,
        foreign_downlink_denied: foreignDownlink
      },
      null,
      2
    )
  );
} finally {
  client.end(true);
}
