/**
 * @file Regression tests for scripts/hook-handler.js delivery behavior. The
 * handler must never block Claude Code waiting for the dashboard's HTTP
 * response — it delivers the event (flushes the request) and exits, leaving the
 * local server to process the buffered request on its own schedule. These tests
 * lock in that non-blocking contract so a future refactor can't reintroduce the
 * "stuck running hooks" stall (handler waiting up to the per-request timeout for
 * a slow/busy/wedged dashboard to reply).
 * @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
 */

const { describe, it } = require("node:test");
const assert = require("node:assert/strict");
const path = require("path");
const http = require("http");
const { spawn } = require("child_process");

const HANDLER = path.resolve(__dirname, "../../scripts/hook-handler.js");

// A mock dashboard that fully RECEIVES the request (records the body) but can be
// told to delay its HTTP response — emulating a busy/slow/wedged server.
function startMockServer({ responseDelayMs }) {
  const received = [];
  const server = http.createServer((req, res) => {
    let body = "";
    req.on("data", (c) => (body += c));
    req.on("end", () => {
      received.push(body);
      const reply = () => {
        try {
          res.end('{"ok":true}');
        } catch {
          /* client already gone — expected when the handler exits early */
        }
      };
      if (responseDelayMs > 0) setTimeout(reply, responseDelayMs);
      else reply();
    });
  });
  return new Promise((resolve) => {
    server.listen(0, "127.0.0.1", () => {
      resolve({ server, port: server.address().port, received });
    });
  });
}

// Spawn the real handler, pipe a hook payload to stdin, and time how long it
// takes to exit.
function runHandler({ port, hookType = "Stop", payload }) {
  return new Promise((resolve, reject) => {
    const start = process.hrtime.bigint();
    const child = spawn(process.execPath, [HANDLER, hookType], {
      env: { ...process.env, CLAUDE_DASHBOARD_PORT: String(port) },
      stdio: ["pipe", "ignore", "ignore"],
    });
    child.on("error", reject);
    child.on("exit", (code) => {
      resolve({ code, ms: Number(process.hrtime.bigint() - start) / 1e6 });
    });
    child.stdin.write(JSON.stringify(payload));
    child.stdin.end();
  });
}

describe("hook-handler non-blocking delivery", () => {
  it("exits without waiting for a slow dashboard response, yet still delivers the event", async () => {
    // Server takes 5s to respond — far longer than the handler's own safety net.
    const { server, port, received } = await startMockServer({ responseDelayMs: 5000 });
    try {
      const { code, ms } = await runHandler({
        port,
        payload: { session_id: "hh-slow", stop_reason: "end_turn" },
      });

      assert.equal(code, 0, "handler should exit cleanly");
      // Must NOT have waited on the 5s response (and must beat its 2.5s safety
      // net): a healthy deliver-and-exit is well under a second.
      assert.ok(
        ms < 2000,
        `handler should exit fast (was ${ms.toFixed(0)}ms) despite the 5s server response`
      );

      // Delivery is preserved even though we exited before the reply.
      await new Promise((r) => setTimeout(r, 200));
      assert.equal(received.length, 1, "event should be delivered exactly once");
      assert.match(received[0], /hh-slow/, "delivered payload should carry the session id");
      assert.match(received[0], /"hook_type":"Stop"/, "payload should be wrapped with hook_type");
    } finally {
      server.close();
    }
  });

  it("exits promptly when no dashboard is listening (connection refused)", async () => {
    // Grab a port then close it so nothing is listening there.
    const { server, port } = await startMockServer({ responseDelayMs: 0 });
    await new Promise((r) => server.close(r));

    const { code, ms } = await runHandler({
      port,
      payload: { session_id: "hh-dead", stop_reason: "end_turn" },
    });

    assert.equal(code, 0, "handler should still exit cleanly with no listener");
    assert.ok(
      ms < 2000,
      `handler should exit fast on a refused connection (was ${ms.toFixed(0)}ms)`
    );
  });
});
