import React from 'react';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import {
    CrmConvertLeadModal,
    type CrmConvertLeadAction,
    type CrmConvertLeadApi,
    type CrmConvertLeadLookupProps,
    type CrmConvertLeadHost,
    type CrmConvertLeadObjectMetadata,
    type CrmConvertLeadRecord,
} from '../src/convert-lead-modal';

type TestConvertLeadAction = CrmConvertLeadAction & Record<string, unknown>;

interface TestConvertLeadModalProps {
    isOpen: boolean;
    onClose: () => void;
    action: TestConvertLeadAction;
    record: CrmConvertLeadRecord;
    objectMetadata: CrmConvertLeadObjectMetadata;
    host: CrmConvertLeadHost;
}

const dataAPIMock = vi.hoisted(() => ({
    executeStandardAction: vi.fn(),
    query: vi.fn(),
    previewStandardAction: vi.fn(),
}));

const MockSearchableLookup: React.FC<CrmConvertLeadLookupProps> = ({
    placeholder,
    value,
    onChange,
    disabled,
}) => {
    const key = (placeholder || 'lookup').toLowerCase().replace(/\s+/g, '-');
    // Account lookup cycles: first click -> 001ACCOUNT, second click -> 002ACCOUNT_B
    const accountClickRef = React.useRef(0);
    const accountSelections = [
        { id: '001ACCOUNT', name: 'Account A' },
        { id: '002ACCOUNT_B', name: 'Account B' },
    ];
    const nextSelection = placeholder === 'Search accounts'
        ? accountSelections[accountClickRef.current % accountSelections.length]
        : { id: '003MANUAL', name: 'Manual Contact' };

    return (
        <div data-testid={`lookup-${key}`}>
            <div data-testid={`lookup-${key}-value`}>{value || ''}</div>
            <button
                type="button"
                disabled={disabled}
                onClick={() => {
                    const sel = placeholder === 'Search accounts'
                        ? accountSelections[accountClickRef.current % accountSelections.length]
                        : nextSelection;
                    if (placeholder === 'Search accounts') {
                        accountClickRef.current += 1;
                    }
                    onChange(sel.id, { Id: sel.id, Name: sel.name });
                }}
            >
                {`mock-select-${key}`}
            </button>
        </div>
    );
};

const convertLeadApi: CrmConvertLeadApi = {
    query: request => dataAPIMock.query(request),
    previewStandardAction: (actionPreviewEndpoint, input) => (
        dataAPIMock.previewStandardAction(actionPreviewEndpoint, input)
    ),
    executeStandardAction: (actionEndpoint, inputs) => (
        dataAPIMock.executeStandardAction(actionEndpoint, inputs)
    ),
    getSchema: vi.fn().mockResolvedValue({ schema: null }),
    warn: vi.fn(),
};
const getSchemaMock = vi.mocked(convertLeadApi.getSchema);

const ConvertLeadModal: React.FC<TestConvertLeadModalProps> = ({
    isOpen,
    onClose,
    record,
    objectMetadata,
    action,
    host,
}) => (
    <CrmConvertLeadModal
        isOpen={isOpen}
        onClose={onClose}
        record={record}
        objectMetadata={objectMetadata}
        action={action}
        host={host}
        api={convertLeadApi}
        lookupComponent={MockSearchableLookup}
    />
);

const baseAction: TestConvertLeadAction = {
    label: 'Convert Lead',
    name: 'convertLead',
    type: 'standard',
    endpoint: '/services/data/v63.0/actions/standard/convertLead',
    rendererId: 'app-pack:crm/action-renderer/lead-convert',
    targetObjects: ['Lead'],
    inputs: [
        {
            name: 'accountId',
            label: 'Account',
            type: 'reference',
            required: true,
            referenceTo: 'Account',
        },
        {
            name: 'contactId',
            label: 'Contact',
            type: 'reference',
            referenceTo: 'Contact',
        },
        {
            name: 'contactFirstName',
            label: 'First Name',
            type: 'string',
            uiMode: 'create',
            uiSection: 'Contact',
            targetObject: 'Contact',
            targetFieldAPIName: 'FirstName',
            prefillSourceFieldAPIName: 'FirstName',
        },
        {
            name: 'contactLastName',
            label: 'Last Name',
            type: 'string',
            required: true,
            uiMode: 'create',
            uiSection: 'Contact',
            targetObject: 'Contact',
            targetFieldAPIName: 'LastName',
            prefillSourceFieldAPIName: 'LastName',
        },
    ],
};

const onClose = vi.fn();
const refreshRecord = vi.fn();

const baseProps: TestConvertLeadModalProps = {
    isOpen: true,
    onClose,
    action: baseAction,
    record: {
        Id: '00Q000000000001',
        FirstName: 'Ada',
        LastName: 'Lovelace',
        Email: 'match@example.invalid',
    },
    objectMetadata: {
        name: 'Lead',
        fields: [],
    },
    host: {
        navigateToRecord: vi.fn(),
        navigateToNewRecord: vi.fn(),
        refreshRecord,
    },
};

// Inputs that give accountId a server-side default so convertDisabled starts
// as false and preview fires on mount without requiring an explicit account pick.
const previewReadyInputs = [
    {
        name: 'accountId',
        label: 'Account',
        type: 'reference' as const,
        required: true,
        referenceTo: 'Account',
        defaulted: true,
        defaultValue: '001DEFAULT',
    },
    ...(baseAction.inputs ?? []).filter(i => i.name !== 'accountId'),
];

function renderModal(action: Partial<TestConvertLeadAction> = {}) {
    const mergedAction: TestConvertLeadAction = {
        ...baseAction,
        ...action,
        inputs: action.inputs ?? baseAction.inputs,
        outputs: action.outputs ?? baseAction.outputs,
        inputGuards: action.inputGuards ?? baseAction.inputGuards,
        config: action.config ?? baseAction.config,
    };

    return render(
        <ConvertLeadModal
            {...baseProps}
            action={mergedAction}
        />
    );
}

beforeEach(() => {
    // Default: getSchema resolves with a minimal schema so the metadata fetch
    // effect does not produce unhandled rejections in tests that don't need
    // specific schema data.
    vi.mocked(convertLeadApi.getSchema).mockResolvedValue({ schema: null });
});

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

describe('ConvertLeadModal', () => {
    it('renders the campaign attribution preview section from action config', () => {
        renderModal({
            config: {
                campaignAttributionPreview: {
                    title: 'Campaign Attribution',
                    summary: 'Conversion will preserve campaign influence from the source Lead.',
                    rows: [
                        { label: 'Primary Campaign Source', value: 'Spring Launch 2026' },
                        { label: 'Campaign Member Status', value: 'Responded' },
                    ],
                    warnings: [
                        'Existing Contact campaign history will remain unchanged.',
                    ],
                },
            },
        });

        expect(screen.getByText('Campaign Attribution')).toBeInTheDocument();
        expect(screen.getByText('Conversion will preserve campaign influence from the source Lead.')).toBeInTheDocument();
        expect(screen.getByText('Primary Campaign Source')).toBeInTheDocument();
        expect(screen.getByText('Spring Launch 2026')).toBeInTheDocument();
        expect(screen.getByText('Campaign Member Status')).toBeInTheDocument();
        expect(screen.getByText('Responded')).toBeInTheDocument();
        expect(screen.getByText('Existing Contact campaign history will remain unchanged.')).toBeInTheDocument();
    });

    it('does not render the campaign attribution section when the payload is absent', () => {
        renderModal();

        expect(screen.queryByText('Campaign Attribution')).not.toBeInTheDocument();
    });

    it('ignores incomplete leadConvertMatch payloads and stays in create mode', async () => {
        renderModal({
            config: {
                leadConvertMatch: {
                    contactByAccountAndEmail: {
                        enabled: true,
                        accountInput: 'accountId',
                        emailField: 'Email',
                    },
                },
            },
        });

        fireEvent.click(screen.getByRole('button', { name: 'mock-select-search-accounts' }));

        await waitFor(() => {
            expect(screen.getByRole('button', { name: 'Create New' })).toHaveAttribute('aria-pressed', 'true');
        });
        expect(screen.getByRole('button', { name: 'Choose Existing' })).toHaveAttribute('aria-pressed', 'false');
        expect(dataAPIMock.query).not.toHaveBeenCalled();
        expect(screen.queryByTestId('lookup-search-contacts-value')).not.toBeInTheDocument();
    });

    it('switches to existing contact mode and preselects the matched contact when account selection matches the payload', async () => {
        dataAPIMock.query.mockResolvedValue([
            { Id: '003MATCH', Name: 'Ada Lovelace' },
        ]);

        renderModal({
            config: {
                leadConvertMatch: {
                    contactByAccountAndEmail: {
                        enabled: true,
                        accountInput: 'accountId',
                        emailField: 'Email',
                        targetInput: 'contactId',
                        switchModeTo: 'reuse',
                    },
                },
            },
        });

        fireEvent.click(screen.getByRole('button', { name: 'mock-select-search-accounts' }));

        await waitFor(() => {
            expect(dataAPIMock.query).toHaveBeenCalledWith({
                objectApiName: 'Contact',
                selectFields: ['Id', 'Name'],
                filterExpr: "AccountId = '001ACCOUNT' AND Email = 'match@example.invalid'",
                limit: 2,
            });
        });
        await waitFor(() => {
            expect(screen.getByRole('button', { name: 'Choose Existing' })).toHaveAttribute('aria-pressed', 'true');
        });
        expect(screen.getByRole('button', { name: 'Create New' })).toHaveAttribute('aria-pressed', 'false');
        expect(screen.getByTestId('lookup-search-contacts-value')).toHaveTextContent('003MATCH');
        expect(screen.queryByLabelText('First Name')).not.toBeInTheDocument();
    });

    it('submits the matched contactId through the reuse path after the auto-switch', async () => {
        dataAPIMock.query.mockResolvedValue([
            { Id: '003MATCH', Name: 'Ada Lovelace' },
        ]);
        dataAPIMock.executeStandardAction.mockResolvedValue({});

        renderModal({
            config: {
                leadConvertMatch: {
                    contactByAccountAndEmail: {
                        enabled: true,
                        accountInput: 'accountId',
                        emailField: 'Email',
                        targetInput: 'contactId',
                        switchModeTo: 'reuse',
                    },
                },
            },
        });

        fireEvent.click(screen.getByRole('button', { name: 'mock-select-search-accounts' }));

        await waitFor(() => {
            expect(screen.getByRole('button', { name: /convert/i })).toBeEnabled();
        });

        fireEvent.click(screen.getByRole('button', { name: /convert/i }));

        await waitFor(() => {
            expect(dataAPIMock.executeStandardAction).toHaveBeenCalledWith(
                '/services/data/v63.0/actions/standard/convertLead',
                [{
                    leadId: '00Q000000000001',
                    accountId: '001ACCOUNT',
                    contactId: '003MATCH',
                }],
            );
        });
        expect(refreshRecord).toHaveBeenCalled();
        expect(onClose).toHaveBeenCalled();
    });

    it('keeps a user-selected existing contact explicit after the auto-switch', async () => {
        dataAPIMock.query.mockResolvedValue([
            { Id: '003MATCH', Name: 'Ada Lovelace' },
        ]);

        renderModal({
            config: {
                leadConvertMatch: {
                    contactByAccountAndEmail: {
                        enabled: true,
                        accountInput: 'accountId',
                        emailField: 'Email',
                        targetInput: 'contactId',
                        switchModeTo: 'reuse',
                    },
                },
            },
        });

        fireEvent.click(screen.getByRole('button', { name: 'mock-select-search-accounts' }));

        await waitFor(() => {
            expect(screen.getByTestId('lookup-search-contacts-value')).toHaveTextContent('003MATCH');
        });

        fireEvent.click(screen.getByRole('button', { name: 'mock-select-search-contacts' }));

        await waitFor(() => {
            expect(screen.getByTestId('lookup-search-contacts-value')).toHaveTextContent('003MANUAL');
        });
        expect(dataAPIMock.query).toHaveBeenCalledTimes(1);
    });

    it('keeps create mode when the user opts out before the contact auto-match resolves', async () => {
        let resolveMatch: ((value: Array<{ Id: string; Name: string }>) => void) | undefined;
        dataAPIMock.query.mockImplementationOnce(() => new Promise<Array<{ Id: string; Name: string }>>((resolve) => {
            resolveMatch = resolve;
        }));

        renderModal({
            config: {
                leadConvertMatch: {
                    contactByAccountAndEmail: {
                        enabled: true,
                        accountInput: 'accountId',
                        emailField: 'Email',
                        targetInput: 'contactId',
                        switchModeTo: 'reuse',
                    },
                },
            },
        });

        fireEvent.click(screen.getByRole('button', { name: 'mock-select-search-accounts' }));

        await waitFor(() => {
            expect(dataAPIMock.query).toHaveBeenCalledTimes(1);
        });

        fireEvent.click(screen.getByRole('button', { name: 'Create New' }));

        expect(screen.getByRole('button', { name: 'Create New' })).toHaveAttribute('aria-pressed', 'true');
        expect(screen.getByRole('button', { name: 'Choose Existing' })).toHaveAttribute('aria-pressed', 'false');

        expect(resolveMatch).toBeDefined();
        resolveMatch!([{ Id: '003MATCH', Name: 'Ada Lovelace' }]);

        await waitFor(() => {
            expect(screen.getByRole('button', { name: 'Create New' })).toHaveAttribute('aria-pressed', 'true');
        });
        expect(screen.getByRole('button', { name: 'Choose Existing' })).toHaveAttribute('aria-pressed', 'false');
        expect(screen.queryByTestId('lookup-search-contacts-value')).not.toBeInTheDocument();
    });

    it('clears stale auto-matched contact synchronously when account changes before new query resolves', async () => {
        // First query resolves immediately with a match for account A
        dataAPIMock.query.mockImplementationOnce(() =>
            Promise.resolve([{ Id: '003C1', Name: 'Contact One' }]),
        );
        // Second query for account B never resolves during this test
        let resolveSecond: ((value: Array<{ Id: string; Name: string }>) => void) | undefined;
        dataAPIMock.query.mockImplementationOnce(() => new Promise<Array<{ Id: string; Name: string }>>((resolve) => {
            resolveSecond = resolve;
        }));

        renderModal({
            config: {
                leadConvertMatch: {
                    contactByAccountAndEmail: {
                        enabled: true,
                        accountInput: 'accountId',
                        emailField: 'Email',
                        targetInput: 'contactId',
                        switchModeTo: 'reuse',
                    },
                },
            },
        });

        // Select first account — auto-match fires and resolves to 003C1
        fireEvent.click(screen.getByRole('button', { name: 'mock-select-search-accounts' }));

        await waitFor(() => {
            expect(screen.getByRole('button', { name: 'Choose Existing' })).toHaveAttribute('aria-pressed', 'true');
        });
        expect(screen.getByTestId('lookup-search-contacts-value')).toHaveTextContent('003C1');

        // Change to second account — second query is in-flight but not yet resolved
        fireEvent.click(screen.getByRole('button', { name: 'mock-select-search-accounts' }));

        // Synchronous cleanup: mode must revert to create (stale contactId cleared)
        // The contact lookup is only rendered in reuse mode — its absence confirms
        // the mode has switched back to create before the new query resolves.
        await waitFor(() => {
            expect(screen.getByRole('button', { name: 'Create New' })).toHaveAttribute('aria-pressed', 'true');
        });
        expect(screen.getByRole('button', { name: 'Choose Existing' })).toHaveAttribute('aria-pressed', 'false');
        expect(screen.queryByTestId('lookup-search-contacts-value')).not.toBeInTheDocument();

        // Second query still pending — cleanup can be called
        expect(resolveSecond).toBeDefined();
        resolveSecond!([]);
    });

    it('stays in create mode and does not auto-select when multiple contacts match account and email', async () => {
        dataAPIMock.query.mockResolvedValue([
            { Id: '003C1', Name: 'Contact One' },
            { Id: '003C2', Name: 'Contact Two' },
        ]);

        renderModal({
            config: {
                leadConvertMatch: {
                    contactByAccountAndEmail: {
                        enabled: true,
                        accountInput: 'accountId',
                        emailField: 'Email',
                        targetInput: 'contactId',
                        switchModeTo: 'reuse',
                    },
                },
            },
        });

        fireEvent.click(screen.getByRole('button', { name: 'mock-select-search-accounts' }));

        await waitFor(() => {
            expect(dataAPIMock.query).toHaveBeenCalledTimes(1);
        });

        // Ambiguous match — modal must stay in create mode
        await waitFor(() => {
            expect(screen.getByRole('button', { name: 'Create New' })).toHaveAttribute('aria-pressed', 'true');
        });
        expect(screen.getByRole('button', { name: 'Choose Existing' })).toHaveAttribute('aria-pressed', 'false');
        expect(screen.queryByTestId('lookup-search-contacts-value')).not.toBeInTheDocument();
    });

    it('disables Convert while a contact match query is in flight and re-enables after resolve', async () => {
        let resolveMatch: ((value: Array<{ Id: string; Name: string }>) => void) | undefined;
        dataAPIMock.query.mockImplementationOnce(() => new Promise<Array<{ Id: string; Name: string }>>((resolve) => {
            resolveMatch = resolve;
        }));

        renderModal({
            config: {
                leadConvertMatch: {
                    contactByAccountAndEmail: {
                        enabled: true,
                        accountInput: 'accountId',
                        emailField: 'Email',
                        targetInput: 'contactId',
                        switchModeTo: 'reuse',
                    },
                },
            },
        });

        fireEvent.click(screen.getByRole('button', { name: 'mock-select-search-accounts' }));

        await waitFor(() => {
            expect(dataAPIMock.query).toHaveBeenCalledTimes(1);
        });

        // While in-flight, Convert must be disabled
        expect(screen.getByRole('button', { name: /convert/i })).toBeDisabled();

        // Resolve the query with no match
        expect(resolveMatch).toBeDefined();
        resolveMatch!([]);

        // After the query resolves, Convert becomes enabled again
        await waitFor(() => {
            expect(screen.getByRole('button', { name: /convert/i })).toBeEnabled();
        });
    });

    // ─── Flow create preview tests ────────────────────────────────────────────

    describe('Flow create preview (previewEndpoint)', () => {
        const previewEndpoint = '/services/data/v63.0/actions/standard/convertLead/preview';
        const leadId = '00Q000000000001';

        function renderModalWithPreview(previewMockImpl?: () => Promise<unknown>) {
            if (previewMockImpl) {
                dataAPIMock.previewStandardAction.mockImplementation(previewMockImpl);
            }
            // previewReadyInputs gives accountId a server-side default so
            // convertDisabled starts as false and preview fires on mount.
            return renderModal({ previewEndpoint, inputs: previewReadyInputs });
        }

        it('calls previewStandardAction once on mount with at least the lead id', async () => {
            dataAPIMock.previewStandardAction.mockResolvedValue({
                actionName: 'convertLead',
                isSuccess: true,
                parentPreview: { object: 'Account', fields: { Name: 'Acme Corp' } },
                childPreview: [],
            });

            renderModalWithPreview();

            // Preview now mirrors the full convert payload; leadId is always included.
            await waitFor(() => {
                expect(dataAPIMock.previewStandardAction).toHaveBeenCalledTimes(1);
                expect(dataAPIMock.previewStandardAction).toHaveBeenCalledWith(
                    previewEndpoint,
                    expect.objectContaining({ leadId }),
                );
            });
        });

        it('renders the parentPreview object and fields when preview succeeds', async () => {
            dataAPIMock.previewStandardAction.mockResolvedValue({
                actionName: 'convertLead',
                isSuccess: true,
                parentPreview: {
                    object: 'Account',
                    fields: { Name: 'Acme Corp', Industry: 'Technology' },
                },
                childPreview: [
                    { object: 'Contact', count: 1 },
                    { object: 'Opportunity', count: 1 },
                ],
            });

            renderModalWithPreview();

            await waitFor(() => {
                expect(screen.getByText('Preview: Account')).toBeInTheDocument();
            });
            expect(screen.getByText('Acme Corp')).toBeInTheDocument();
            expect(screen.getByText('Technology')).toBeInTheDocument();
            expect(screen.getByText('Contact: 1 record')).toBeInTheDocument();
            expect(screen.getByText('Opportunity: 1 record')).toBeInTheDocument();
        });

        it('renders plural label when child count is not 1', async () => {
            dataAPIMock.previewStandardAction.mockResolvedValue({
                actionName: 'convertLead',
                isSuccess: true,
                parentPreview: { object: 'Account', fields: {} },
                childPreview: [{ object: 'Contact', count: 3 }],
            });

            renderModalWithPreview();

            await waitFor(() => {
                expect(screen.getByText('Preview: Account')).toBeInTheDocument();
            });
            expect(screen.getByText('Contact: 3 records')).toBeInTheDocument();
        });

        it('does not render preview block when backend returns no parentPreview', async () => {
            dataAPIMock.previewStandardAction.mockResolvedValue({
                actionName: 'convertLead',
                isSuccess: true,
                parentPreview: null,
                childPreview: [],
            });

            renderModalWithPreview();

            // Give the async effect time to settle
            await waitFor(() => {
                expect(dataAPIMock.previewStandardAction).toHaveBeenCalledTimes(1);
            });
            expect(screen.queryByLabelText('Flow create preview')).not.toBeInTheDocument();
        });

        it('shows fixed preview error copy when backend returns 4xx/5xx without leaking raw message', async () => {
            dataAPIMock.previewStandardAction.mockRejectedValue(new Error('Service unavailable'));

            renderModalWithPreview();

            await waitFor(() => {
                expect(dataAPIMock.previewStandardAction).toHaveBeenCalledTimes(1);
            });
            // No preview section visible
            expect(screen.queryByLabelText('Flow create preview')).not.toBeInTheDocument();
            // Fixed copy is shown — raw error message must NOT appear
            await waitFor(() => {
                expect(screen.getByText(/Preview unavailable\. Try again later\./i)).toBeInTheDocument();
            });
            expect(screen.queryByText(/service unavailable/i)).not.toBeInTheDocument();
        });

        it('does not call previewStandardAction when action has no previewEndpoint', async () => {
            renderModal(); // baseAction has no previewEndpoint

            // Allow any pending effects to flush
            await waitFor(() => {
                expect(dataAPIMock.previewStandardAction).not.toHaveBeenCalled();
            });
        });

        it('does not re-fetch preview when modal re-renders without identity change', async () => {
            dataAPIMock.previewStandardAction.mockResolvedValue({
                actionName: 'convertLead',
                isSuccess: true,
                parentPreview: { object: 'Account', fields: { Name: 'Stable Corp' } },
                childPreview: [],
            });

            const { rerender } = renderModalWithPreview();

            await waitFor(() => {
                expect(screen.getByText('Preview: Account')).toBeInTheDocument();
            });

            // Re-render with same props — should NOT trigger another call
            rerender(
                <ConvertLeadModal
                    {...baseProps}
                    action={{ ...baseAction, previewEndpoint }}
                />,
            );

            expect(dataAPIMock.previewStandardAction).toHaveBeenCalledTimes(1);
        });

        it('includes full convert inputs in preview payload (not just leadId)', async () => {
            // Prepare action with opportunityId and ownerId inputs so they appear
            // in the payload when set.
            const actionWithOwner = {
                ...baseAction,
                previewEndpoint,
                inputs: [
                    ...(baseAction.inputs ?? []),
                    {
                        name: 'doNotCreateOpportunity',
                        label: 'Do Not Create Opportunity',
                        type: 'boolean' as const,
                        defaultValue: false,
                    },
                    {
                        name: 'ownerId',
                        label: 'Owner',
                        type: 'reference' as const,
                        referenceTo: 'User',
                    },
                ],
            };

            dataAPIMock.previewStandardAction.mockResolvedValue({
                actionName: 'convertLead',
                isSuccess: true,
                parentPreview: { object: 'Account', fields: { Name: 'Acme' } },
                childPreview: [],
            });

            render(
                <ConvertLeadModal
                    {...baseProps}
                    action={actionWithOwner}
                />,
            );

            // Select an account to populate accountId
            fireEvent.click(screen.getByRole('button', { name: 'mock-select-search-accounts' }));

            await waitFor(() => {
                expect(dataAPIMock.previewStandardAction).toHaveBeenCalledWith(
                    previewEndpoint,
                    expect.objectContaining({ leadId, accountId: '001ACCOUNT' }),
                );
            });
        });

        it('clears stale preview immediately when a new preview request starts', async () => {
            // First preview resolves successfully; second is permanently pending so we
            // can observe the intermediate "cleared" state.
            dataAPIMock.previewStandardAction
                .mockResolvedValueOnce({
                    actionName: 'convertLead',
                    isSuccess: true,
                    parentPreview: { object: 'Account', fields: { Name: 'Old Corp' } },
                    childPreview: [],
                })
                .mockImplementation(() => new Promise(() => { /* intentionally never resolves */ }));

            renderModalWithPreview();

            // Wait for the first successful preview to render.
            await waitFor(() => {
                expect(screen.getByText('Preview: Account')).toBeInTheDocument();
            });
            expect(screen.getByText('Old Corp')).toBeInTheDocument();

            // Trigger a new preview by changing the account selection.
            fireEvent.click(screen.getByRole('button', { name: 'mock-select-search-accounts' }));

            // The new effect run clears serverPreviewResult synchronously before
            // the debounce fires, so the stale preview must disappear.
            await waitFor(() => {
                expect(screen.queryByLabelText('Flow create preview')).not.toBeInTheDocument();
            });

            // Second call is eventually made (after the 300 ms debounce).
            await waitFor(() => {
                expect(dataAPIMock.previewStandardAction).toHaveBeenCalledTimes(2);
            });
        });

        it('clears stale preview when a subsequent preview request fails', async () => {
            // First preview resolves with results, second rejects.
            dataAPIMock.previewStandardAction
                .mockResolvedValueOnce({
                    actionName: 'convertLead',
                    isSuccess: true,
                    parentPreview: { object: 'Account', fields: { Name: 'Success Corp' } },
                    childPreview: [],
                })
                .mockRejectedValueOnce(new Error('Service unavailable'));

            renderModalWithPreview();

            // Wait for the first successful preview.
            await waitFor(() => {
                expect(screen.getByText('Preview: Account')).toBeInTheDocument();
            });
            expect(screen.getByText('Success Corp')).toBeInTheDocument();

            // Change an input to trigger a new preview fetch that will fail.
            fireEvent.click(screen.getByRole('button', { name: 'mock-select-search-accounts' }));

            // After the rejection the preview block must not be rendered,
            // but the fixed error copy must appear.
            await waitFor(() => {
                expect(dataAPIMock.previewStandardAction).toHaveBeenCalledTimes(2);
            });
            expect(screen.queryByLabelText('Flow create preview')).not.toBeInTheDocument();
            await waitFor(() => {
                expect(screen.getByText(/Preview unavailable\. Try again later\./i)).toBeInTheDocument();
            });
        });

        it('resolves preview in React StrictMode double-mount without suppression', async () => {
            dataAPIMock.previewStandardAction.mockResolvedValue({
                actionName: 'convertLead',
                isSuccess: true,
                parentPreview: { object: 'Account', fields: { Name: 'StrictMode Corp' } },
                childPreview: [],
            });

            render(
                <React.StrictMode>
                    <ConvertLeadModal
                        {...baseProps}
                        action={{ ...baseAction, previewEndpoint, inputs: previewReadyInputs }}
                    />
                </React.StrictMode>,
            );

            // In StrictMode the effect runs twice; with AbortController the second
            // mount fires a fresh request and the result must eventually render.
            await waitFor(() => {
                expect(screen.getByText('Preview: Account')).toBeInTheDocument();
            });
            expect(screen.getByText('StrictMode Corp')).toBeInTheDocument();
        });
    });

    // ─── Flow preview UX follow-ups (issue #1058) ────────────────────────────

    describe('Flow preview UX follow-ups (issue #1058)', () => {
        const previewEndpoint = '/services/data/v63.0/actions/standard/convertLead/preview';

        beforeEach(() => {
            dataAPIMock.previewStandardAction.mockReset();
            getSchemaMock.mockReset();
            // Default no-op so the metadata-fetch effect never throws unhandled rejections.
            getSchemaMock.mockResolvedValue({ schema: null } as never);
        });

        it('renders preview error state when previewStandardAction rejects', async () => {
            dataAPIMock.previewStandardAction.mockRejectedValue(new Error('Server down'));

            render(
                <ConvertLeadModal
                    {...baseProps}
                    action={{ ...baseAction, previewEndpoint, inputs: previewReadyInputs }}
                />,
            );

            await waitFor(() => {
                expect(screen.getByText(/Preview unavailable\. Try again later\./i)).toBeInTheDocument();
            });
            // Raw error message must never reach the DOM (regression guard).
            expect(screen.queryByText(/Server down/i)).not.toBeInTheDocument();
        });

        it('renders parent fields with metadata labels from getSchema', async () => {
            dataAPIMock.previewStandardAction.mockResolvedValue({
                actionName: 'convertLead',
                isSuccess: true,
                parentPreview: {
                    object: 'Account',
                    fields: { Name: 'Acme', Industry: 'Tech' },
                },
                childPreview: [],
            });
            getSchemaMock.mockImplementation((apiName: string) => {
                if (apiName === 'Account') {
                    return Promise.resolve({
                        schema: {
                            name: 'Account',
                            label: 'Account',
                            fields: [
                                { name: 'Name', label: 'Account Name' },
                                { name: 'Industry', label: 'Industry Type' },
                            ],
                        },
                    } as never);
                }
                return Promise.resolve({ schema: null } as never);
            });

            render(
                <ConvertLeadModal
                    {...baseProps}
                    action={{ ...baseAction, previewEndpoint, inputs: previewReadyInputs }}
                />,
            );

            // Field labels (from schema) must appear.
            await waitFor(() => {
                expect(screen.getByText('Account Name')).toBeInTheDocument();
            });
            expect(screen.getByText('Industry Type')).toBeInTheDocument();
            // Field values must also appear.
            expect(screen.getByText('Acme')).toBeInTheDocument();
            expect(screen.getByText('Tech')).toBeInTheDocument();
        });

        it('renders child object with metadata label from getSchema', async () => {
            dataAPIMock.previewStandardAction.mockResolvedValue({
                actionName: 'convertLead',
                isSuccess: true,
                parentPreview: { object: 'Account', fields: {} },
                childPreview: [{ object: 'Contact', count: 2 }],
            });
            getSchemaMock.mockImplementation((apiName: string) => {
                if (apiName === 'Contact') {
                    return Promise.resolve({
                        schema: {
                            name: 'Contact',
                            label: 'Sales Contact',
                            fields: [],
                        },
                    } as never);
                }
                return Promise.resolve({ schema: null } as never);
            });

            render(
                <ConvertLeadModal
                    {...baseProps}
                    action={{ ...baseAction, previewEndpoint, inputs: previewReadyInputs }}
                />,
            );

            await waitFor(() => {
                expect(screen.getByText('Sales Contact: 2 records')).toBeInTheDocument();
            });
        });

        it('filters synthetic preview-* IDs from parent field rendering', async () => {
            dataAPIMock.previewStandardAction.mockResolvedValue({
                actionName: 'convertLead',
                isSuccess: true,
                parentPreview: {
                    object: 'Account',
                    fields: {
                        Id: 'preview-Account-1',
                        Name: 'Acme',
                        SyntheticRef: 'preview-Account-1',
                    },
                },
                childPreview: [],
            });

            render(
                <ConvertLeadModal
                    {...baseProps}
                    action={{ ...baseAction, previewEndpoint, inputs: previewReadyInputs }}
                />,
            );

            await waitFor(() => {
                // 'Acme' passes through; it is a real value.
                expect(screen.getByText('Acme')).toBeInTheDocument();
            });
            // The synthetic preview ID must be entirely absent from the DOM.
            expect(screen.queryByText('preview-Account-1')).not.toBeInTheDocument();
        });

        it('does NOT fire preview when previewReady is false (contact reuse mode without contactId)', async () => {
            // Build an action that has contactId as a reuse-only option (no create inputs)
            // so the initial contact selection is 'reuse' with no contactId selected,
            // making contactReuseRequiresLocalValue = true → convertDisabled = true → previewReady = false.
            const reuseOnlyAction: TestConvertLeadAction = {
                ...baseAction,
                previewEndpoint,
                inputs: [
                    {
                        name: 'accountId',
                        label: 'Account',
                        type: 'reference',
                        required: true,
                        referenceTo: 'Account',
                    },
                    {
                        name: 'contactId',
                        label: 'Contact',
                        type: 'reference',
                        required: true,
                        referenceTo: 'Contact',
                    },
                    // No create-mode contact inputs → canCreateContact = false → initialContactSelection = 'reuse'
                ],
            };

            render(
                <ConvertLeadModal
                    {...baseProps}
                    action={reuseOnlyAction}
                />,
            );

            // Allow effects to settle — preview must NOT fire since contactReuseRequiresLocalValue = true.
            await new Promise(r => setTimeout(r, 100));

            expect(dataAPIMock.previewStandardAction).not.toHaveBeenCalled();
        });
    });
});
