import { BestAgentError } from "@best-agent/foundation";
import { describe, expect, it, vi } from "vitest";
import type { ProviderConfig, ToolResultItem, Turn } from "@best-agent/protocol";
import type { ToolResumeContext } from "../checkpoint-codec.js";
import {
  continueProviderToolOutputBoundary,
  renderStructuredToolObservation,
} from "./provider-tool-loop.js";
import { ProviderTurnBudget, type ProviderTurnInvokeContext } from "./provider-turn-budget.js";
import type { ProviderToolLoopRuntime } from "./provider-tool-loop-runtime.js";

describe("renderStructuredToolObservation", () => {
  it("renders tool output once", () => {
    const result = renderStructuredToolObservation({
      toolCall: {
        outcome: "success",
        toolCallId: "call_echo",
        toolInput: {},
        toolName: "echo",
        toolOutput: "hello",
      },
      toolOutput: "hello",
    });

    expect(result).toContain("Tool observation:");
    expect(result).toContain("Output excerpt:\nhello");
    expect(result.match(/hello/gu)).toHaveLength(1);
  });
});

describe("continueProviderToolOutputBoundary", () => {
  it("passes the follow-up system prompt into recursive provider resolution", async () => {
    const turn = buildTurn();
    const context = buildToolResumeContext({
      systemPrompt: "base prompt",
    });
    const runtime = buildRuntime({
      decide: async () => ({ kind: "respond", text: "done" }),
    });

    await continueProviderToolOutputBoundary({
      context,
      invokeContext: buildInvokeContext(context),
      runtime,
      toolOutput: "tool output",
      turn,
    });

    expect(runtime.resolveProviderDecision).toHaveBeenCalledOnce();
    const resolvedInput = vi.mocked(runtime.resolveProviderDecision).mock.calls[0]?.[0];
    expect(resolvedInput?.systemPrompt).toContain("base prompt");
    expect(resolvedInput?.systemPrompt).toContain("You have just received structured tool results");
  });

  it("keeps the no-action runtime observation for recursive provider resolution", async () => {
    const turn = buildTurn();
    const context = buildToolResumeContext({
      objective: "run the test suite",
      systemPrompt: "base prompt",
    });
    const noActionError = new BestAgentError("PROVIDER_NO_ACTION", "empty provider output");
    let attempts = 0;
    const runtime = buildRuntime({
      decide: async () => {
        attempts += 1;
        if (attempts === 1) {
          throw noActionError;
        }
        return { kind: "respond", text: "done" };
      },
    });

    await continueProviderToolOutputBoundary({
      context,
      invokeContext: buildInvokeContext(context),
      runtime,
      toolOutput: "tool output",
      turn,
    });

    expect(runtime.resolveProviderDecision).toHaveBeenCalledOnce();
    const resolvedInput = vi.mocked(runtime.resolveProviderDecision).mock.calls[0]?.[0];
    expect(resolvedInput?.objective).toBe("run the test suite");
    expect(resolvedInput?.messages.some((message) => message.role === "runtime")).toBe(true);
    expect(resolvedInput?.messages.at(-1)?.content).toContain("PROVIDER_NO_ACTION");
    expect(resolvedInput?.systemPrompt).toContain("You have just received structured tool results");
  });

  it("preserves phase-result controls for tool follow-up decisions", async () => {
    const turn = buildTurn();
    const context = buildToolResumeContext({
      allowPhaseResult: true,
      phaseName: "change",
      phaseResultPhaseName: "change",
      systemPrompt: "base prompt",
    });
    const runtime = buildRuntime({
      decide: async () => ({
        kind: "phase_result",
        model: "test-model",
        phaseResult: {
          phaseName: "change",
          summary: "change complete",
        },
        provider: "openai",
      }),
    });

    await continueProviderToolOutputBoundary({
      context,
      invokeContext: buildInvokeContext(context),
      runtime,
      toolOutput: "tool output",
      turn,
    });

    expect(runtime.provider.decide).toHaveBeenCalledOnce();
    expect(vi.mocked(runtime.provider.decide).mock.calls[0]?.[2]).toMatchObject({
      allowPhaseResult: true,
      phaseResultPhaseName: "change",
    });
    expect(runtime.resolveProviderDecision).toHaveBeenCalledOnce();
    expect(vi.mocked(runtime.resolveProviderDecision).mock.calls[0]?.[0]).toMatchObject({
      controls: {
        allowPhaseResult: true,
        allowPlanUpdate: false,
        phaseResultPhaseName: "change",
      },
      decision: {
        kind: "phase_result",
        phaseResult: {
          phaseName: "change",
        },
      },
    });
  });
});

function buildTurn(): Turn {
  return { id: "turn_1", threadId: "thread_1" } as Turn;
}

function buildToolResumeContext(overrides: Partial<ToolResumeContext> = {}): ToolResumeContext {
  return {
    activeSkillIds: [],
    availableTools: [
      {
        description: "Run a command",
        effect: "execute",
        inputSchema: {},
        name: "shell",
      },
    ],
    cacheControl: undefined,
    messages: [{ role: "user", content: "run the tests" }],
    mode: "provider-tool-loop",
    profileName: "default",
    providerTurnBudgeted: true,
    providerTurnCount: 0,
    systemPrompt: undefined,
    toolCallId: "item_1",
    toolInput: { command: "pnpm test" },
    toolName: "shell",
    toolStepCount: 0,
    ...overrides,
  };
}

function buildInvokeContext(context: ToolResumeContext): ProviderTurnInvokeContext {
  return {
    activeSkillIds: context.activeSkillIds,
    availableTools: context.availableTools,
    budget: new ProviderTurnBudget({ enabled: true, max: 3, used: 0 }),
    objective: context.objective,
    profile: { name: "default" },
    profileName: "default",
  } as ProviderTurnInvokeContext;
}

function buildRuntime(input: {
  decide: ProviderToolLoopRuntime["provider"]["decide"];
}): ProviderToolLoopRuntime {
  const toolResult = {
    callId: "item_1",
    kind: "tool-result",
    outcome: "success",
    output: "tool output",
  } as ToolResultItem;
  return {
    approvals: {
      resolveApprovalTurn: vi.fn(async (turn) => turn),
    },
    checkpoints: {
      resolveReplayToolInput: vi.fn(),
      restoreToolOutputCheckpointArtifacts: vi.fn(async () => toolResult),
    },
    execution: {
      executeToolInvocation: vi.fn(),
    },
    observations: {
      collectArtifactObligationFailuresForTurn: vi.fn(async () => []),
      cwd: "/tmp/workspace",
      renderToolInputForProviderHistory: vi.fn((_, value) => value),
      takePendingToolHookContextPrompt: vi.fn(() => undefined),
      toolReplayPolicyForFollowUp: vi.fn(() => "auto-if-idempotent"),
    },
    profiles: {
      resolveProfile: vi.fn(() => ({ name: "default" }) as never),
    },
    provider: {
      createCorrectionTrace: vi.fn(() => ({ code: "PROVIDER_NO_ACTION", id: "trace_1" })),
      createDecisionStreamPreview: vi.fn(() => ({
        callbacks: {},
        finalize: vi.fn(async () => undefined),
        resolveToolCallId: vi.fn(() => undefined),
      })),
      decide: vi.fn(input.decide),
      decisionNoActionTimeoutMs: vi.fn(() => 1),
      generate: vi.fn(),
      generateUnbudgetedFinalization: vi.fn(),
    },
    resolveProviderDecision: vi.fn(async () => "done"),
    responses: {
      finalizeGeneratedResponse: vi.fn(async ({ text }) => text),
    },
    routing: {
      advancedPhaseReopenCount: vi.fn(async () => 0),
      resolveProviderRoute: vi.fn(() => ({ config: {} as ProviderConfig })),
    },
  };
}
