// @vitest-environment jsdom
//
// Approval card rendering + decision routing. The card is the only
// UI gate between the agent and a destructive action, so the test
// pins:
//   - destructive vs benign chrome distinguishable to a screen
//     reader (role=alertdialog, AlertTriangle vs Shield, label),
//   - benign branch: single-tap Allow / Always / Deny each route
//     `onResolve` with the matching ApprovalDecision,
//   - destructive branch: only Hold-to-allow + Deny; instant click
//     does NOT resolve until LONG_PRESS_MS elapses,
//   - args_preview rendering: parsed JSON → <dl> with `_aoe_*` keys
//     hidden; non-object → raw <pre>,
//   - offline + rolled-back states disable the action surface.

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { act, cleanup, fireEvent, render, screen } from "@testing-library/react";

import { ApprovalCard } from "./ApprovalCard";
import type { Approval, ApprovalDecision } from "../../lib/cockpitTypes";

vi.mock("../../lib/connectionState", () => ({
  useServerDown: () => false,
  OFFLINE_TITLE: "Disconnected",
}));

function makeApproval(over: Partial<Approval> = {}): Approval {
  return {
    nonce: "n-1",
    tool_call: {
      id: "t-1",
      name: "Bash",
      kind: "execute",
      args_preview: JSON.stringify({ command: "ls -al" }),
      started_at: "2026-05-21T00:00:00Z",
    },
    destructive: false,
    requested_at: "2026-05-21T00:00:00Z",
    ...over,
  };
}

afterEach(() => {
  cleanup();
});

describe("ApprovalCard (benign)", () => {
  it("renders the tool name and Approval-needed chrome", () => {
    const onResolve = vi.fn().mockResolvedValue(undefined);
    render(<ApprovalCard approval={makeApproval()} onResolve={onResolve} />);
    expect(
      screen.getByRole("alertdialog", { name: /Approval needed: Bash/i }),
    ).toBeTruthy();
    expect(screen.getByText("Approval needed")).toBeTruthy();
    expect(screen.getByText("Bash")).toBeTruthy();
  });

  it("renders the args JSON as a key/value list", () => {
    const onResolve = vi.fn().mockResolvedValue(undefined);
    render(
      <ApprovalCard
        approval={makeApproval({
          tool_call: {
            id: "t-1",
            name: "Bash",
            kind: "execute",
            args_preview: JSON.stringify({ command: "ls", cwd: "/tmp" }),
            started_at: "2026-05-21T00:00:00Z",
          },
        })}
        onResolve={onResolve}
      />,
    );
    expect(screen.getByText("command")).toBeTruthy();
    expect(screen.getByText("ls")).toBeTruthy();
    expect(screen.getByText("cwd")).toBeTruthy();
    expect(screen.getByText("/tmp")).toBeTruthy();
  });

  it("hides bookkeeping keys whose name starts with _aoe_", () => {
    const onResolve = vi.fn().mockResolvedValue(undefined);
    render(
      <ApprovalCard
        approval={makeApproval({
          tool_call: {
            id: "t-1",
            name: "Bash",
            kind: "execute",
            args_preview: JSON.stringify({
              command: "ls",
              _aoe_parent_tool_call_id: "parent-123",
            }),
            started_at: "2026-05-21T00:00:00Z",
          },
        })}
        onResolve={onResolve}
      />,
    );
    expect(screen.queryByText("_aoe_parent_tool_call_id")).toBeNull();
    expect(screen.queryByText("parent-123")).toBeNull();
    expect(screen.getByText("command")).toBeTruthy();
  });

  it("falls back to a raw pre block when args_preview is not a JSON object", () => {
    const onResolve = vi.fn().mockResolvedValue(undefined);
    render(
      <ApprovalCard
        approval={makeApproval({
          tool_call: {
            id: "t-1",
            name: "Bash",
            kind: "execute",
            args_preview: "raw text [truncated]",
            started_at: "2026-05-21T00:00:00Z",
          },
        })}
        onResolve={onResolve}
      />,
    );
    expect(screen.getByText("raw text [truncated]")).toBeTruthy();
  });

  it("routes the Allow button to onResolve('Allow')", async () => {
    const onResolve = vi.fn().mockResolvedValue(undefined);
    render(<ApprovalCard approval={makeApproval()} onResolve={onResolve} />);
    fireEvent.click(screen.getByText("Allow"));
    expect(onResolve).toHaveBeenCalledTimes(1);
    expect(onResolve).toHaveBeenCalledWith<ApprovalDecision[]>("Allow");
  });

  it("routes Always to onResolve('AllowAlways')", () => {
    const onResolve = vi.fn().mockResolvedValue(undefined);
    render(<ApprovalCard approval={makeApproval()} onResolve={onResolve} />);
    fireEvent.click(screen.getByText("Always"));
    expect(onResolve).toHaveBeenCalledWith("AllowAlways");
  });

  it("routes Deny to onResolve('Deny')", () => {
    const onResolve = vi.fn().mockResolvedValue(undefined);
    render(<ApprovalCard approval={makeApproval()} onResolve={onResolve} />);
    fireEvent.click(screen.getByText("Deny"));
    expect(onResolve).toHaveBeenCalledWith("Deny");
  });

  it("shows the rolled-back message when onResolve rejects", async () => {
    const onResolve = vi.fn().mockRejectedValue(new Error("network"));
    render(<ApprovalCard approval={makeApproval()} onResolve={onResolve} />);
    await act(async () => {
      fireEvent.click(screen.getByText("Allow"));
    });
    expect(
      screen.getByText(/Could not reach the server/i),
    ).toBeTruthy();
  });
});

describe("ApprovalCard (destructive)", () => {
  beforeEach(() => {
    vi.useFakeTimers();
  });
  afterEach(() => {
    vi.useRealTimers();
  });

  it("renders the destructive chrome (AlertTriangle + 'Destructive action' label)", () => {
    const onResolve = vi.fn().mockResolvedValue(undefined);
    render(
      <ApprovalCard
        approval={makeApproval({ destructive: true })}
        onResolve={onResolve}
      />,
    );
    expect(screen.getByText("Destructive action")).toBeTruthy();
    expect(screen.getByText("Hold to allow")).toBeTruthy();
    expect(screen.queryByText("Always")).toBeNull();
  });

  it("does not approve on a quick click of Hold to allow", () => {
    const onResolve = vi.fn().mockResolvedValue(undefined);
    render(
      <ApprovalCard
        approval={makeApproval({ destructive: true })}
        onResolve={onResolve}
      />,
    );
    const btn = screen.getByText("Hold to allow");
    fireEvent.mouseDown(btn);
    act(() => {
      vi.advanceTimersByTime(100);
    });
    fireEvent.mouseUp(btn);
    expect(onResolve).not.toHaveBeenCalled();
  });

  it("approves after a sustained 800ms hold", () => {
    const onResolve = vi.fn().mockResolvedValue(undefined);
    render(
      <ApprovalCard
        approval={makeApproval({ destructive: true })}
        onResolve={onResolve}
      />,
    );
    const btn = screen.getByText("Hold to allow");
    fireEvent.mouseDown(btn);
    act(() => {
      vi.advanceTimersByTime(800);
    });
    expect(onResolve).toHaveBeenCalledTimes(1);
    expect(onResolve).toHaveBeenCalledWith("Allow");
  });

  it("routes Deny without requiring a hold even in destructive mode", () => {
    const onResolve = vi.fn().mockResolvedValue(undefined);
    render(
      <ApprovalCard
        approval={makeApproval({ destructive: true })}
        onResolve={onResolve}
      />,
    );
    fireEvent.click(screen.getByText("Deny"));
    expect(onResolve).toHaveBeenCalledWith("Deny");
  });
});
