import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { describe, expect, it, vi } from "vitest";
import type { WebInboundMsg } from "./types.js";
import { saveSessionStore } from "../../config/sessions.js";
import { isBotMentionedFromTargets, resolveMentionTargets } from "./mentions.js";
import { getSessionSnapshot } from "./session-snapshot.js";
import { elide, isLikelyWhatsAppCryptoError } from "./util.js";

const makeMsg = (overrides: Partial<WebInboundMsg>): WebInboundMsg =>
  ({
    id: "m1",
    from: "120363401234567890@g.us",
    conversationId: "120363401234567890@g.us",
    to: "15551234567@s.whatsapp.net",
    accountId: "default",
    body: "",
    chatType: "group",
    chatId: "120363401234567890@g.us",
    sendComposing: async () => {},
    reply: async () => {},
    sendMedia: async () => {},
    ...overrides,
  }) as WebInboundMsg;

describe("isBotMentionedFromTargets", () => {
  const mentionCfg = { mentionRegexes: [/\bopenclaw\b/i] };

  it("ignores regex matches when other mentions are present", () => {
    const msg = makeMsg({
      body: "@OpenClaw please help",
      mentionedJids: ["19998887777@s.whatsapp.net"],
      selfE164: "+15551234567",
      selfJid: "15551234567@s.whatsapp.net",
    });
    const targets = resolveMentionTargets(msg);
    expect(isBotMentionedFromTargets(msg, mentionCfg, targets)).toBe(false);
  });

  it("matches explicit self mentions", () => {
    const msg = makeMsg({
      body: "hey",
      mentionedJids: ["15551234567@s.whatsapp.net"],
      selfE164: "+15551234567",
      selfJid: "15551234567@s.whatsapp.net",
    });
    const targets = resolveMentionTargets(msg);
    expect(isBotMentionedFromTargets(msg, mentionCfg, targets)).toBe(true);
  });

  it("falls back to regex when no mentions are present", () => {
    const msg = makeMsg({
      body: "openclaw can you help?",
      selfE164: "+15551234567",
      selfJid: "15551234567@s.whatsapp.net",
    });
    const targets = resolveMentionTargets(msg);
    expect(isBotMentionedFromTargets(msg, mentionCfg, targets)).toBe(true);
  });

  it("ignores JID mentions in self-chat mode", () => {
    const cfg = { mentionRegexes: [/\bopenclaw\b/i], allowFrom: ["+999"] };
    const msg = makeMsg({
      body: "@owner ping",
      mentionedJids: ["999@s.whatsapp.net"],
      selfE164: "+999",
      selfJid: "999@s.whatsapp.net",
    });
    const targets = resolveMentionTargets(msg);
    expect(isBotMentionedFromTargets(msg, cfg, targets)).toBe(false);

    const msgTextMention = makeMsg({
      body: "openclaw ping",
      selfE164: "+999",
      selfJid: "999@s.whatsapp.net",
    });
    const targetsText = resolveMentionTargets(msgTextMention);
    expect(isBotMentionedFromTargets(msgTextMention, cfg, targetsText)).toBe(true);
  });
});

describe("resolveMentionTargets with @lid mapping", () => {
  it("resolves mentionedJids via lid reverse mapping in authDir", async () => {
    const authDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-lid-mapping-"));
    try {
      await fs.writeFile(
        path.join(authDir, "lid-mapping-777_reverse.json"),
        JSON.stringify("+1777"),
      );
      const msg = makeMsg({
        body: "ping",
        mentionedJids: ["777@lid"],
        selfE164: "+15551234567",
        selfJid: "15551234567@s.whatsapp.net",
      });
      const targets = resolveMentionTargets(msg, authDir);
      expect(targets.normalizedMentions).toContain("+1777");
    } finally {
      await fs.rm(authDir, { recursive: true, force: true });
    }
  });

  it("derives selfE164 from selfJid when selfJid is @lid and mapping exists", async () => {
    const authDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-lid-mapping-"));
    try {
      await fs.writeFile(
        path.join(authDir, "lid-mapping-777_reverse.json"),
        JSON.stringify("+1777"),
      );
      const msg = makeMsg({
        body: "ping",
        selfJid: "777@lid",
      });
      const targets = resolveMentionTargets(msg, authDir);
      expect(targets.selfE164).toBe("+1777");
    } finally {
      await fs.rm(authDir, { recursive: true, force: true });
    }
  });
});

describe("getSessionSnapshot", () => {
  it("uses channel reset overrides when configured", async () => {
    vi.useFakeTimers();
    vi.setSystemTime(new Date(2026, 0, 18, 5, 0, 0));
    try {
      const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-snapshot-"));
      const storePath = path.join(root, "sessions.json");
      const sessionKey = "agent:main:whatsapp:dm:s1";

      await saveSessionStore(storePath, {
        [sessionKey]: {
          sessionId: "snapshot-session",
          updatedAt: new Date(2026, 0, 18, 3, 30, 0).getTime(),
          lastChannel: "whatsapp",
        },
      });

      const cfg = {
        session: {
          store: storePath,
          reset: { mode: "daily", atHour: 4, idleMinutes: 240 },
          resetByChannel: {
            whatsapp: { mode: "idle", idleMinutes: 360 },
          },
        },
      } as Parameters<typeof getSessionSnapshot>[0];

      const snapshot = getSessionSnapshot(cfg, "whatsapp:+15550001111", true, {
        sessionKey,
      });

      expect(snapshot.resetPolicy.mode).toBe("idle");
      expect(snapshot.resetPolicy.idleMinutes).toBe(360);
      expect(snapshot.fresh).toBe(true);
      expect(snapshot.dailyResetAt).toBeUndefined();
    } finally {
      vi.useRealTimers();
    }
  });
});

describe("web auto-reply util", () => {
  describe("elide", () => {
    it("returns undefined for undefined input", () => {
      expect(elide(undefined)).toBe(undefined);
    });

    it("returns input when under limit", () => {
      expect(elide("hi", 10)).toBe("hi");
    });

    it("returns input when exactly at limit", () => {
      expect(elide("12345", 5)).toBe("12345");
    });

    it("truncates and annotates when over limit", () => {
      expect(elide("abcdef", 3)).toBe("abc… (truncated 3 chars)");
    });
  });

  describe("isLikelyWhatsAppCryptoError", () => {
    it("returns false for non-matching reasons", () => {
      expect(isLikelyWhatsAppCryptoError(new Error("boom"))).toBe(false);
      expect(isLikelyWhatsAppCryptoError("boom")).toBe(false);
      expect(isLikelyWhatsAppCryptoError({ message: "bad mac" })).toBe(false);
    });

    it("matches known Baileys crypto auth errors (string)", () => {
      expect(
        isLikelyWhatsAppCryptoError(
          "baileys: unsupported state or unable to authenticate data (noise-handler)",
        ),
      ).toBe(true);
      expect(isLikelyWhatsAppCryptoError("bad mac in aesDecryptGCM (baileys)")).toBe(true);
    });

    it("matches known Baileys crypto auth errors (Error)", () => {
      const err = new Error("bad mac");
      err.stack = "at something\nat @whiskeysockets/baileys/noise-handler\n";
      expect(isLikelyWhatsAppCryptoError(err)).toBe(true);
    });

    it("does not throw on circular objects", () => {
      const circular: Record<string, unknown> = {};
      circular.self = circular;
      expect(isLikelyWhatsAppCryptoError(circular)).toBe(false);
    });

    it("handles non-string reasons without throwing", () => {
      expect(isLikelyWhatsAppCryptoError(null)).toBe(false);
      expect(isLikelyWhatsAppCryptoError(123)).toBe(false);
      expect(isLikelyWhatsAppCryptoError(true)).toBe(false);
      expect(isLikelyWhatsAppCryptoError(123n)).toBe(false);
      expect(isLikelyWhatsAppCryptoError(Symbol("bad mac"))).toBe(false);
      expect(isLikelyWhatsAppCryptoError(function namedFn() {})).toBe(false);
    });
  });
});
