// Covers JSONL socket request framing and response handling.
import net from "node:net";
import path from "node:path";
import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion";
import { describe, expect, it } from "vitest";
import { withTempDir } from "../test-helpers/temp-dir.js";
import { requestJsonlSocket, testApi } from "./jsonl-socket.js";

async function listenOnSocket(server: net.Server, socketPath: string): Promise<boolean> {
  try {
    await new Promise<void>((resolve, reject) => {
      server.once("error", reject);
      server.listen(socketPath, resolve);
    });
    return true;
  } catch (err) {
    const code = (err as NodeJS.ErrnoException).code;
    if (code === "EPERM" || code === "EACCES") {
      return false;
    }
    throw err;
  }
}

function acceptDoneValue(msg: unknown): number | null | undefined {
  const value = msg as { type?: string; value?: number };
  return value.type === "done" ? (value.value ?? null) : undefined;
}

describe.runIf(process.platform !== "win32")("requestJsonlSocket", () => {
  it("caps oversized socket timeouts before arming the watchdog", () => {
    expect(testApi.resolveJsonlSocketTimeoutMs(Number.MAX_SAFE_INTEGER)).toBe(MAX_TIMER_TIMEOUT_MS);
  });

  it("ignores malformed and non-accepted lines until one is accepted", async () => {
    await withTempDir({ prefix: "openclaw-jsonl-socket-" }, async (dir) => {
      const socketPath = path.join(dir, "socket.sock");
      const server = net.createServer((socket) => {
        socket.on("data", () => {
          socket.write("{bad json}\n");
          socket.write('{"type":"ignore"}\n');
          socket.write('{"type":"done","value":42}\n');
        });
      });
      const listening = await listenOnSocket(server, socketPath);
      if (!listening) {
        return;
      }

      try {
        await expect(
          requestJsonlSocket({
            socketPath,
            requestLine: '{"hello":"world"}',
            timeoutMs: 500,
            accept: acceptDoneValue,
          }),
        ).resolves.toBe(42);
      } finally {
        server.close();
      }
    });
  });

  it("half-closes the write side after sending the request line", async () => {
    await withTempDir({ prefix: "openclaw-jsonl-socket-" }, async (dir) => {
      const socketPath = path.join(dir, "socket.sock");
      let receivedBuffer: string | null = null;
      const server = net.createServer((socket) => {
        let buffer = "";
        socket.on("data", (chunk) => {
          buffer += chunk.toString("utf8");
        });
        socket.on("end", () => {
          receivedBuffer = buffer;
          socket.end('{"type":"done","value":7}\n');
        });
      });
      const listening = await listenOnSocket(server, socketPath);
      if (!listening) {
        return;
      }

      try {
        await expect(
          requestJsonlSocket({
            socketPath,
            requestLine: '{"hello":"world"}',
            timeoutMs: 500,
            accept: acceptDoneValue,
          }),
        ).resolves.toBe(7);
        expect(receivedBuffer).toBe('{"hello":"world"}\n');
      } finally {
        server.close();
      }
    });
  });

  it("returns null on timeout and on socket errors", async () => {
    await withTempDir({ prefix: "openclaw-jsonl-socket-" }, async (dir) => {
      const socketPath = path.join(dir, "socket.sock");
      const server = net.createServer(() => {
        // Intentionally never reply.
      });
      const listening = await listenOnSocket(server, socketPath);
      if (!listening) {
        return;
      }

      try {
        await expect(
          requestJsonlSocket({
            socketPath,
            requestLine: "{}",
            timeoutMs: 50,
            accept: () => undefined,
          }),
        ).resolves.toBeNull();
      } finally {
        server.close();
      }

      await expect(
        requestJsonlSocket({
          socketPath,
          requestLine: "{}",
          timeoutMs: 50,
          accept: () => undefined,
        }),
      ).resolves.toBeNull();
    });
  });

  it("returns null when the socket closes without an accepted response", async () => {
    await withTempDir({ prefix: "openclaw-jsonl-socket-" }, async (dir) => {
      const socketPath = path.join(dir, "socket.sock");
      const server = net.createServer((socket) => {
        socket.on("data", () => {
          socket.destroy();
        });
      });
      const listening = await listenOnSocket(server, socketPath);
      if (!listening) {
        return;
      }

      try {
        const startMs = Date.now();
        const result = await requestJsonlSocket({
          socketPath,
          requestLine: "{}",
          timeoutMs: 250,
          accept: () => undefined,
        });

        expect(result).toBeNull();
        expect(Date.now() - startMs).toBeLessThan(100);
      } finally {
        server.close();
      }
    });
  });

  it("accepts a complete response line even when trailing data would exceed the cap", async () => {
    await withTempDir({ prefix: "openclaw-jsonl-socket-" }, async (dir) => {
      const socketPath = path.join(dir, "socket.sock");
      const server = net.createServer((socket) => {
        socket.on("data", () => {
          socket.end(`{"type":"done","value":7}\n${"x".repeat(65)}`);
        });
      });
      const listening = await listenOnSocket(server, socketPath);
      if (!listening) {
        return;
      }

      try {
        await expect(
          testApi.requestJsonlSocketWithMaxLineBytes(
            {
              socketPath,
              requestLine: "{}",
              timeoutMs: 500,
              accept: acceptDoneValue,
            },
            64,
          ),
        ).resolves.toBe(7);
      } finally {
        server.close();
      }
    });
  });

  it("rejects oversized complete and unterminated response lines before timeout", async () => {
    for (const response of ["x".repeat(65), `${"x".repeat(65)}\n{"type":"done","value":9}\n`]) {
      await withTempDir({ prefix: "openclaw-jsonl-socket-" }, async (dir) => {
        const socketPath = path.join(dir, "socket.sock");
        const server = net.createServer((socket) => {
          socket.on("data", () => {
            socket.write(response);
          });
        });
        const listening = await listenOnSocket(server, socketPath);
        if (!listening) {
          return;
        }

        try {
          const startMs = Date.now();
          const result = await testApi.requestJsonlSocketWithMaxLineBytes(
            {
              socketPath,
              requestLine: "{}",
              timeoutMs: 500,
              accept: acceptDoneValue,
            },
            64,
          );
          expect(result).toBeNull();
          expect(Date.now() - startMs).toBeLessThan(250);
        } finally {
          server.close();
        }
      });
    }
  });
});
