import { describe, expect, test } from "bun:test";
import { renderSessionSendError, renderSessionSendResult } from "./agentrun/session-send-render";
import type { AgentRunOutputMode, AgentRunResourceOptions } from "./agentrun/utils";

const command = "agentrun send session/sess_admission_fixture --prompt-stdin";
const sessionId = "sess_admission_fixture";
const runId = "run_admission_fixture";
const commandId = "cmd_admission_fixture";
const dispatchIntentId = "rdi_admission_fixture";
const plannedRunnerJobId = "rjob_planned_fixture";

const durableAdmissionFixture: Record<string, unknown> = {
  ok: true,
  command,
  data: {
    action: "session-send",
    dryRun: false,
    mutation: true,
    partialWrite: false,
    sessionId,
    decision: "turn",
    internalCommandType: "turn",
    run: { id: runId, status: "queued", sessionRef: { sessionId } },
    command: {
      id: commandId,
      state: "pending",
      type: "turn",
      dispatchIntent: {
        id: dispatchIntentId,
        state: "pending",
        runnerJobId: plannedRunnerJobId,
        actualRunnerJobId: null,
        attemptCount: 0,
        durable: true,
      },
    },
    runnerJob: {
      action: "runner-dispatch-admitted",
      state: "pending",
      runId,
      commandId,
      runnerJobId: plannedRunnerJobId,
      dispatchIntentId,
      durable: true,
    },
    runnerAdmission: {
      mode: "durable-dispatch-intent",
      state: "pending",
      reason: "session-runner-admission-recovered",
      disposition: "recovered",
      failureKind: null,
      sessionId,
      runId,
      commandId,
      dispatchIntentId,
      plannedRunnerJobId,
      actualRunnerJobId: null,
      attemptCount: 0,
      durable: true,
      mutation: true,
      partialWrite: false,
      recoveredPriorPartialWrite: true,
      retryable: true,
      retryAuthority: "dispatcher",
      recoveryActions: [
        { operation: "describe", resourceKind: "session", resourceName: sessionId },
        { operation: "events", resourceKind: "run", resourceName: runId, runId, afterSeq: 0, limit: 100 },
      ],
      valuesPrinted: false,
    },
    pollActions: [
      { operation: "logs", resourceKind: "session", resourceName: sessionId, sessionId, limit: 100 },
      { operation: "cancel", resourceKind: "session", resourceName: sessionId },
    ],
    valuesPrinted: false,
  },
};

const legacySuccessFixture: Record<string, unknown> = {
  ok: true,
  command,
  data: {
    action: "session-send",
    dryRun: false,
    mutation: true,
    partialWrite: false,
    sessionId,
    decision: "turn",
    internalCommandType: "turn",
    run: { id: runId, status: "queued", sessionRef: { sessionId } },
    command: { id: commandId, state: "pending", type: "turn" },
    runnerJob: { id: "rjob_legacy_fixture", state: "queued", runId, commandId },
    pollActions: [
      { operation: "describe", resourceKind: "session", resourceName: sessionId },
      { operation: "events", resourceKind: "run", resourceName: runId, runId, afterSeq: 0, limit: 100 },
    ],
    valuesPrinted: false,
  },
};

const typedFailureFixture: Record<string, unknown> = {
  ok: false,
  command,
  failureKind: "validation-failed",
  message: "durable runner dispatch is not enabled for session turn admission",
  httpStatus: 503,
  agentrun: {
    failureKind: "infra-failed",
    error: {
      name: "AgentRunError",
      failureKind: "infra-failed",
      details: {
        code: "runner-admission-failed",
        reason: "session-runner-dispatcher-disabled",
        phase: "runner-admission",
        sessionId,
        mutation: false,
        partialWrite: false,
        retryable: false,
        retryAuthority: "none",
        recoveryAction: "restore-runner-dispatcher",
        recoveryActions: [
          { operation: "describe", resourceKind: "session", resourceName: sessionId },
        ],
        valuesPrinted: false,
      },
    },
  },
};

describe("AgentRun session send runner admission rendering", () => {
  test("default output exposes durable admission without claiming a Kubernetes Job exists", () => {
    const result = renderSessionSendResult(command, durableAdmissionFixture, options("human"));
    expect(result.ok).toBe(true);
    expect(result.renderedText).toContain("RunnerAdmission:");
    expect(result.renderedText).toContain("mode=durable-dispatch-intent state=pending disposition=recovered");
    expect(result.renderedText).toContain(`runId=${runId} commandId=${commandId} dispatchIntentId=${dispatchIntentId}`);
    expect(result.renderedText).toContain(`plannedRunnerJobId=${plannedRunnerJobId} actualRunnerJobId=-`);
    expect(result.renderedText).toContain("mutation=true partialWrite=false recoveredPriorPartialWrite=true");
    expect(result.renderedText).toContain("retryable=true retryAuthority=dispatcher");
    expect(result.renderedText).toContain(`planned-runnerjob/${plannedRunnerJobId}`);
    expect(result.renderedText).toContain("它本身不是 Kubernetes Job 已创建的证据");
    expect(result.renderedText).toContain("durable dispatcher 负责自动重试；这不是重复执行 session send 的授权");
    expect(result.renderedText).not.toContain(`describe runnerjob/${plannedRunnerJobId}`);
    expect(result.renderedText).not.toContain("agentrun send session/");
    expect(Buffer.byteLength(result.renderedText, "utf8")).toBeLessThan(7000);
  });

  test("JSON and YAML expose the same bounded runner admission contract and read-only Next", () => {
    const jsonText = renderSessionSendResult(command, durableAdmissionFixture, options("json")).renderedText;
    const yamlText = renderSessionSendResult(command, durableAdmissionFixture, options("yaml")).renderedText;
    const json = JSON.parse(jsonText) as Record<string, any>;
    const yaml = Bun.YAML.parse(yamlText) as Record<string, any>;
    expect(yaml).toEqual(json);
    expect(json.runnerAdmission).toMatchObject({
      mode: "durable-dispatch-intent",
      state: "pending",
      reason: "session-runner-admission-recovered",
      disposition: "recovered",
      runId,
      commandId,
      dispatchIntentId,
      plannedRunnerJobId,
      actualRunnerJobId: null,
      mutation: true,
      partialWrite: false,
      recoveredPriorPartialWrite: true,
      retryable: true,
      retryAuthority: "dispatcher",
    });
    expect(json.resources.runnerJob).toBeNull();
    expect(json.resources.plannedRunnerJob).toEqual({
      kind: "planned-runnerjob",
      id: plannedRunnerJobId,
      identityRole: "durable-planned",
      kubernetesJobCreated: null,
      kubernetesJobCreationEvidence: "not-asserted",
      state: "pending",
      durable: true,
    });
    expect(json.next.actions.length).toBeGreaterThan(0);
    for (const action of json.next.actions) {
      expect(action.mutation).toBe(false);
      expect(action.command).toMatch(/^bun scripts\/cli\.ts agentrun (?:describe|events|logs|result)\b/u);
      expect(action.command).not.toContain("agentrun send");
      expect(action.command).not.toContain(`runnerjob/${plannedRunnerJobId}`);
    }
    for (const output of [jsonText, yamlText]) expect(Buffer.byteLength(output, "utf8")).toBeLessThan(7000);
  });

  test("legacy manager success output keeps runnerJob behavior when runnerAdmission is absent", () => {
    const human = renderSessionSendResult(command, legacySuccessFixture, options("human")).renderedText;
    const json = JSON.parse(renderSessionSendResult(command, legacySuccessFixture, options("json")).renderedText) as Record<string, any>;
    expect(human).not.toContain("RunnerAdmission:");
    expect(human).toContain("runnerjob/rjob_legacy_fixture state=queued");
    expect(human).toContain("只使用 manager 返回的资源 identity 做只读下钻");
    expect(json.runnerAdmission).toBeNull();
    expect(json.resources.runnerJob).toEqual({ kind: "runnerjob", id: "rjob_legacy_fixture", state: "queued" });
    expect(json.resources).not.toHaveProperty("plannedRunnerJob");
    expect(json.next.actions.some((action: Record<string, unknown>) => String(action.command).includes("describe runnerjob/rjob_legacy_fixture"))).toBe(true);
  });

  test("typed failure remains fail-closed, bounded, and does not suggest a repeated send", () => {
    const human = renderSessionSendError(command, typedFailureFixture, options("human"));
    const jsonText = renderSessionSendError(command, typedFailureFixture, options("json")).renderedText;
    const yamlText = renderSessionSendError(command, typedFailureFixture, options("yaml")).renderedText;
    const json = JSON.parse(jsonText) as Record<string, any>;
    const yaml = Bun.YAML.parse(yamlText) as Record<string, any>;
    expect(human.ok).toBe(false);
    expect(human.renderedText).toContain("TypedFailure: infra-failed");
    expect(human.renderedText).toContain("Reason: session-runner-dispatcher-disabled");
    expect(human.renderedText).toContain("Mutation: false");
    expect(human.renderedText).toContain("PartialWrite: false");
    expect(human.renderedText).toContain("Retryable: false");
    expect(human.renderedText).toContain("RetryAuthority: none");
    expect(human.renderedText).toContain("RecoveryAction: restore-runner-dispatcher");
    expect(human.renderedText).not.toContain("agentrun send session/");
    expect(yaml).toEqual(json);
    expect(json).toMatchObject({
      kind: "SessionSendError",
      mutation: false,
      partialWrite: false,
      error: {
        failureKind: "validation-failed",
        typedFailureKind: "infra-failed",
        code: "runner-admission-failed",
        reason: "session-runner-dispatcher-disabled",
        retryable: false,
        retryAuthority: "none",
        recoveryAction: "restore-runner-dispatcher",
      },
    });
    for (const output of [human.renderedText, jsonText, yamlText]) expect(Buffer.byteLength(output, "utf8")).toBeLessThan(7000);
  });
});

function options(output: AgentRunOutputMode): AgentRunResourceOptions {
  return {
    output,
    full: false,
    raw: false,
    debug: false,
    taskInput: false,
    limit: 20,
    cursor: null,
    queue: null,
    state: null,
    unread: false,
    readerId: "cli",
    taskId: null,
    runId: null,
    commandId: null,
    sessionId: null,
    afterSeq: null,
    eventDetailSeq: null,
    tail: null,
    fullText: false,
    reason: null,
    dryRun: false,
    file: null,
    aipod: null,
    title: null,
    idempotencyKey: null,
    promptStdin: false,
    node: null,
    lane: null,
    passthroughArgs: [],
  };
}
