import assert from "node:assert/strict";
import test from "node:test";
import {
  clearWecomAccessTokenCacheForTests,
  resolveWecomConfig,
  sendWecomAppMessage,
  sendWecomRobotMessage,
  WecomClientError,
} from "../lib/wecom-client.ts";

const baseBindings = {
  WECOM_CORP_ID: "corp-test",
  WECOM_AGENT_ID: "1000001",
  WECOM_SECRET: "secret-test",
  WECOM_ROBOT_WEBHOOK:
    "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=robot-test",
  WECOM_NOTIFY_DUE_DAYS: "3",
};

function fakeWecom(options = {}) {
  const calls = [];
  const fetchImpl = async (input, init = {}) => {
    const url = new URL(String(input));
    calls.push({ url, init });
    if (url.pathname.endsWith("/cgi-bin/gettoken")) {
      return Response.json({
        errcode: 0,
        errmsg: "ok",
        access_token: "token-test",
        expires_in: 7200,
      });
    }
    if (url.pathname.endsWith("/cgi-bin/webhook/send")) {
      return Response.json({
        errcode: options.robotErrcode ?? 0,
        errmsg: options.robotErrmsg ?? "ok",
      });
    }
    if (url.pathname.endsWith("/cgi-bin/message/send")) {
      const body = JSON.parse(String(init.body ?? "{}"));
      const invalid = options.invalidUser
        ? body.touser.split("|").filter((u) => u === options.invalidUser)
        : [];
      return Response.json({
        errcode: 0,
        errmsg: "ok",
        invaliduser: invalid.join("|"),
      });
    }
    return new Response("not found", { status: 404 });
  };
  return { calls, fetchImpl };
}

test.beforeEach(() => {
  clearWecomAccessTokenCacheForTests();
});

test("resolveWecomConfig applies dueDays and trims values", () => {
  const config = resolveWecomConfig({
    WECOM_CORP_ID: "  corp  ",
    WECOM_AGENT_ID: "  10  ",
    WECOM_SECRET: "  s  ",
    WECOM_ROBOT_WEBHOOK: "  https://hook  ",
    WECOM_NOTIFY_DUE_DAYS: "5",
  });
  assert.equal(config.corpId, "corp");
  assert.equal(config.agentId, "10");
  assert.equal(config.secret, "s");
  assert.equal(config.robotWebhook, "https://hook");
  assert.equal(config.dueDays, 5);
});

test("resolveWecomConfig falls back to default dueDays for bad input", () => {
  assert.equal(resolveWecomConfig({ WECOM_NOTIFY_DUE_DAYS: "0" }).dueDays, 3);
  assert.equal(resolveWecomConfig({ WECOM_NOTIFY_DUE_DAYS: "abc" }).dueDays, 3);
  assert.equal(
    resolveWecomConfig({ WECOM_NOTIFY_DUE_DAYS: "100" }).dueDays,
    30,
  );
});

test("sendWecomRobotMessage posts text payload and returns void", async () => {
  const { calls, fetchImpl } = fakeWecom();
  await sendWecomRobotMessage(
    "hello",
    resolveWecomConfig(baseBindings),
    fetchImpl,
  );
  const hookCall = calls.find((c) =>
    c.url.pathname.endsWith("/cgi-bin/webhook/send"),
  );
  assert.ok(hookCall, "robot webhook was called");
  const body = JSON.parse(String(hookCall.init.body));
  assert.equal(body.msgtype, "text");
  assert.equal(body.text.content, "hello");
});

test("sendWecomRobotMessage skips silently when webhook is empty", async () => {
  const { calls, fetchImpl } = fakeWecom();
  const config = resolveWecomConfig({ ...baseBindings, WECOM_ROBOT_WEBHOOK: "" });
  await sendWecomRobotMessage("hello", config, fetchImpl);
  assert.equal(
    calls.filter((c) =>
      c.url.pathname.endsWith("/cgi-bin/webhook/send"),
    ).length,
    0,
  );
});

test("sendWecomRobotMessage throws WecomClientError on provider error", async () => {
  const { fetchImpl } = fakeWecom({
    robotErrcode: 93000,
    robotErrmsg: "invalid webhook url",
  });
  await assert.rejects(
    () =>
      sendWecomRobotMessage(
        "hello",
        resolveWecomConfig(baseBindings),
        fetchImpl,
      ),
    (error) => {
      assert.ok(error instanceof WecomClientError);
      assert.equal(error.status, 502);
      return true;
    },
  );
});

test("sendWecomAppMessage fetches access_token then sends to message/send", async () => {
  const { calls, fetchImpl } = fakeWecom();
  const result = await sendWecomAppMessage(
    ["user-a", "user-b"],
    "催办内容",
    resolveWecomConfig(baseBindings),
    fetchImpl,
  );
  assert.equal(result.sent, 2);
  assert.equal(result.failed, 0);
  assert.equal(result.skipped, false);
  const tokenCall = calls.find((c) =>
    c.url.pathname.endsWith("/cgi-bin/gettoken"),
  );
  assert.ok(tokenCall, "gettoken was called");
  const sendCall = calls.find((c) =>
    c.url.pathname.endsWith("/cgi-bin/message/send"),
  );
  assert.ok(sendCall, "message/send was called");
  assert.equal(sendCall.url.searchParams.get("access_token"), "token-test");
  const body = JSON.parse(String(sendCall.init.body));
  assert.equal(body.touser, "user-a|user-b");
  assert.equal(body.agentid, 1000001);
  assert.equal(body.text.content, "催办内容");
});

test("sendWecomAppMessage skips when no external user ids", async () => {
  const { calls, fetchImpl } = fakeWecom();
  const result = await sendWecomAppMessage(
    [],
    "催办",
    resolveWecomConfig(baseBindings),
    fetchImpl,
  );
  assert.equal(result.skipped, true);
  assert.equal(
    calls.filter((c) =>
      c.url.pathname.endsWith("/cgi-bin/message/send"),
    ).length,
    0,
  );
});

test("sendWecomAppMessage skips when corp credentials are missing", async () => {
  const { calls, fetchImpl } = fakeWecom();
  const result = await sendWecomAppMessage(
    ["user-a"],
    "催办",
    resolveWecomConfig({
      ...baseBindings,
      WECOM_CORP_ID: "",
      WECOM_AGENT_ID: "",
      WECOM_SECRET: "",
    }),
    fetchImpl,
  );
  assert.equal(result.skipped, true);
  assert.equal(
    calls.filter((c) =>
      c.url.pathname.endsWith("/cgi-bin/gettoken"),
    ).length,
    0,
  );
});

test("sendWecomAppMessage reports failed when provider marks invaliduser", async () => {
  const { fetchImpl } = fakeWecom({ invalidUser: "user-a" });
  const result = await sendWecomAppMessage(
    ["user-a", "user-b"],
    "催办",
    resolveWecomConfig(baseBindings),
    fetchImpl,
  );
  assert.equal(result.sent, 1);
  assert.equal(result.failed, 1);
});

test("access_token is cached across calls", async () => {
  const { calls, fetchImpl } = fakeWecom();
  const config = resolveWecomConfig(baseBindings);
  await sendWecomAppMessage(["u1"], "a", config, fetchImpl);
  await sendWecomAppMessage(["u2"], "b", config, fetchImpl);
  const tokenCalls = calls.filter((c) =>
    c.url.pathname.endsWith("/cgi-bin/gettoken"),
  );
  assert.equal(tokenCalls.length, 1, "access_token cached for second call");
});
