import { describe, expect, test } from "bun:test";
import {
  StatusBuffer,
  classifyMessage,
  parseMarker,
  routeCodexMessage,
} from "../message-filter";
import type { BridgeMessage } from "../types";

const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));

function makeMsg(content: string, ts?: number): BridgeMessage {
  return { id: `test_${Date.now()}`, source: "codex", content, timestamp: ts ?? Date.now() };
}

describe("parseMarker", () => {
  test("extracts [IMPORTANT] marker", () => {
    const r = parseMarker("[IMPORTANT] task done");
    expect(r.marker).toBe("important");
    expect(r.body).toBe("task done");
  });

  test("extracts [STATUS] marker case-insensitive", () => {
    const r = parseMarker("[status] progress update");
    expect(r.marker).toBe("status");
    expect(r.body).toBe("progress update");
  });

  test("extracts [FYI] marker", () => {
    const r = parseMarker("[FYI] background info");
    expect(r.marker).toBe("fyi");
    expect(r.body).toBe("background info");
  });

  test("returns untagged for no marker", () => {
    const r = parseMarker("plain message");
    expect(r.marker).toBe("untagged");
    expect(r.body).toBe("plain message");
  });

  test("handles leading whitespace before marker", () => {
    const r = parseMarker("  [STATUS] progress");
    expect(r.marker).toBe("status");
    expect(r.body).toBe("progress");
  });

  test("handles leading newline before marker", () => {
    const r = parseMarker("\n[IMPORTANT] urgent");
    expect(r.marker).toBe("important");
    expect(r.body).toBe("urgent");
  });
});

describe("classifyMessage", () => {
  test("forwards IMPORTANT in filtered mode", () => {
    expect(classifyMessage("[IMPORTANT] x", "filtered")).toEqual({ action: "forward", marker: "important" });
  });

  test("buffers STATUS in filtered mode", () => {
    expect(classifyMessage("[STATUS] x", "filtered")).toEqual({ action: "buffer", marker: "status" });
  });

  test("drops FYI in filtered mode", () => {
    expect(classifyMessage("[FYI] x", "filtered")).toEqual({ action: "drop", marker: "fyi" });
  });

  test("forwards untagged in filtered mode", () => {
    expect(classifyMessage("hello", "filtered")).toEqual({ action: "forward", marker: "untagged" });
  });

  test("forwards everything in full mode", () => {
    expect(classifyMessage("[FYI] x", "full")).toEqual({ action: "forward", marker: "untagged" });
    expect(classifyMessage("[STATUS] x", "full")).toEqual({ action: "forward", marker: "untagged" });
  });
});

describe("routeCodexMessage", () => {
  const markerCases = [
    { marker: "important", content: "[IMPORTANT] important update" },
    { marker: "status", content: "[STATUS] progress update" },
    { marker: "fyi", content: "[FYI] background update" },
    { marker: "untagged", content: "plain update" },
  ] as const;

  test("covers the full mode x reply armed x attention window x marker matrix", () => {
    for (const mode of ["filtered", "full"] as const) {
      for (const replyArmed of [false, true]) {
        for (const inAttentionWindow of [false, true]) {
          for (const markerCase of markerCases) {
            const route = routeCodexMessage(markerCase.content, { mode, replyArmed, inAttentionWindow });

            if (replyArmed) {
              expect(route).toEqual({
                action: "forward",
                marker: mode === "full" ? "untagged" : markerCase.marker,
                reason: "force-forward-reply-required",
                flushStatusBuffer: true,
                noteReplyForwarded: true,
              });
              continue;
            }

            if (mode === "full") {
              expect(route).toEqual({
                action: "forward",
                marker: "untagged",
                reason: "forward",
              });
              continue;
            }

            if (inAttentionWindow && markerCase.marker === "status") {
              expect(route).toEqual({
                action: "buffer",
                marker: "status",
                reason: "buffer-attention",
              });
              continue;
            }

            switch (markerCase.marker) {
              case "important":
                expect(route).toEqual({
                  action: "forward",
                  marker: "important",
                  reason: "forward",
                  flushStatusBuffer: true,
                  startAttentionWindow: true,
                });
                break;
              case "status":
                expect(route).toEqual({
                  action: "buffer",
                  marker: "status",
                  reason: "buffer",
                });
                break;
              case "fyi":
                expect(route).toEqual({
                  action: "drop",
                  marker: "fyi",
                  reason: "drop",
                });
                break;
              case "untagged":
                expect(route).toEqual({
                  action: "forward",
                  marker: "untagged",
                  reason: "forward",
                });
                break;
            }
          }
        }
      }
    }
  });

  test("does not start an attention window for IMPORTANT in full mode", () => {
    expect(routeCodexMessage("[IMPORTANT] x", {
      mode: "full",
      replyArmed: false,
      inAttentionWindow: false,
    })).toEqual({
      action: "forward",
      marker: "untagged",
      reason: "forward",
    });
  });
});

describe("StatusBuffer", () => {
  test("flushes when threshold reached", () => {
    const flushed: BridgeMessage[] = [];
    const buf = new StatusBuffer((m) => flushed.push(m), { flushThreshold: 2, flushTimeoutMs: 60000 });
    buf.add(makeMsg("[STATUS] a"));
    expect(flushed).toHaveLength(0);
    buf.add(makeMsg("[STATUS] b"));
    expect(flushed).toHaveLength(1);
    expect(flushed[0].content).toContain("2 update(s)");
    buf.dispose();
  });

  test("flushes on timeout", async () => {
    const flushed: BridgeMessage[] = [];
    const buf = new StatusBuffer((m) => flushed.push(m), { flushThreshold: 10, flushTimeoutMs: 20 });
    buf.add(makeMsg("[STATUS] a"));
    await sleep(40);
    expect(flushed).toHaveLength(1);
    buf.dispose();
  });

  test("manual flush clears buffer", () => {
    const flushed: BridgeMessage[] = [];
    const buf = new StatusBuffer((m) => flushed.push(m), { flushThreshold: 10, flushTimeoutMs: 60000 });
    buf.add(makeMsg("[STATUS] a"));
    buf.add(makeMsg("[STATUS] b"));
    buf.flush("turn completed");
    expect(flushed).toHaveLength(1);
    expect(flushed[0].content).toContain("flushed: turn completed");
    expect(buf.size).toBe(0);
    buf.dispose();
  });

  test("flush on empty buffer is no-op", () => {
    const flushed: BridgeMessage[] = [];
    const buf = new StatusBuffer((m) => flushed.push(m));
    buf.flush("test");
    expect(flushed).toHaveLength(0);
    buf.dispose();
  });

  test("dispose clears timer and buffer", async () => {
    const flushed: BridgeMessage[] = [];
    const buf = new StatusBuffer((m) => flushed.push(m), { flushThreshold: 10, flushTimeoutMs: 20 });
    buf.add(makeMsg("[STATUS] a"));
    buf.dispose();
    await sleep(40);
    expect(flushed).toHaveLength(0);
  });
});

describe("StatusBuffer pause/resume", () => {
  test("pause suppresses threshold flush", () => {
    const flushed: BridgeMessage[] = [];
    const buf = new StatusBuffer((m) => flushed.push(m), { flushThreshold: 2, flushTimeoutMs: 60000 });
    buf.pause();
    buf.add(makeMsg("[STATUS] a"));
    buf.add(makeMsg("[STATUS] b"));
    buf.add(makeMsg("[STATUS] c"));
    expect(flushed).toHaveLength(0);
    expect(buf.size).toBe(3);
    buf.dispose();
  });

  test("pause suppresses timeout flush", async () => {
    const flushed: BridgeMessage[] = [];
    const buf = new StatusBuffer((m) => flushed.push(m), { flushThreshold: 10, flushTimeoutMs: 20 });
    buf.pause();
    buf.add(makeMsg("[STATUS] a"));
    await sleep(40);
    expect(flushed).toHaveLength(0);
    buf.dispose();
  });

  test("manual flush still works while paused", () => {
    const flushed: BridgeMessage[] = [];
    const buf = new StatusBuffer((m) => flushed.push(m), { flushThreshold: 10, flushTimeoutMs: 60000 });
    buf.pause();
    buf.add(makeMsg("[STATUS] a"));
    buf.flush("important message arrived");
    expect(flushed).toHaveLength(1);
    expect(flushed[0].content).toContain("flushed: important message arrived");
    buf.dispose();
  });

  test("resume triggers threshold flush if buffer is full", () => {
    const flushed: BridgeMessage[] = [];
    const buf = new StatusBuffer((m) => flushed.push(m), { flushThreshold: 2, flushTimeoutMs: 60000 });
    buf.pause();
    buf.add(makeMsg("[STATUS] a"));
    buf.add(makeMsg("[STATUS] b"));
    buf.add(makeMsg("[STATUS] c"));
    expect(flushed).toHaveLength(0);
    buf.resume();
    expect(flushed).toHaveLength(1);
    expect(flushed[0].content).toContain("3 update(s)");
    buf.dispose();
  });

  test("resume restarts timeout timer", async () => {
    const flushed: BridgeMessage[] = [];
    const buf = new StatusBuffer((m) => flushed.push(m), { flushThreshold: 10, flushTimeoutMs: 20 });
    buf.pause();
    buf.add(makeMsg("[STATUS] a"));
    await sleep(40);
    expect(flushed).toHaveLength(0); // still paused
    buf.resume();
    await sleep(30);
    expect(flushed).toHaveLength(1); // timer restarted on resume
    buf.dispose();
  });
});

describe("StatusBuffer summary id uniqueness (LOW-1)", () => {
  // The summary id is used as the dedup key by ClaudeAdapter.rememberDelivery.
  // Two flushes within the same millisecond MUST NOT collide, otherwise the
  // second summary is silently suppressed as a "duplicate".
  test("two flushes in the same millisecond produce distinct ids", () => {
    const flushed: BridgeMessage[] = [];
    const buf = new StatusBuffer((m) => flushed.push(m), { flushThreshold: 1, flushTimeoutMs: 60000 });
    buf.add(makeMsg("[STATUS] a"));
    buf.add(makeMsg("[STATUS] b"));
    expect(flushed).toHaveLength(2);
    expect(flushed[0].id).not.toBe(flushed[1].id);
    buf.dispose();
  });

  test("ids are monotonic within a process and prefixed with a stable salt", () => {
    const flushed: BridgeMessage[] = [];
    const buf = new StatusBuffer((m) => flushed.push(m), { flushThreshold: 1, flushTimeoutMs: 60000 });
    for (let i = 0; i < 5; i++) buf.add(makeMsg(`[STATUS] ${i}`));
    expect(flushed).toHaveLength(5);
    const ids = flushed.map((m) => m.id);
    // All distinct.
    expect(new Set(ids).size).toBe(ids.length);
    // Shape: status_summary_<salt>_<counter>. Salt is shared, counter strictly increases.
    const parsed = ids.map((id) => {
      const m = id.match(/^status_summary_([^_]+)_(\d+)$/);
      expect(m).not.toBeNull();
      return { salt: m![1], counter: Number(m![2]) };
    });
    const salt = parsed[0].salt;
    for (const p of parsed) expect(p.salt).toBe(salt);
    for (let i = 1; i < parsed.length; i++) {
      expect(parsed[i].counter).toBeGreaterThan(parsed[i - 1].counter);
    }
    buf.dispose();
  });

  test("timestamp field still reflects wall-clock time", () => {
    const flushed: BridgeMessage[] = [];
    const buf = new StatusBuffer((m) => flushed.push(m), { flushThreshold: 1, flushTimeoutMs: 60000 });
    const before = Date.now();
    buf.add(makeMsg("[STATUS] a"));
    const after = Date.now();
    expect(flushed[0].timestamp).toBeGreaterThanOrEqual(before);
    expect(flushed[0].timestamp).toBeLessThanOrEqual(after);
    buf.dispose();
  });
});

describe("StatusBuffer bounded buffer (LOW-2)", () => {
  // Without an upper bound, a long attention window (auto-flush paused) lets
  // STATUS messages accumulate without limit. Cap the buffer and drop oldest.
  test("drops oldest beyond maxBuffered while paused", () => {
    const flushed: BridgeMessage[] = [];
    const buf = new StatusBuffer((m) => flushed.push(m), {
      flushThreshold: 100,
      flushTimeoutMs: 60000,
      maxBuffered: 3,
    });
    buf.pause();
    for (let i = 0; i < 6; i++) buf.add(makeMsg(`[STATUS] msg${i}`));
    // Buffer never exceeds the cap.
    expect(buf.size).toBe(3);
    buf.flush("manual");
    expect(flushed).toHaveLength(1);
    const content = flushed[0].content;
    // Oldest (msg0..msg2) dropped, newest (msg3..msg5) retained.
    expect(content).toContain("msg5");
    expect(content).toContain("msg3");
    expect(content).not.toContain("msg0");
    expect(content).not.toContain("msg2");
    // Sentinel reflects the dropped count.
    expect(content).toContain("3");
    buf.dispose();
  });

  test("dropped count resets after a flush", () => {
    const flushed: BridgeMessage[] = [];
    const buf = new StatusBuffer((m) => flushed.push(m), {
      flushThreshold: 100,
      flushTimeoutMs: 60000,
      maxBuffered: 2,
    });
    buf.pause();
    for (let i = 0; i < 5; i++) buf.add(makeMsg(`[STATUS] a${i}`)); // drops 3
    buf.flush("first");
    // After flush, a fresh fill that stays under the cap must NOT report drops.
    buf.add(makeMsg("[STATUS] b0"));
    buf.add(makeMsg("[STATUS] b1"));
    buf.flush("second");
    expect(flushed).toHaveLength(2);
    expect(flushed[1].content).not.toContain("dropped");
    buf.dispose();
  });

  test("default maxBuffered allows normal small bursts without drops", () => {
    const flushed: BridgeMessage[] = [];
    const buf = new StatusBuffer((m) => flushed.push(m), { flushThreshold: 100, flushTimeoutMs: 60000 });
    buf.pause();
    for (let i = 0; i < 50; i++) buf.add(makeMsg(`[STATUS] x${i}`));
    expect(buf.size).toBe(50);
    buf.flush("manual");
    expect(flushed[0].content).not.toContain("dropped");
    buf.dispose();
  });
});
// (The bridge-contract marker spec moved to AGENTS_MD_SECTION; see role-patterns.test.ts.)
