#!/usr/bin/env node
import assert from "node:assert/strict";
import { spawn } from "node:child_process";
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { request as httpRequest } from "node:http";
import { tmpdir } from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";

const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const fixturePath = path.join(repoRoot, "fixtures/mvp/patch-panel-runtime/two-box-sync.json");

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

async function freePort() {
  const { createServer } = await import("node:net");
  return new Promise((resolve, reject) => {
    const server = createServer();
    server.on("error", reject);
    server.listen(0, "127.0.0.1", () => {
      const address = server.address();
      const port = typeof address === "object" && address ? address.port : null;
      server.close(() => {
        if (!port) {
          reject(new Error("failed to allocate a free port"));
          return;
        }
        resolve(port);
      });
    });
  });
}

function startNode(scriptPath, env) {
  const child = spawn(process.execPath, [scriptPath], {
    cwd: repoRoot,
    env: {
      ...process.env,
      ...env
    },
    stdio: ["ignore", "pipe", "pipe"]
  });
  child.stdout.setEncoding("utf8");
  child.stderr.setEncoding("utf8");
  return child;
}

function stopProcess(child) {
  if (!child || child.exitCode !== null) {
    return Promise.resolve();
  }
  return new Promise((resolve) => {
    const timeout = setTimeout(() => {
      child.kill("SIGKILL");
      resolve();
    }, 2000);
    child.once("exit", () => {
      clearTimeout(timeout);
      resolve();
    });
    child.kill("SIGTERM");
  });
}

async function requestJson(url, options = {}) {
  const body = options.body ? JSON.stringify(options.body) : null;
  return new Promise((resolve, reject) => {
    const req = httpRequest(
      new URL(url),
      {
        method: options.method ?? (body ? "POST" : "GET"),
        headers: body
          ? {
              "content-type": "application/json",
              "content-length": Buffer.byteLength(body)
            }
          : {},
        timeout: options.timeoutMs ?? 5000
      },
      (response) => {
        let text = "";
        response.setEncoding("utf8");
        response.on("data", (chunk) => {
          text += chunk;
        });
        response.on("end", () => {
          let json = null;
          try {
            json = text ? JSON.parse(text) : null;
          } catch {
            json = null;
          }
          resolve({
            response,
            status: response.statusCode ?? 0,
            body: json,
            text
          });
        });
      }
    );
    req.on("timeout", () => {
      req.destroy(new Error(`request timed out for ${url}`));
    });
    req.on("error", reject);
    if (body) {
      req.write(body);
    }
    req.end();
  });
}

async function waitForHttp(url, child) {
  const startedAt = Date.now();
  let lastError = null;

  while (Date.now() - startedAt < 5000) {
    if (child.exitCode !== null) {
      throw new Error(`process exited before ${url} became ready`);
    }
    try {
      const result = await requestJson(url, { timeoutMs: 500 });
      if (result.status >= 200 && result.status < 300) {
        return result;
      }
    } catch (error) {
      lastError = error;
    }
    await new Promise((resolve) => setTimeout(resolve, 100));
  }

  throw new Error(`timed out waiting for ${url}: ${lastError?.message ?? "no response"}`);
}

function assertPatchPanelDiagnostics(diagnostics) {
  assert.equal(diagnostics.serviceId, "hwlab-patch-panel");
  assert.equal(diagnostics.environment, "dev");
  assert.equal(diagnostics.state, "active");
  assert.equal(diagnostics.summary.configApplied, true);
  assert.equal(diagnostics.summary.connectionCount, 3);
  assert.equal(diagnostics.summary.syncableConnectionCount, 3);
  assert.equal(diagnostics.summary.diagnostics.errorCount, 0);
}

async function main() {
  const fixture = JSON.parse(await readFile(fixturePath, "utf8"));
  const box1Port = await freePort();
  const box2Port = await freePort();
  const patchPort = await freePort();
  const tempDir = await mkdtemp(path.join(tmpdir(), "hwlab-patch-panel-runtime-"));
  const configPath = path.join(tempDir, "runtime-config.json");
  const runtimeConfig = clone(fixture.runtimeConfig);
  runtimeConfig.endpointMap = {
    res_boxsimu_runtime_1: `http://127.0.0.1:${box1Port}`,
    res_boxsimu_runtime_2: `http://127.0.0.1:${box2Port}`
  };
  await writeFile(configPath, `${JSON.stringify(runtimeConfig, null, 2)}\n`, "utf8");

  const box1 = startNode("cmd/hwlab-box-simu/main.mjs", {
    PORT: String(box1Port),
    HWLAB_BOX_ID: fixture.boxes[0].boxId
  });
  const box2 = startNode("cmd/hwlab-box-simu/main.mjs", {
    PORT: String(box2Port),
    HWLAB_BOX_ID: fixture.boxes[1].boxId
  });
  const patchPanel = startNode("cmd/hwlab-patch-panel/main.mjs", {
    PORT: String(patchPort),
    HWLAB_WIRING_CONFIG_PATH: configPath
  });

  try {
    const box1Base = `http://127.0.0.1:${box1Port}`;
    const box2Base = `http://127.0.0.1:${box2Port}`;
    const patchBase = `http://127.0.0.1:${patchPort}`;

    await Promise.all([
      waitForHttp(`${box1Base}/health/live`, box1),
      waitForHttp(`${box2Base}/health/live`, box2),
      waitForHttp(`${patchBase}/health/live`, patchPanel)
    ]);

    const diagnostics = await requestJson(`${patchBase}/diagnostics`);
    assert.equal(diagnostics.status, 200);
    assertPatchPanelDiagnostics(diagnostics.body);

    const status = await requestJson(`${patchBase}/status`);
    assert.equal(status.status, 200);
    assert.equal(status.body.serviceId, "hwlab-patch-panel");
    assert.equal(status.body.state, "active");
    assert.equal(status.body.activeConnections.length, 3);

    const tick = await requestJson(`${patchBase}/sync/tick`, {
      method: "POST",
      body: {
        signals: fixture.signals
      }
    });
    assert.equal(tick.status, 200);
    assert.equal(tick.body.accepted, true);
    assert.equal(tick.body.deliveryCount, 3);

    const targetState = await requestJson(`${box2Base}/status`);
    assert.equal(targetState.status, 200);
    assert.equal(targetState.body.ports.DI1.value, fixture.expected.ports.DI1);
    assert.equal(targetState.body.ports.DI1.propagatedBy, "hwlab-patch-panel");
    assert.equal(targetState.body.ports.DI1.source, "patch-panel");
    assert.equal(targetState.body.ports.DI1.sourceResourceId, fixture.signals[0].fromResourceId);
    assert.equal(targetState.body.ports.AI1.value, fixture.expected.ports.AI1);
    assert.equal(targetState.body.ports.AI1.propagatedBy, "hwlab-patch-panel");
    assert.equal(targetState.body.ports.AI1.source, "patch-panel");
    assert.equal(targetState.body.ports.AI1.sourceResourceId, fixture.signals[1].fromResourceId);
    assert.equal(targetState.body.ports.FREQ_IN1.value, fixture.expected.ports.FREQ_IN1);
    assert.equal(targetState.body.ports.FREQ_IN1.propagatedBy, "hwlab-patch-panel");
    assert.equal(targetState.body.ports.FREQ_IN1.source, "patch-panel");
    assert.equal(targetState.body.ports.FREQ_IN1.sourceResourceId, fixture.signals[2].fromResourceId);

    const invalidConfig = clone(runtimeConfig);
    invalidConfig.wiringConfig = {
      ...invalidConfig.wiringConfig,
      wiringConfigId: "wir_patch_panel_runtime_invalid",
      connections: [fixture.invalidWiringProbe]
    };
    const invalidApply = await requestJson(`${patchBase}/wiring/apply`, {
      method: "POST",
      body: invalidConfig
    });
    assert.equal(invalidApply.status, 200);
    assert.equal(invalidApply.body.accepted, false);
    assert.ok(
      invalidApply.body.diagnostics.some((item) => item.code === "source_port_not_output"),
      "invalid wiring must report source_port_not_output"
    );

    console.log("patch-panel runtime smoke passed");
    console.log("wiring: DO1->DI1, AO1->AI1, and FREQ_OUT1->FREQ_IN1 synchronized through hwlab-patch-panel");
    console.log("diagnostics: invalid DI1->DO1 wiring reported source_port_not_output");
  } finally {
    await Promise.all([stopProcess(patchPanel), stopProcess(box1), stopProcess(box2)]);
    await rm(tempDir, { recursive: true, force: true });
  }
}

try {
  await main();
} catch (error) {
  console.error(`[patch-panel-runtime-smoke] ${error instanceof Error ? error.message : String(error)}`);
  process.exitCode = 1;
}
