/**
 * F2/F3 integration (ticket #4, develop base — busy is the sole F3 authority):
 *
 *  - {@link TargetAnnouncementsOverlay} (F3) is driven by the target's **busy**
 *    state, NOT by the presence of announcements. It mounts on
 *    `agentBusyChanged { busy:true }` (or a read-model hydration snapshot) and
 *    retracts on `busy:false`, **including when no completion event is ever
 *    emitted** — the interruption/crash/rate-limit case that used to stick the
 *    overlay. There is no `agentTurnEvent` on this base.
 *  - {@link AnnouncementsPreview} (F2) shows only the caller-requester's
 *    announcements on a shared target thread (`requester == self`).
 */

import { describe, it, expect } from "vitest";
import { render, screen, act } from "@testing-library/react";

import type { DomainEvent } from "@/domain";
import type { Gateways } from "@/ports";
import { MockSystemGateway, MockWorkStateGateway } from "@/adapters/mock";
import { DIProvider } from "@/app/di";
import { AnnouncementsProvider } from "./AnnouncementsProvider";
import { AnnouncementsPreview } from "./AnnouncementsPreview";
import { TargetAnnouncementsOverlay } from "./TargetAnnouncementsOverlay";

function setup(
  node: React.ReactNode,
  opts: { workState?: MockWorkStateGateway } = {},
) {
  const system = new MockSystemGateway();
  const workState = opts.workState;
  const gateways = { system, workState } as unknown as Gateways;
  render(
    <DIProvider gateways={gateways}>
      <AnnouncementsProvider>{node}</AnnouncementsProvider>
    </DIProvider>,
  );
  return { system };
}

type AnnouncementEvent = Extract<DomainEvent, { type: "agentAnnouncement" }>;

function announcement(over: Partial<AnnouncementEvent> = {}): AnnouncementEvent {
  return {
    type: "agentAnnouncement",
    projectId: "p1",
    requester: "agent-A",
    target: "agent-T",
    ticketId: "t1",
    text: "je réfléchis…",
    atMs: 1,
    ...over,
  };
}

function busy(agentId: string, isBusy: boolean): DomainEvent {
  return { type: "agentBusyChanged", agentId, busy: isBusy };
}

// The provider subscribes asynchronously (onDomainEvent returns a Promise) and
// hydration awaits the read-model; flush microtasks before/after emitting.
async function flush() {
  await act(async () => {
    await Promise.resolve();
    await Promise.resolve();
  });
}

const overlay = () => screen.queryByTestId("target-announcements-overlay");

describe("F3 — overlay lifecycle is the target's busy state", () => {
  it("mounts on busy:true and shows the streamed announcement as content", async () => {
    const { system } = setup(
      <TargetAnnouncementsOverlay projectId="p1" agentId="agent-T" />,
    );
    await flush();
    expect(overlay()).toBeNull();

    act(() => {
      system.emit(busy("agent-T", true));
      system.emit(announcement({ target: "agent-T", text: "en cours" }));
    });

    expect(overlay()).not.toBeNull();
    expect(screen.getByText("en cours")).not.toBeNull();
  });

  it("mounts on busy:true even before any announcement (banner only)", async () => {
    const { system } = setup(
      <TargetAnnouncementsOverlay projectId="p1" agentId="agent-T" />,
    );
    await flush();

    act(() => system.emit(busy("agent-T", true)));

    expect(overlay()).not.toBeNull();
  });

  it("RETRACTS on busy:false even when NO completion event is emitted", async () => {
    // The regression the arbitrage targets: a turn that ends without any
    // completion signal (interruption / error / crash) must still bring the
    // overlay down. On develop the only signal is busy — and it suffices.
    const { system } = setup(
      <TargetAnnouncementsOverlay projectId="p1" agentId="agent-T" />,
    );
    await flush();

    act(() => {
      system.emit(busy("agent-T", true));
      system.emit(announcement({ target: "agent-T", text: "orphan" }));
    });
    expect(overlay()).not.toBeNull();

    act(() => system.emit(busy("agent-T", false)));

    expect(overlay()).toBeNull();
  });

  it("clears stale content on busy:false so a next turn starts fresh", async () => {
    const { system } = setup(
      <TargetAnnouncementsOverlay projectId="p1" agentId="agent-T" />,
    );
    await flush();

    act(() => {
      system.emit(busy("agent-T", true));
      system.emit(announcement({ target: "agent-T", text: "old" }));
      system.emit(busy("agent-T", false));
      system.emit(busy("agent-T", true));
    });

    // New turn: the previous "old" réflexion must not be shown.
    expect(overlay()).not.toBeNull();
    expect(screen.queryByText("old")).toBeNull();
  });

  it("hydrates busy from the reconciled read-model at mount (no live event)", async () => {
    const workState = new MockWorkStateGateway();
    workState._setProjectWorkState("p1", {
      agents: [
        {
          agentId: "agent-T",
          name: "Target",
          profileId: "prof",
          busy: { state: "busy", ticket: "t9", sinceMs: 123 },
          tickets: [],
        },
      ],
      conversations: [],
    });

    setup(<TargetAnnouncementsOverlay projectId="p1" agentId="agent-T" />, {
      workState,
    });
    await flush();

    expect(overlay()).not.toBeNull();
  });

  it("stays down when the read-model reports the agent idle", async () => {
    const workState = new MockWorkStateGateway();
    workState._setProjectWorkState("p1", {
      agents: [
        {
          agentId: "agent-T",
          name: "Target",
          profileId: "prof",
          busy: { state: "idle" },
          tickets: [],
        },
      ],
      conversations: [],
    });

    setup(<TargetAnnouncementsOverlay projectId="p1" agentId="agent-T" />, {
      workState,
    });
    await flush();

    expect(overlay()).toBeNull();
  });

  it("lets a live busy:false win over a stale busy hydration snapshot", async () => {
    // Hydration seeds only when the agent is unknown; a later live idle event must
    // still retract (live authority wins).
    const workState = new MockWorkStateGateway();
    workState._setProjectWorkState("p1", {
      agents: [
        {
          agentId: "agent-T",
          name: "Target",
          profileId: "prof",
          busy: { state: "busy", ticket: "t9", sinceMs: 1 },
          tickets: [],
        },
      ],
      conversations: [],
    });

    const { system } = setup(
      <TargetAnnouncementsOverlay projectId="p1" agentId="agent-T" />,
      { workState },
    );
    await flush();
    expect(overlay()).not.toBeNull();

    act(() => system.emit(busy("agent-T", false)));
    expect(overlay()).toBeNull();
  });
});

describe("F2 — AnnouncementsPreview (requester filter)", () => {
  it("shows the requester's own announcements and hides another's", async () => {
    const { system } = setup(<AnnouncementsPreview requester="agent-A" />);
    await flush();

    act(() => {
      system.emit(announcement({ requester: "agent-A", text: "for-A" }));
      system.emit(
        announcement({ requester: "agent-B", ticketId: "t2", text: "for-B" }),
      );
    });

    expect(screen.getByText("for-A")).not.toBeNull();
    expect(screen.queryByText("for-B")).toBeNull();
  });

  it("renders nothing when the requester has no announcements", async () => {
    setup(<AnnouncementsPreview requester="agent-A" />);
    await flush();
    expect(screen.queryByTestId("announcements-preview")).toBeNull();
  });
});
