import { IncomingMessage, ServerResponse } from "node:http";
import { Socket } from "node:net";

import { describe, expect, it } from "vitest";

import type { OpenClawConfig } from "openclaw/plugin-sdk";

import type { ResolvedWecomAccount } from "./types/index.js";
import { computeWecomMsgSignature, decryptWecomEncrypted, encryptWecomPlaintext } from "./crypto.js";
import { handleWecomWebhookRequest, registerAgentWebhookTarget, registerWecomWebhookTarget } from "./monitor.js";

function createMockRequest(params: {
  method: "GET" | "POST";
  url: string;
  body?: unknown;
  rawBody?: string;
}): IncomingMessage {
  const socket = new Socket();
  const req = new IncomingMessage(socket);
  req.method = params.method;
  req.url = params.url;
  if (params.method === "POST") {
    if (typeof params.rawBody === "string") {
      req.push(params.rawBody);
    } else {
      req.push(JSON.stringify(params.body ?? {}));
    }
  }
  req.push(null);
  return req;
}

function createMockResponse(): ServerResponse & {
  _getData: () => string;
  _getStatusCode: () => number;
} {
  const req = new IncomingMessage(new Socket());
  const res = new ServerResponse(req);
  let data = "";
  res.write = (chunk: any) => {
    data += String(chunk);
    return true;
  };
  res.end = (chunk: any) => {
    if (chunk) data += String(chunk);
    return res;
  };
  (res as any)._getData = () => data;
  (res as any)._getStatusCode = () => res.statusCode;
  return res as any;
}

describe("handleWecomWebhookRequest", () => {
  const token = "test-token";
  const encodingAESKey = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG";

  it("handles GET url verification", async () => {
    const account: ResolvedWecomAccount = {
      accountId: "default",
      name: "Test",
      enabled: true,
      configured: true,
      token,
      encodingAESKey,
      receiveId: "",
      config: { webhookPath: "/hook", token, encodingAESKey },
    };

    const unregister = registerWecomWebhookTarget({
      account,
      config: {} as OpenClawConfig,
      runtime: {},
      core: {} as any,
      path: "/hook",
    });

    try {
      const timestamp = "13500001234";
      const nonce = "123412323";
      const echostr = encryptWecomPlaintext({
        encodingAESKey,
        receiveId: "",
        plaintext: "ping",
      });
      const msg_signature = computeWecomMsgSignature({ token, timestamp, nonce, encrypt: echostr });
      const req = createMockRequest({
        method: "GET",
        url: `/hook?msg_signature=${encodeURIComponent(msg_signature)}&timestamp=${encodeURIComponent(timestamp)}&nonce=${encodeURIComponent(nonce)}&echostr=${encodeURIComponent(echostr)}`,
      });
      const res = createMockResponse();
      const handled = await handleWecomWebhookRequest(req, res);
      expect(handled).toBe(true);
      expect(res._getStatusCode()).toBe(200);
      expect(res._getData()).toBe("ping");
    } finally {
      unregister();
    }
  });

  it("handles POST callback and returns encrypted stream placeholder", async () => {
    const account: ResolvedWecomAccount = {
      accountId: "default",
      name: "Test",
      enabled: true,
      configured: true,
      token,
      encodingAESKey,
      receiveId: "",
      config: { webhookPath: "/hook", token, encodingAESKey },
    };

    const unregister = registerWecomWebhookTarget({
      account,
      config: {} as OpenClawConfig,
      runtime: {},
      core: {} as any,
      path: "/hook",
    });

    try {
      const timestamp = "1700000000";
      const nonce = "nonce";
      const plain = JSON.stringify({
        msgid: "MSGID",
        aibotid: "AIBOTID",
        chattype: "single",
        from: { userid: "USERID" },
        response_url: "RESPONSEURL",
        msgtype: "text",
        text: { content: "hello" },
      });
      const encrypt = encryptWecomPlaintext({ encodingAESKey, receiveId: "", plaintext: plain });
      const msg_signature = computeWecomMsgSignature({ token, timestamp, nonce, encrypt });

      const req = createMockRequest({
        method: "POST",
        url: `/hook?msg_signature=${encodeURIComponent(msg_signature)}&timestamp=${encodeURIComponent(timestamp)}&nonce=${encodeURIComponent(nonce)}`,
        body: { encrypt },
      });
      const res = createMockResponse();
      const handled = await handleWecomWebhookRequest(req, res);
      expect(handled).toBe(true);
      expect(res._getStatusCode()).toBe(200);

      const json = JSON.parse(res._getData()) as any;
      expect(typeof json.encrypt).toBe("string");
      expect(typeof json.msgsignature).toBe("string");
      expect(typeof json.timestamp).toBe("string");
      expect(typeof json.nonce).toBe("string");

      const replyPlain = decryptWecomEncrypted({
        encodingAESKey,
        receiveId: "",
        encrypt: json.encrypt,
      });
      const reply = JSON.parse(replyPlain) as any;
      expect(reply.msgtype).toBe("stream");
      expect(reply.stream?.content).toBe("1");
      expect(reply.stream?.finish).toBe(false);
      expect(typeof reply.stream?.id).toBe("string");
      expect(reply.stream?.id.length).toBeGreaterThan(0);

      const expectedSig = computeWecomMsgSignature({
        token,
        timestamp: String(json.timestamp),
        nonce: String(json.nonce),
        encrypt: String(json.encrypt),
      });
      expect(json.msgsignature).toBe(expectedSig);
    } finally {
      unregister();
    }
  });

  it("supports custom streamPlaceholderContent", async () => {
    const account: ResolvedWecomAccount = {
      accountId: "default",
      name: "Test",
      enabled: true,
      configured: true,
      token,
      encodingAESKey,
      receiveId: "",
      config: { webhookPath: "/hook", token, encodingAESKey, streamPlaceholderContent: "正在思考..." },
    };

    const unregister = registerWecomWebhookTarget({
      account,
      config: {} as OpenClawConfig,
      runtime: {},
      core: {} as any,
      path: "/hook",
    });

    try {
      const timestamp = "1700000001";
      const nonce = "nonce2";
      const plain = JSON.stringify({
        msgid: "MSGID2",
        aibotid: "AIBOTID",
        chattype: "single",
        from: { userid: "USERID2" },
        response_url: "RESPONSEURL",
        msgtype: "text",
        text: { content: "hello" },
      });
      const encrypt = encryptWecomPlaintext({ encodingAESKey, receiveId: "", plaintext: plain });
      const msg_signature = computeWecomMsgSignature({ token, timestamp, nonce, encrypt });

      const req = createMockRequest({
        method: "POST",
        url: `/hook?msg_signature=${encodeURIComponent(msg_signature)}&timestamp=${encodeURIComponent(timestamp)}&nonce=${encodeURIComponent(nonce)}`,
        body: { encrypt },
      });
      const res = createMockResponse();
      const handled = await handleWecomWebhookRequest(req, res);
      expect(handled).toBe(true);
      expect(res._getStatusCode()).toBe(200);

      const json = JSON.parse(res._getData()) as any;
      const replyPlain = decryptWecomEncrypted({
        encodingAESKey,
        receiveId: "",
        encrypt: json.encrypt,
      });
      const reply = JSON.parse(replyPlain) as any;
      expect(reply.msgtype).toBe("stream");
      expect(reply.stream?.content).toBe("正在思考...");
      expect(reply.stream?.finish).toBe(false);
    } finally {
      unregister();
    }
  });

  it("skips bot callbacks with missing sender and returns empty ack", async () => {
    const account: ResolvedWecomAccount = {
      accountId: "default",
      name: "Test",
      enabled: true,
      configured: true,
      token,
      encodingAESKey,
      receiveId: "",
      config: { webhookPath: "/hook", token, encodingAESKey },
    };

    const unregister = registerWecomWebhookTarget({
      account,
      config: {} as OpenClawConfig,
      runtime: {},
      core: {} as any,
      path: "/hook",
    });

    try {
      const timestamp = "1700000001";
      const nonce = "nonce-missing-sender";
      const plain = JSON.stringify({
        msgid: "MSGID-MISSING-SENDER",
        aibotid: "AIBOTID",
        chattype: "single",
        msgtype: "text",
        text: { content: "hello" },
      });
      const encrypt = encryptWecomPlaintext({ encodingAESKey, receiveId: "", plaintext: plain });
      const msg_signature = computeWecomMsgSignature({ token, timestamp, nonce, encrypt });

      const req = createMockRequest({
        method: "POST",
        url: `/hook?msg_signature=${encodeURIComponent(msg_signature)}&timestamp=${encodeURIComponent(timestamp)}&nonce=${encodeURIComponent(nonce)}`,
        body: { encrypt },
      });
      const res = createMockResponse();
      const handled = await handleWecomWebhookRequest(req, res);
      expect(handled).toBe(true);
      expect(res._getStatusCode()).toBe(200);

      const json = JSON.parse(res._getData()) as any;
      const replyPlain = decryptWecomEncrypted({
        encodingAESKey,
        receiveId: "",
        encrypt: json.encrypt,
      });
      expect(JSON.parse(replyPlain)).toEqual({});
    } finally {
      unregister();
    }
  });

  it("returns a queued stream for 2, and an ack stream for merged follow-ups", async () => {
    const token = "TOKEN";
    const encodingAESKey = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG";
    const account: any = {
      accountId: "default",
      enabled: true,
      configured: true,
      token,
      encodingAESKey,
      receiveId: "",
      config: {
        streamPlaceholderContent: "正在思考...",
        debounceMs: 10_000,
      },
    };

    const unregister = registerWecomWebhookTarget({
      account,
      config: {} as OpenClawConfig,
      runtime: {},
      core: {} as any,
      path: "/hook-merge",
    });

    try {
      const timestamp = "1700000002";
      const nonce = "nonce-merge";

      const makeReq = (msgid: string) => {
        const plain = JSON.stringify({
          msgid,
          aibotid: "AIBOTID",
          chattype: "single",
          from: { userid: "USERID_QUEUE" },
          response_url: "RESPONSEURL",
          msgtype: "text",
          text: { content: "hello" },
        });
        const encrypt = encryptWecomPlaintext({ encodingAESKey, receiveId: "", plaintext: plain });
        const msg_signature = computeWecomMsgSignature({ token, timestamp, nonce, encrypt });
        return createMockRequest({
          method: "POST",
          url: `/hook-merge?msg_signature=${encodeURIComponent(msg_signature)}&timestamp=${encodeURIComponent(timestamp)}&nonce=${encodeURIComponent(nonce)}`,
          body: { encrypt },
        });
      };

      const res1 = createMockResponse();
      await handleWecomWebhookRequest(makeReq("MSGID-M1"), res1);
      const json1 = JSON.parse(res1._getData()) as any;
      const replyPlain1 = decryptWecomEncrypted({ encodingAESKey, receiveId: "", encrypt: json1.encrypt });
      const reply1 = JSON.parse(replyPlain1) as any;
      expect(reply1.msgtype).toBe("stream");
      expect(reply1.stream?.finish).toBe(false);

      const res2 = createMockResponse();
      await handleWecomWebhookRequest(makeReq("MSGID-M2"), res2);
      const json2 = JSON.parse(res2._getData()) as any;
      const replyPlain2 = decryptWecomEncrypted({ encodingAESKey, receiveId: "", encrypt: json2.encrypt });
      const reply2 = JSON.parse(replyPlain2) as any;
      expect(reply2.msgtype).toBe("stream");
      expect(reply2.stream?.finish).toBe(false);
      expect(reply2.stream?.id).not.toBe(reply1.stream?.id);
      expect(reply2.stream?.content).toContain("排队");

      const res3 = createMockResponse();
      await handleWecomWebhookRequest(makeReq("MSGID-M3"), res3);
      const json3 = JSON.parse(res3._getData()) as any;
      const replyPlain3 = decryptWecomEncrypted({ encodingAESKey, receiveId: "", encrypt: json3.encrypt });
      const reply3 = JSON.parse(replyPlain3) as any;
      expect(reply3.msgtype).toBe("stream");
      // merged follow-up should get its own ack stream (not finished yet);
      // it will be updated to a final hint after the merged batch completes.
      expect(reply3.stream?.finish).toBe(false);
      expect(reply3.stream?.id).not.toBe(reply1.stream?.id);
      expect(reply3.stream?.id).not.toBe(reply2.stream?.id);
      expect(reply3.stream?.content).toContain("合并");
    } finally {
      unregister();
    }
  });

  it("routes bot callback by explicit plugin account path", async () => {
    const token = "MATRIX-TOKEN";
    const encodingAESKey = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG";

    const unregisterA = registerWecomWebhookTarget({
      account: {
        accountId: "acct-a",
        enabled: true,
        configured: true,
        token,
        encodingAESKey,
        receiveId: "",
        config: {
          token,
          encodingAESKey,
          streamPlaceholderContent: "A处理中",
        } as any,
      } as any,
      config: { channels: { wecom: { accounts: {} } } } as OpenClawConfig,
      runtime: {},
      core: {} as any,
      path: "/plugins/wecom/bot/acct-a",
    });
    const unregisterB = registerWecomWebhookTarget({
      account: {
        accountId: "acct-b",
        enabled: true,
        configured: true,
        token,
        encodingAESKey,
        receiveId: "",
        config: {
          token,
          encodingAESKey,
          streamPlaceholderContent: "B处理中",
        } as any,
      } as any,
      config: { channels: { wecom: { accounts: {} } } } as OpenClawConfig,
      runtime: {},
      core: {} as any,
      path: "/plugins/wecom/bot/acct-b",
    });

    try {
      const timestamp = "1700000999";
      const nonce = "nonce-plugin-account";
      const plain = JSON.stringify({
        msgid: "MATRIX-MSG-1",
        aibotid: "BOT_B",
        chattype: "single",
        from: { userid: "USERID_B" },
        response_url: "RESPONSEURL",
        msgtype: "text",
        text: { content: "hello plugin account path" },
      });
      const encrypt = encryptWecomPlaintext({ encodingAESKey, receiveId: "", plaintext: plain });
      const msg_signature = computeWecomMsgSignature({ token, timestamp, nonce, encrypt });
      const req = createMockRequest({
        method: "POST",
        url: `/plugins/wecom/bot/acct-b?msg_signature=${encodeURIComponent(msg_signature)}&timestamp=${encodeURIComponent(timestamp)}&nonce=${encodeURIComponent(nonce)}`,
        body: { encrypt },
      });
      const res = createMockResponse();
      const handled = await handleWecomWebhookRequest(req, res);
      expect(handled).toBe(true);
      expect(res._getStatusCode()).toBe(200);

      const json = JSON.parse(res._getData()) as any;
      const replyPlain = decryptWecomEncrypted({
        encodingAESKey,
        receiveId: "",
        encrypt: json.encrypt,
      });
      const reply = JSON.parse(replyPlain) as any;
      expect(reply.stream?.content).toBe("B处理中");
    } finally {
      unregisterA();
      unregisterB();
    }
  });

  it("routes bot callback by explicit plugin namespace path", async () => {
    const token = "MATRIX-TOKEN-PLUGIN";
    const encodingAESKey = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG";

    const unregisterA = registerWecomWebhookTarget({
      account: {
        accountId: "acct-a",
        enabled: true,
        configured: true,
        token,
        encodingAESKey,
        receiveId: "",
        config: {
          token,
          encodingAESKey,
          streamPlaceholderContent: "A处理中",
        } as any,
      } as any,
      config: { channels: { wecom: { accounts: {} } } } as OpenClawConfig,
      runtime: {},
      core: {} as any,
      path: "/plugins/wecom/bot/acct-a",
    });
    const unregisterB = registerWecomWebhookTarget({
      account: {
        accountId: "acct-b",
        enabled: true,
        configured: true,
        token,
        encodingAESKey,
        receiveId: "",
        config: {
          token,
          encodingAESKey,
          streamPlaceholderContent: "B处理中",
        } as any,
      } as any,
      config: { channels: { wecom: { accounts: {} } } } as OpenClawConfig,
      runtime: {},
      core: {} as any,
      path: "/plugins/wecom/bot/acct-b",
    });

    try {
      const timestamp = "1700001000";
      const nonce = "nonce-matrix-plugin";
      const plain = JSON.stringify({
        msgid: "MATRIX-MSG-PLUGIN-1",
        aibotid: "BOT_B",
        chattype: "single",
        from: { userid: "USERID_B_PLUGIN" },
        response_url: "RESPONSEURL",
        msgtype: "text",
        text: { content: "hello matrix plugin path" },
      });
      const encrypt = encryptWecomPlaintext({ encodingAESKey, receiveId: "", plaintext: plain });
      const msg_signature = computeWecomMsgSignature({ token, timestamp, nonce, encrypt });
      const req = createMockRequest({
        method: "POST",
        url: `/plugins/wecom/bot/acct-b?msg_signature=${encodeURIComponent(msg_signature)}&timestamp=${encodeURIComponent(timestamp)}&nonce=${encodeURIComponent(nonce)}`,
        body: { encrypt },
      });
      const res = createMockResponse();
      const handled = await handleWecomWebhookRequest(req, res);
      expect(handled).toBe(true);
      expect(res._getStatusCode()).toBe(200);

      const json = JSON.parse(res._getData()) as any;
      const replyPlain = decryptWecomEncrypted({
        encodingAESKey,
        receiveId: "",
        encrypt: json.encrypt,
      });
      const reply = JSON.parse(replyPlain) as any;
      expect(reply.stream?.content).toBe("B处理中");
    } finally {
      unregisterA();
      unregisterB();
    }
  });

  it("does not reject when aibotid mismatches configured value", async () => {
    const token = "MATRIX-TOKEN-2";
    const encodingAESKey = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG";
    const unregister = registerWecomWebhookTarget({
      account: {
        accountId: "acct-a",
        enabled: true,
        configured: true,
        token,
        encodingAESKey,
        receiveId: "",
        config: {
          token,
          encodingAESKey,
          aibotid: "BOT_ONLY",
        } as any,
      } as any,
      config: { channels: { wecom: { accounts: {} } } } as OpenClawConfig,
      runtime: {},
      core: {} as any,
      path: "/hook-matrix-mismatch",
    });

    try {
      const timestamp = "1700001001";
      const nonce = "nonce-matrix-mismatch";
      const plain = JSON.stringify({
        msgid: "MATRIX-MSG-2",
        aibotid: "BOT_OTHER",
        chattype: "single",
        from: { userid: "USERID_X" },
        response_url: "RESPONSEURL",
        msgtype: "text",
        text: { content: "hello mismatch" },
      });
      const encrypt = encryptWecomPlaintext({ encodingAESKey, receiveId: "", plaintext: plain });
      const msg_signature = computeWecomMsgSignature({ token, timestamp, nonce, encrypt });
      const req = createMockRequest({
        method: "POST",
        url: `/hook-matrix-mismatch?msg_signature=${encodeURIComponent(msg_signature)}&timestamp=${encodeURIComponent(timestamp)}&nonce=${encodeURIComponent(nonce)}`,
        body: { encrypt },
      });
      const res = createMockResponse();
      const handled = await handleWecomWebhookRequest(req, res);
      expect(handled).toBe(true);
      expect(res._getStatusCode()).toBe(200);
    } finally {
      unregister();
    }
  });

  it("rejects legacy paths and accountless plugin paths", async () => {
    const token = "MATRIX-TOKEN-3";
    const encodingAESKey = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG";
    const unregister = registerWecomWebhookTarget({
      account: {
        accountId: "acct-a",
        enabled: true,
        configured: true,
        token,
        encodingAESKey,
        receiveId: "",
        config: { token, encodingAESKey } as any,
      } as any,
      config: { channels: { wecom: { accounts: { "acct-a": { bot: {} } } } } } as OpenClawConfig,
      runtime: {},
      core: {} as any,
      path: "/plugins/wecom/bot/acct-a",
    });
    try {
      const req = createMockRequest({
        method: "GET",
        url: "/wecom/bot?timestamp=t&nonce=n&msg_signature=s&echostr=e",
      });
      const res = createMockResponse();
      const handled = await handleWecomWebhookRequest(req, res);
      expect(handled).toBe(true);
      expect(res._getStatusCode()).toBe(401);
      expect(JSON.parse(res._getData())).toMatchObject({
        error: "wecom_matrix_path_required",
      });

      const pluginReq = createMockRequest({
        method: "GET",
        url: "/plugins/wecom/bot?timestamp=t&nonce=n&msg_signature=s&echostr=e",
      });
      const pluginRes = createMockResponse();
      const pluginHandled = await handleWecomWebhookRequest(pluginReq, pluginRes);
      expect(pluginHandled).toBe(true);
      expect(pluginRes._getStatusCode()).toBe(401);
      expect(JSON.parse(pluginRes._getData())).toMatchObject({
        error: "wecom_matrix_path_required",
      });
    } finally {
      unregister();
    }
  });

  it("returns account conflict for agent GET verification when multiple accounts share token", async () => {
    const token = "AGENT-TOKEN";
    const timestamp = "1700002001";
    const nonce = "nonce-agent";
    const echostr = "ECHOSTR";
    const signature = computeWecomMsgSignature({ token, timestamp, nonce, encrypt: echostr });

    const unregisterA = registerAgentWebhookTarget({
      agent: {
        accountId: "agent-a",
        enabled: true,
        configured: true,
        corpId: "corp-a",
        corpSecret: "secret-a",
        agentId: 1001,
        token,
        encodingAESKey: "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG",
        config: {} as any,
      },
      config: { channels: { wecom: { accounts: {} } } } as OpenClawConfig,
      runtime: {},
      path: "/plugins/wecom/agent/default",
    } as any);
    const unregisterB = registerAgentWebhookTarget({
      agent: {
        accountId: "agent-b",
        enabled: true,
        configured: true,
        corpId: "corp-b",
        corpSecret: "secret-b",
        agentId: 1002,
        token,
        encodingAESKey: "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG",
        config: {} as any,
      },
      config: { channels: { wecom: { accounts: {} } } } as OpenClawConfig,
      runtime: {},
      path: "/plugins/wecom/agent/default",
    } as any);

    try {
      const req = createMockRequest({
        method: "GET",
        url: `/plugins/wecom/agent/default?msg_signature=${encodeURIComponent(signature)}&timestamp=${encodeURIComponent(timestamp)}&nonce=${encodeURIComponent(nonce)}&echostr=${encodeURIComponent(echostr)}`,
      });
      const res = createMockResponse();
      const handled = await handleWecomWebhookRequest(req, res);
      expect(handled).toBe(true);
      expect(res._getStatusCode()).toBe(401);
      expect(JSON.parse(res._getData())).toMatchObject({
        error: "wecom_account_conflict",
      });
    } finally {
      unregisterA();
      unregisterB();
    }
  });
});
