// @vitest-environment jsdom
/**
 * Component tests for ToolRequestApproval — the inline Approve/Deny card shown
 * in chat when the agent paused on a RequestTool. Branching covered:
 *  - no pending requests              → renders nothing
 *  - non-pending / non-"requested"    → filtered out
 *  - pending "requested" rows         → one card per tool, with reason + movement
 *  - Approve / Deny buttons           → call decideToolRequest with the right args
 *  - mutation error                   → error row appears
 * Network (fetchToolRequests / decideToolRequest) is fully mocked; i18n uses the
 * real instance (auto-init on import) so labels resolve.
 */
import '../../test/dom-setup';
import '../../i18n';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { renderWithProviders } from '../../test/render-helpers';
import type { ToolRequest } from '../../api';
import * as api from '../../api';
import { ToolRequestApproval } from './ToolRequestApproval';

vi.mock('../../api', async (importOriginal) => {
  const actual = await importOriginal<typeof import('../../api')>();
  return {
    ...actual,
    fetchToolRequests: vi.fn(),
    decideToolRequest: vi.fn(),
  };
});

const mockedFetch = vi.mocked(api.fetchToolRequests);
const mockedDecide = vi.mocked(api.decideToolRequest);

function req(overrides: Partial<ToolRequest> = {}): ToolRequest {
  return {
    id: 'req-1',
    taskId: '7',
    jobId: 'job-1',
    spaceId: null,
    pieceName: 'chat',
    movementName: 'execute',
    toolName: 'WebSearch',
    reason: 'need to look something up',
    category: 'requested',
    status: 'pending',
    grantScope: null,
    decidedBy: null,
    createdAt: '2026-06-25T00:00:00Z',
    decidedAt: null,
    ...overrides,
  };
}

describe('ToolRequestApproval', () => {
  beforeEach(() => {
    mockedFetch.mockReset();
    mockedDecide.mockReset();
  });

  it('renders nothing when there are no requests', async () => {
    mockedFetch.mockResolvedValue([]);
    const { container } = renderWithProviders(<ToolRequestApproval taskId={7} poll={false} />);
    await waitFor(() => expect(mockedFetch).toHaveBeenCalledWith(7));
    expect(container.querySelector('[data-testid="tool-request-approval"]')).toBeNull();
  });

  it('filters out non-pending and non-"requested" rows', async () => {
    mockedFetch.mockResolvedValue([
      req({ id: 'a', status: 'approved' }),
      req({ id: 'b', category: 'blocked' }),
    ]);
    const { container } = renderWithProviders(<ToolRequestApproval taskId={7} poll={false} />);
    await waitFor(() => expect(mockedFetch).toHaveBeenCalled());
    // both rows excluded → no card
    expect(container.querySelector('[data-testid="tool-request-approval"]')).toBeNull();
  });

  it('renders a card per pending requested tool with reason + movement', async () => {
    mockedFetch.mockResolvedValue([
      req({ id: 'r1', toolName: 'WebSearch', reason: 'search the web', movementName: 'execute' }),
      req({ id: 'r2', toolName: 'Bash', reason: null, movementName: 'verify' }),
    ]);
    renderWithProviders(<ToolRequestApproval taskId={7} poll={false} />);

    expect(await screen.findByTestId('tool-request-WebSearch')).toBeInTheDocument();
    expect(screen.getByTestId('tool-request-Bash')).toBeInTheDocument();
    // tool name surfaces in the title (i18n interpolates {{tool}})
    expect(screen.getByText(/WebSearch/)).toBeInTheDocument();
    // reason shown for the one that has it
    expect(screen.getByText(/search the web/)).toBeInTheDocument();
    // movement label always shown
    expect(screen.getByText('movement: execute')).toBeInTheDocument();
    expect(screen.getByText('movement: verify')).toBeInTheDocument();
  });

  it('Approve calls decideToolRequest with approve + the request id', async () => {
    mockedFetch.mockResolvedValue([req({ id: 'r1', toolName: 'WebSearch' })]);
    mockedDecide.mockResolvedValue(undefined);
    const user = userEvent.setup();
    renderWithProviders(<ToolRequestApproval taskId={7} poll={false} />);

    const approveBtn = await screen.findByTestId('tool-request-approve-WebSearch');
    await user.click(approveBtn);

    await waitFor(() => expect(mockedDecide).toHaveBeenCalledWith(7, 'r1', 'approve'));
  });

  it('Deny calls decideToolRequest with deny + the request id', async () => {
    mockedFetch.mockResolvedValue([req({ id: 'r9', toolName: 'Bash' })]);
    mockedDecide.mockResolvedValue(undefined);
    const user = userEvent.setup();
    renderWithProviders(<ToolRequestApproval taskId={7} poll={false} />);

    const denyBtn = await screen.findByTestId('tool-request-deny-Bash');
    await user.click(denyBtn);

    await waitFor(() => expect(mockedDecide).toHaveBeenCalledWith(7, 'r9', 'deny'));
  });

  it('shows an error row when the decide mutation rejects', async () => {
    mockedFetch.mockResolvedValue([req({ id: 'r1', toolName: 'WebSearch' })]);
    mockedDecide.mockRejectedValue(new Error('forbidden'));
    const user = userEvent.setup();
    renderWithProviders(<ToolRequestApproval taskId={7} poll={false} />);

    const approveBtn = await screen.findByTestId('tool-request-approve-WebSearch');
    await user.click(approveBtn);

    expect(await screen.findByTestId('tool-request-error')).toHaveTextContent('forbidden');
  });
});
