import selectEvent from 'react-select-event';
import {urlEncode} from '@sentry/utils';

import {initializeOrg} from 'sentry-test/initializeOrg';
import {mountGlobalModal} from 'sentry-test/modal';
import {act, render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';

import * as indicators from 'sentry/actionCreators/indicator';
import * as modals from 'sentry/actionCreators/modal';
import TagStore from 'sentry/stores/tagStore';
import {TOP_N} from 'sentry/utils/discover/types';
import {
  DashboardDetails,
  DashboardWidgetSource,
  DisplayType,
  Widget,
  WidgetType,
} from 'sentry/views/dashboardsV2/types';
import WidgetBuilder, {WidgetBuilderProps} from 'sentry/views/dashboardsV2/widgetBuilder';

const defaultOrgFeatures = [
  'performance-view',
  'dashboards-edit',
  'global-views',
  'dashboards-mep',
];

// Mocking worldMapChart to avoid act warnings
jest.mock('sentry/components/charts/worldMapChart');

function mockDashboard(dashboard: Partial<DashboardDetails>): DashboardDetails {
  return {
    id: '1',
    title: 'Dashboard',
    createdBy: undefined,
    dateCreated: '2020-01-01T00:00:00.000Z',
    widgets: [],
    projects: [],
    filters: {},
    ...dashboard,
  };
}

function renderTestComponent({
  dashboard,
  query,
  orgFeatures,
  onSave,
  params,
}: {
  dashboard?: WidgetBuilderProps['dashboard'];
  onSave?: WidgetBuilderProps['onSave'];
  orgFeatures?: string[];
  params?: Partial<WidgetBuilderProps['params']>;
  query?: Record<string, any>;
} = {}) {
  const {organization, router, routerContext} = initializeOrg({
    ...initializeOrg(),
    organization: {
      features: orgFeatures ?? defaultOrgFeatures,
    },
    router: {
      location: {
        query: {
          source: DashboardWidgetSource.DASHBOARDS,
          ...query,
        },
      },
    },
  });

  render(
    <WidgetBuilder
      route={{}}
      router={router}
      routes={router.routes}
      routeParams={router.params}
      location={router.location}
      dashboard={{
        id: 'new',
        title: 'Dashboard',
        createdBy: undefined,
        dateCreated: '2020-01-01T00:00:00.000Z',
        widgets: [],
        projects: [],
        filters: {},
        ...dashboard,
      }}
      onSave={onSave ?? jest.fn()}
      params={{
        orgId: organization.slug,
        dashboardId: dashboard?.id ?? 'new',
        ...params,
      }}
    />,
    {
      context: routerContext,
      organization,
    }
  );

  return {router};
}

/**
 * This test suite contains tests that test the generic interactions
 * between most components in the WidgetBuilder. Tests for the
 * SortBy step can be found in (and should be added to)
 * ./widgetBuilderSortBy.spec.tsx and tests for specific dataset
 * behaviour can be found (and should be added to) ./widgetBuilderDataset.spec.tsx.
 * The test files are broken up to allow better parallelization
 * in CI (we currently parallelize files).
 */
describe('WidgetBuilder', function () {
  const untitledDashboard: DashboardDetails = {
    id: '1',
    title: 'Untitled Dashboard',
    createdBy: undefined,
    dateCreated: '2020-01-01T00:00:00.000Z',
    widgets: [],
    projects: [],
    filters: {},
  };

  const testDashboard: DashboardDetails = {
    id: '2',
    title: 'Test Dashboard',
    createdBy: undefined,
    dateCreated: '2020-01-01T00:00:00.000Z',
    widgets: [],
    projects: [],
    filters: {},
  };

  let eventsStatsMock: jest.Mock | undefined;
  let eventsv2Mock: jest.Mock | undefined;
  let eventsMock: jest.Mock | undefined;
  let tagsMock: jest.Mock | undefined;

  beforeEach(function () {
    MockApiClient.addMockResponse({
      url: '/organizations/org-slug/dashboards/',
      body: [
        {...untitledDashboard, widgetDisplay: [DisplayType.TABLE]},
        {...testDashboard, widgetDisplay: [DisplayType.AREA]},
      ],
    });

    MockApiClient.addMockResponse({
      url: '/organizations/org-slug/dashboards/widgets/',
      method: 'POST',
      statusCode: 200,
      body: [],
    });

    eventsv2Mock = MockApiClient.addMockResponse({
      url: '/organizations/org-slug/eventsv2/',
      method: 'GET',
      statusCode: 200,
      body: {
        meta: {},
        data: [],
      },
    });

    eventsMock = MockApiClient.addMockResponse({
      url: '/organizations/org-slug/events/',
      method: 'GET',
      statusCode: 200,
      body: {
        meta: {fields: {}},
        data: [],
      },
    });

    MockApiClient.addMockResponse({
      url: '/organizations/org-slug/projects/',
      method: 'GET',
      body: [],
    });

    MockApiClient.addMockResponse({
      url: '/organizations/org-slug/recent-searches/',
      method: 'GET',
      body: [],
    });

    MockApiClient.addMockResponse({
      url: '/organizations/org-slug/recent-searches/',
      method: 'POST',
      body: [],
    });

    MockApiClient.addMockResponse({
      url: '/organizations/org-slug/issues/',
      method: 'GET',
      body: [],
    });

    eventsStatsMock = MockApiClient.addMockResponse({
      url: '/organizations/org-slug/events-stats/',
      body: [],
    });

    MockApiClient.addMockResponse({
      url: '/organizations/org-slug/tags/event.type/values/',
      body: [{count: 2, name: 'Nvidia 1080ti'}],
    });

    MockApiClient.addMockResponse({
      url: '/organizations/org-slug/events-geo/',
      body: {data: [], meta: {}},
    });

    MockApiClient.addMockResponse({
      url: '/organizations/org-slug/users/',
      body: [],
    });

    MockApiClient.addMockResponse({
      method: 'GET',
      url: '/organizations/org-slug/sessions/',
      body: TestStubs.SessionsField({
        field: `sum(session)`,
      }),
    });

    MockApiClient.addMockResponse({
      method: 'GET',
      url: '/organizations/org-slug/metrics/data/',
      body: TestStubs.MetricsField({
        field: 'sum(sentry.sessions.session)',
      }),
    });

    tagsMock = MockApiClient.addMockResponse({
      url: '/organizations/org-slug/tags/',
      method: 'GET',
      body: TestStubs.Tags(),
    });

    MockApiClient.addMockResponse({
      url: '/organizations/org-slug/measurements-meta/',
      method: 'GET',
      body: {},
    });

    MockApiClient.addMockResponse({
      url: '/organizations/org-slug/tags/is/values/',
      method: 'GET',
      body: [],
    });

    MockApiClient.addMockResponse({
      url: '/organizations/org-slug/tags/transaction.duration/values/',
      method: 'GET',
      body: [],
    });

    TagStore.reset();
  });

  afterEach(function () {
    MockApiClient.clearMockResponses();
    jest.clearAllMocks();
    jest.useRealTimers();
  });

  describe('with eventsv2', function () {
    it('no feature access', function () {
      renderTestComponent({orgFeatures: []});

      expect(
        screen.getByText("You don't have access to this feature")
      ).toBeInTheDocument();
    });

    it('widget not found', function () {
      const widget: Widget = {
        displayType: DisplayType.AREA,
        interval: '1d',
        queries: [
          {
            name: 'Known Users',
            fields: [],
            columns: [],
            aggregates: [],
            conditions: '',
            orderby: '-time',
          },
          {
            name: 'Anonymous Users',
            fields: [],
            columns: [],
            aggregates: [],
            conditions: '',
            orderby: '-time',
          },
        ],
        title: 'Transactions',
        id: '1',
      };

      const dashboard = mockDashboard({widgets: [widget]});

      renderTestComponent({
        dashboard,
        orgFeatures: ['dashboards-edit'],
        params: {
          widgetIndex: '2', // Out of bounds, only one widget
        },
      });

      expect(
        screen.getByText('The widget you want to edit was not found.')
      ).toBeInTheDocument();
    });

    it('renders a widget not found message if the widget index url is not an integer', function () {
      const widget: Widget = {
        displayType: DisplayType.AREA,
        interval: '1d',
        queries: [
          {
            name: 'Known Users',
            fields: [],
            columns: [],
            aggregates: [],
            conditions: '',
            orderby: '-time',
          },
        ],
        title: 'Transactions',
        id: '1',
      };

      const dashboard = mockDashboard({widgets: [widget]});

      renderTestComponent({
        dashboard,
        orgFeatures: ['dashboards-edit'],
        params: {
          widgetIndex: '0.5', // Invalid index
        },
      });

      expect(
        screen.getByText('The widget you want to edit was not found.')
      ).toBeInTheDocument();
    });

    it('renders', async function () {
      renderTestComponent();

      // Header - Breadcrumbs
      expect(await screen.findByRole('link', {name: 'Dashboards'})).toHaveAttribute(
        'href',
        '/organizations/org-slug/dashboards/'
      );
      expect(screen.getByRole('link', {name: 'Dashboard'})).toHaveAttribute(
        'href',
        '/organizations/org-slug/dashboards/new/'
      );
      expect(screen.getByText('Widget Builder')).toBeInTheDocument();

      // Header - Widget Title
      expect(screen.getByRole('heading', {name: 'Custom Widget'})).toBeInTheDocument();

      // Footer - Actions
      expect(screen.getByLabelText('Cancel')).toBeInTheDocument();
      expect(screen.getByLabelText('Add Widget')).toBeInTheDocument();

      // Content - Step 1
      expect(
        screen.getByRole('heading', {name: 'Choose your dataset'})
      ).toBeInTheDocument();
      expect(screen.getByLabelText('Errors and Transactions')).toBeChecked();

      // Content - Step 2
      expect(
        screen.getByRole('heading', {name: 'Choose your visualization'})
      ).toBeInTheDocument();

      // Content - Step 3
      expect(
        screen.getByRole('heading', {name: 'Choose your columns'})
      ).toBeInTheDocument();

      // Content - Step 4
      expect(
        screen.getByRole('heading', {name: 'Filter your results'})
      ).toBeInTheDocument();

      // Content - Step 5
      expect(screen.getByRole('heading', {name: 'Sort by a column'})).toBeInTheDocument();
    });

    it('has links back to the new dashboard if creating', async function () {
      // Dashboard has undefined dashboardId when creating from a new dashboard
      // because of route setup
      renderTestComponent({params: {dashboardId: undefined}});

      expect(await screen.findByRole('link', {name: 'Dashboard'})).toHaveAttribute(
        'href',
        '/organizations/org-slug/dashboards/new/'
      );

      expect(screen.getByLabelText('Cancel')).toHaveAttribute(
        'href',
        '/organizations/org-slug/dashboards/new/'
      );
    });

    it('renders new design', async function () {
      renderTestComponent({
        orgFeatures: [...defaultOrgFeatures],
      });

      // Switch to line chart for time series
      userEvent.click(await screen.findByText('Table'));
      userEvent.click(screen.getByText('Line Chart'));

      // Header - Breadcrumbs
      expect(await screen.findByRole('link', {name: 'Dashboards'})).toHaveAttribute(
        'href',
        '/organizations/org-slug/dashboards/'
      );

      expect(screen.getByRole('link', {name: 'Dashboard'})).toHaveAttribute(
        'href',
        '/organizations/org-slug/dashboards/new/'
      );

      expect(screen.getByText('Widget Builder')).toBeInTheDocument();

      // Header - Widget Title
      expect(screen.getByRole('heading', {name: 'Custom Widget'})).toBeInTheDocument();

      // Footer - Actions
      expect(screen.getByLabelText('Cancel')).toBeInTheDocument();
      expect(screen.getByLabelText('Add Widget')).toBeInTheDocument();

      // Content - Step 1
      expect(
        screen.getByRole('heading', {name: 'Choose your dataset'})
      ).toBeInTheDocument();
      expect(screen.getByLabelText('Errors and Transactions')).toBeChecked();

      // Content - Step 2
      expect(
        screen.getByRole('heading', {name: 'Choose your visualization'})
      ).toBeInTheDocument();

      // Content - Step 3
      expect(
        screen.getByRole('heading', {name: 'Choose what to plot in the y-axis'})
      ).toBeInTheDocument();

      // Content - Step 4
      expect(
        screen.getByRole('heading', {name: 'Filter your results'})
      ).toBeInTheDocument();

      // Content - Step 5
      expect(
        screen.getByRole('heading', {name: 'Group your results'})
      ).toBeInTheDocument();
    });

    it('can update the title', async function () {
      renderTestComponent({
        query: {source: DashboardWidgetSource.DISCOVERV2},
      });

      const customWidgetLabels = await screen.findAllByText('Custom Widget');
      // EditableText and chart title
      expect(customWidgetLabels).toHaveLength(2);

      userEvent.click(customWidgetLabels[0]);
      userEvent.clear(screen.getByRole('textbox', {name: 'Widget title'}));
      userEvent.paste(
        screen.getByRole('textbox', {name: 'Widget title'}),
        'Unique Users'
      );
      userEvent.keyboard('{enter}');

      expect(screen.queryByText('Custom Widget')).not.toBeInTheDocument();

      expect(screen.getAllByText('Unique Users')).toHaveLength(2);
    });

    it('can add query conditions', async function () {
      const {router} = renderTestComponent({
        query: {source: DashboardWidgetSource.DISCOVERV2},
        dashboard: testDashboard,
      });

      userEvent.type(
        await screen.findByRole('textbox', {name: 'Search events'}),
        'color:blue{enter}'
      );

      userEvent.click(screen.getByText('Add Widget'));

      await waitFor(() => {
        expect(router.push).toHaveBeenCalledWith(
          expect.objectContaining({
            pathname: '/organizations/org-slug/dashboard/2/',
            query: {
              displayType: 'table',
              interval: '5m',
              title: 'Custom Widget',
              queryNames: [''],
              queryConditions: ['color:blue'],
              queryFields: ['count()'],
              queryOrderby: '-count()',
              start: null,
              end: null,
              statsPeriod: '24h',
              utc: null,
              project: [],
              environment: [],
            },
          })
        );
      });
    });

    it('can choose a field', async function () {
      const {router} = renderTestComponent({
        query: {source: DashboardWidgetSource.DISCOVERV2},
        dashboard: testDashboard,
      });

      expect(await screen.findAllByText('Custom Widget')).toHaveLength(2);

      // No delete button as there is only one query.
      expect(screen.queryByLabelText('Remove query')).not.toBeInTheDocument();

      // 1 in the table header, 1 in the column selector, 1 in the sort field
      const countFields = screen.getAllByText('count()');
      expect(countFields).toHaveLength(3);

      await selectEvent.select(countFields[1], ['last_seen()']);

      userEvent.click(screen.getByText('Add Widget'));

      await waitFor(() => {
        expect(router.push).toHaveBeenCalledWith(
          expect.objectContaining({
            pathname: '/organizations/org-slug/dashboard/2/',
            query: {
              displayType: 'table',
              interval: '5m',
              title: 'Custom Widget',
              queryNames: [''],
              queryConditions: [''],
              queryFields: ['last_seen()'],
              queryOrderby: '-last_seen()',
              start: null,
              end: null,
              statsPeriod: '24h',
              utc: null,
              project: [],
              environment: [],
            },
          })
        );
      });
    });

    it('can add additional fields', async function () {
      const handleSave = jest.fn();

      renderTestComponent({onSave: handleSave});

      userEvent.click(await screen.findByText('Table'));

      // Select line chart display
      userEvent.click(screen.getByText('Line Chart'));

      // Click the add overlay button
      userEvent.click(screen.getByLabelText('Add Overlay'));
      await selectEvent.select(screen.getByText('(Required)'), ['count_unique(…)']);

      userEvent.click(screen.getByLabelText('Add Widget'));

      await waitFor(() => {
        expect(handleSave).toHaveBeenCalledWith([
          expect.objectContaining({
            title: 'Custom Widget',
            displayType: DisplayType.LINE,
            interval: '5m',
            widgetType: WidgetType.DISCOVER,
            queries: [
              {
                conditions: '',
                fields: ['count()', 'count_unique(user)'],
                aggregates: ['count()', 'count_unique(user)'],
                fieldAliases: [],
                columns: [],
                orderby: '',
                name: '',
              },
            ],
          }),
        ]);
      });

      expect(handleSave).toHaveBeenCalledTimes(1);
    });

    it('can add equation fields', async function () {
      const handleSave = jest.fn();

      renderTestComponent({onSave: handleSave});
      userEvent.click(await screen.findByText('Table'));

      // Select line chart display
      userEvent.click(screen.getByText('Line Chart'));

      // Click the add an equation button
      userEvent.click(screen.getByLabelText('Add an Equation'));

      expect(screen.getByPlaceholderText('Equation')).toBeInTheDocument();

      userEvent.paste(screen.getByPlaceholderText('Equation'), 'count() + 100');

      userEvent.click(screen.getByLabelText('Add Widget'));

      await waitFor(() => {
        expect(handleSave).toHaveBeenCalledWith([
          expect.objectContaining({
            title: 'Custom Widget',
            displayType: DisplayType.LINE,
            interval: '5m',
            widgetType: WidgetType.DISCOVER,
            queries: [
              {
                name: '',
                fields: ['count()', 'equation|count() + 100'],
                aggregates: ['count()', 'equation|count() + 100'],
                columns: [],
                fieldAliases: [],
                conditions: '',
                orderby: '',
              },
            ],
          }),
        ]);
      });

      expect(handleSave).toHaveBeenCalledTimes(1);
    });

    it('can respond to validation feedback', async function () {
      jest.spyOn(indicators, 'addErrorMessage');

      renderTestComponent();

      userEvent.click(await screen.findByText('Table'));

      const customWidgetLabels = await screen.findAllByText('Custom Widget');
      // EditableText and chart title
      expect(customWidgetLabels).toHaveLength(2);

      userEvent.click(customWidgetLabels[0]);
      userEvent.clear(screen.getByRole('textbox', {name: 'Widget title'}));

      userEvent.keyboard('{enter}');

      expect(indicators.addErrorMessage).toHaveBeenCalledWith('Widget title is required');
    });

    it('sets up widget data in edit correctly', async function () {
      const widget: Widget = {
        id: '1',
        title: 'Errors over time',
        interval: '5m',
        displayType: DisplayType.LINE,
        queries: [
          {
            name: 'errors',
            conditions: 'event.type:error',
            fields: ['count()', 'count_unique(id)'],
            aggregates: ['count()', 'count_unique(id)'],
            columns: [],
            orderby: '',
          },
          {
            name: 'csp',
            conditions: 'event.type:csp',
            fields: ['count()', 'count_unique(id)'],
            aggregates: ['count()', 'count_unique(id)'],
            columns: [],
            orderby: '',
          },
        ],
      };

      const dashboard = mockDashboard({widgets: [widget]});

      renderTestComponent({dashboard, params: {widgetIndex: '0'}});

      await screen.findByText('Line Chart');

      // Should be in edit 'mode'
      expect(await screen.findByText('Update Widget')).toBeInTheDocument();

      // Should set widget data up.
      expect(screen.getByText('Update Widget')).toBeInTheDocument();

      // Filters
      expect(
        screen.getAllByPlaceholderText('Search for events, users, tags, and more')
      ).toHaveLength(2);
      expect(screen.getByText('event.type:csp')).toBeInTheDocument();
      expect(screen.getByText('event.type:error')).toBeInTheDocument();

      // Y-axis
      expect(screen.getAllByRole('button', {name: 'Remove query'})).toHaveLength(2);
      expect(screen.getByText('count()')).toBeInTheDocument();
      expect(screen.getByText('count_unique(…)')).toBeInTheDocument();
      expect(screen.getByText('id')).toBeInTheDocument();

      // Expect events-stats endpoint to be called for each search conditions with
      // the same y-axis parameters
      expect(eventsStatsMock).toHaveBeenNthCalledWith(
        1,
        '/organizations/org-slug/events-stats/',
        expect.objectContaining({
          query: expect.objectContaining({
            query: 'event.type:error',
            yAxis: ['count()', 'count_unique(id)'],
          }),
        })
      );

      expect(eventsStatsMock).toHaveBeenNthCalledWith(
        2,
        '/organizations/org-slug/events-stats/',
        expect.objectContaining({
          query: expect.objectContaining({
            query: 'event.type:csp',
            yAxis: ['count()', 'count_unique(id)'],
          }),
        })
      );
    });

    it('can edit a widget', async function () {
      const widget: Widget = {
        id: '1',
        title: 'Errors over time',
        interval: '5m',
        displayType: DisplayType.LINE,
        queries: [
          {
            name: 'errors',
            conditions: 'event.type:error',
            fields: ['count()', 'count_unique(id)'],
            aggregates: ['count()', 'count_unique(id)'],
            columns: [],
            orderby: '',
          },
          {
            name: 'csp',
            conditions: 'event.type:csp',
            fields: ['count()', 'count_unique(id)'],
            aggregates: ['count()', 'count_unique(id)'],
            columns: [],
            orderby: '',
          },
        ],
      };

      const dashboard = mockDashboard({widgets: [widget]});

      const handleSave = jest.fn();

      renderTestComponent({onSave: handleSave, dashboard, params: {widgetIndex: '0'}});

      await screen.findByText('Line Chart');

      // Should be in edit 'mode'
      expect(await screen.findByText('Update Widget')).toBeInTheDocument();

      const customWidgetLabels = await screen.findAllByText(widget.title);
      // EditableText and chart title
      expect(customWidgetLabels).toHaveLength(2);
      userEvent.click(customWidgetLabels[0]);

      userEvent.clear(screen.getByRole('textbox', {name: 'Widget title'}));
      userEvent.paste(screen.getByRole('textbox', {name: 'Widget title'}), 'New Title');

      userEvent.click(screen.getByRole('button', {name: 'Update Widget'}));

      await waitFor(() => {
        expect(handleSave).toHaveBeenCalledWith([
          expect.objectContaining({
            ...widget,
            title: 'New Title',
          }),
        ]);
      });

      expect(handleSave).toHaveBeenCalledTimes(1);
    });

    it('renders column inputs for table widgets', async function () {
      const widget: Widget = {
        id: '0',
        title: 'sdk usage',
        interval: '5m',
        displayType: DisplayType.TABLE,
        queries: [
          {
            name: 'errors',
            conditions: 'event.type:error',
            fields: ['sdk.name', 'count()'],
            columns: ['sdk.name'],
            aggregates: ['count()'],
            orderby: '',
          },
        ],
      };

      const dashboard = mockDashboard({widgets: [widget]});

      renderTestComponent({dashboard, params: {widgetIndex: '0'}});

      // Should be in edit 'mode'
      expect(await screen.findByText('Update Widget')).toBeInTheDocument();

      // Should set widget data up.
      expect(screen.getByRole('heading', {name: widget.title})).toBeInTheDocument();
      expect(screen.getByText('Table')).toBeInTheDocument();
      expect(screen.getByLabelText('Search events')).toBeInTheDocument();

      // Should have an orderby select
      expect(screen.getByText('Sort by a column')).toBeInTheDocument();

      // Add a column, and choose a value,
      expect(screen.getByLabelText('Add a Column')).toBeInTheDocument();
    });

    it('can save table widgets', async function () {
      const widget: Widget = {
        id: '0',
        title: 'sdk usage',
        interval: '5m',
        displayType: DisplayType.TABLE,
        queries: [
          {
            name: 'errors',
            conditions: 'event.type:error',
            fields: ['sdk.name', 'count()'],
            columns: ['sdk.name'],
            aggregates: ['count()'],
            orderby: '-count()',
          },
        ],
      };

      const dashboard = mockDashboard({widgets: [widget]});

      const handleSave = jest.fn();

      renderTestComponent({dashboard, onSave: handleSave, params: {widgetIndex: '0'}});

      // Should be in edit 'mode'
      expect(await screen.findByText('Update Widget')).toBeInTheDocument();
      // Add a column, and choose a value,
      userEvent.click(screen.getByLabelText('Add a Column'));
      await selectEvent.select(screen.getByText('(Required)'), 'trace');

      // Save widget
      userEvent.click(screen.getByLabelText('Update Widget'));

      await waitFor(() => {
        expect(handleSave).toHaveBeenCalledWith([
          expect.objectContaining({
            id: '0',
            title: 'sdk usage',
            displayType: DisplayType.TABLE,
            interval: '5m',
            queries: [
              {
                name: 'errors',
                conditions: 'event.type:error',
                fields: ['sdk.name', 'count()', 'trace'],
                aggregates: ['count()'],
                columns: ['sdk.name', 'trace'],
                orderby: '-count()',
                fieldAliases: ['', '', ''],
              },
            ],
            widgetType: WidgetType.DISCOVER,
          }),
        ]);
      });

      expect(handleSave).toHaveBeenCalledTimes(1);
    });

    it('should properly query for table fields', async function () {
      const defaultWidgetQuery = {
        name: '',
        fields: ['title', 'count()'],
        columns: ['title'],
        aggregates: ['count()'],
        conditions: '',
        orderby: '',
      };

      const defaultTableColumns = ['title', 'count()', 'count_unique(user)', 'epm()'];

      renderTestComponent({
        query: {
          source: DashboardWidgetSource.DISCOVERV2,
          defaultWidgetQuery: urlEncode(defaultWidgetQuery),
          displayType: DisplayType.LINE,
          defaultTableColumns,
        },
      });

      expect(await screen.findByText('Line Chart')).toBeInTheDocument();
      userEvent.click(screen.getByText('Line Chart'));
      userEvent.click(screen.getByText('Table'));

      await waitFor(() => {
        expect(eventsv2Mock).toHaveBeenLastCalledWith(
          '/organizations/org-slug/eventsv2/',
          expect.objectContaining({
            query: expect.objectContaining({
              field: defaultTableColumns,
            }),
          })
        );
      });
    });

    it('should use defaultWidgetQuery Y-Axis and Conditions if given a defaultWidgetQuery', async function () {
      const defaultWidgetQuery = {
        name: '',
        fields: ['count()', 'failure_count()', 'count_unique(user)'],
        columns: [],
        aggregates: ['count()', 'failure_count()', 'count_unique(user)'],
        conditions: 'tag:value',
        orderby: '',
      };

      renderTestComponent({
        query: {
          source: DashboardWidgetSource.DISCOVERV2,
          defaultWidgetQuery: urlEncode(defaultWidgetQuery),
        },
      });

      expect(await screen.findByText('tag:value')).toBeInTheDocument();

      // Table display, column, and sort field
      expect(screen.getAllByText('count()')).toHaveLength(3);
      // Table display and column
      expect(screen.getAllByText('failure_count()')).toHaveLength(2);
      // Table display
      expect(screen.getByText('count_unique(user)')).toBeInTheDocument();
      // Column
      expect(screen.getByText('count_unique(…)')).toBeInTheDocument();
      // Column
      expect(screen.getByText('user')).toBeInTheDocument();
    });

    it('uses displayType if given a displayType', async function () {
      renderTestComponent({
        query: {
          displayType: DisplayType.BAR,
        },
      });

      expect(await screen.findByText('Bar Chart')).toBeInTheDocument();
    });

    it('deletes the widget when the modal is confirmed', async () => {
      const handleSave = jest.fn();
      const widget: Widget = {
        id: '1',
        title: 'Errors over time',
        interval: '5m',
        displayType: DisplayType.LINE,
        queries: [
          {
            name: 'errors',
            conditions: 'event.type:error',
            fields: ['count()', 'count_unique(id)'],
            aggregates: ['count()', 'count_unique(id)'],
            columns: [],
            orderby: '',
          },
          {
            name: 'csp',
            conditions: 'event.type:csp',
            fields: ['count()', 'count_unique(id)'],
            aggregates: ['count()', 'count_unique(id)'],
            columns: [],
            orderby: '',
          },
        ],
      };

      const dashboard = mockDashboard({widgets: [widget]});

      renderTestComponent({onSave: handleSave, dashboard, params: {widgetIndex: '0'}});

      userEvent.click(await screen.findByText('Delete'));

      await mountGlobalModal();
      userEvent.click(await screen.findByText('Confirm'));

      await waitFor(() => {
        // The only widget was deleted
        expect(handleSave).toHaveBeenCalledWith([]);
      });

      expect(handleSave).toHaveBeenCalledTimes(1);
    });

    it('persists the page filter period when updating a widget', async () => {
      const widget: Widget = {
        id: '1',
        title: 'Errors over time',
        interval: '5m',
        displayType: DisplayType.LINE,
        queries: [
          {
            name: 'errors',
            conditions: 'event.type:error',
            fields: ['count()', 'count_unique(id)'],
            aggregates: ['count()', 'count_unique(id)'],
            columns: [],
            orderby: '',
          },
        ],
      };

      const dashboard = mockDashboard({widgets: [widget]});

      const {router} = renderTestComponent({
        dashboard,
        params: {orgId: 'org-slug', widgetIndex: '0'},
        query: {statsPeriod: '90d'},
      });

      await screen.findByText('Update Widget');
      await screen.findByText('90D');

      expect(screen.getByTestId('page-filter-timerange-selector')).toBeEnabled();

      userEvent.click(screen.getByText('Update Widget'));

      await waitFor(() => {
        expect(router.push).toHaveBeenLastCalledWith(
          expect.objectContaining({
            pathname: '/organizations/org-slug/dashboard/1/',
            query: expect.objectContaining({
              statsPeriod: '90d',
            }),
          })
        );
      });
    });

    it('renders page filters in the filter step', async () => {
      const mockReleases = MockApiClient.addMockResponse({
        url: '/organizations/org-slug/releases/',
        body: [TestStubs.Release()],
      });

      renderTestComponent({
        params: {orgId: 'org-slug'},
        query: {statsPeriod: '90d'},
        orgFeatures: [...defaultOrgFeatures, 'dashboards-top-level-filter'],
      });

      await screen.findByText('90D');
      expect(screen.getByTestId('page-filter-timerange-selector')).toBeDisabled();
      expect(screen.getByTestId('page-filter-environment-selector')).toBeDisabled();
      expect(screen.getByTestId('page-filter-project-selector-loading')).toBeDisabled();

      await waitFor(() => {
        expect(mockReleases).toHaveBeenCalled();
      });

      expect(screen.getByRole('button', {name: /all releases/i})).toBeDisabled();
    });

    it('appends dashboard filters to widget builder fetch data request', async () => {
      MockApiClient.addMockResponse({
        url: '/organizations/org-slug/releases/',
        body: [TestStubs.Release()],
      });

      const mock = MockApiClient.addMockResponse({
        url: '/organizations/org-slug/eventsv2/',
        body: [],
      });

      renderTestComponent({
        dashboard: {
          id: 'new',
          title: 'Dashboard',
          createdBy: undefined,
          dateCreated: '2020-01-01T00:00:00.000Z',
          widgets: [],
          projects: [],
          filters: {release: ['abc@1.2.0']},
        },
        params: {orgId: 'org-slug'},
        query: {statsPeriod: '90d'},
        orgFeatures: [...defaultOrgFeatures, 'dashboards-top-level-filter'],
      });

      await waitFor(() => {
        expect(mock).toHaveBeenCalledWith(
          '/organizations/org-slug/eventsv2/',
          expect.objectContaining({
            query: expect.objectContaining({
              query: ' release:abc@1.2.0 ',
            }),
          })
        );
      });
    });

    it('does not error when query conditions field is blurred', async function () {
      jest.useFakeTimers();
      const widget: Widget = {
        id: '0',
        title: 'sdk usage',
        interval: '5m',
        displayType: DisplayType.BAR,
        queries: [
          {
            name: 'filled in',
            conditions: 'event.type:error',
            fields: ['count()', 'count_unique(id)'],
            aggregates: ['count()', 'count_unique(id)'],
            columns: [],
            orderby: '-count()',
          },
        ],
      };

      const dashboard = mockDashboard({widgets: [widget]});

      const handleSave = jest.fn();

      renderTestComponent({dashboard, onSave: handleSave, params: {widgetIndex: '0'}});

      await act(async () => {
        userEvent.click(await screen.findByLabelText('Add Query'));

        // Triggering the onBlur of the new field should not error
        userEvent.click(
          screen.getAllByPlaceholderText('Search for events, users, tags, and more')[1]
        );
        userEvent.keyboard('{esc}');

        // Run all timers because the handleBlur contains a setTimeout
        jest.runAllTimers();
      });
    });

    it('does not wipe column changes when filters are modified', async function () {
      jest.useFakeTimers();

      // widgetIndex: undefined means creating a new widget
      renderTestComponent({params: {widgetIndex: undefined}});

      userEvent.click(await screen.findByLabelText('Add a Column'));
      await selectEvent.select(screen.getByText('(Required)'), /project/);

      // Triggering the onBlur of the filter should not error
      userEvent.click(
        screen.getByPlaceholderText('Search for events, users, tags, and more')
      );
      userEvent.keyboard('{enter}');
      act(() => {
        // Run all timers because the handleBlur contains a setTimeout
        jest.runAllTimers();
      });

      expect(await screen.findAllByText('project')).toHaveLength(2);
    });

    it('renders fields with commas properly', async () => {
      const defaultWidgetQuery = {
        conditions: '',
        fields: ['equation|count_if(transaction.duration,equals,300)*2'],
        aggregates: ['equation|count_if(transaction.duration,equals,300)*2'],
        columns: [],
        orderby: '',
        name: '',
      };
      const defaultTableColumns = [
        'count_if(transaction.duration,equals,300)',
        'equation|count_if(transaction.duration,equals,300)*2',
      ];
      renderTestComponent({
        query: {
          source: DashboardWidgetSource.DISCOVERV2,
          defaultWidgetQuery: urlEncode(defaultWidgetQuery),
          defaultTableColumns,
          yAxis: ['equation|count_if(transaction.duration,equals,300)*2'],
        },
      });

      expect(
        await screen.findByText('count_if(transaction.duration,equals,300)*2')
      ).toBeInTheDocument();
    });

    it('sets the correct fields for a top n widget', async () => {
      renderTestComponent({
        orgFeatures: [...defaultOrgFeatures, 'performance-view'],
        query: {
          displayType: DisplayType.TOP_N,
        },
      });

      // Top N now opens as Area Chart
      await screen.findByText('Area Chart');

      // Add a group by
      userEvent.click(screen.getByText('Add Overlay'));
      await selectEvent.select(screen.getByText('Select group'), /project/);

      // Change the y-axis
      await selectEvent.select(screen.getAllByText('count()')[0], 'eps()');

      await waitFor(() => {
        expect(eventsStatsMock).toHaveBeenLastCalledWith(
          '/organizations/org-slug/events-stats/',
          expect.objectContaining({
            query: expect.objectContaining({
              query: '',
              yAxis: ['eps()'],
              field: ['project', 'eps()'],
              topEvents: TOP_N,
              orderby: '-eps()',
            }),
          })
        );
      });
    });

    it('fetches tags when tag store is empty', async function () {
      await act(async () => {
        renderTestComponent();
        await tick();
      });
      expect(tagsMock).toHaveBeenCalled();
    });

    it('does not fetch tags when tag store is not empty', async function () {
      await act(async () => {
        TagStore.loadTagsSuccess(TestStubs.Tags());
        renderTestComponent();
        await tick();
      });
      expect(tagsMock).not.toHaveBeenCalled();
    });

    it('excludes the Other series when grouping and using multiple y-axes', async function () {
      renderTestComponent({
        orgFeatures: [...defaultOrgFeatures],
        query: {
          displayType: DisplayType.LINE,
        },
      });

      await selectEvent.select(await screen.findByText('Select group'), 'project');

      userEvent.click(screen.getByText('Add Overlay'));
      await selectEvent.select(screen.getByText('(Required)'), /count_unique/);

      await waitFor(() => {
        expect(eventsStatsMock).toHaveBeenCalledWith(
          '/organizations/org-slug/events-stats/',
          expect.objectContaining({
            query: expect.objectContaining({excludeOther: '1'}),
          })
        );
      });
    });

    it('excludes the Other series when grouping and using multiple queries', async function () {
      renderTestComponent({
        orgFeatures: [...defaultOrgFeatures],
        query: {
          displayType: DisplayType.LINE,
        },
      });

      await selectEvent.select(await screen.findByText('Select group'), 'project');
      userEvent.click(screen.getByText('Add Query'));

      await waitFor(() => {
        expect(eventsStatsMock).toHaveBeenCalledWith(
          '/organizations/org-slug/events-stats/',
          expect.objectContaining({
            query: expect.objectContaining({excludeOther: '1'}),
          })
        );
      });
    });

    it('includes Other series when there is only one query and one y-axis', async function () {
      renderTestComponent({
        orgFeatures: [...defaultOrgFeatures],
        query: {
          displayType: DisplayType.LINE,
        },
      });

      await selectEvent.select(await screen.findByText('Select group'), 'project');

      await waitFor(() => {
        expect(eventsStatsMock).toHaveBeenCalledWith(
          '/organizations/org-slug/events-stats/',
          expect.objectContaining({
            query: expect.not.objectContaining({excludeOther: '1'}),
          })
        );
      });
    });

    it('decreases the limit when more y-axes and queries are added', async function () {
      renderTestComponent({
        orgFeatures: [...defaultOrgFeatures],
        query: {
          displayType: DisplayType.LINE,
        },
      });

      await selectEvent.select(await screen.findByText('Select group'), 'project');

      screen.getByText('Limit to 5 results');

      userEvent.click(screen.getByText('Add Query'));
      userEvent.click(screen.getByText('Add Overlay'));

      await screen.findByText('Limit to 2 results');
    });

    it('alerts the user if there are unsaved changes', async function () {
      const {router} = renderTestComponent();

      const alertMock = jest.fn();
      const setRouteLeaveHookMock = jest.spyOn(router, 'setRouteLeaveHook');
      setRouteLeaveHookMock.mockImplementationOnce((_route, _callback) => {
        alertMock();
      });

      const customWidgetLabels = await screen.findAllByText('Custom Widget');
      // EditableText and chart title
      expect(customWidgetLabels).toHaveLength(2);

      // Change title text
      userEvent.click(customWidgetLabels[0]);
      userEvent.clear(screen.getByRole('textbox', {name: 'Widget title'}));
      userEvent.paste(
        screen.getByRole('textbox', {name: 'Widget title'}),
        'Unique Users'
      );
      userEvent.keyboard('{enter}');

      // Click Cancel
      userEvent.click(screen.getByText('Cancel'));

      // Assert an alert was triggered
      expect(alertMock).toHaveBeenCalled();
    });

    it('does not trigger alert dialog if no changes', async function () {
      const {router} = renderTestComponent();

      const alertMock = jest.fn();
      const setRouteLeaveHookMock = jest.spyOn(router, 'setRouteLeaveHook');
      setRouteLeaveHookMock.mockImplementationOnce((_route, _callback) => {
        alertMock();
      });

      await screen.findAllByText('Custom Widget');

      // Click Cancel
      userEvent.click(screen.getByText('Cancel'));

      // Assert an alert was triggered
      expect(alertMock).not.toHaveBeenCalled();
    });

    describe('Widget creation coming from other verticals', function () {
      it('redirects correctly when creating a new dashboard', async function () {
        const {router} = renderTestComponent({
          query: {source: DashboardWidgetSource.DISCOVERV2},
        });

        userEvent.click(await screen.findByText('Add Widget'));

        await waitFor(() => {
          expect(router.push).toHaveBeenCalledWith(
            expect.objectContaining({
              pathname: '/organizations/org-slug/dashboards/new/',
              query: {
                displayType: 'table',
                interval: '5m',
                title: 'Custom Widget',
                queryNames: [''],
                queryConditions: [''],
                queryFields: ['count()'],
                queryOrderby: '-count()',
                start: null,
                end: null,
                statsPeriod: '24h',
                utc: null,
                project: [],
                environment: [],
              },
            })
          );
        });
      });

      it('redirects correctly when choosing an existing dashboard', async function () {
        const {router} = renderTestComponent({
          query: {source: DashboardWidgetSource.DISCOVERV2},
          dashboard: testDashboard,
        });

        userEvent.click(await screen.findByText('Add Widget'));

        await waitFor(() => {
          expect(router.push).toHaveBeenCalledWith(
            expect.objectContaining({
              pathname: '/organizations/org-slug/dashboard/2/',
              query: {
                displayType: 'table',
                interval: '5m',
                title: 'Custom Widget',
                queryNames: [''],
                queryConditions: [''],
                queryFields: ['count()'],
                queryOrderby: '-count()',
                start: null,
                end: null,
                statsPeriod: '24h',
                utc: null,
                project: [],
                environment: [],
              },
            })
          );
        });
      });

      it('shows the correct orderby when switching from a line chart to table', async function () {
        const defaultWidgetQuery = {
          name: '',
          fields: ['count_unique(user)'],
          columns: [],
          aggregates: ['count_unique(user)'],
          conditions: '',
          orderby: 'count_unique(user)',
        };

        const defaultTableColumns = ['title', 'count_unique(user)'];

        renderTestComponent({
          orgFeatures: [...defaultOrgFeatures],
          query: {
            source: DashboardWidgetSource.DISCOVERV2,
            defaultWidgetQuery: urlEncode(defaultWidgetQuery),
            displayType: DisplayType.LINE,
            defaultTableColumns,
          },
        });

        userEvent.click(await screen.findByText('Line Chart'));
        userEvent.click(screen.getByText('Table'));

        expect(screen.getByText('count_unique(user)')).toBeInTheDocument();
        await waitFor(() => {
          expect(eventsv2Mock).toHaveBeenLastCalledWith(
            '/organizations/org-slug/eventsv2/',
            expect.objectContaining({
              query: expect.objectContaining({
                field: defaultTableColumns,
                sort: ['count_unique(user)'],
              }),
            })
          );
        });
      });

      it('does not send request with orderby if a timeseries chart without grouping', async function () {
        const defaultWidgetQuery = {
          name: '',
          fields: ['count_unique(user)'],
          columns: [],
          aggregates: ['count_unique(user)'],
          conditions: '',
          orderby: 'count_unique(user)',
        };

        const defaultTableColumns = ['title', 'count_unique(user)'];

        renderTestComponent({
          orgFeatures: [...defaultOrgFeatures],
          query: {
            source: DashboardWidgetSource.DISCOVERV2,
            defaultWidgetQuery: urlEncode(defaultWidgetQuery),
            displayType: DisplayType.LINE,
            defaultTableColumns,
          },
        });

        await waitFor(() => {
          expect(eventsStatsMock).toHaveBeenLastCalledWith(
            '/organizations/org-slug/events-stats/',
            expect.objectContaining({
              query: expect.objectContaining({
                orderby: '',
              }),
            })
          );
        });
      });
    });

    it('opens top-N widgets as area display', async function () {
      const widget: Widget = {
        id: '1',
        title: 'Errors over time',
        interval: '5m',
        displayType: DisplayType.TOP_N,
        queries: [
          {
            name: '',
            conditions: '',
            fields: ['count()', 'count_unique(id)'],
            aggregates: ['count()', 'count_unique(id)'],
            columns: [],
            orderby: '-count()',
          },
        ],
      };

      const dashboard = mockDashboard({widgets: [widget]});

      renderTestComponent({
        orgFeatures: [...defaultOrgFeatures],
        dashboard,
        params: {
          widgetIndex: '0',
        },
      });

      expect(await screen.findByText('Area Chart')).toBeInTheDocument();
    });

    it('Update table header values (field alias)', async function () {
      const handleSave = jest.fn();

      renderTestComponent({
        onSave: handleSave,
        orgFeatures: [...defaultOrgFeatures],
      });

      await screen.findByText('Table');

      userEvent.paste(screen.getByPlaceholderText('Alias'), 'First Alias');

      userEvent.click(screen.getByLabelText('Add a Column'));

      userEvent.paste(screen.getAllByPlaceholderText('Alias')[1], 'Second Alias');

      userEvent.click(screen.getByText('Add Widget'));

      await waitFor(() => {
        expect(handleSave).toHaveBeenCalledWith([
          expect.objectContaining({
            queries: [
              expect.objectContaining({fieldAliases: ['First Alias', 'Second Alias']}),
            ],
          }),
        ]);
      });
    });

    it('does not wipe equation aliases when a column alias is updated', async function () {
      renderTestComponent({
        orgFeatures: [...defaultOrgFeatures],
      });

      await screen.findByText('Table');

      userEvent.click(screen.getByText('Add an Equation'));
      userEvent.paste(screen.getAllByPlaceholderText('Alias')[1], 'This should persist');
      userEvent.type(screen.getAllByPlaceholderText('Alias')[0], 'A');

      expect(await screen.findByText('This should persist')).toBeInTheDocument();
    });

    it('does not wipe equation aliases when a column selection is made', async function () {
      renderTestComponent({
        orgFeatures: [...defaultOrgFeatures],
      });

      await screen.findByText('Table');

      userEvent.click(screen.getByText('Add an Equation'));
      userEvent.paste(screen.getAllByPlaceholderText('Alias')[1], 'This should persist');

      // 1 for the table, 1 for the the column selector, 1 for the sort
      await waitFor(() => expect(screen.getAllByText('count()')).toHaveLength(3));
      await selectEvent.select(screen.getAllByText('count()')[1], /count_unique/);

      expect(screen.getByText('This should persist')).toBeInTheDocument();
    });

    it('copies over the orderby from the previous query if adding another', async function () {
      renderTestComponent({
        orgFeatures: [...defaultOrgFeatures],
      });

      userEvent.click(await screen.findByText('Table'));
      userEvent.click(screen.getByText('Line Chart'));
      await selectEvent.select(screen.getByText('Select group'), 'project');
      await selectEvent.select(screen.getAllByText('count()')[1], 'count_unique(…)');

      MockApiClient.clearMockResponses();
      eventsStatsMock = MockApiClient.addMockResponse({
        url: '/organizations/org-slug/events-stats/',
        body: [],
      });

      userEvent.click(screen.getByText('Add Query'));

      // Assert on two calls, one for each query
      const expectedArgs = expect.objectContaining({
        query: expect.objectContaining({
          orderby: '-count_unique(user)',
        }),
      });
      expect(eventsStatsMock).toHaveBeenNthCalledWith(
        1,
        '/organizations/org-slug/events-stats/',
        expectedArgs
      );

      expect(eventsStatsMock).toHaveBeenNthCalledWith(
        2,
        '/organizations/org-slug/events-stats/',
        expectedArgs
      );
    });

    it('disables add widget button and prevents widget previewing from firing widget query if widget query condition is invalid', async function () {
      renderTestComponent({
        orgFeatures: [...defaultOrgFeatures],
      });
      userEvent.click(await screen.findByText('Table'));
      userEvent.click(screen.getByText('Line Chart'));
      expect(eventsStatsMock).toHaveBeenCalledTimes(1);

      userEvent.type(
        screen.getByTestId('smart-search-input'),
        'transaction.duration:123a'
      );

      // Unfocus input
      userEvent.click(screen.getByText('Filter your results'));

      await waitFor(() =>
        expect(screen.getByText('Add Widget').closest('button')).toBeDisabled()
      );
      expect(screen.getByText('Widget query condition is invalid.')).toBeInTheDocument();
      expect(eventsStatsMock).toHaveBeenCalledTimes(1);
    });

    describe('Widget Library', function () {
      it('renders', async function () {
        renderTestComponent();
        expect(await screen.findByText('Widget Library')).toBeInTheDocument();
      });

      it('only opens the modal when the query data is changed', async function () {
        const mockModal = jest.spyOn(modals, 'openWidgetBuilderOverwriteModal');
        renderTestComponent();
        await screen.findByText('Widget Library');

        userEvent.click(screen.getByText('Duration Distribution'));

        // Widget Library, Builder title, and Chart title
        expect(await screen.findAllByText('Duration Distribution')).toHaveLength(3);

        // Confirm modal doesn't open because no changes were made
        expect(mockModal).not.toHaveBeenCalled();

        userEvent.click(screen.getAllByLabelText('Remove this Y-Axis')[0]);
        userEvent.click(screen.getByText('High Throughput Transactions'));

        // Should not have overwritten widget data, and confirm modal should open
        expect(await screen.findAllByText('Duration Distribution')).toHaveLength(3);
        expect(mockModal).toHaveBeenCalled();
      });
    });

    describe('group by field', function () {
      it('does not contain functions as options', async function () {
        renderTestComponent({
          query: {displayType: 'line'},
          orgFeatures: [...defaultOrgFeatures],
        });

        await screen.findByText('Group your results');

        expect(screen.getByText('Select group')).toBeInTheDocument();

        userEvent.click(screen.getByText('Select group'));

        // Only one f(x) field set in the y-axis selector
        expect(screen.getByText('f(x)')).toBeInTheDocument();
      });

      it('adds more fields when Add Group is clicked', async function () {
        renderTestComponent({
          query: {displayType: 'line'},
          orgFeatures: [...defaultOrgFeatures],
        });

        await screen.findByText('Group your results');
        userEvent.click(screen.getByText('Add Group'));
        expect(await screen.findAllByText('Select group')).toHaveLength(2);
      });

      it("doesn't reset group by when changing y-axis", async function () {
        renderTestComponent({
          query: {displayType: 'line'},
          orgFeatures: [...defaultOrgFeatures],
        });

        await selectEvent.select(await screen.findByText('Select group'), 'project');
        userEvent.click(screen.getAllByText('count()')[0], undefined, {skipHover: true});
        userEvent.click(screen.getByText(/count_unique/), undefined, {skipHover: true});

        expect(await screen.findByText('project')).toBeInTheDocument();
      });

      it("doesn't erase the selection when switching to another time series", async function () {
        renderTestComponent({
          query: {displayType: 'line'},
          orgFeatures: [...defaultOrgFeatures],
        });

        await selectEvent.select(await screen.findByText('Select group'), 'project');

        userEvent.click(screen.getByText('Line Chart'));
        userEvent.click(screen.getByText('Area Chart'));

        expect(await screen.findByText('project')).toBeInTheDocument();
      });

      it('sends a top N request when a grouping is selected', async function () {
        renderTestComponent({
          query: {displayType: 'line'},
          orgFeatures: [...defaultOrgFeatures],
        });

        userEvent.click(await screen.findByText('Group your results'));
        userEvent.type(screen.getByText('Select group'), 'project{enter}');

        await waitFor(() =>
          expect(eventsStatsMock).toHaveBeenNthCalledWith(
            2,
            '/organizations/org-slug/events-stats/',
            expect.objectContaining({
              query: expect.objectContaining({
                query: '',
                yAxis: ['count()'],
                field: ['project', 'count()'],
                topEvents: TOP_N,
                orderby: '-count()',
              }),
            })
          )
        );
      });

      it('allows deleting groups until there is one left', async function () {
        renderTestComponent({
          query: {displayType: 'line'},
          orgFeatures: [...defaultOrgFeatures],
        });

        await screen.findByText('Group your results');
        userEvent.click(screen.getByText('Add Group'));
        expect(screen.getAllByLabelText('Remove group')).toHaveLength(2);

        userEvent.click(screen.getAllByLabelText('Remove group')[1]);
        await waitFor(() =>
          expect(screen.queryByLabelText('Remove group')).not.toBeInTheDocument()
        );
      });

      it("display 'remove' and 'drag to reorder' buttons", async function () {
        renderTestComponent({
          query: {displayType: 'line'},
          orgFeatures: [...defaultOrgFeatures],
        });

        await screen.findByText('Select group');

        expect(screen.queryByLabelText('Remove group')).not.toBeInTheDocument();

        await selectEvent.select(screen.getByText('Select group'), 'project');

        expect(screen.getByLabelText('Remove group')).toBeInTheDocument();
        expect(screen.queryByLabelText('Drag to reorder')).not.toBeInTheDocument();

        userEvent.click(screen.getByText('Add Group'));

        expect(screen.getAllByLabelText('Remove group')).toHaveLength(2);
        expect(screen.getAllByLabelText('Drag to reorder')).toHaveLength(2);
      });

      it.todo(
        'Since simulate drag and drop with RTL is not recommended because of browser layout, remember to create acceptance test for this'
      );
    });

    describe('limit field', function () {
      it('renders if groupBy value is present', async function () {
        const handleSave = jest.fn();

        renderTestComponent({
          query: {displayType: 'line'},
          orgFeatures: [...defaultOrgFeatures],
          onSave: handleSave,
        });

        await selectEvent.select(await screen.findByText('Select group'), 'project');

        expect(screen.getByText('Limit to 5 results')).toBeInTheDocument();

        userEvent.click(screen.getByText('Add Widget'));

        await waitFor(() =>
          expect(handleSave).toHaveBeenCalledWith([
            expect.objectContaining({
              limit: 5,
            }),
          ])
        );
      });

      it('update value', async function () {
        renderTestComponent({
          query: {displayType: 'line'},
          orgFeatures: [...defaultOrgFeatures],
        });

        await selectEvent.select(await screen.findByText('Select group'), 'project');

        userEvent.click(screen.getByText('Limit to 5 results'));
        userEvent.click(screen.getByText('Limit to 2 results'));

        await waitFor(() =>
          expect(eventsStatsMock).toHaveBeenCalledWith(
            '/organizations/org-slug/events-stats/',
            expect.objectContaining({
              query: expect.objectContaining({
                query: '',
                yAxis: ['count()'],
                field: ['project', 'count()'],
                topEvents: 2,
                orderby: '-count()',
              }),
            })
          )
        );
      });

      it('gets removed if no groupBy value', async function () {
        renderTestComponent({
          query: {displayType: 'line'},
          orgFeatures: [...defaultOrgFeatures],
        });

        await selectEvent.select(await screen.findByText('Select group'), 'project');

        expect(screen.getByText('Limit to 5 results')).toBeInTheDocument();

        userEvent.click(screen.getByLabelText('Remove group'));

        await waitFor(() =>
          expect(screen.queryByText('Limit to 5 results')).not.toBeInTheDocument()
        );
      });

      it('applies a limit when switching from a table to timeseries chart with grouping', async function () {
        const widget: Widget = {
          displayType: DisplayType.TABLE,
          interval: '1d',
          queries: [
            {
              name: 'Test Widget',
              fields: ['count()', 'count_unique(user)', 'epm()', 'project'],
              columns: ['project'],
              aggregates: ['count()', 'count_unique(user)', 'epm()'],
              conditions: '',
              orderby: '',
            },
          ],
          title: 'Transactions',
          id: '1',
        };

        const dashboard = mockDashboard({widgets: [widget]});

        renderTestComponent({
          dashboard,
          orgFeatures: [...defaultOrgFeatures],
          params: {
            widgetIndex: '0',
          },
        });

        userEvent.click(await screen.findByText('Table'));
        userEvent.click(screen.getByText('Line Chart'));

        expect(screen.getByText('Limit to 3 results')).toBeInTheDocument();
        expect(eventsStatsMock).toHaveBeenCalledWith(
          '/organizations/org-slug/events-stats/',
          expect.objectContaining({
            query: expect.objectContaining({
              topEvents: 3,
            }),
          })
        );
      });

      it('persists the limit when switching between timeseries charts', async function () {
        const widget: Widget = {
          displayType: DisplayType.AREA,
          interval: '1d',
          queries: [
            {
              name: 'Test Widget',
              fields: ['count()', 'count_unique(user)', 'epm()', 'project'],
              columns: ['project'],
              aggregates: ['count()', 'count_unique(user)', 'epm()'],
              conditions: '',
              orderby: '',
            },
          ],
          title: 'Transactions',
          id: '1',
          limit: 1,
        };

        const dashboard = mockDashboard({widgets: [widget]});

        renderTestComponent({
          dashboard,
          orgFeatures: [...defaultOrgFeatures],
          params: {
            widgetIndex: '0',
          },
        });

        userEvent.click(await screen.findByText('Area Chart'));
        userEvent.click(screen.getByText('Line Chart'));

        expect(screen.getByText('Limit to 1 result')).toBeInTheDocument();
        expect(eventsStatsMock).toHaveBeenCalledWith(
          '/organizations/org-slug/events-stats/',
          expect.objectContaining({
            query: expect.objectContaining({
              topEvents: 1,
            }),
          })
        );
      });

      it('unsets the limit when going from timeseries to table', async function () {
        const widget: Widget = {
          displayType: DisplayType.AREA,
          interval: '1d',
          queries: [
            {
              name: 'Test Widget',
              fields: ['count()', 'count_unique(user)', 'epm()', 'project'],
              columns: ['project'],
              aggregates: ['count()', 'count_unique(user)', 'epm()'],
              conditions: '',
              orderby: '',
            },
          ],
          title: 'Transactions',
          id: '1',
          limit: 1,
        };

        const dashboard = mockDashboard({widgets: [widget]});

        renderTestComponent({
          dashboard,
          orgFeatures: [...defaultOrgFeatures],
          params: {
            widgetIndex: '0',
          },
        });

        userEvent.click(await screen.findByText('Area Chart'));
        userEvent.click(screen.getByText('Table'));

        expect(screen.queryByText('Limit to 1 result')).not.toBeInTheDocument();
        expect(eventsv2Mock).toHaveBeenCalledWith(
          '/organizations/org-slug/eventsv2/',
          expect.objectContaining({
            query: expect.objectContaining({
              topEvents: undefined,
            }),
          })
        );
      });
    });
  });

  describe('with events', function () {
    it('should properly query for table fields', async function () {
      const defaultWidgetQuery = {
        name: '',
        fields: ['title', 'count()'],
        columns: ['title'],
        aggregates: ['count()'],
        conditions: '',
        orderby: '',
      };

      const defaultTableColumns = ['title', 'count()', 'count_unique(user)', 'epm()'];

      renderTestComponent({
        query: {
          source: DashboardWidgetSource.DISCOVERV2,
          defaultWidgetQuery: urlEncode(defaultWidgetQuery),
          displayType: DisplayType.LINE,
          defaultTableColumns,
        },
        orgFeatures: [...defaultOrgFeatures, 'discover-frontend-use-events-endpoint'],
      });

      expect(await screen.findByText('Line Chart')).toBeInTheDocument();
      userEvent.click(screen.getByText('Line Chart'));
      userEvent.click(screen.getByText('Table'));

      await waitFor(() => {
        expect(eventsMock).toHaveBeenLastCalledWith(
          '/organizations/org-slug/events/',
          expect.objectContaining({
            query: expect.objectContaining({
              field: defaultTableColumns,
            }),
          })
        );
      });
    });

    describe('Widget creation coming from other verticals', function () {
      it('shows the correct orderby when switching from a line chart to table', async function () {
        const defaultWidgetQuery = {
          name: '',
          fields: ['count_unique(user)'],
          columns: [],
          aggregates: ['count_unique(user)'],
          conditions: '',
          orderby: 'count_unique(user)',
        };

        const defaultTableColumns = ['title', 'count_unique(user)'];

        renderTestComponent({
          orgFeatures: [...defaultOrgFeatures, 'discover-frontend-use-events-endpoint'],
          query: {
            source: DashboardWidgetSource.DISCOVERV2,
            defaultWidgetQuery: urlEncode(defaultWidgetQuery),
            displayType: DisplayType.LINE,
            defaultTableColumns,
          },
        });

        userEvent.click(await screen.findByText('Line Chart'));
        userEvent.click(screen.getByText('Table'));

        expect(screen.getByText('count_unique(user)')).toBeInTheDocument();
        await waitFor(() => {
          expect(eventsMock).toHaveBeenLastCalledWith(
            '/organizations/org-slug/events/',
            expect.objectContaining({
              query: expect.objectContaining({
                field: defaultTableColumns,
                sort: ['count_unique(user)'],
              }),
            })
          );
        });
      });
    });
  });
});