import { act, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { type FormEvent } from 'react';
import * as React from 'react';
import { of } from 'rxjs';

import {
  LoadingState,
  type PanelData,
  getDefaultTimeRange,
  toDataFrame,
  FieldType,
  VariableSupportType,
} from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { setRunRequest } from '@grafana/runtime';
import { VariableRefresh, VariableSort } from '@grafana/schema';
import { mockDataSource } from 'app/features/alerting/unified/mocks';
import { LegacyVariableQueryEditor } from 'app/features/variables/editor/LegacyVariableQueryEditor';
import { getVariableQueryEditor } from 'app/features/variables/editor/getVariableQueryEditor';

import { QueryVariableEditorForm } from './QueryVariableForm';

const defaultDatasource = mockDataSource({
  name: 'Default Test Data Source',
  type: 'test',
});

const promDatasource = mockDataSource({
  name: 'Prometheus',
  type: 'prometheus',
});

jest.mock('@grafana/runtime', () => ({
  ...jest.requireActual('@grafana/runtime'),
  config: {
    ...jest.requireActual('@grafana/runtime').config,
    featureToggles: {
      multiPropsVariables: true,
    },
  },
  getDataSourceSrv: () => ({
    get: async () => ({
      ...defaultDatasource,
      variables: {
        getType: () => VariableSupportType.Custom,
        query: jest.fn(),
        editor: jest.fn().mockImplementation(LegacyVariableQueryEditor),
      },
    }),
    getList: () => [defaultDatasource, promDatasource],
    getInstanceSettings: (uid: string) => (uid === promDatasource.uid ? promDatasource : defaultDatasource),
  }),
}));

const runRequestMock = jest.fn().mockReturnValue(
  of<PanelData>({
    state: LoadingState.Done,
    series: [
      toDataFrame({
        fields: [{ name: 'text', type: FieldType.string, values: ['val1', 'val2', 'val11'] }],
      }),
    ],
    timeRange: getDefaultTimeRange(),
  })
);

setRunRequest(runRequestMock);

jest.mock('app/features/variables/editor/getVariableQueryEditor', () => ({
  ...jest.requireActual('app/features/variables/editor/getVariableQueryEditor'),
  getVariableQueryEditor: jest.fn(),
}));

describe('QueryVariableEditorForm', () => {
  const mockOnDataSourceChange = jest.fn();
  const mockOnQueryChange = jest.fn();
  const mockOnLegacyQueryChange = jest.fn();
  const mockOnRegExChange = jest.fn();
  const mockOnRegexApplyToChange = jest.fn();
  const mockOnSortChange = jest.fn();
  const mockOnRefreshChange = jest.fn();
  const mockOnMultiChange = jest.fn();
  const mockOnIncludeAllChange = jest.fn();
  const mockOnAllValueChange = jest.fn();
  const mockOnAllowCustomValueChange = jest.fn();
  const mockOnStaticOptionsChange = jest.fn();
  const mockOnStaticOptionsOrderChange = jest.fn();

  const defaultProps: React.ComponentProps<typeof QueryVariableEditorForm> = {
    datasource: { uid: defaultDatasource.uid, type: defaultDatasource.type },
    onDataSourceChange: mockOnDataSourceChange,
    query: 'my-query',
    onQueryChange: mockOnQueryChange,
    onLegacyQueryChange: mockOnLegacyQueryChange,
    timeRange: getDefaultTimeRange(),
    regex: '.*',
    onRegExChange: mockOnRegExChange,
    regexApplyTo: 'value',
    onRegexApplyToChange: mockOnRegexApplyToChange,
    sort: VariableSort.alphabeticalAsc,
    onSortChange: mockOnSortChange,
    refresh: VariableRefresh.onDashboardLoad,
    onRefreshChange: mockOnRefreshChange,
    allowCustomValue: true,
    isMulti: true,
    onMultiChange: mockOnMultiChange,
    includeAll: true,
    onIncludeAllChange: mockOnIncludeAllChange,
    allValue: 'custom all value',
    onAllValueChange: mockOnAllValueChange,
    onAllowCustomValueChange: mockOnAllowCustomValueChange,
    onStaticOptionsChange: mockOnStaticOptionsChange,
    onStaticOptionsOrderChange: mockOnStaticOptionsOrderChange,
    options: [],
  };

  async function setup(props?: React.ComponentProps<typeof QueryVariableEditorForm>) {
    jest.mocked(getVariableQueryEditor).mockResolvedValue(LegacyVariableQueryEditor);
    return {
      renderer: await act(() => render(<QueryVariableEditorForm {...defaultProps} {...props} />)),
      user: userEvent.setup(),
    };
  }

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

  it('should render the component with initializing the components correctly', async () => {
    const {
      renderer: { getByTestId, getByRole },
    } = await setup();
    const dataSourcePicker = getByTestId(selectors.components.DataSourcePicker.inputV2);
    //const queryEditor = getByTestId('query-editor');
    const regexInput = getByTestId(
      selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2
    );
    const regexApplyToSelect = getByTestId(
      selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExApplyToSelectV2
    );
    const sortSelect = getByTestId(
      selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsSortSelectV2
    );
    const refreshSelect = getByTestId(
      selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRefreshSelectV2
    );

    const multiSwitch = getByTestId(
      selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsMultiSwitch
    );
    const includeAllSwitch = getByTestId(
      selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsIncludeAllSwitch
    );
    const allValueInput = getByTestId(
      selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsCustomAllInput
    );
    const allowCustomValueSwitch = getByTestId(
      selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsAllowCustomValueSwitch
    );

    const staticOptionsToggle = getByTestId(
      selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsToggle
    );

    expect(dataSourcePicker).toBeInTheDocument();
    expect(dataSourcePicker.getAttribute('placeholder')).toBe('Default Test Data Source');
    expect(regexInput).toBeInTheDocument();
    expect(regexInput).toHaveValue('.*');
    expect(regexApplyToSelect).toBeInTheDocument();
    expect(getByRole('radio', { name: 'Variable value' })).toBeChecked();
    expect(sortSelect).toBeInTheDocument();
    expect(sortSelect).toHaveTextContent('Alphabetical (asc)');
    expect(refreshSelect).toBeInTheDocument();
    expect(getByRole('radio', { name: 'On dashboard load' })).toBeChecked();
    expect(multiSwitch).toBeInTheDocument();
    expect(multiSwitch).toBeChecked();
    expect(allowCustomValueSwitch).toBeInTheDocument();
    expect(allowCustomValueSwitch).toBeChecked();
    expect(includeAllSwitch).toBeInTheDocument();
    expect(includeAllSwitch).toBeChecked();
    expect(allValueInput).toBeInTheDocument();
    expect(allValueInput).toHaveValue('custom all value');
    expect(staticOptionsToggle).toBeInTheDocument();
  });

  it('should call onDataSourceChange when changing the datasource', async () => {
    const {
      renderer: { getByTestId },
    } = await setup();
    const dataSourcePicker = getByTestId(selectors.components.DataSourcePicker.inputV2);
    await userEvent.click(dataSourcePicker);
    await userEvent.click(screen.getByText(/prometheus/i));

    expect(mockOnDataSourceChange).toHaveBeenCalledTimes(1);
    expect(mockOnDataSourceChange).toHaveBeenCalledWith(promDatasource);
  });

  it('should call onQueryChange when changing the query', async () => {
    const {
      renderer: { getByTestId },
    } = await setup();
    const queryEditor = getByTestId(
      selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsQueryInput
    );

    await waitFor(async () => {
      await userEvent.type(queryEditor, '-new');
      await userEvent.tab();
    });

    expect(mockOnLegacyQueryChange).toHaveBeenCalledTimes(1);
    expect(mockOnLegacyQueryChange).toHaveBeenCalledWith('my-query-new', expect.anything());
  });

  it('should call onRegExChange when changing the regex', async () => {
    const {
      renderer: { getByTestId },
    } = await setup();
    const regexInput = getByTestId(
      selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2
    );
    await userEvent.type(regexInput, '{backspace}?');
    await userEvent.tab();
    expect(mockOnRegExChange).toHaveBeenCalledTimes(1);
    expect(
      ((mockOnRegExChange.mock.calls[0][0] as FormEvent<HTMLTextAreaElement>).target as HTMLTextAreaElement).value
    ).toBe('.?');
  });

  it('should call onRegexApplyToChange when selecting the regex apply to option', async () => {
    const {
      renderer: { getByTestId },
    } = await setup();
    const regexApplyToSelect = getByTestId(
      selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExApplyToSelectV2
    );
    await userEvent.click(regexApplyToSelect);
    const anotherOption = screen.getByText('Display text');
    await userEvent.click(anotherOption);

    expect(mockOnRegexApplyToChange).toHaveBeenCalledTimes(1);
    expect(mockOnRegexApplyToChange).toHaveBeenCalledWith('text');
  });

  it('should call onSortChange when changing the sort', async () => {
    const {
      renderer: { getByTestId },
    } = await setup();
    const sortSelect = getByTestId(
      selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsSortSelectV2
    );
    await userEvent.click(sortSelect); // open the select
    const anotherOption = await screen.getByText('Alphabetical (desc)');
    await userEvent.click(anotherOption);

    expect(mockOnSortChange).toHaveBeenCalledTimes(1);
    expect(mockOnSortChange).toHaveBeenCalledWith(
      expect.objectContaining({ value: VariableSort.alphabeticalDesc }),
      expect.anything()
    );
  });

  it('should call onRefreshChange when changing the refresh', async () => {
    const {
      renderer: { getByTestId },
    } = await setup();
    const refreshSelect = getByTestId(
      selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRefreshSelectV2
    );
    await userEvent.click(refreshSelect); // open the select
    const anotherOption = await screen.getByText('On time range change');
    await userEvent.click(anotherOption);

    expect(mockOnRefreshChange).toHaveBeenCalledTimes(1);
    expect(mockOnRefreshChange).toHaveBeenCalledWith(VariableRefresh.onTimeRangeChanged);
  });

  it('should call onMultiChange when changing the multi switch', async () => {
    const {
      renderer: { getByTestId },
    } = await setup();
    const multiSwitch = getByTestId(
      selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsMultiSwitch
    );
    await userEvent.click(multiSwitch);
    expect(mockOnMultiChange).toHaveBeenCalledTimes(1);
    expect(
      (mockOnMultiChange.mock.calls[0][0] as FormEvent<HTMLInputElement>).target as HTMLInputElement
    ).toBeChecked();
  });

  it('should call onIncludeAllChange when changing the include all switch', async () => {
    const {
      renderer: { getByTestId },
    } = await setup();
    const includeAllSwitch = getByTestId(
      selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsIncludeAllSwitch
    );
    await userEvent.click(includeAllSwitch);
    expect(mockOnIncludeAllChange).toHaveBeenCalledTimes(1);
    expect(
      (mockOnIncludeAllChange.mock.calls[0][0] as FormEvent<HTMLInputElement>).target as HTMLInputElement
    ).toBeChecked();
  });

  it('should call onAllowCustomValue when changing the allow custom value switch', async () => {
    const {
      renderer: { getByTestId },
    } = await setup();
    const allowCustomValue = getByTestId(
      selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsAllowCustomValueSwitch
    );
    await userEvent.click(allowCustomValue);
    expect(mockOnAllowCustomValueChange).toHaveBeenCalledTimes(1);
    expect(
      (mockOnAllowCustomValueChange.mock.calls[0][0] as FormEvent<HTMLInputElement>).target as HTMLInputElement
    ).toBeChecked();
  });

  it('should call onAllValueChange when changing the all value', async () => {
    const {
      renderer: { getByTestId },
    } = await setup();
    const allValueInput = getByTestId(
      selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsCustomAllInput
    );
    await userEvent.type(allValueInput, ' and another value');
    await userEvent.tab();
    expect(mockOnAllValueChange).toHaveBeenCalledTimes(1);
    expect(
      ((mockOnAllValueChange.mock.calls[0][0] as FormEvent<HTMLInputElement>).target as HTMLInputElement).value
    ).toBe('custom all value and another value');
  });

  it('should call onStaticOptionsOrderChange when changing the static options order', async () => {
    const {
      renderer: { getByTestId },
    } = await setup();

    // First enable static options
    const staticOptionsToggle = getByTestId(
      selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsToggle
    );
    await userEvent.click(staticOptionsToggle);

    // Then access the dropdown
    const staticOptionsOrderDropdown = getByTestId(
      selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsOrderDropdown
    );
    await userEvent.click(staticOptionsOrderDropdown); // open the select
    const anotherOption = await screen.getByText('After query values');
    await userEvent.click(anotherOption);

    expect(mockOnStaticOptionsOrderChange).toHaveBeenCalledTimes(1);
    expect(mockOnStaticOptionsOrderChange.mock.calls[0][0]).toBe('after');
  });

  it('should call onStaticOptionsChange when adding a static option', async () => {
    const {
      renderer: { getByTestId, getByPlaceholderText },
    } = await setup();

    // First enable static options
    const staticOptionsToggle = getByTestId(
      selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsToggle
    );
    await userEvent.click(staticOptionsToggle);

    const addButton = getByTestId(selectors.pages.Dashboard.Settings.Variables.Edit.StaticOptionsEditor.addButton);
    await userEvent.click(addButton);

    // Enter label for the new option
    await userEvent.type(getByPlaceholderText('text'), 'New Option Label[Tab]');
    await userEvent.type(getByPlaceholderText('value'), 'new-option-value[Tab]');
    await screen.findByDisplayValue('new-option-value');

    expect(mockOnStaticOptionsChange).toHaveBeenCalled();
    expect(mockOnStaticOptionsChange.mock.lastCall[0]).toEqual([
      {
        value: 'new-option-value',
        label: 'New Option Label',
        properties: { value: 'new-option-value', text: 'New Option Label' },
      },
    ]);
  });

  it('should call onStaticOptionsChange when removing a static option', async () => {
    const staticOptions = [
      { value: 'option1', label: 'Option 1', properties: { value: 'option1', text: 'Option 1' } },
      { value: 'option2', label: 'Option 2', properties: { value: 'option2', text: 'Option 2' } },
    ];
    const {
      renderer: { getAllByLabelText },
    } = await setup({
      ...defaultProps,
      staticOptions,
    });

    const deleteButtons = getAllByLabelText('Remove option');

    // Remove the first option
    await userEvent.click(deleteButtons[0]);

    expect(mockOnStaticOptionsChange).toHaveBeenCalledTimes(1);
    // Should call with only the second option remaining
    expect(mockOnStaticOptionsChange.mock.calls[0][0]).toEqual([staticOptions[1]]);
  });

  it('should call onStaticOptionsChange when editing a static option label', async () => {
    const {
      renderer: { getByPlaceholderText },
    } = await setup({
      ...defaultProps,
      staticOptions: [{ value: 'test', label: 'Test Label', properties: { value: 'test', text: 'Test Label' } }],
    });

    const labelInput = getByPlaceholderText('text');
    await userEvent.clear(labelInput);
    await userEvent.type(labelInput, 'Updated Label[Tab]');

    expect(mockOnStaticOptionsChange).toHaveBeenCalledWith([
      { value: 'test', label: 'Updated Label', properties: { value: 'test', text: 'Updated Label' } },
    ]);
  });

  it('should call onStaticOptionsChange when editing a static option value', async () => {
    const {
      renderer: { getByPlaceholderText },
    } = await setup({
      ...defaultProps,
      staticOptions: [
        { value: 'old-value', label: 'Test Label', properties: { value: 'old-value', text: 'Test Label' } },
      ],
    });

    const valueInput = getByPlaceholderText('value');

    await userEvent.clear(valueInput);
    await userEvent.type(valueInput, 'new-value[Tab]');

    expect(mockOnStaticOptionsChange).toHaveBeenCalledWith([
      { value: 'new-value', label: 'Test Label', properties: { value: 'new-value', text: 'Test Label' } },
    ]);
  });

  it('should remove static options and hide UI elements when static options switch is unchecked', async () => {
    const {
      renderer: { getByTestId, queryByTestId, getAllByTestId },
    } = await setup({
      ...defaultProps,
      staticOptions: [
        { value: 'option1', label: 'Option 1' },
        { value: 'option2', label: 'Option 2' },
      ],
    });

    // Static options should be visible initially
    expect(
      getByTestId(selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsToggle)
    ).toBeChecked();
    expect(
      getByTestId(
        selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsOrderDropdown
      )
    ).toBeInTheDocument();

    // Option rows should be visible
    expect(getAllByTestId(selectors.pages.Dashboard.Settings.Variables.Edit.StaticOptionsEditor.row)).toHaveLength(2);

    // Uncheck the static options switch
    const staticOptionsToggle = getByTestId(
      selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsToggle
    );
    await userEvent.click(staticOptionsToggle);

    // Should call onStaticOptionsChange to remove static options
    expect(mockOnStaticOptionsChange).toHaveBeenCalledTimes(1);
    expect(mockOnStaticOptionsChange).toHaveBeenCalledWith(undefined);

    // Static options UI elements should be hidden
    expect(
      queryByTestId(
        selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsOrderDropdown
      )
    ).not.toBeInTheDocument();
    expect(
      queryByTestId(selectors.pages.Dashboard.Settings.Variables.Edit.StaticOptionsEditor.row)
    ).not.toBeInTheDocument();
  });
});
