import test from "node:test";
import assert from "node:assert/strict";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { execFile as execFileCallback } from "node:child_process";
import { promisify } from "node:util";
import { randomUUID } from "node:crypto";

import WebSocket from "ws";

import { createBrokerServer } from "../server.mjs";

const execFile = promisify(execFileCallback);
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const fakeAgentScriptPath = path.resolve(__dirname, "../../edge-agent/test-support/fake-agent.mjs");

async function runCommand(command, args, { allowFailure = false } = {}) {
  try {
    return await execFile(command, args, {
      windowsHide: true,
      encoding: "utf8",
    });
  } catch (error) {
    if (allowFailure) {
      return {
        stdout: error.stdout || "",
        stderr: error.stderr || "",
        code: error.code,
      };
    }
    throw error;
  }
}

async function removeContainer(name) {
  await runCommand("docker", ["rm", "-f", name], { allowFailure: true });
}

async function startFakeAgent({ port, gatewayId, agentToken, behavior }) {
  const containerName = `edge-live-${behavior}-${randomUUID().slice(0, 8)}`;
  const brokerUrl = `http://127.0.0.1:${port}`;

  await runCommand("docker", [
    "run",
    "-d",
    "--rm",
    "--name",
    containerName,
    "-e",
    `EDGE_TEST_BROKER_URL=${brokerUrl}`,
    "-e",
    `EDGE_TEST_GATEWAY_ID=${gatewayId}`,
    "-e",
    `EDGE_TEST_AGENT_TOKEN=${agentToken}`,
    "-e",
    `EDGE_TEST_BEHAVIOR=${behavior}`,
    "-v",
    `${fakeAgentScriptPath}:/app/fake-agent.mjs:ro`,
    "node:22-alpine",
    "node",
    "/app/fake-agent.mjs",
  ]);

  return containerName;
}

async function waitForCondition(predicate, { timeoutMs = 5_000, intervalMs = 50, message = "Timed out" } = {}) {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    if (await predicate()) {
      return;
    }
    await new Promise((resolve) => setTimeout(resolve, intervalMs));
  }
  throw new Error(message);
}

function waitForSocketEvent(socket, eventName) {
  return new Promise((resolve) => {
    socket.once(eventName, (...args) => resolve(args));
  });
}

function collectMessages(socket) {
  const messages = [];
  socket.on("message", (raw) => {
    messages.push(JSON.parse(raw.toString()));
  });
  return messages;
}

async function createHarness(t, options = {}) {
  const broker = createBrokerServer({
    authMode: "stub",
    sharedSecret: "secret",
    commandTimeoutMs: options.commandTimeoutMs ?? 300,
    validateShellSession: async () => ({
      id: "shell-live-1",
      gateway_id: String(options.gatewayId ?? 701),
      reason: "diagnostic",
    }),
  });
  const address = await broker.listen(0);
  const port = address.port;
  const gatewayId = String(options.gatewayId ?? 701);
  const agentToken = options.agentToken ?? "agent-token";
  const containers = [];

  t.after(async () => {
    await Promise.all(containers.map((name) => removeContainer(name)));
    await broker.close();
  });

  return {
    broker,
    port,
    gatewayId,
    agentToken,
    async startAgent(behavior) {
      const containerName = await startFakeAgent({
        port,
        gatewayId,
        agentToken,
        behavior,
      });
      containers.push(containerName);
      if (behavior !== "offline") {
        await waitForCondition(
          () => broker.state.agents.has(gatewayId),
          { message: `Fake agent did not connect for behavior ${behavior}` }
        );
      }
      return containerName;
    },
  };
}

test("live smoke: dockerized fake agent handles command and shell round-trips", async (t) => {
  const harness = await createHarness(t);
  await harness.startAgent("success");

  const commandResponse = await fetch(`http://127.0.0.1:${harness.port}/api/gateways/${harness.gatewayId}/commands`, {
    method: "POST",
    headers: {
      "content-type": "application/json",
      "x-edge-broker-secret": "secret",
    },
    body: JSON.stringify({
      commandType: "GET_RELAY_STATUS",
      payload: { relayId: "M-7", localIp: "10.1.0.31" },
    }),
  });
  const commandJson = await commandResponse.json();

  assert.equal(commandResponse.status, 200);
  assert.equal(commandJson.ok, true);
  assert.deepEqual(commandJson.payload.echo, {
    relayId: "M-7",
    localIp: "10.1.0.31",
  });

  const browser = new WebSocket(`ws://127.0.0.1:${harness.port}/ws/browser-shell?token=session-token`);
  const browserMessages = collectMessages(browser);
  t.after(() => browser.readyState < 2 && browser.close());

  await waitForSocketEvent(browser, "open");
  await waitForCondition(
    () => browserMessages.some((message) => message.type === "opened"),
    { message: "Browser shell never opened" }
  );

  browser.send(JSON.stringify({ type: "input", data: "ls\r" }));
  await waitForCondition(
    () => browserMessages.some((message) => message.type === "output" && message.data.includes("echo:ls")),
    { message: "Browser shell never received echoed input" }
  );

  browser.send(JSON.stringify({ type: "close" }));
  await waitForCondition(
    () => browserMessages.some((message) => message.type === "closed" && message.code === 0),
    { message: "Browser shell never closed cleanly" }
  );
});

test("live smoke: offline agent returns gateway unavailable", async (t) => {
  const harness = await createHarness(t);
  await harness.startAgent("offline");
  await new Promise((resolve) => setTimeout(resolve, 150));

  const response = await fetch(`http://127.0.0.1:${harness.port}/api/gateways/${harness.gatewayId}/commands`, {
    method: "POST",
    headers: {
      "content-type": "application/json",
      "x-edge-broker-secret": "secret",
    },
    body: JSON.stringify({
      commandType: "GET_RELAY_STATUS",
      payload: { relayId: "M-7" },
    }),
  });
  const json = await response.json();

  assert.equal(response.status, 503);
  assert.equal(json.error, "Gateway agent is offline");
});

test("live smoke: unresponsive agent times out command dispatch", async (t) => {
  const harness = await createHarness(t, { commandTimeoutMs: 200 });
  await harness.startAgent("timeout");

  const response = await fetch(`http://127.0.0.1:${harness.port}/api/gateways/${harness.gatewayId}/commands`, {
    method: "POST",
    headers: {
      "content-type": "application/json",
      "x-edge-broker-secret": "secret",
    },
    body: JSON.stringify({
      commandType: "GET_RELAY_STATUS",
      payload: { relayId: "M-7" },
    }),
  });
  const json = await response.json();

  assert.equal(response.status, 504);
  assert.equal(json.ok, false);
  assert.equal(json.error, "Agent command timed out");
});

test("live smoke: bad shared secret is rejected", async (t) => {
  const harness = await createHarness(t);
  await harness.startAgent("success");

  const response = await fetch(`http://127.0.0.1:${harness.port}/api/gateways/${harness.gatewayId}/commands`, {
    method: "POST",
    headers: {
      "content-type": "application/json",
      "x-edge-broker-secret": "wrong-secret",
    },
    body: JSON.stringify({
      commandType: "GET_RELAY_STATUS",
      payload: { relayId: "M-7" },
    }),
  });
  const json = await response.json();

  assert.equal(response.status, 403);
  assert.equal(json.error, "Forbidden");
});

test("live smoke: browser shell fails cleanly when agent disconnects before shell open", async (t) => {
  const harness = await createHarness(t);
  await harness.startAgent("shell-open-fail");

  const browser = new WebSocket(`ws://127.0.0.1:${harness.port}/ws/browser-shell?token=session-token`);
  const browserMessages = collectMessages(browser);
  const closePromise = waitForSocketEvent(browser, "close");

  await waitForSocketEvent(browser, "open");
  await closePromise;

  assert.deepEqual(browserMessages, []);
  assert.equal(harness.broker.state.browserSessions.size, 0);
  assert.equal(harness.broker.state.agents.size, 0);
});
