/**
 * LS7-front — session-limit state in {@link useAgents} (ARCHITECTURE §21).
 *
 * Drives the hook behind the real {@link DIProvider} with an in-memory
 * {@link MockSystemGateway} (to emit the limit domain events) and a
 * {@link MockInputGateway} (to record `cancelResume`). Verifies that each of the
 * five limit events folds correctly into `limitByAgent`, plus the optimistic
 * `cancelResume` action and the backend verdict it returns.
 *
 * Cases:
 *  - `agentRateLimited` → `{ limitedUntil, suspected: false }`
 *  - `agentResumeScheduled` → arms `resumeFireAt`
 *  - `agentResumeCancelled` → drops `resumeFireAt`, stays limité
 *  - `agentResumed` → clears the entry entirely
 *  - `agentRateLimitSuspected` (with & without `resetsAtMs`) → `suspected: true`
 *  - realistic sequence rateLimited → scheduled → cancelResume (action)
 *  - `cancelResume` action: optimistic drop + both backend verdicts
 */

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

import type { DomainEvent } from "@/domain";
import type { Gateways } from "@/ports";
import { MockInputGateway, MockSystemGateway } from "@/adapters/mock";
import { DIProvider } from "@/app/di";
import { useAgents } from "./useAgents";

const PROJECT_ID = "proj-limits-001";
const AGENT = "agent-A";

function setup() {
  const system = new MockSystemGateway();
  const input = new MockInputGateway();
  // useAgents calls agent.listAgents / profile.listProfiles on mount; provide
  // minimal stubs so the mount effect resolves without a backend.
  const agent = {
    listAgents: async () => [],
    listLiveAgents: async () => [],
  };
  const profile = { listProfiles: async () => [] };
  const gateways = { system, input, agent, profile } as unknown as Gateways;
  const wrapper = ({ children }: { children: React.ReactNode }) => (
    <DIProvider gateways={gateways}>{children}</DIProvider>
  );
  const view = renderHook(() => useAgents(PROJECT_ID), { wrapper });
  return { system, input, view };
}

/** Emits an event and flushes the async subscription/microtasks. */
async function emit(system: MockSystemGateway, event: DomainEvent) {
  await act(async () => {
    system.emit(event);
    await Promise.resolve();
  });
}

describe("useAgents — session-limit state (§21)", () => {
  it("agentRateLimited with a reset time → { limitedUntil, suspected:false }", async () => {
    const { system, view } = setup();
    await emit(system, {
      type: "agentRateLimited",
      agentId: AGENT,
      resetsAtMs: 1_800_000_000_000,
    });
    expect(view.result.current.limitByAgent[AGENT]).toEqual({
      limitedUntil: 1_800_000_000_000,
      resumeFireAt: undefined,
      suspected: false,
    });
  });

  it("agentResumeScheduled arms resumeFireAt on top of the limited state", async () => {
    const { system, view } = setup();
    await emit(system, {
      type: "agentRateLimited",
      agentId: AGENT,
      resetsAtMs: 1_800_000_000_000,
    });
    await emit(system, {
      type: "agentResumeScheduled",
      agentId: AGENT,
      fireAtMs: 1_800_000_300_000,
    });
    expect(view.result.current.limitByAgent[AGENT]).toMatchObject({
      limitedUntil: 1_800_000_000_000,
      resumeFireAt: 1_800_000_300_000,
      suspected: false,
    });
  });

  it("agentResumeCancelled drops resumeFireAt but keeps the agent limité", async () => {
    const { system, view } = setup();
    await emit(system, {
      type: "agentRateLimited",
      agentId: AGENT,
      resetsAtMs: 1_800_000_000_000,
    });
    await emit(system, {
      type: "agentResumeScheduled",
      agentId: AGENT,
      fireAtMs: 1_800_000_300_000,
    });
    await emit(system, { type: "agentResumeCancelled", agentId: AGENT });

    const state = view.result.current.limitByAgent[AGENT];
    expect(state).toBeDefined();
    expect(state.resumeFireAt).toBeUndefined();
    expect(state.limitedUntil).toBe(1_800_000_000_000);
  });

  it("agentResumeCancelled on an unknown agent is a no-op (no entry created)", async () => {
    const { system, view } = setup();
    await emit(system, { type: "agentResumeCancelled", agentId: AGENT });
    expect(view.result.current.limitByAgent[AGENT]).toBeUndefined();
  });

  it("agentResumed clears the limit entry entirely", async () => {
    const { system, view } = setup();
    await emit(system, {
      type: "agentRateLimited",
      agentId: AGENT,
      resetsAtMs: 1_800_000_000_000,
    });
    await emit(system, { type: "agentResumed", agentId: AGENT });
    expect(view.result.current.limitByAgent[AGENT]).toBeUndefined();
    expect(AGENT in view.result.current.limitByAgent).toBe(false);
  });

  it("agentRateLimitSuspected with a time → { limitedUntil, suspected:true }", async () => {
    const { system, view } = setup();
    await emit(system, {
      type: "agentRateLimitSuspected",
      agentId: AGENT,
      resetsAtMs: 1_800_000_500_000,
    });
    expect(view.result.current.limitByAgent[AGENT]).toMatchObject({
      limitedUntil: 1_800_000_500_000,
      suspected: true,
    });
  });

  it("agentRateLimitSuspected WITHOUT a time → suspected:true, no limitedUntil", async () => {
    const { system, view } = setup();
    await emit(system, { type: "agentRateLimitSuspected", agentId: AGENT });
    const state = view.result.current.limitByAgent[AGENT];
    expect(state.suspected).toBe(true);
    expect(state.limitedUntil).toBeUndefined();
  });

  it("realistic sequence: rateLimited → scheduled → cancelResume (action)", async () => {
    const { system, input, view } = setup();
    await emit(system, {
      type: "agentRateLimited",
      agentId: AGENT,
      resetsAtMs: 1_800_000_000_000,
    });
    await emit(system, {
      type: "agentResumeScheduled",
      agentId: AGENT,
      fireAtMs: 1_800_000_300_000,
    });
    expect(view.result.current.limitByAgent[AGENT].resumeFireAt).toBe(
      1_800_000_300_000,
    );

    // The user clicks "Annuler la reprise": optimistic drop + port call.
    let verdict: boolean | undefined;
    await act(async () => {
      verdict = await view.result.current.cancelResume(AGENT);
    });

    // Optimistic: the countdown is gone immediately; the agent stays limité.
    expect(view.result.current.limitByAgent[AGENT].resumeFireAt).toBeUndefined();
    expect(view.result.current.limitByAgent[AGENT].limitedUntil).toBe(
      1_800_000_000_000,
    );
    // The action routed through the InputGateway and returned the verdict.
    expect(input.cancelledResumes).toEqual([AGENT]);
    expect(verdict).toBe(true);
  });

  it("cancelResume relays a `false` backend verdict (already fired / none armed)", async () => {
    const { system, input, view } = setup();
    input.cancelResumeResult = false;
    await emit(system, {
      type: "agentResumeScheduled",
      agentId: AGENT,
      fireAtMs: 1_800_000_300_000,
    });

    let verdict: boolean | undefined;
    await act(async () => {
      verdict = await view.result.current.cancelResume(AGENT);
    });
    expect(verdict).toBe(false);
    expect(input.cancelledResumes).toEqual([AGENT]);
    // Still optimistically dropped locally.
    expect(view.result.current.limitByAgent[AGENT]?.resumeFireAt).toBeUndefined();
  });

  it("cancelResume with no armed resume still calls the port (no-op locally)", async () => {
    const { input, view } = setup();
    // No event emitted: the agent has no entry at all.
    await waitFor(() => expect(view.result.current).toBeTruthy());
    let verdict: boolean | undefined;
    await act(async () => {
      verdict = await view.result.current.cancelResume(AGENT);
    });
    expect(verdict).toBe(true);
    expect(input.cancelledResumes).toEqual([AGENT]);
    expect(view.result.current.limitByAgent[AGENT]).toBeUndefined();
  });

  it("folds limits for two agents independently", async () => {
    const { system, view } = setup();
    await emit(system, {
      type: "agentRateLimited",
      agentId: "agent-A",
      resetsAtMs: 111,
    });
    await emit(system, {
      type: "agentRateLimitSuspected",
      agentId: "agent-B",
    });
    expect(view.result.current.limitByAgent["agent-A"]).toMatchObject({
      limitedUntil: 111,
      suspected: false,
    });
    expect(view.result.current.limitByAgent["agent-B"]).toMatchObject({
      suspected: true,
    });
    expect(view.result.current.limitByAgent["agent-B"].limitedUntil).toBeUndefined();
  });
});
