import { BestAgentError } from "@best-agent/foundation";
import { describe, expect, it } from "vitest";
import type { Turn } from "@best-agent/protocol";
import type { ConversationMessage } from "../../provider/model-provider.js";
import type { ToolResumeContext } from "../checkpoint-codec.js";
import { ProviderTurnBudget, type ProviderTurnInvokeContext } from "./provider-turn-budget.js";
import { finalizeProviderToolBudgetExhausted } from "./provider-tool-budget-finalizer.js";
import type { ProviderToolLoopRuntime } from "./provider-tool-loop-runtime.js";
import { assertValidProviderProseResponse } from "./provider-response-validation.js";

describe("finalizeProviderToolBudgetExhausted", () => {
  it("validates generated fallback text before returning it", async () => {
    const turn = { id: "turn_1", threadId: "thread_1" } as Turn;
    const context: ToolResumeContext = {
      activeSkillIds: [],
      availableTools: [
        {
          description: "Run a command",
          effect: "execute",
          inputSchema: {},
          name: "shell",
        },
      ],
      messages: [{ role: "user", content: "run the tests" }],
      mode: "provider-tool-loop",
      profileName: "default",
      systemPrompt: undefined,
      toolCallId: "item_1",
      toolInput: {},
      toolName: "shell",
      toolStepCount: 0,
    };
    const invokeContext = {
      activeSkillIds: [],
      availableTools: context.availableTools,
      budget: new ProviderTurnBudget({ enabled: true, max: 1, used: 1 }),
      profile: { name: "default" },
      profileName: "default",
    } as ProviderTurnInvokeContext;
    const runtime = {
      provider: {
        generateUnbudgetedFinalization: async () => ({ text: "Tests passed successfully." }),
      },
      responses: {
        finalizeGeneratedResponse: async (input) =>
          assertValidProviderProseResponse({
            ...input,
            assertValidTerminalAssistantText: (text) => text,
          }),
      },
    } as ProviderToolLoopRuntime;

    await expect(
      finalizeProviderToolBudgetExhausted({
        advancedPhaseEscalationLevel: 0,
        consumedToolStepCount: 1,
        context,
        followUpMessages: context.messages,
        invokeContext,
        runtime,
        turn,
      }),
    ).rejects.toThrow(/no execution tool evidence/u);
  });

  it("passes the turn objective into generated fallback final validation", async () => {
    const turn = { id: "turn_1", threadId: "thread_1" } as Turn;
    const context: ToolResumeContext = {
      activeSkillIds: [],
      availableTools: [],
      messages: [{ role: "user", content: "continue" }],
      mode: "provider-tool-loop",
      objective: "Run the full test suite.",
      profileName: "default",
      systemPrompt: undefined,
      toolCallId: "item_1",
      toolInput: {},
      toolName: "shell",
      toolStepCount: 0,
    };
    const invokeContext = {
      activeSkillIds: [],
      availableTools: context.availableTools,
      budget: new ProviderTurnBudget({ enabled: true, max: 1, used: 1 }),
      objective: context.objective,
      profile: { name: "default" },
      profileName: "default",
    } as ProviderTurnInvokeContext;
    const runtime = {
      provider: {
        generateUnbudgetedFinalization: async () => ({ text: "I could not run the tests." }),
      },
      responses: {
        finalizeGeneratedResponse: async (input) => {
          expect(input.objective).toBe("Run the full test suite.");
          return input.text;
        },
      },
    } as ProviderToolLoopRuntime;

    await expect(
      finalizeProviderToolBudgetExhausted({
        advancedPhaseEscalationLevel: 0,
        consumedToolStepCount: 1,
        context,
        followUpMessages: context.messages,
        invokeContext,
        runtime,
        turn,
      }),
    ).resolves.toBe("I could not run the tests.");
  });

  it("repairs generated fallback text that leaks a control payload", async () => {
    const turn = { id: "turn_1", threadId: "thread_1" } as Turn;
    const context: ToolResumeContext = {
      activeSkillIds: [],
      availableTools: [{ description: "Run a command", effect: "execute", name: "shell" }],
      messages: [{ role: "user", content: "run the tests" }],
      mode: "provider-tool-loop",
      profileName: "default",
      systemPrompt: undefined,
      toolCallId: "item_1",
      toolInput: {},
      toolName: "shell",
      toolStepCount: 0,
    };
    const invokeContext = {
      activeSkillIds: [],
      availableTools: context.availableTools,
      budget: new ProviderTurnBudget({ enabled: true, max: 1, used: 1 }),
      profile: { name: "default" },
      profileName: "default",
    } as ProviderTurnInvokeContext;
    const generationMessages: ConversationMessage[][] = [];
    const generationOperations: string[] = [];
    const generationCorrectionCodes: Array<string | undefined> = [];
    let generateCount = 0;
    const runtime = {
      provider: {
        createCorrectionTrace: (
          input: Parameters<ProviderToolLoopRuntime["provider"]["createCorrectionTrace"]>[0],
        ) => ({
          code: input.code,
          id: "correction_1",
        }),
        generateUnbudgetedFinalization: async (
          _threadId,
          _turnId,
          request: { messages: ConversationMessage[] },
          operation: string,
          generateContext: ProviderTurnInvokeContext,
        ) => {
          generateCount += 1;
          generationMessages.push(request.messages);
          generationOperations.push(operation);
          generationCorrectionCodes.push(generateContext.correctionTrace?.code);
          return { text: generateCount === 1 ? '{"tool":"shell"}' : "I could not run the tests." };
        },
      },
      responses: {
        finalizeGeneratedResponse: async (input) => {
          if (input.text.includes('"tool"')) {
            throw new BestAgentError("TERMINAL_OUTPUT_INVALID", "control payload");
          }
          expect(input.messages.at(-1)?.role).toBe("runtime");
          expect(input.messages.at(-1)?.content).toContain("CONTROL_PAYLOAD_LEAK");
          return input.text;
        },
      },
    } as ProviderToolLoopRuntime;

    await expect(
      finalizeProviderToolBudgetExhausted({
        advancedPhaseEscalationLevel: 0,
        consumedToolStepCount: 1,
        context,
        followUpMessages: context.messages,
        invokeContext,
        runtime,
        turn,
      }),
    ).resolves.toBe("I could not run the tests.");
    expect(generationMessages).toHaveLength(2);
    expect(generationMessages[1]?.at(-1)?.role).toBe("runtime");
    expect(generationOperations).toEqual(["generate", "repair"]);
    expect(generationCorrectionCodes).toEqual([undefined, "TERMINAL_OUTPUT_INVALID"]);
  });

  it("falls back to sanitized tool evidence when finalization repair still leaks control payload", async () => {
    const turn = { id: "turn_1", threadId: "thread_1" } as Turn;
    const context: ToolResumeContext = {
      activeSkillIds: [],
      availableTools: [{ description: "Run a command", effect: "execute", name: "shell" }],
      messages: [{ role: "user", content: "run the tests" }],
      mode: "provider-tool-loop",
      profileName: "default",
      systemPrompt: undefined,
      toolCallId: "item_1",
      toolInput: {},
      toolName: "shell",
      toolStepCount: 0,
    };
    const followUpMessages: ConversationMessage[] = [
      ...context.messages,
      {
        content: "Command exited with code 1",
        errorCode: "EXEC_FAILED",
        outcome: "error",
        role: "tool",
        toolCallId: "item_tool",
        toolName: "shell",
      },
    ];
    const invokeContext = {
      activeSkillIds: [],
      availableTools: context.availableTools,
      budget: new ProviderTurnBudget({ enabled: true, max: 1, used: 1 }),
      profile: { name: "default" },
      profileName: "default",
    } as ProviderTurnInvokeContext;
    let generateCount = 0;
    const runtime = {
      provider: {
        createCorrectionTrace: () => ({
          code: "TERMINAL_OUTPUT_INVALID",
          id: "correction_1",
        }),
        generateUnbudgetedFinalization: async () => {
          generateCount += 1;
          return { text: `<tool_call>shell {"attempt":${generateCount}}` };
        },
      },
      responses: {
        finalizeGeneratedResponse: async (input) => {
          if (input.text.includes("<tool_call>")) {
            throw new BestAgentError("TERMINAL_OUTPUT_INVALID", "control payload");
          }
          return input.text;
        },
      },
    } as ProviderToolLoopRuntime;

    const output = await finalizeProviderToolBudgetExhausted({
      advancedPhaseEscalationLevel: 0,
      consumedToolStepCount: 1,
      context,
      followUpMessages,
      invokeContext,
      runtime,
      turn,
    });

    expect(generateCount).toBe(2);
    expect(output).toContain("I did not complete this turn cleanly");
    expect(output).toContain("shell failed (EXEC_FAILED): Command exited with code 1");
    expect(output).not.toContain("<tool_call>");
    expect(output).not.toContain("TERMINAL_OUTPUT_INVALID");
  });

  it("falls back to a user-facing stale process-session status", async () => {
    const turn = { id: "turn_1", threadId: "thread_1" } as Turn;
    const context: ToolResumeContext = {
      activeSkillIds: [],
      availableTools: [
        { description: "Read process session", effect: "read", name: "process-session-read" },
      ],
      messages: [{ role: "user", content: "跑的怎么样了" }],
      mode: "provider-tool-loop",
      profileName: "default",
      systemPrompt: undefined,
      toolCallId: "item_1",
      toolInput: { session_id: "proc_dead" },
      toolName: "process-session-read",
      toolStepCount: 0,
    };
    const followUpMessages: ConversationMessage[] = [
      ...context.messages,
      {
        content:
          "Tool observation:\nTool: process-session-read\nSummary: Tool process-session-read returned:\nOutcome: success\nOutput excerpt:\nsession_id: proc_dead\nstatus: unavailable\nreason: no live process session exists in this runtime",
        outcome: "success",
        role: "tool",
        toolCallId: "item_tool",
        toolName: "process-session-read",
      },
    ];
    const invokeContext = {
      activeSkillIds: [],
      availableTools: context.availableTools,
      budget: new ProviderTurnBudget({ enabled: true, max: 1, used: 1 }),
      profile: { name: "default" },
      profileName: "default",
    } as ProviderTurnInvokeContext;
    const runtime = {
      provider: {
        createCorrectionTrace: () => ({
          code: "TERMINAL_OUTPUT_INVALID",
          id: "correction_1",
        }),
        generateUnbudgetedFinalization: async () => ({ text: "<tool_call>process-session-read" }),
      },
      responses: {
        finalizeGeneratedResponse: async (input) => {
          if (input.text.includes("<tool_call>")) {
            throw new BestAgentError("TERMINAL_OUTPUT_INVALID", "control payload");
          }
          return input.text;
        },
      },
    } as ProviderToolLoopRuntime;

    const output = await finalizeProviderToolBudgetExhausted({
      advancedPhaseEscalationLevel: 0,
      consumedToolStepCount: 1,
      context,
      followUpMessages,
      invokeContext,
      runtime,
      turn,
    });

    expect(output).toContain("process session proc_dead is no longer available");
    expect(output).toContain("I did not restart it");
    expect(output).not.toContain("Recent tool evidence");
    expect(output).not.toContain("<tool_call>");
  });
});
