// Copyright 2018-2026 the Deno authors. MIT license.

import { assert, assertEquals } from "@std/assert";
import { execCode } from "../unit/test_util.ts";
import { createSocket, type Socket } from "node:dgram";

const listenPort = 4503;
const listenPort2 = 4504;

Deno.test("[node/dgram] udp ref and unref", {
  permissions: { read: true, run: true, net: true },
}, async () => {
  const { promise, resolve } = Promise.withResolvers<void>();

  const udpSocket = createSocket("udp4");
  udpSocket.bind(listenPort);

  udpSocket.unref();
  udpSocket.ref();

  let data;
  udpSocket.on("message", (buffer, _rinfo) => {
    data = Uint8Array.from(buffer);
    udpSocket.close();
  });
  udpSocket.on("close", () => {
    resolve();
  });

  const conn = await Deno.listenDatagram({
    port: listenPort2,
    transport: "udp",
  });
  await conn.send(new Uint8Array([0, 1, 2, 3]), {
    transport: "udp",
    port: listenPort,
    hostname: "127.0.0.1",
  });

  await promise;
  conn.close();
  assertEquals(data, new Uint8Array([0, 1, 2, 3]));
});

Deno.test("[node/dgram] udp unref", {
  permissions: { read: true, run: true, net: true },
}, async () => {
  const [statusCode, _output] = await execCode(`
      import { createSocket } from "node:dgram";
      const udpSocket = createSocket('udp4');
      udpSocket.bind(${listenPort2});
      // This should let the program to exit without waiting for the
      // udp socket to close.
      udpSocket.unref();
      udpSocket.on('message', (buffer, rinfo) => {
      });
    `);
  assertEquals(statusCode, 0);
});

Deno.test("[node/dgram] addMembership works", async () => {
  const { promise, resolve } = Promise.withResolvers<void>();
  const socket = createSocket("udp4");
  socket.bind(0, () => {
    try {
      socket.addMembership("224.0.0.114");
    } finally {
      socket.close();
    }
  });
  socket.on("close", () => resolve());
  await promise;
});

Deno.test("[node/dgram] addMembership accepts scoped IPv6 interface", async () => {
  // Regression test for https://github.com/denoland/deno/issues/34838.
  // A scoped IPv6 interface whose zone id names no existing interface must
  // resolve to the default interface (index 0) — exactly like passing no
  // interface at all — instead of being rejected with EINVAL. Node.js (via
  // libuv's `uv_ip6_addr`) behaves the same.
  const { promise, resolve, reject } = Promise.withResolvers<void>();
  const socket = createSocket({ type: "udp6", ipv6Only: true });
  let bound = false;
  socket.on("error", (err) => {
    // IPv6 may be unavailable before binding; treat that as a skip.
    socket.close();
    if (bound) {
      reject(err);
    } else {
      resolve();
    }
  });
  socket.bind(0, () => {
    bound = true;
    try {
      // First join without an interface to determine whether IPv6 multicast is
      // available at all in this environment (some CI sandboxes have no
      // multicast-capable default interface). If it fails, skip the test.
      socket.addMembership("ff02::fb");
    } catch {
      socket.close(() => resolve());
      return;
    }
    try {
      // The scoped interface resolves to the same default interface as the
      // baseline join above, so it must be accepted too. On Unix a numeric
      // zone is resolved via `if_nametoindex` (-> 0 for non-names), which is
      // the reporter's exact "::%12" case; on Windows a numeric zone is a
      // literal index (`atoi`), so use a non-numeric unknown name there.
      const scopedIface = Deno.build.os === "windows"
        ? "::%nonexistent0"
        : "::%9999999";
      // Use a different multicast group so this is a fresh join rather than a
      // duplicate of the baseline join above.
      socket.addMembership("ff02::1:3", scopedIface);
      socket.close(() => resolve());
    } catch (err) {
      socket.close();
      reject(err);
    }
  });
  await promise;
});

Deno.test("[node/dgram] createSocket, reuseAddr option", async () => {
  const { promise, resolve } = Promise.withResolvers<string>();
  const socket0 = createSocket({ type: "udp4", reuseAddr: true });
  let socket1: Socket | undefined;
  socket0.bind(0, "0.0.0.0", () => {
    const port = socket0.address().port;
    socket1 = createSocket({ type: "udp4", reuseAddr: true });
    socket1.bind(port, "0.0.0.0", () => {
      const socket = createSocket({ type: "udp4" });
      socket.send("hello", port, "localhost", () => {
        socket.close();
      });
    });
    socket1.on("message", (msg) => {
      resolve(msg.toString());
    });
  });
  socket0.on("message", (msg) => {
    resolve(msg.toString());
  });
  assertEquals(await promise, "hello");
  socket0.close();
  socket1?.close();
});

Deno.test("[node/dgram] addMembership, setBroadcast, setMulticastTTL after bind", async () => {
  const { promise, resolve, reject } = Promise.withResolvers<void>();

  const socket = createSocket({ type: "udp4", reuseAddr: true });

  socket.on("error", (err) => {
    reject(err);
  });

  socket.bind(0, "0.0.0.0", () => {
    try {
      socket.addMembership("239.255.255.250");
      socket.setBroadcast(true);
      socket.setMulticastTTL(4);
      socket.dropMembership("239.255.255.250");
      resolve();
    } catch (err) {
      reject(err);
    } finally {
      socket.close();
    }
  });

  await promise;
});

Deno.test("[node/dgram] setTTL sets unicast TTL without error", async () => {
  const { promise, resolve } = Promise.withResolvers<void>();
  const socket = createSocket("udp4");
  socket.bind(0, () => {
    socket.setTTL(128);
    socket.close(() => resolve());
  });
  await promise;
});

Deno.test("[node/dgram] setTTL throws on invalid TTL", async () => {
  const { promise, resolve } = Promise.withResolvers<void>();
  const socket = createSocket("udp4");
  socket.bind(0, () => {
    try {
      socket.setTTL(0);
      assert(false, "should have thrown");
    } catch (e) {
      assert(e instanceof Error);
    }
    try {
      socket.setTTL(256);
      assert(false, "should have thrown");
    } catch (e) {
      assert(e instanceof Error);
    }
    socket.close(() => resolve());
  });
  await promise;
});

Deno.test("[node/dgram] setMulticastInterface sets interface without error", async () => {
  const { promise, resolve } = Promise.withResolvers<void>();
  const socket = createSocket("udp4");
  socket.bind(0, () => {
    socket.setMulticastInterface("0.0.0.0");
    socket.close(() => resolve());
  });
  await promise;
});

Deno.test("[node/dgram] addSourceSpecificMembership and dropSourceSpecificMembership", async () => {
  const { promise, resolve } = Promise.withResolvers<void>();
  const socket = createSocket("udp4");
  socket.bind(0, () => {
    socket.addSourceSpecificMembership("127.0.0.1", "232.1.1.1");
    socket.dropSourceSpecificMembership("127.0.0.1", "232.1.1.1");
    socket.close(() => resolve());
  });
  await promise;
});

Deno.test("[node/dgram] send checks destination permission", {
  permissions: { read: true, write: true, run: true, net: true },
}, async () => {
  // Verify that a subprocess with restricted --allow-net cannot send to
  // destinations outside the allowed set.
  const tempFile = Deno.makeTempFileSync({ suffix: ".ts" });
  Deno.writeTextFileSync(
    tempFile,
    `import dgram from "node:dgram";
const socket = dgram.createSocket("udp4");
socket.bind(0, "0.0.0.0", () => {
  socket.send("test", 9999, "127.0.0.1", (err) => {
    if (err) {
      console.log("SEND_BLOCKED:" + err.message);
    } else {
      console.log("SEND_ALLOWED");
    }
    socket.close();
  });
});
socket.on("error", (err) => {
  console.log("ERROR:" + err.message);
  socket.close();
});
`,
  );
  try {
    const { stdout, stderr } = await new Deno.Command(Deno.execPath(), {
      args: [
        "run",
        "--no-prompt",
        "--allow-net=0.0.0.0:0",
        "--allow-read=" + tempFile,
        tempFile,
      ],
      stdout: "piped",
      stderr: "piped",
    }).output();
    const output = new TextDecoder().decode(stdout) +
      new TextDecoder().decode(stderr);
    assert(
      !output.includes("SEND_ALLOWED"),
      `Send should have been blocked, but got: ${output}`,
    );
  } finally {
    Deno.removeSync(tempFile);
  }
});

Deno.test("[node/dgram] large recvBufferSize and sendBufferSize do not throw", async () => {
  const { promise, resolve, reject } = Promise.withResolvers<void>();
  const socket = createSocket({
    type: "udp4",
    recvBufferSize: 4194304,
    sendBufferSize: 4194304,
  });
  socket.on("error", (err) => {
    reject(err);
  });
  socket.bind(0, () => {
    socket.close(() => resolve());
  });
  await promise;
});
