import { afterEach, describe, expect, it, vi } from "vitest";

vi.mock("./tools/gateway.js", () => ({
  callGatewayTool: vi.fn(async () => ({ status: "ok" })),
}));

vi.mock("../infra/outbound/message.js", () => ({
  sendMessage: vi.fn(async () => ({ ok: true })),
}));

import { sendMessage } from "../infra/outbound/message.js";
import {
  buildExecApprovalFollowupPrompt,
  sendExecApprovalFollowup,
} from "./bash-tools.exec-approval-followup.js";
import { callGatewayTool } from "./tools/gateway.js";

afterEach(() => {
  vi.resetAllMocks();
});

function requireRecord(value: unknown, label: string): Record<string, unknown> {
  if (!value || typeof value !== "object" || Array.isArray(value)) {
    throw new Error(`expected ${label}`);
  }
  return value as Record<string, unknown>;
}

function requireFirstMockCall(mock: unknown, label: string): unknown[] {
  const call = (mock as { mock?: { calls?: unknown[][] } }).mock?.calls?.[0];
  if (!call) {
    throw new Error(`expected ${label}`);
  }
  return call;
}

function expectGatewayAgentFollowup(expected: Record<string, unknown>) {
  const call = requireFirstMockCall(callGatewayTool, "callGatewayTool call");
  expect(call[0]).toBe("agent");
  requireRecord(call[1], "gateway tool context");
  const params = requireRecord(call[2], "gateway tool params");
  for (const [key, value] of Object.entries(expected)) {
    expect(params[key]).toBe(value);
  }
  expect(call[3]).toBeUndefined();
  return params;
}

function expectGatewayAgentWait(expected: Record<string, unknown>) {
  const call = (callGatewayTool as { mock?: { calls?: unknown[][] } }).mock?.calls?.[1];
  if (!call) {
    throw new Error("expected agent.wait call");
  }
  expect(call[0]).toBe("agent.wait");
  requireRecord(call[1], "gateway wait context");
  const params = requireRecord(call[2], "gateway wait params");
  for (const [key, value] of Object.entries(expected)) {
    expect(params[key]).toBe(value);
  }
  expect(call[3]).toBeUndefined();
  return params;
}

function expectDirectSend(expected: Record<string, unknown>) {
  const call = requireFirstMockCall(sendMessage, "sendMessage call");
  const params = requireRecord(call[0], "sendMessage params");
  for (const [key, value] of Object.entries(expected)) {
    expect(params[key]).toBe(value);
  }
}

describe("exec approval followup", () => {
  it("uses an explicit denial prompt when the command did not run", () => {
    const prompt = buildExecApprovalFollowupPrompt(
      "Exec denied (gateway id=req-1, user-denied): uname -a",
    );

    expect(prompt).toContain("did not run");
    expect(prompt).toContain("Do not mention, summarize, or reuse output");
    expect(prompt).not.toContain("already approved has completed");
  });

  it("uses the denied followup branch for nested-parentheses denial metadata", () => {
    const prompt = buildExecApprovalFollowupPrompt(
      "Exec denied (gateway id=req-1, approval-timeout (allowlist-miss)): uname -a",
    );

    expect(prompt).toContain("did not run");
    expect(prompt).toContain("Do not mention, summarize, or reuse output");
    expect(prompt).not.toContain("already approved has completed");
  });

  it("tells the agent to continue the task before replying when the command succeeds", () => {
    const prompt = buildExecApprovalFollowupPrompt("Exec finished (gateway id=req-1, code 0)\nok");

    expect(prompt).toContain("continue from this result before replying to the user");
    expect(prompt).toContain("Continue the task if needed, then reply to the user");
  });

  it("keeps followups internal when no external route is available", async () => {
    await sendExecApprovalFollowup({
      approvalId: "req-1",
      sessionKey: "agent:main:main",
      resultText: "Exec completed: echo ok",
    });

    expectGatewayAgentFollowup({
      sessionKey: "agent:main:main",
      deliver: false,
      channel: undefined,
      to: undefined,
    });
    expect(sendMessage).not.toHaveBeenCalled();
  });

  it("routes denied followups through the originating main session", async () => {
    await sendExecApprovalFollowup({
      approvalId: "req-denied-main",
      sessionKey: "agent:main:main",
      turnSourceChannel: "webchat",
      resultText: "Exec denied (gateway id=req-denied-main, user-denied): uname -a",
    });

    const agentArgs = expectGatewayAgentFollowup({
      sessionKey: "agent:main:main",
      deliver: false,
      channel: "webchat",
      idempotencyKey: "exec-approval-followup:req-denied-main",
    });
    expect(agentArgs.message).toContain("An async command did not run.");
    expect(agentArgs.message).toContain(
      "Exec denied (gateway id=req-denied-main, user-denied): uname -a",
    );
    expect(agentArgs.message).not.toContain("missing tool result");
    expect(agentArgs.message).not.toContain("transcript repair");
    expect(sendMessage).not.toHaveBeenCalled();
  });

  it.each([
    {
      channel: "slack",
      sessionKey: "agent:main:slack:channel:C123",
      to: "channel:C123",
      accountId: "default",
      threadId: "1712419200.1234",
    },
    {
      channel: "discord",
      sessionKey: "agent:main:discord:channel:123",
      to: "123",
      accountId: "default",
      threadId: "456",
    },
    {
      channel: "telegram",
      sessionKey: "agent:main:telegram:-100123",
      to: "-100123",
      accountId: "default",
      threadId: "789",
    },
  ])("uses agent continuation for $channel followups when a session exists", async (target) => {
    await sendExecApprovalFollowup({
      approvalId: `req-${target.channel}`,
      sessionKey: target.sessionKey,
      turnSourceChannel: target.channel,
      turnSourceTo: target.to,
      turnSourceAccountId: target.accountId,
      turnSourceThreadId: target.threadId,
      resultText: "slack exec approval smoke",
    });

    expectGatewayAgentFollowup({
      sessionKey: target.sessionKey,
      deliver: true,
      bestEffortDeliver: true,
      channel: target.channel,
      to: target.to,
      accountId: target.accountId,
      threadId: target.threadId,
      idempotencyKey: `exec-approval-followup:req-${target.channel}`,
    });
    expect(sendMessage).not.toHaveBeenCalled();
  });

  it("waits for accepted agent followups without direct fallback", async () => {
    vi.mocked(callGatewayTool)
      .mockResolvedValueOnce({
        runId: "exec-approval-followup:req-wait:nonce:nonce-wait",
        status: "accepted",
      })
      .mockResolvedValueOnce({
        runId: "exec-approval-followup:req-wait:nonce:nonce-wait",
        status: "ok",
      });

    await sendExecApprovalFollowup({
      approvalId: "req-wait",
      sessionKey: "agent:main:telegram:direct:123",
      turnSourceChannel: "telegram",
      turnSourceTo: "123",
      turnSourceAccountId: "default",
      resultText: "Exec finished (gateway id=req-wait, session=sess_1, code 0)\nall good",
      idempotencyKey: "exec-approval-followup:req-wait:nonce:nonce-wait",
    });

    expectGatewayAgentFollowup({
      sessionKey: "agent:main:telegram:direct:123",
      deliver: true,
      channel: "telegram",
      to: "123",
      idempotencyKey: "exec-approval-followup:req-wait:nonce:nonce-wait",
    });
    expectGatewayAgentWait({
      runId: "exec-approval-followup:req-wait:nonce:nonce-wait",
      timeoutMs: 60_000,
    });
    expect(sendMessage).not.toHaveBeenCalled();
  });

  it("falls back to sanitized direct external delivery only when no session exists", async () => {
    await sendExecApprovalFollowup({
      approvalId: "req-no-session",
      turnSourceChannel: "discord",
      turnSourceTo: "123",
      turnSourceAccountId: "default",
      turnSourceThreadId: "456",
      resultText: "Exec finished (gateway id=req-no-session, session=sess_1, code 0)\nall good",
    });

    expectDirectSend({
      channel: "discord",
      to: "123",
      accountId: "default",
      threadId: "456",
      content: "all good",
      idempotencyKey: "exec-approval-followup:req-no-session",
    });
    expect(callGatewayTool).not.toHaveBeenCalled();
  });

  it("can force direct delivery even when a session key exists", async () => {
    await sendExecApprovalFollowup({
      approvalId: "req-direct",
      sessionKey: "agent:main:telegram:direct:123",
      turnSourceChannel: "telegram",
      turnSourceTo: "123",
      turnSourceAccountId: "default",
      resultText:
        "Exec finished (gateway id=req-direct, session=sess_1, code 0)\npasteable diagnostics report",
      direct: true,
    });

    expectDirectSend({
      channel: "telegram",
      to: "123",
      accountId: "default",
      content: "pasteable diagnostics report",
      idempotencyKey: "exec-approval-followup:req-direct",
    });
    expect(callGatewayTool).not.toHaveBeenCalled();
  });

  it("falls back to sanitized direct delivery without alarming prefix for successful completions", async () => {
    vi.mocked(callGatewayTool).mockRejectedValueOnce(new Error("session missing"));

    await sendExecApprovalFollowup({
      approvalId: "req-session-resume-failed",
      sessionKey: "agent:main:discord:channel:123",
      turnSourceChannel: "discord",
      turnSourceTo: "123",
      turnSourceAccountId: "default",
      turnSourceThreadId: "456",
      resultText:
        "Exec finished (gateway id=req-session-resume-failed, session=sess_1, code 0)\nall good",
    });

    expectDirectSend({
      content: "all good",
      idempotencyKey: "exec-approval-followup:req-session-resume-failed",
    });
  });

  it("uses a generic summary when a no-session completion has no user-visible output", async () => {
    await sendExecApprovalFollowup({
      approvalId: "req-no-session-empty",
      turnSourceChannel: "discord",
      turnSourceTo: "123",
      turnSourceAccountId: "default",
      turnSourceThreadId: "456",
      resultText: "Exec finished (gateway id=req-no-session-empty, session=sess_2, code 0)",
    });

    expectDirectSend({
      content: "Background command finished.",
      idempotencyKey: "exec-approval-followup:req-no-session-empty",
    });
  });

  it("falls back to safe direct denied copy when session resume fails", async () => {
    vi.mocked(callGatewayTool).mockRejectedValueOnce(new Error("session missing"));

    await sendExecApprovalFollowup({
      approvalId: "req-denied-resume-failed",
      sessionKey: "agent:main:telegram:-100123",
      turnSourceChannel: "telegram",
      turnSourceTo: "-100123",
      turnSourceAccountId: "default",
      turnSourceThreadId: "789",
      resultText: "Exec denied (gateway id=req-denied-resume-failed, approval-timeout): uname -a",
    });

    expectDirectSend({
      content: "Command did not run: approval timed out.",
      idempotencyKey: "exec-approval-followup:req-denied-resume-failed",
    });
    expect(callGatewayTool).toHaveBeenCalledTimes(1);
  });

  it("falls back to safe direct denied copy for nested-parentheses denial metadata", async () => {
    vi.mocked(callGatewayTool).mockRejectedValueOnce(new Error("session missing"));

    await sendExecApprovalFollowup({
      approvalId: "req-denied-resume-failed-nested",
      sessionKey: "agent:main:telegram:-100123",
      turnSourceChannel: "telegram",
      turnSourceTo: "-100123",
      turnSourceAccountId: "default",
      turnSourceThreadId: "789",
      resultText:
        "Exec denied (gateway id=req-denied-resume-failed-nested, approval-timeout (allowlist-miss)): uname -a",
    });

    expectDirectSend({
      content: "Command did not run: approval timed out.",
      idempotencyKey: "exec-approval-followup:req-denied-resume-failed-nested",
    });
    expect(callGatewayTool).toHaveBeenCalledTimes(1);
  });

  it("suppresses denied followups for subagent sessions", async () => {
    await expect(
      sendExecApprovalFollowup({
        approvalId: "req-denied-subagent",
        sessionKey: "agent:main:subagent:test",
        turnSourceChannel: "telegram",
        turnSourceTo: "123",
        turnSourceAccountId: "default",
        resultText: "Exec denied (gateway id=req-denied-subagent, approval-timeout): uname -a",
      }),
    ).resolves.toBe(false);

    expect(callGatewayTool).not.toHaveBeenCalled();
    expect(sendMessage).not.toHaveBeenCalled();
  });

  it.each([
    "Exec denied (gateway id=req-denied-nosession, approval-timeout): uname -a",
    "exec denied (gateway id=req-denied-nosession, approval-timeout): uname -a",
  ])("does not mirror raw denied followups without a session: %s", async (resultText) => {
    await expect(
      sendExecApprovalFollowup({
        approvalId: "req-denied-nosession",
        turnSourceChannel: "telegram",
        turnSourceTo: "123",
        turnSourceAccountId: "default",
        resultText,
      }),
    ).resolves.toBe(false);

    expect(callGatewayTool).not.toHaveBeenCalled();
    expect(sendMessage).not.toHaveBeenCalled();
  });

  it("preserves turnSourceChannel as messageProvider on the followup run when no deliverable route exists", async () => {
    // Regression: #74646 — tools.elevated.allowFrom.<provider> fails in approval followup
    await sendExecApprovalFollowup({
      approvalId: "req-elevated-74646",
      sessionKey: "agent:main:telegram:-100123",
      turnSourceChannel: "telegram",
      resultText: "Exec completed: systemctl status gateway",
    });

    expectGatewayAgentFollowup({
      sessionKey: "agent:main:telegram:-100123",
      deliver: false,
      channel: "telegram",
    });
    expect(sendMessage).not.toHaveBeenCalled();
  });

  it("carries the runtime handoff separately from idempotency without exposing elevated defaults", async () => {
    await sendExecApprovalFollowup({
      approvalId: "req-elevated-75832",
      sessionKey: "agent:main:telegram:direct:123",
      turnSourceChannel: "telegram",
      resultText: "Exec finished (gateway id=req-elevated-75832, code 0)\nok",
      internalRuntimeHandoffId: "handoff-75832",
      idempotencyKey: "exec-approval-followup:req-elevated-75832:nonce:nonce-75832",
    });

    const agentArgs = expectGatewayAgentFollowup({
      sessionKey: "agent:main:telegram:direct:123",
      channel: "telegram",
      idempotencyKey: "exec-approval-followup:req-elevated-75832:nonce:nonce-75832",
      internalRuntimeHandoffId: "handoff-75832",
    });
    expect(agentArgs).not.toHaveProperty("bashElevated");
    expect(agentArgs).not.toHaveProperty("execApprovalFollowupToken");
  });

  it("throws when neither a session nor a deliverable route is available", async () => {
    await expect(
      sendExecApprovalFollowup({
        approvalId: "req-missing",
        turnSourceChannel: "slack",
        resultText: "Exec completed: echo ok",
      }),
    ).rejects.toThrow("Session key or deliverable origin route is required");
  });
});
