/**
 * usage-reaches-session-stats.e2e.test.ts — the premise #193 rests on, checked
 * against the REAL pi runtime.
 *
 * Every unit test for usage reporting asserts that our tool results carry a
 * `usage` field. None of them can establish the thing that makes carrying it
 * worth doing: that pi picks it up. That happens entirely inside pi —
 * `createToolResultMessage` copies `AgentToolResult.usage` onto the persisted
 * message, and `getSessionStats()` folds `toolResult.usage` into the tokens and
 * cost the footer, the statusline and `/cost` read. Mock pi, and a release that
 * stopped doing either would leave the whole feature reporting into a void with
 * a green suite.
 *
 * So this drives a real `AgentSession` and reads its real `getSessionStats()`,
 * with the exact object `PendingUsagePool.drain()` produces — including the
 * `cacheRead` our own display total drops but this report must carry, and the
 * cost breakdown whose `total` pi reads with no guard at all.
 *
 * No network/LLM and no model turn: the message is appended through pi's own
 * `sessionManager.appendMessage`, because what is under test is the accounting,
 * not the streaming that would normally produce the message.
 *
 * This test is also what set the peer floor. Pi began folding `toolResult.usage`
 * into `getSessionStats()` in 0.81.0, when the computation moved to walking
 * session entries through `addUsageToTotals`; every 0.80.x sums assistant
 * messages alone and drops the field. Running unconditionally is the point —
 * against a Pi that does not aggregate, this fails rather than skipping, which
 * is how the range stays honest. `peerDependencies` moved to `>=0.81.0` for
 * exactly this reason, so the CI floor job runs it too.
 */
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { createAgentSession, SessionManager } from "@earendil-works/pi-coding-agent";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { PendingUsagePool } from "../../src/usage.js";
import { fauxModelBackend } from "../helpers/faux-model-backend.js";
import { registerFauxProvider } from "../helpers/pi-ai.js";

// Real pi session construction; a cold first run under full-suite CPU
// contention can exceed vitest's 5s default.
vi.setConfig({ testTimeout: 30_000 });

describe("subagent usage reaches the parent session's stats (real pi)", () => {
  let cwd: string;
  let faux: ReturnType<typeof registerFauxProvider>;

  beforeEach(() => {
    cwd = mkdtempSync(join(tmpdir(), "subagents-usage-e2e-"));
    faux = registerFauxProvider({ provider: "faux", models: [{ id: "faux-1", contextWindow: 200_000 }] });
  });
  afterEach(() => {
    faux.unregister();
    rmSync(cwd, { recursive: true, force: true });
  });

  /** A real session, in memory, on a faux model. */
  async function realSession() {
    const model = faux.getModel();
    const backend = fauxModelBackend(model);
    const { session } = await createAgentSession({
      cwd,
      sessionManager: SessionManager.inMemory(cwd),
      model: model as any,
      modelRegistry: backend.modelRegistry,
      modelRuntime: backend.modelRuntime,
      tools: [],
    } as any);
    return session;
  }

  /** The tool result our `Agent` tool returns, as pi would persist it. */
  function toolResultCarrying(usage: unknown) {
    return {
      role: "toolResult" as const,
      toolCallId: "tc-1",
      toolName: "Agent",
      content: [{ type: "text" as const, text: "Agent completed." }],
      isError: false,
      timestamp: 1,
      usage,
    };
  }

  it("pi adds our reported tokens and cost to getSessionStats()", async () => {
    const session = await realSession();
    try {
      const before = session.getSessionStats();

      const pool = new PendingUsagePool();
      pool.add({ input: 1000, output: 400, cacheWrite: 100, cacheRead: 9000, cost: 0.0123 });
      pool.add({ input: 2000, output: 600, cacheWrite: 200, cacheRead: 18_000, cost: 0.0077 });
      const usage = pool.drain();

      session.sessionManager.appendMessage(toolResultCarrying(usage) as any);
      const after = session.getSessionStats();

      // Exactly what we reported, on every component pi tracks — cacheRead
      // included, which is the one pi counts for its own messages and our own
      // display total leaves out.
      expect(after.tokens.input - before.tokens.input).toBe(3000);
      expect(after.tokens.output - before.tokens.output).toBe(1000);
      expect(after.tokens.cacheWrite - before.tokens.cacheWrite).toBe(300);
      expect(after.tokens.cacheRead - before.tokens.cacheRead).toBe(27_000);

      // The cost: the whole point of the feature for anyone watching a
      // statusline. `addUsageToTotals` reads `usage.cost.total` with no guard,
      // so an incomplete object would have thrown before reaching here.
      expect(after.cost - before.cost).toBeCloseTo(0.02, 10);
    } finally {
      session.dispose?.();
    }
  });

  it("leaves the context-window percentage alone", async () => {
    // pi derives context usage from assistant messages only. If that ever
    // changed, a delegating session would look like it was filling its context
    // with work that happened somewhere else entirely — and users would compact
    // for no reason.
    const session = await realSession();
    try {
      const before = session.getSessionStats().contextUsage?.percent ?? null;

      const pool = new PendingUsagePool();
      pool.add({ input: 150_000, output: 400, cacheWrite: 100, cost: 1.5 });
      session.sessionManager.appendMessage(toolResultCarrying(pool.drain()) as any);

      expect(session.getSessionStats().contextUsage?.percent ?? null).toBe(before);
    } finally {
      session.dispose?.();
    }
  });

  it("counts nothing for a tool result that carries no usage", async () => {
    // The `reportUsage: false` shape, and every other tool in the session.
    const session = await realSession();
    try {
      const before = session.getSessionStats();
      session.sessionManager.appendMessage(toolResultCarrying(undefined) as any);
      const after = session.getSessionStats();

      expect(after.tokens.input).toBe(before.tokens.input);
      expect(after.cost).toBe(before.cost);
    } finally {
      session.dispose?.();
    }
  });
});
