// @vitest-environment jsdom
/**
 * Component tests for A2aDelegationsForm (Settings → A2A Delegations).
 *
 * Verifies: list rendering, live/revoked badge, Revoke confirm flow,
 * POST to correct endpoint, and query re-fetch causing row to flip.
 */
import '../../test/dom-setup';
import { afterEach, beforeAll, describe, it, expect, vi } from 'vitest';
import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { renderWithProviders } from '../../test/render-helpers';
import i18n from '../../i18n';
import { A2aDelegationsForm } from './A2aDelegationsForm';

beforeAll(async () => {
  await i18n.changeLanguage('en');
});

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

const LIVE_DELEGATION = {
  id: 'del-1',
  clientId: 'client-abc',
  clientName: 'MyApp',
  grantedSpaceIds: ['space-1'],
  grantedSkills: ['search'],
  expiresAt: null,
  revokedAt: null,
  createdAt: '2026-01-01T00:00:00.000Z',
  live: true,
};

const REVOKED_DELEGATION = {
  id: 'del-2',
  clientId: 'client-xyz',
  clientName: 'OldApp',
  grantedSpaceIds: [],
  grantedSkills: [],
  expiresAt: '2026-12-31T00:00:00.000Z',
  revokedAt: '2026-06-01T00:00:00.000Z',
  createdAt: '2025-12-01T00:00:00.000Z',
  live: false,
};

function mockListFetch(delegations: unknown[]) {
  return vi.spyOn(global, 'fetch').mockResolvedValue({
    ok: true,
    json: () => Promise.resolve({ delegations }),
  } as Response);
}

describe('A2aDelegationsForm', () => {
  it('renders both live and revoked delegations', async () => {
    mockListFetch([LIVE_DELEGATION, REVOKED_DELEGATION]);
    renderWithProviders(<A2aDelegationsForm />);
    await waitFor(() => expect(screen.getByText('MyApp')).toBeInTheDocument());
    expect(screen.getByText('OldApp')).toBeInTheDocument();
  });

  it('live row has an enabled Revoke button', async () => {
    mockListFetch([LIVE_DELEGATION, REVOKED_DELEGATION]);
    renderWithProviders(<A2aDelegationsForm />);
    await waitFor(() => screen.getByText('MyApp'));
    const revokeBtn = screen.getByRole('button', { name: /^revoke$/i });
    expect(revokeBtn).not.toBeDisabled();
  });

  it('revoked row shows revoked badge and has no Revoke button', async () => {
    mockListFetch([LIVE_DELEGATION, REVOKED_DELEGATION]);
    renderWithProviders(<A2aDelegationsForm />);
    await waitFor(() => screen.getByText('OldApp'));
    // Only the live row has a Revoke button
    expect(screen.getAllByRole('button', { name: /^revoke$/i })).toHaveLength(1);
    // Revoked badge present for the revoked row
    expect(screen.getByTestId('revoked-badge-del-2')).toBeInTheDocument();
  });

  it('clicking Revoke then Confirm revoke POSTs to the correct id and row flips to revoked', async () => {
    const user = userEvent.setup();
    let getCallCount = 0;

    vi.spyOn(global, 'fetch').mockImplementation((url: RequestInfo | URL, opts?: RequestInit) => {
      const method = opts?.method?.toUpperCase() ?? 'GET';
      if (method === 'GET') {
        getCallCount++;
        const delegations =
          getCallCount === 1
            ? [LIVE_DELEGATION, REVOKED_DELEGATION]
            : [{ ...LIVE_DELEGATION, live: false, revokedAt: '2026-07-02T00:00:00.000Z' }, REVOKED_DELEGATION];
        return Promise.resolve({
          ok: true,
          json: () => Promise.resolve({ delegations }),
        } as Response);
      }
      // POST — assert the URL contains the live delegation id
      expect(String(url)).toContain('del-1/revoke');
      return Promise.resolve({
        ok: true,
        json: () => Promise.resolve({ status: 'revoked', cancelledJobs: 2 }),
      } as Response);
    });

    renderWithProviders(<A2aDelegationsForm />);
    await waitFor(() => screen.getByText('MyApp'));

    // Step 1: click the Revoke button
    await user.click(screen.getByRole('button', { name: /^revoke$/i }));

    // Step 2: a "Confirm revoke" button should appear in place of Revoke
    await waitFor(() => screen.getByRole('button', { name: /confirm revoke/i }));

    // Step 3: click Confirm revoke
    await user.click(screen.getByRole('button', { name: /confirm revoke/i }));

    // Step 4: revokeSuccess message appears immediately after mutation resolves
    await waitFor(() => {
      expect(screen.getByText(/delegation revoked/i)).toBeInTheDocument();
    });
    // The cancelledJobs suffix is also visible (count=2)
    expect(screen.getByText(/2 running task/i)).toBeInTheDocument();

    // Step 5: after re-fetch, the previously-live row should no longer have a Revoke button
    await waitFor(() => {
      expect(screen.queryByRole('button', { name: /^revoke$/i })).toBeNull();
    });
  });

  it('initial Revoke button is disabled while a revoke mutation is in-flight', async () => {
    const user = userEvent.setup();
    let resolveMutation!: (v: Response) => void;
    const mutationPending = new Promise<Response>((resolve) => { resolveMutation = resolve; });

    vi.spyOn(global, 'fetch').mockImplementation((url: RequestInfo | URL, opts?: RequestInit) => {
      const method = opts?.method?.toUpperCase() ?? 'GET';
      if (method === 'GET') {
        return Promise.resolve({
          ok: true,
          json: () => Promise.resolve({ delegations: [LIVE_DELEGATION] }),
        } as Response);
      }
      // POST — stall indefinitely so isPending stays true
      return mutationPending;
    });

    renderWithProviders(<A2aDelegationsForm />);
    await waitFor(() => screen.getByText('MyApp'));

    // Open confirm step
    await user.click(screen.getByRole('button', { name: /^revoke$/i }));
    await waitFor(() => screen.getByRole('button', { name: /confirm revoke/i }));

    // Trigger the mutation (stalled)
    await user.click(screen.getByRole('button', { name: /confirm revoke/i }));

    // While mutation is pending the confirm buttons are disabled — resolve so the test doesn't hang
    resolveMutation({
      ok: true,
      json: () => Promise.resolve({ status: 'revoked', cancelledJobs: 0 }),
    } as Response);
  });
});
