/**
 * F1 — pure announcements store (ticket #4): indexation, bornage, purge per
 * busy-cycle, and the requester filter that backs F2 (`requester == self` on a
 * shared thread).
 */

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

import {
  DEFAULT_ANNOUNCEMENT_CAP,
  announcementsForRequester,
  announcementsForTarget,
  appendAnnouncement,
  emptyAnnouncementIndex,
  purgeAllForTarget,
  targetHasActiveAnnouncements,
  type AnnouncementInput,
} from "./announcementsStore";

function ann(over: Partial<AnnouncementInput> = {}): AnnouncementInput {
  return {
    requester: "agent-req",
    target: "agent-tgt",
    ticketId: "ticket-1",
    text: "réflexion",
    atMs: 1000,
    ...over,
  };
}

describe("appendAnnouncement — indexation", () => {
  it("indexes by (target, ticketId) and assigns monotonic seq", () => {
    let idx = emptyAnnouncementIndex();
    idx = appendAnnouncement(idx, ann({ text: "a", atMs: 1 }));
    idx = appendAnnouncement(idx, ann({ text: "b", atMs: 2 }));

    const list = announcementsForTarget(idx, "agent-tgt");
    expect(list.map((a) => a.text)).toEqual(["a", "b"]);
    expect(list.map((a) => a.seq)).toEqual([1, 2]);
    expect(idx.nextSeq).toBe(3);
  });

  it("keeps separate buckets per ticket under the same target", () => {
    let idx = emptyAnnouncementIndex();
    idx = appendAnnouncement(idx, ann({ ticketId: "t1", text: "x" }));
    idx = appendAnnouncement(idx, ann({ ticketId: "t2", text: "y" }));

    expect(Object.keys(idx.byTarget["agent-tgt"])).toEqual(["t1", "t2"]);
    expect(announcementsForTarget(idx, "agent-tgt").map((a) => a.text)).toEqual([
      "x",
      "y",
    ]);
  });

  it("orders a flattened target list by time then seq", () => {
    let idx = emptyAnnouncementIndex();
    idx = appendAnnouncement(idx, ann({ ticketId: "t1", text: "late", atMs: 30 }));
    idx = appendAnnouncement(idx, ann({ ticketId: "t2", text: "early", atMs: 10 }));

    expect(announcementsForTarget(idx, "agent-tgt").map((a) => a.text)).toEqual([
      "early",
      "late",
    ]);
  });

  it("does not mutate the previous index (immutability)", () => {
    const idx0 = emptyAnnouncementIndex();
    const idx1 = appendAnnouncement(idx0, ann());
    expect(idx0.byTarget).toEqual({});
    expect(idx1).not.toBe(idx0);
  });
});

describe("appendAnnouncement — bornage", () => {
  it("bounds a bucket to the cap, dropping the oldest", () => {
    let idx = emptyAnnouncementIndex();
    const cap = 3;
    for (let i = 0; i < 5; i++) {
      idx = appendAnnouncement(idx, ann({ text: `m${i}`, atMs: i }), cap);
    }
    const list = announcementsForTarget(idx, "agent-tgt");
    expect(list).toHaveLength(cap);
    // Oldest (m0, m1) dropped; most recent kept.
    expect(list.map((a) => a.text)).toEqual(["m2", "m3", "m4"]);
  });

  it("defaults to DEFAULT_ANNOUNCEMENT_CAP", () => {
    let idx = emptyAnnouncementIndex();
    for (let i = 0; i < DEFAULT_ANNOUNCEMENT_CAP + 10; i++) {
      idx = appendAnnouncement(idx, ann({ text: `m${i}`, atMs: i }));
    }
    expect(announcementsForTarget(idx, "agent-tgt")).toHaveLength(
      DEFAULT_ANNOUNCEMENT_CAP,
    );
  });
});

describe("purgeAllForTarget — retract au busy:false (idle, sans final)", () => {
  it("drops every ticket of the target at once", () => {
    let idx = emptyAnnouncementIndex();
    idx = appendAnnouncement(idx, ann({ ticketId: "t1", text: "a" }));
    idx = appendAnnouncement(idx, ann({ ticketId: "t2", text: "b" }));

    idx = purgeAllForTarget(idx, "agent-tgt");

    expect(targetHasActiveAnnouncements(idx, "agent-tgt")).toBe(false);
    expect(idx.byTarget["agent-tgt"]).toBeUndefined();
  });

  it("leaves other targets untouched", () => {
    let idx = emptyAnnouncementIndex();
    idx = appendAnnouncement(idx, ann({ target: "T1", text: "x" }));
    idx = appendAnnouncement(idx, ann({ target: "T2", text: "y" }));

    idx = purgeAllForTarget(idx, "T1");

    expect(targetHasActiveAnnouncements(idx, "T1")).toBe(false);
    expect(announcementsForTarget(idx, "T2").map((a) => a.text)).toEqual(["y"]);
  });

  it("is a no-op (stable reference) for an unknown target", () => {
    const idx = appendAnnouncement(emptyAnnouncementIndex(), ann());
    expect(purgeAllForTarget(idx, "unknown")).toBe(idx);
  });
});

describe("announcementsForRequester — F2 filter (requester == self)", () => {
  it("returns only the caller's announcements on a shared target thread", () => {
    // Two requesters (A, B) both talk to the same target on distinct tickets:
    // the target thread is shared, so filtering must be by requester.
    let idx = emptyAnnouncementIndex();
    idx = appendAnnouncement(
      idx,
      ann({ requester: "A", ticketId: "ta", text: "for-A" }),
    );
    idx = appendAnnouncement(
      idx,
      ann({ requester: "B", ticketId: "tb", text: "for-B" }),
    );

    expect(announcementsForRequester(idx, "A").map((a) => a.text)).toEqual([
      "for-A",
    ]);
    expect(announcementsForRequester(idx, "B").map((a) => a.text)).toEqual([
      "for-B",
    ]);
  });

  it("scopes to a single ticket when ticketId is given", () => {
    let idx = emptyAnnouncementIndex();
    idx = appendAnnouncement(
      idx,
      ann({ requester: "A", ticketId: "ta", text: "ta-msg", atMs: 1 }),
    );
    idx = appendAnnouncement(
      idx,
      ann({ requester: "A", ticketId: "tb", text: "tb-msg", atMs: 2 }),
    );

    expect(
      announcementsForRequester(idx, "A", "ta").map((a) => a.text),
    ).toEqual(["ta-msg"]);
  });

  it("returns nothing for a requester with no announcements", () => {
    const idx = appendAnnouncement(emptyAnnouncementIndex(), ann({ requester: "A" }));
    expect(announcementsForRequester(idx, "someone-else")).toEqual([]);
  });
});
