import { BestAgentError } from "@best-agent/foundation";
import type { ProviderTurnActionPolicy, Turn } from "@best-agent/protocol";
import { describe, expect, it, vi } from "vitest";
import type { ProviderRequestCheckpointPayload } from "../checkpoint-codec.js";
import { repairReplayedTerminalProviderResponse } from "./provider-replay-terminal-repair.js";
import type { ProviderReplayRepairRuntime } from "./provider-replay-context.js";

const shellTool = {
  description: "Run a shell command.",
  effect: "execute",
  inputSchema: {},
  name: "shell",
} as const;

describe("repairReplayedTerminalProviderResponse", () => {
  it("uses the terminal repair action policy when replaying decide repairs", async () => {
    let repairActionPolicy: ProviderTurnActionPolicy | undefined;
    const runtime = buildReplayRuntime({
      decide: async (_threadId, _turnId, request) => {
        repairActionPolicy = request.actionPolicy;
        return {
          input: { command: "true" },
          kind: "tool",
          model: "test-provider",
          provider: "openai",
          toolName: "shell",
        };
      },
      resolveProviderDecision: async (input) => {
        expect(input.actionPolicyOverride).toMatchObject({ directResponseAllowed: true });
        expect(input.actionPolicyOverride?.required).toBeUndefined();
        return "repaired";
      },
    });

    await expect(
      repairReplayedTerminalProviderResponse({
        payload: decidePayload(),
        responseText: '<tool_call>shell {"command":"true"}</tool_call>',
        runtime,
        turn: buildTurn(),
      }),
    ).resolves.toBe("repaired");

    expect(repairActionPolicy).toMatchObject({ directResponseAllowed: true });
    expect(repairActionPolicy?.required).toBeUndefined();
  });

  it("keeps phase_result controls for phase-result terminal replay repair", async () => {
    let repairRequest: Parameters<ProviderReplayRepairRuntime["provider"]["decide"]>[2] | undefined;
    const runtime = buildReplayRuntime({
      decide: async (_threadId, _turnId, request) => {
        repairRequest = request;
        return {
          kind: "phase_result",
          model: "test-provider",
          phaseResult: {
            phaseName: "change",
            summary: "continued phase",
          },
          provider: "openai",
        };
      },
    });

    await expect(
      repairReplayedTerminalProviderResponse({
        payload: phaseResultPayload(),
        responseText: '<tool_call>shell {"command":"true"}</tool_call>',
        runtime,
        turn: buildTurn(),
      }),
    ).resolves.toBe("repaired");

    expect(repairRequest).toMatchObject({
      allowPhaseResult: true,
      allowPlanUpdate: false,
      phaseResultPhaseName: "change",
    });
    expect(repairRequest?.actionPolicy).toEqual({
      allowedActions: ["tool", "phase_result"],
      directResponseAllowed: false,
      required: undefined,
    });
  });

  it("does not expose phase_result when an old replay checkpoint lacks a phase name", async () => {
    let repairRequest: Parameters<ProviderReplayRepairRuntime["provider"]["decide"]>[2] | undefined;
    let resolveControls:
      | Parameters<ProviderReplayRepairRuntime["resolveProviderDecision"]>[0]["controls"]
      | undefined;
    const runtime = buildReplayRuntime({
      decide: async (_threadId, _turnId, request) => {
        repairRequest = request;
        return {
          kind: "respond",
          model: "test-provider",
          provider: "openai",
          text: "repaired",
        };
      },
      resolveProviderDecision: async (input) => {
        resolveControls = input.controls;
        return "repaired";
      },
    });

    await expect(
      repairReplayedTerminalProviderResponse({
        payload: oldPhaseResultPayloadWithoutPhaseName(),
        responseText: '<tool_call>shell {"command":"true"}</tool_call>',
        runtime,
        turn: buildTurn(),
      }),
    ).resolves.toBe("repaired");

    expect(repairRequest).toMatchObject({
      allowPhaseResult: false,
      allowPlanUpdate: false,
    });
    expect(repairRequest).not.toHaveProperty("phaseResultPhaseName");
    expect(repairRequest?.actionPolicy?.allowedActions).not.toContain("phase_result");
    expect(resolveControls).toEqual({
      allowPhaseResult: false,
      allowPlanUpdate: false,
    });
  });

  it("final-validates replayed generate terminal repairs", async () => {
    const finalizeGeneratedResponse = vi.fn(async () => "finalized response");
    const runtime = buildReplayRuntime({
      assertValidTerminalAssistantTextOrFallback: vi.fn(async () => {
        throw new Error("terminal fallback should not be used for generate repair");
      }),
      finalizeGeneratedResponse,
      generate: async () => ({
        model: "test-provider",
        provider: "openai",
        text: "Tests passed successfully.",
      }),
    });

    await expect(
      repairReplayedTerminalProviderResponse({
        payload: generatePayload(),
        responseText: "<control>bad</control>",
        runtime,
        turn: buildTurn(),
      }),
    ).resolves.toBe("finalized response");

    expect(finalizeGeneratedResponse).toHaveBeenCalledWith(
      expect.objectContaining({
        availableTools: [shellTool],
        objective: "Run the test suite.",
        text: "Tests passed successfully.",
      }),
    );
    const messages = finalizeGeneratedResponse.mock.calls[0]?.[0].messages ?? [];
    expect(messages.some((message) => message.role === "runtime")).toBe(true);
    expect(finalizeGeneratedResponse).toHaveBeenCalledTimes(1);
  });

  it("final-validates replayed generate responses when terminal output is already valid", async () => {
    const finalizeGeneratedResponse = vi.fn(async () => "finalized response");
    const generate = vi.fn();
    const runtime = buildReplayRuntime({
      assertValidTerminalAssistantText: vi.fn((text) => text),
      finalizeGeneratedResponse,
      generate,
    });

    await expect(
      repairReplayedTerminalProviderResponse({
        payload: generatePayload(),
        responseText: "Tests passed successfully.",
        runtime,
        turn: buildTurn(),
      }),
    ).resolves.toBe("finalized response");

    expect(finalizeGeneratedResponse).toHaveBeenCalledWith(
      expect.objectContaining({
        availableTools: [shellTool],
        messages: [{ content: "Run tests.", role: "user" }],
        objective: "Run the test suite.",
        text: "Tests passed successfully.",
      }),
    );
    expect(generate).not.toHaveBeenCalled();
  });
});

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

function decidePayload(): ProviderRequestCheckpointPayload {
  return {
    kind: "provider-request",
    operation: "decide",
    replayContext: {
      activeSkillIds: [],
      availableTools: [shellTool],
      objective: "Run shell.",
      phaseName: undefined,
      profileName: "default",
      providerTurnBudgeted: true,
      providerTurnCount: 0,
      requiredToolConstraint: {
        forbiddenToolNames: [],
        reason: "explicit-required-tool",
        requiredAllToolNames: ["shell"],
        requiredReleasePolicy: "after-required-tools-succeeded",
        requiredToolNames: ["shell"],
        restrictionScope: "until-required-satisfied",
      },
      toolStepCount: 0,
    },
    request: {
      allowPhaseResult: false,
      allowPlanUpdate: false,
      messages: [{ content: "Use shell.", role: "user" }],
      systemPrompt: undefined,
      tools: [shellTool],
    },
    requestKind: "decide",
  } as ProviderRequestCheckpointPayload;
}

function phaseResultPayload(): ProviderRequestCheckpointPayload {
  return {
    kind: "provider-request",
    operation: "decide",
    replayContext: {
      activeSkillIds: [],
      availableTools: [shellTool],
      objective: "Complete the change phase.",
      phaseName: "change",
      profileName: "default",
      providerTurnBudgeted: true,
      providerTurnCount: 0,
      toolStepCount: 0,
    },
    request: {
      allowPhaseResult: true,
      allowPlanUpdate: false,
      messages: [{ content: "Finish the phase.", role: "user" }],
      phaseResultPhaseName: "change",
      systemPrompt: undefined,
      tools: [shellTool],
    },
    requestKind: "decide",
  } as ProviderRequestCheckpointPayload;
}

function oldPhaseResultPayloadWithoutPhaseName(): ProviderRequestCheckpointPayload {
  return {
    kind: "provider-request",
    operation: "decide",
    replayContext: {
      activeSkillIds: [],
      availableTools: [shellTool],
      objective: "Complete the change phase.",
      phaseName: "change",
      profileName: "default",
      providerTurnBudgeted: true,
      providerTurnCount: 0,
      toolStepCount: 0,
    },
    request: {
      allowPhaseResult: true,
      allowPlanUpdate: false,
      messages: [{ content: "Finish the phase.", role: "user" }],
      systemPrompt: undefined,
      tools: [shellTool],
    },
    requestKind: "decide",
  } as ProviderRequestCheckpointPayload;
}

function generatePayload(): ProviderRequestCheckpointPayload {
  return {
    kind: "provider-request",
    operation: "generate",
    replayContext: {
      activeSkillIds: [],
      availableTools: [shellTool],
      objective: "Run the test suite.",
      phaseName: undefined,
      profileName: "default",
      providerTurnBudgeted: true,
      providerTurnCount: 0,
      toolStepCount: 0,
    },
    request: {
      messages: [{ content: "Run tests.", role: "user" }],
      systemPrompt: undefined,
    },
    requestKind: "generate",
  } as ProviderRequestCheckpointPayload;
}

function buildReplayRuntime(
  overrides: Partial<ProviderReplayRepairRuntime> & {
    decide?: ProviderReplayRepairRuntime["provider"]["decide"];
    generate?: ProviderReplayRepairRuntime["provider"]["generate"];
  } = {},
): ProviderReplayRepairRuntime {
  return {
    advancedPhaseReopenCount: vi.fn(async () => 0),
    assertArtifactObligationsSatisfied: vi.fn(async () => undefined),
    assertValidTerminalAssistantText:
      overrides.assertValidTerminalAssistantText ??
      vi.fn(() => {
        throw new BestAgentError("TERMINAL_OUTPUT_INVALID", "terminal output invalid");
      }),
    assertValidTerminalAssistantTextOrFallback:
      overrides.assertValidTerminalAssistantTextOrFallback ?? vi.fn(async (_, text) => text),
    buildCorrectionTrace: vi.fn(({ code }) => ({ code, id: "trace_1" })),
    collectArtifactObligationFailuresForTurn: vi.fn(async () => []),
    cwd: "/tmp/workspace",
    defaultMaxProviderTurns: 3,
    evidenceList: vi.fn(async () => []),
    finalizeGeneratedResponse:
      overrides.finalizeGeneratedResponse ?? vi.fn(async ({ text }) => text),
    hydrateProviderDecideCheckpointRequest: vi.fn((request) => request as never),
    hydrateProviderReplayContextAvailableTools: vi.fn(
      (payload) => payload.replayContext?.availableTools,
    ),
    hydrateProviderRequestReplayConfig: vi.fn(() => undefined),
    listItems: vi.fn(async () => []),
    maxProviderTurnsForProfile: vi.fn(() => 3),
    provider: {
      decide:
        overrides.decide ??
        vi.fn(async () => ({
          kind: "respond",
          model: "test-provider",
          provider: "openai",
          text: "repaired",
        })),
      generate:
        overrides.generate ??
        vi.fn(async () => ({
          model: "test-provider",
          provider: "openai",
          text: "repaired",
        })),
    },
    renderToolInputForProviderHistory: vi.fn((_, input) => input),
    requireProfile: vi.fn((profileName) => ({ name: profileName })),
    resolveProviderDecision: overrides.resolveProviderDecision ?? vi.fn(async () => "repaired"),
    resolveProviderRoute: vi.fn(() => ({ config: {} })),
    resolveReplayPolicyForFollowUp: vi.fn(() => undefined),
  };
}
