#!/usr/bin/env node
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";

import { createPatchPanel } from "../internal/patchpanel/model.mjs";

const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const fixturePath = path.resolve(
  repoRoot,
  process.argv[2] ?? "fixtures/mvp/m3-hardware-loop/topology.json"
);

const REQUIRED_AUDIT_FIELDS = Object.freeze([
  "auditId",
  "traceId",
  "actorType",
  "actorId",
  "action",
  "targetType",
  "targetId",
  "serviceId",
  "environment",
  "occurredAt"
]);

const RECOMMENDED_AUDIT_FIELDS = Object.freeze([
  "projectId",
  "gatewaySessionId",
  "operationId",
  "outcome",
  "metadata"
]);

function clone(value) {
  return JSON.parse(JSON.stringify(value));
}

function assertPort(box, port) {
  assert.ok(box.ports, `${box.boxId} missing ports`);
  assert.ok(Object.hasOwn(box.ports, port), `${box.boxId} missing port ${port}`);
  assert.equal(box.ports[port].port, port, `${box.boxId}.${port} port name`);
}

function assertTimestamp(value, name) {
  assert.equal(typeof value, "string", `${name} must be a string`);
  assert.ok(!Number.isNaN(Date.parse(value)), `${name} must be an RFC 3339 timestamp`);
}

function assertOnePatchPanel(topology) {
  const patchPanel = topology.services?.patchPanel;
  assert.ok(patchPanel, "topology requires one patch panel");
  assert.equal(patchPanel.serviceId, "hwlab-patch-panel");
  assert.equal(patchPanel.state, "active");
  assert.equal(patchPanel.environment, "dev");
  assert.equal(patchPanel.projectId, topology.projectId);
  assert.equal(patchPanel.wiringConfigId, topology.wiringConfig.wiringConfigId);
  assert.equal(patchPanel.metadata?.propagation, "patch-panel-only");
  assert.equal(patchPanel.activeConnections.length, topology.wiringConfig.connections.length);
  return patchPanel;
}

function indexBoxes(boxes) {
  const byResourceId = new Map();
  const byAlias = new Map();

  for (const box of boxes) {
    assert.equal(box.serviceId, "hwlab-box-simu");
    assert.equal(box.live, true, `${box.boxId} must be live`);
    assert.equal(box.projectId, box.resource.projectId, `${box.boxId} project must match resource`);
    assert.equal(box.environment ?? box.resource.environment, "dev", `${box.boxId} environment`);
    assert.equal(box.constraints.crossDevicePropagation, "patch-panel-only");
    assert.equal(box.constraints.localLoopbackEnabled, false);
    assert.equal(box.resource.metadata.propagation, "patch-panel-only");
    assertPort(box, "DO1");
    assertPort(box, "DI1");

    byResourceId.set(box.resource.resourceId, box);
    for (const alias of box.aliases ?? []) {
      byAlias.set(alias, box);
    }
  }

  return { byResourceId, byAlias };
}

function assertGateways(topology, resourceIds) {
  const gateways = topology.services?.gateways;
  assert.ok(Array.isArray(gateways), "topology requires gateways");
  assert.equal(gateways.length, 2, "topology requires exactly two gateway simulators");

  const seenSessions = new Set();
  for (const gateway of gateways) {
    assert.equal(gateway.serviceId, "hwlab-gateway-simu");
    assert.equal(gateway.live, true, `${gateway.gatewayId} must be live`);
    assert.equal(gateway.projectId, topology.projectId);
    assert.equal(gateway.session.serviceId, "hwlab-gateway-simu");
    assert.equal(gateway.session.status, "connected");
    assert.equal(gateway.session.environment, "dev");
    assert.equal(gateway.session.projectId, topology.projectId);
    seenSessions.add(gateway.session.gatewaySessionId);

    assert.ok(Array.isArray(gateway.boxes) && gateway.boxes.length >= 1, `${gateway.gatewayId} boxes`);
    for (const resourceId of gateway.boxes) {
      assert.ok(resourceIds.has(resourceId), `${gateway.gatewayId} references known box resource`);
    }
  }

  assert.ok(
    seenSessions.has(topology.wiringConfig.gatewaySessionId),
    "wiring gateway session must belong to a gateway simulator"
  );
}

function assertWiringMatchesPatchPanel(wiringConfig, patchPanel) {
  assert.equal(wiringConfig.status, "active");
  assert.equal(wiringConfig.constraints.propagation, "patch-panel-only");
  assert.equal(wiringConfig.constraints.localLoopbackSubstituteAllowed, false);
  assert.equal(wiringConfig.connections.length, 1, "M3 smoke expects one DO1->DI1 connection");

  const connection = wiringConfig.connections[0];
  const activeConnection = patchPanel.activeConnections[0];
  assert.deepEqual(activeConnection, {
    fromResourceId: connection.from.resourceId,
    fromPort: connection.from.port,
    toResourceId: connection.to.resourceId,
    toPort: connection.to.port
  });

  return connection;
}

function assertAuditEvent(topology) {
  const auditEvent = topology.expectedAuditEvent;
  assert.ok(auditEvent && typeof auditEvent === "object", "expectedAuditEvent is required");

  for (const field of REQUIRED_AUDIT_FIELDS) {
    assert.ok(Object.hasOwn(auditEvent, field), `expected audit.${field}`);
  }
  for (const field of RECOMMENDED_AUDIT_FIELDS) {
    assert.ok(Object.hasOwn(auditEvent, field), `expected audit.${field}`);
  }

  assert.equal(auditEvent.traceId, topology.directCall.traceId);
  assert.equal(auditEvent.projectId, topology.projectId);
  assert.equal(auditEvent.gatewaySessionId, topology.wiringConfig.gatewaySessionId);
  assert.equal(auditEvent.operationId, topology.directCall.operationId);
  assert.equal(auditEvent.targetType, "wiring_config");
  assert.equal(auditEvent.targetId, topology.wiringConfig.wiringConfigId);
  assert.equal(auditEvent.serviceId, "hwlab-patch-panel");
  assert.equal(auditEvent.environment, "dev");
  assert.equal(auditEvent.outcome, "succeeded");
  assert.equal(auditEvent.metadata.databaseWriteRequired, false);
  assert.equal(auditEvent.metadata.directCall, true);
  assertTimestamp(auditEvent.occurredAt, "audit.occurredAt");
}

function applyOutput(box, port, value, updatedAt) {
  assertPort(box, port);
  assert.equal(box.ports[port].direction, "output", `${box.boxId}.${port} must be an output`);
  box.ports[port] = {
    ...box.ports[port],
    value,
    source: "local",
    updatedAt
  };
}

function applyDeliveries(boxesByResourceId, deliveries) {
  for (const delivery of deliveries) {
    const box = boxesByResourceId.get(delivery.resourceId);
    assert.ok(box, `delivery target ${delivery.resourceId} exists`);
    assertPort(box, delivery.port);
    assert.equal(box.ports[delivery.port].direction, "input", `${box.boxId}.${delivery.port} must be an input`);
    box.ports[delivery.port] = {
      ...box.ports[delivery.port],
      value: delivery.value,
      source: delivery.sourceResourceId,
      sourcePort: delivery.sourcePort,
      propagatedBy: "hwlab-patch-panel",
      updatedAt: delivery.observedAt
    };
  }
}

const raw = await readFile(fixturePath, "utf8");
const topology = JSON.parse(raw);

assert.equal(topology.environment, "dev");
assert.equal(topology.contract.mode, "local-direct-call");
assert.equal(topology.contract.databaseWriteRequired, false);
assert.equal(topology.contract.realHardwareRequired, false);
assert.equal(topology.contract.cloudApiRequired, false);
assert.equal(topology.constraints.patchPanelIsOnlyCrossDeviceChannel, true);
assert.equal(topology.constraints.boxSimuLocalLoopbackForCrossDeviceSync, false);
assert.equal(topology.constraints.realDevHardwareMutationAllowed, false);
assert.equal(topology.constraints.cloudApiImplementationMutationAllowed, false);

const boxes = topology.services?.boxes;
assert.ok(Array.isArray(boxes), "topology requires boxes");
assert.equal(boxes.length, 2, "topology requires exactly two box simulators");

const { byResourceId, byAlias } = indexBoxes(boxes);
assertGateways(topology, new Set(byResourceId.keys()));

const patchPanelStatus = assertOnePatchPanel(topology);
const connection = assertWiringMatchesPatchPanel(topology.wiringConfig, patchPanelStatus);
assertAuditEvent(topology);

const directCall = topology.directCall;
const sourceBox = byAlias.get(directCall.source.boxAlias);
const targetBox = byAlias.get(directCall.target.boxAlias);
assert.ok(sourceBox, "box-simu-1 alias must exist");
assert.ok(targetBox, "box-simu-2 alias must exist");
assert.equal(connection.from.resourceId, sourceBox.resource.resourceId);
assert.equal(connection.from.port, "DO1");
assert.equal(connection.to.resourceId, targetBox.resource.resourceId);
assert.equal(connection.to.port, "DI1");
assert.equal(directCall.source.resourceId, sourceBox.resource.resourceId);
assert.equal(directCall.source.port, "DO1");
assert.equal(directCall.target.resourceId, targetBox.resource.resourceId);
assert.equal(directCall.target.port, "DI1");

const localBoxes = new Map([...byResourceId].map(([resourceId, box]) => [resourceId, clone(box)]));
applyOutput(
  localBoxes.get(directCall.source.resourceId),
  directCall.source.port,
  directCall.source.value,
  directCall.expected.observedAt
);

const patchPanel = createPatchPanel({
  wiringConfig: topology.wiringConfig,
  now: () => directCall.expected.observedAt
});

const routeResult = patchPanel.routeSignal({
  fromResourceId: directCall.source.resourceId,
  fromPort: directCall.source.port,
  value: directCall.source.value
});

assert.equal(routeResult.accepted, directCall.expected.accepted);
assert.equal(routeResult.propagatedBy, directCall.expected.propagatedBy);
assert.equal(routeResult.deliveryCount, directCall.expected.deliveryCount);
assert.deepEqual(routeResult.deliveries, [
  {
    resourceId: directCall.target.resourceId,
    port: directCall.target.port,
    value: directCall.target.expectedValue,
    sourceResourceId: directCall.source.resourceId,
    sourcePort: directCall.source.port,
    observedAt: directCall.expected.observedAt
  }
]);

applyDeliveries(localBoxes, routeResult.deliveries);

const targetState = localBoxes.get(directCall.target.resourceId).ports[directCall.target.port];
assert.equal(targetState.value, directCall.target.expectedValue);
assert.equal(targetState.propagatedBy, "hwlab-patch-panel");
assert.equal(targetState.source, directCall.source.resourceId);
assert.equal(targetState.sourcePort, directCall.source.port);

const bypassProbe = patchPanel.routeSignal({
  fromResourceId: directCall.source.resourceId,
  fromPort: "DI1",
  value: true
});
assert.equal(bypassProbe.deliveryCount, 0, "unwired direct box signal must not cross devices");

console.log("M3 hardware loop smoke passed");
console.log("topology: 2 box simulators, 2 gateway simulators, 1 patch panel");
console.log("wiring: box-simu-1 DO1 -> box-simu-2 DI1 via hwlab-patch-panel");
console.log(`audit expectation: ${[...REQUIRED_AUDIT_FIELDS, ...RECOMMENDED_AUDIT_FIELDS].join(", ")}`);
