const assert = require("node:assert");
const { spawn } = require("node:child_process");
const net = require("node:net");
const { test } = require("node:test");
const {
  resolveBackendExecutable,
  ensurePortFree,
  isPortFree,
  resolveRuntimePlan,
  shouldStartBackend,
} = require("./desktop-hot.js");

function findFreePort() {
  return new Promise((resolve, reject) => {
    const server = net.createServer();
    server.listen(0, "127.0.0.1", () => {
      const address = server.address();
      if (!address || typeof address === "string") {
        server.close(() => reject(new Error("无法分配测试端口")));
        return;
      }
      const port = address.port;
      server.close(() => resolve(port));
    });
    server.on("error", reject);
  });
}

function startPythonListener(port) {
  const pythonBin = resolveBackendExecutable("PYTHON_BIN", "python");
  const child = spawn(
    pythonBin,
    [
      "-c",
      `
import socket
import time
import threading
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(("127.0.0.1", ${port}))
server.listen(16)
def accept_loop():
    while True:
        conn, _addr = server.accept()
        conn.close()
threading.Thread(target=accept_loop, daemon=True).start()
print("ready", flush=True)
while True:
    time.sleep(1)
      `,
    ],
    {
      stdio: ["ignore", "pipe", "pipe"],
    },
  );

  return new Promise((resolve, reject) => {
    const timeout = setTimeout(() => {
      child.kill("SIGKILL");
      reject(new Error("测试监听进程启动超时"));
    }, 3000);

    child.stdout.on("data", (chunk) => {
      if (chunk.toString("utf8").includes("ready")) {
        clearTimeout(timeout);
        resolve(child);
      }
    });

    child.on("error", (error) => {
      clearTimeout(timeout);
      reject(error);
    });
  });
}

function waitForExit(child, timeoutMs = 3000) {
  if (child.exitCode !== null || child.signalCode !== null) {
    return Promise.resolve();
  }

  return new Promise((resolve, reject) => {
    const timeout = setTimeout(() => {
      child.off("exit", onExit);
      reject(new Error(`进程 ${child.pid} 未在 ${timeoutMs}ms 内退出`));
    }, timeoutMs);
    const onExit = () => {
      clearTimeout(timeout);
      resolve();
    };
    child.once("exit", onExit);
  });
}

test("ensurePortFree 在非 Windows 平台能释放后端监听进程", async (t) => {
  if (process.platform === "win32") {
    t.skip("该用例只覆盖 Linux/macOS 分支");
    return;
  }

  const port = await findFreePort();
  const child = await startPythonListener(port);

  t.after(() => {
    if (!child.killed) {
      child.kill("SIGKILL");
    }
  });

  assert.equal(await isPortFree("127.0.0.1", port), false);
  const ok = await ensurePortFree(port, "backend");
  assert.equal(ok, true);
  assert.equal(await isPortFree("127.0.0.1", port), true);
  await waitForExit(child);
});

test("默认热启动计划只使用 mnote-web 作为 3000 owner", () => {
  const plan = resolveRuntimePlan({
    FRONTEND_PORT: "3000",
  });

  assert.equal(plan.skipGateway, false);
  assert.equal(plan.publicPort, 3000);
  assert.equal(plan.mnoteWebCommand, "cargo run -p mnote-web --bin mnote-web");
  assert.deepEqual(plan.mnoteWebEnv, {
    MNOTE_WEB_BIND: "0.0.0.0:3000",
    MNOTE_WEB_PUBLIC_BIND: "127.0.0.1:3000",
  });
});

test("默认跳过 FastAPI 后端，只有显式开启时才启动", () => {
  assert.equal(shouldStartBackend({}), false);
  assert.equal(shouldStartBackend({ ENABLE_BACKEND: "1" }), true);
  assert.equal(shouldStartBackend({ ENABLE_BACKEND: "true" }), true);
  assert.equal(shouldStartBackend({ BACKEND_CMD: "custom-backend" }), true);
  assert.equal(shouldStartBackend({ ENABLE_BACKEND: "1", SKIP_BACKEND: "1" }), false);
});
