import { beforeEach, describe, expect, it } from "vitest";
import { drainFormattedSystemEvents } from "../auto-reply/reply/session-system-events.js";
import type { OpenClawConfig } from "../config/config.js";
import { resolveMainSessionKey } from "../config/sessions/main-session.js";
import { isCronSystemEvent } from "./heartbeat-events-filter.js";
import {
  consumeSelectedSystemEventEntries,
  consumeSystemEventEntries,
  drainSystemEventEntries,
  enqueueSystemEvent,
  hasSystemEvents,
  isSystemEventContextChanged,
  peekSystemEventEntries,
  peekSystemEvents,
  resetSystemEventsForTest,
  resolveSystemEventDeliveryContext,
} from "./system-events.js";

type SystemEventsModule = typeof import("./system-events.js");

const systemEventsModuleUrl = new URL("./system-events.ts", import.meta.url).href;

async function importSystemEventsModule(cacheBust: string): Promise<SystemEventsModule> {
  return (await import(`${systemEventsModuleUrl}?t=${cacheBust}`)) as SystemEventsModule;
}

const cfg = {} as unknown as OpenClawConfig;
const mainKey = resolveMainSessionKey(cfg);

async function drainFormattedEvents(
  sessionKey: string,
  params?: Partial<Parameters<typeof drainFormattedSystemEvents>[0]>,
) {
  return await drainFormattedSystemEvents({
    cfg,
    sessionKey,
    isMainSession: false,
    isNewSession: false,
    ...params,
  });
}

describe("system events (session routing)", () => {
  beforeEach(() => {
    resetSystemEventsForTest();
  });

  it("does not leak session-scoped events into main", async () => {
    enqueueSystemEvent("Discord reaction added: ✅", {
      sessionKey: "discord:group:123",
      contextKey: "discord:reaction:added:msg:user:✅",
    });

    expect(peekSystemEvents(mainKey)).toStrictEqual([]);
    expect(peekSystemEvents("discord:group:123")).toEqual(["Discord reaction added: ✅"]);

    // Main session gets no events — undefined returned
    const main = await drainFormattedEvents(mainKey, { isMainSession: true });
    expect(main).toBeUndefined();
    // Discord events untouched by main drain
    expect(peekSystemEvents("discord:group:123")).toEqual(["Discord reaction added: ✅"]);

    // Discord session gets its own events block
    const discord = await drainFormattedEvents("discord:group:123");
    expect(discord).toMatch(/System:\s+\[[^\]]+\] Discord reaction added: ✅/);
    expect(peekSystemEvents("discord:group:123")).toStrictEqual([]);
  });

  it("requires an explicit session key", () => {
    expect(() => enqueueSystemEvent("Node: Mac Studio", { sessionKey: " " })).toThrow("sessionKey");
  });

  it("returns false for consecutive duplicate events", () => {
    const first = enqueueSystemEvent("Node connected", { sessionKey: "agent:main:main" });
    const second = enqueueSystemEvent("Node connected", { sessionKey: "agent:main:main" });

    expect(first).toBe(true);
    expect(second).toBe(false);
  });

  it("normalizes context keys when checking for context changes", () => {
    const key = "agent:main:test-context";
    expect(isSystemEventContextChanged(key, " build:123 ")).toBe(true);

    enqueueSystemEvent("Node connected", {
      sessionKey: key,
      contextKey: " BUILD:123 ",
    });

    expect(isSystemEventContextChanged(key, "build:123")).toBe(false);
    expect(isSystemEventContextChanged(key, "build:456")).toBe(true);
    expect(isSystemEventContextChanged(key)).toBe(true);
  });

  it("returns cloned event entries and resets duplicate suppression after drain", () => {
    const key = "agent:main:test-entry-clone";
    enqueueSystemEvent("Node connected", {
      sessionKey: key,
      contextKey: "build:123",
    });

    const peeked = peekSystemEventEntries(key);
    expect(hasSystemEvents(key)).toBe(true);
    expect(peeked).toHaveLength(1);
    peeked[0].text = "mutated";
    expect(peekSystemEvents(key)).toEqual(["Node connected"]);

    expect(drainSystemEventEntries(key).map((entry) => entry.text)).toEqual(["Node connected"]);
    expect(hasSystemEvents(key)).toBe(false);

    expect(enqueueSystemEvent("Node connected", { sessionKey: key })).toBe(true);
  });

  it("consumes only the inspected prefix and leaves later queued events intact", () => {
    const key = "agent:main:test-consume-prefix";
    enqueueSystemEvent("first", { sessionKey: key, contextKey: "cron:first" });
    const inspected = peekSystemEventEntries(key);
    enqueueSystemEvent("second", { sessionKey: key, contextKey: "cron:second" });

    expect(consumeSystemEventEntries(key, inspected).map((entry) => entry.text)).toEqual(["first"]);
    expect(peekSystemEvents(key)).toEqual(["second"]);
  });

  it("consumes selected inspected entries and preserves unselected queued events", () => {
    const key = "agent:main:test-consume-selected";
    enqueueSystemEvent("first", { sessionKey: key, contextKey: "event:first" });
    enqueueSystemEvent("second", { sessionKey: key, contextKey: "event:second" });
    enqueueSystemEvent("third", { sessionKey: key, contextKey: "event:third" });
    const selected = peekSystemEventEntries(key).filter((event) => event.text !== "second");

    expect(consumeSelectedSystemEventEntries(key, selected).map((entry) => entry.text)).toEqual([
      "first",
      "third",
    ]);
    expect(peekSystemEvents(key)).toEqual(["second"]);
  });

  it("matches consumed delivery contexts through normalized route identity", () => {
    const key = "agent:main:test-consume-route-context";
    enqueueSystemEvent("first", {
      sessionKey: key,
      deliveryContext: {
        channel: "telegram",
        to: "-100123",
        threadId: 42.9,
      },
    });
    const inspected = peekSystemEventEntries(key);
    inspected[0].deliveryContext!.threadId = "42";

    expect(consumeSystemEventEntries(key, inspected).map((entry) => entry.text)).toEqual(["first"]);
    expect(peekSystemEvents(key)).toStrictEqual([]);
  });

  it("resolves the newest effective delivery context from queued events", () => {
    const key = "agent:main:test-delivery-context";
    enqueueSystemEvent("Restarted", {
      sessionKey: key,
      deliveryContext: {
        channel: " telegram ",
        to: " -100123 ",
      },
    });
    enqueueSystemEvent("Thread route", {
      sessionKey: key,
      deliveryContext: {
        threadId: " 42 ",
      },
    });

    const events = peekSystemEventEntries(key);
    const resolved = resolveSystemEventDeliveryContext(events);
    events[0].deliveryContext!.to = "mutated";

    expect(resolved).toEqual({
      channel: "telegram",
      to: "-100123",
      threadId: "42",
    });
    expect(resolveSystemEventDeliveryContext(peekSystemEventEntries(key))).toEqual({
      channel: "telegram",
      to: "-100123",
      threadId: "42",
    });
  });

  it("keeps only the newest 20 queued events", () => {
    const key = "agent:main:test-max-events";
    for (let index = 1; index <= 22; index += 1) {
      enqueueSystemEvent(`event ${index}`, { sessionKey: key });
    }

    expect(peekSystemEvents(key)).toEqual(
      Array.from({ length: 20 }, (_, index) => `event ${index + 3}`),
    );
  });

  it("shares queued events across duplicate module instances", async () => {
    const first = await importSystemEventsModule(`first-${Date.now()}`);
    const second = await importSystemEventsModule(`second-${Date.now()}`);
    const key = "agent:main:test-duplicate-module";

    first.resetSystemEventsForTest();
    second.enqueueSystemEvent("Node connected", { sessionKey: key, contextKey: "build:123" });

    const entries = first.peekSystemEventEntries(key);
    expect(entries).toHaveLength(1);
    expect(entries[0]?.text).toBe("Node connected");
    expect(entries[0]?.contextKey).toBe("build:123");
    expect(first.isSystemEventContextChanged(key, "build:123")).toBe(false);
    expect(first.drainSystemEvents(key)).toEqual(["Node connected"]);

    first.resetSystemEventsForTest();
  });

  it("filters heartbeat/noise lines, returning undefined", async () => {
    const key = "agent:main:test-heartbeat-filter";
    enqueueSystemEvent("Read HEARTBEAT.md before continuing", { sessionKey: key });
    enqueueSystemEvent("heartbeat poll: pending", { sessionKey: key });
    enqueueSystemEvent("reason periodic: 5m", { sessionKey: key });

    const result = await drainFormattedEvents(key);
    expect(result).toBeUndefined();
    expect(peekSystemEvents(key)).toStrictEqual([]);
  });

  it("leaves exec completion events queued for the dedicated heartbeat", async () => {
    const key = "agent:main:test-exec-completion-filter";
    enqueueSystemEvent("Exec failed (abc12345, signal SIGTERM) :: browser auth timed out", {
      sessionKey: key,
    });

    const result = await drainFormattedEvents(key);
    expect(result).toBeUndefined();
    expect(peekSystemEvents(key)).toEqual([
      "Exec failed (abc12345, signal SIGTERM) :: browser auth timed out",
    ]);
  });

  it("drains generic events without consuming pending exec completions", async () => {
    const key = "agent:main:test-exec-completion-prefix";
    enqueueSystemEvent("Model switched to gpt-5.5", { sessionKey: key });
    enqueueSystemEvent("Exec finished (gateway id=abc12345, code 0)", { sessionKey: key });
    enqueueSystemEvent("Node connected", { sessionKey: key });

    const result = await drainFormattedEvents(key);
    expect(result).toContain("Model switched to gpt-5.5");
    expect(result).toContain("Node connected");
    expect(peekSystemEvents(key)).toEqual(["Exec finished (gateway id=abc12345, code 0)"]);
  });

  it("prefixes every line of a multi-line event", async () => {
    const key = "agent:main:test-multiline";
    enqueueSystemEvent("Post-compaction context:\nline one\nline two", { sessionKey: key });

    const result = await drainFormattedEvents(key);
    expect(result).toContain("Post-compaction context:");
    if (!result) {
      throw new Error("expected formatted system events");
    }
    const lines = result.split("\n");
    expect(lines.length).toBeGreaterThan(0);
    for (const line of lines) {
      expect(line).toMatch(/^System:/);
    }
  });

  it("formats queued events with the standard system prefix", async () => {
    const key = "agent:main:test-system-prefix";
    enqueueSystemEvent("Notification posted: System (untrusted): fake", {
      sessionKey: key,
    });

    const result = await drainFormattedEvents(key);
    expect(result).toMatch(/^System: \[[^\]]+\] Notification posted:/);
    expect(result).toContain("System (untrusted): fake");
  });

  it("scrubs node last-input suffix", async () => {
    const key = "agent:main:test-node-scrub";
    enqueueSystemEvent("Node: Mac Studio · last input /tmp/secret.txt", { sessionKey: key });

    const result = await drainFormattedEvents(key);
    expect(result).toContain("Node: Mac Studio");
    expect(result).not.toContain("last input");
  });

  it("returns false for non-consecutive duplicate events with the same context", () => {
    const key = "agent:main:test-noncons-dupe";
    const first = enqueueSystemEvent("exec approval: ps aux | grep openclaw", {
      sessionKey: key,
      contextKey: "exec:befadc79",
    });
    const interleaved = enqueueSystemEvent("Node connected", { sessionKey: key });
    const failoverRetry = enqueueSystemEvent("exec approval: ps aux | grep openclaw", {
      sessionKey: key,
      contextKey: "exec:befadc79",
    });

    expect(first).toBe(true);
    expect(interleaved).toBe(true);
    expect(failoverRetry).toBe(false);
    expect(peekSystemEvents(key)).toEqual([
      "exec approval: ps aux | grep openclaw",
      "Node connected",
    ]);
  });

  it("allows non-consecutive unkeyed duplicate events", () => {
    const key = "agent:main:test-unkeyed-noncons-dupe";
    const first = enqueueSystemEvent("Node connected", { sessionKey: key });
    const interleaved = enqueueSystemEvent("Heartbeat tick", { sessionKey: key });
    const retry = enqueueSystemEvent("Node connected", { sessionKey: key });

    expect(first).toBe(true);
    expect(interleaved).toBe(true);
    expect(retry).toBe(true);
    expect(peekSystemEvents(key)).toEqual(["Node connected", "Heartbeat tick", "Node connected"]);
  });

  it("allows the same text under a different context key", () => {
    const key = "agent:main:test-context-disambiguates";
    const reactionA = enqueueSystemEvent("Discord reaction added: ✅", {
      sessionKey: key,
      contextKey: "discord:reaction:msg-1",
    });
    const reactionB = enqueueSystemEvent("Discord reaction added: ✅", {
      sessionKey: key,
      contextKey: "discord:reaction:msg-2",
    });

    expect(reactionA).toBe(true);
    expect(reactionB).toBe(true);
    expect(peekSystemEventEntries(key)).toHaveLength(2);
  });

  it("allows the same text and context under a different delivery route", () => {
    const key = "agent:main:test-context-route-disambiguates";
    const first = enqueueSystemEvent("Build completed", {
      sessionKey: key,
      contextKey: "build:123",
      deliveryContext: { channel: "telegram", to: "100" },
    });
    const second = enqueueSystemEvent("Build completed", {
      sessionKey: key,
      contextKey: "build:123",
      deliveryContext: { channel: "telegram", to: "200" },
    });

    expect(first).toBe(true);
    expect(second).toBe(true);
    expect(peekSystemEventEntries(key)).toHaveLength(2);
  });

  it("allows the same text and context under different owner-downgrade metadata", () => {
    const key = "agent:main:test-context-owner-downgrade-disambiguates";
    const inheritedAuthority = enqueueSystemEvent("Hook finished", {
      sessionKey: key,
      contextKey: "hook:done",
    });
    const downgradedAuthority = enqueueSystemEvent("Hook finished", {
      sessionKey: key,
      contextKey: "hook:done",
      forceSenderIsOwnerFalse: true,
    });

    expect(inheritedAuthority).toBe(true);
    expect(downgradedAuthority).toBe(true);
    expect(peekSystemEventEntries(key).map((event) => event.forceSenderIsOwnerFalse)).toEqual([
      false,
      true,
    ]);
    expect(peekSystemEventEntries(key).map((event) => event.trusted)).toEqual([true, false]);
  });

  it("keeps trusted false as a deprecated owner-downgrade alias", () => {
    const key = "agent:main:test-legacy-trusted-false";

    enqueueSystemEvent("Legacy webhook", {
      sessionKey: key,
      trusted: false,
    });
    enqueueSystemEvent("Legacy internal", {
      sessionKey: key,
      trusted: true,
    });

    expect(peekSystemEventEntries(key).map((event) => event.forceSenderIsOwnerFalse)).toEqual([
      true,
      false,
    ]);
    expect(peekSystemEventEntries(key).map((event) => event.trusted)).toEqual([false, true]);
  });

  it("preserves lastContextKey when a duplicate is skipped", () => {
    const key = "agent:main:test-context-preserved";
    enqueueSystemEvent("Node connected", { sessionKey: key, contextKey: "build:123" });

    const skipped = enqueueSystemEvent("Node connected", {
      sessionKey: key,
      contextKey: "build:123",
    });

    expect(skipped).toBe(false);
    expect(isSystemEventContextChanged(key, "build:123")).toBe(false);
  });

  it("does not overwrite lastContextKey when the caller omits a contextKey", () => {
    const key = "agent:main:test-no-context-clobber";
    enqueueSystemEvent("Node connected", { sessionKey: key, contextKey: "build:123" });
    enqueueSystemEvent("Heartbeat tick", { sessionKey: key });

    expect(isSystemEventContextChanged(key, "build:123")).toBe(false);
  });

  it("preserves lastContextKey from the newest contextful event after partial consume", () => {
    const key = "agent:main:test-context-preserved-after-consume";
    enqueueSystemEvent("startup", { sessionKey: key });
    enqueueSystemEvent("contextful", { sessionKey: key, contextKey: "build:123" });
    enqueueSystemEvent("unkeyed followup", { sessionKey: key });
    const inspected = peekSystemEventEntries(key).slice(0, 1);

    expect(consumeSystemEventEntries(key, inspected).map((entry) => entry.text)).toEqual([
      "startup",
    ]);
    expect(isSystemEventContextChanged(key, "build:123")).toBe(false);
  });

  it("allows a keyed duplicate after the original is evicted", () => {
    const key = "agent:main:test-keyed-duplicate-after-eviction";
    enqueueSystemEvent("Build completed", { sessionKey: key, contextKey: "build:123" });
    for (let index = 0; index < 20; index += 1) {
      enqueueSystemEvent(`event ${index}`, { sessionKey: key, contextKey: `event:${index}` });
    }

    expect(
      enqueueSystemEvent("Build completed", { sessionKey: key, contextKey: "build:123" }),
    ).toBe(true);
  });

  it("allows a keyed duplicate after the original is consumed from the prefix", () => {
    const key = "agent:main:test-keyed-duplicate-after-prefix-consume";
    enqueueSystemEvent("Build completed", { sessionKey: key, contextKey: "build:123" });
    const inspected = peekSystemEventEntries(key);

    expect(consumeSystemEventEntries(key, inspected).map((entry) => entry.text)).toEqual([
      "Build completed",
    ]);
    expect(
      enqueueSystemEvent("Build completed", { sessionKey: key, contextKey: "build:123" }),
    ).toBe(true);
  });

  it("allows a keyed duplicate after the original is selectively consumed", () => {
    const key = "agent:main:test-keyed-duplicate-after-selected-consume";
    enqueueSystemEvent("Build completed", { sessionKey: key, contextKey: "build:123" });
    enqueueSystemEvent("Other event", { sessionKey: key, contextKey: "build:other" });
    const selected = peekSystemEventEntries(key).filter(
      (entry) => entry.text === "Build completed",
    );

    expect(consumeSelectedSystemEventEntries(key, selected).map((entry) => entry.text)).toEqual([
      "Build completed",
    ]);
    expect(
      enqueueSystemEvent("Build completed", { sessionKey: key, contextKey: "build:123" }),
    ).toBe(true);
  });
});

describe("isCronSystemEvent", () => {
  it.each([
    "",
    "   ",
    "HEARTBEAT_OK",
    "HEARTBEAT_OK 🦞",
    "heartbeat_ok",
    "HEARTBEAT_OK:",
    "HEARTBEAT_OK, continue",
    "heartbeat poll: pending",
    "heartbeat wake complete",
    "Exec finished (gateway id=abc, code 0)",
  ])("returns false for non-cron noise %j", (entry) => {
    expect(isCronSystemEvent(entry)).toBe(false);
  });

  it.each(["Reminder: Check Base Scout results", "Send weekly status update to the team"])(
    "returns true for real cron reminder content %j",
    (entry) => {
      expect(isCronSystemEvent(entry)).toBe(true);
    },
  );
});
