import { Matcher, render, waitFor } from '@testing-library/react';
import { Provider } from 'react-redux';
import { BackendSrv, locationService, setBackendSrv, setDataSourceSrv } from '@grafana/runtime';
import { configureStore } from 'app/store/configureStore';
import RuleEditor from './RuleEditor';
import { Route, Router } from 'react-router-dom';
import React from 'react';
import { byLabelText, byRole, byTestId, byText } from 'testing-library-selector';
import { selectOptionInTest } from '@grafana/ui';
import { contextSrv } from 'app/core/services/context_srv';
import { mockDataSource, MockDataSourceSrv } from './mocks';
import userEvent from '@testing-library/user-event';
import { DataSourceInstanceSettings } from '@grafana/data';
import { typeAsJestMock } from 'test/helpers/typeAsJestMock';
import { getAllDataSources } from './utils/config';
import { fetchRulerRules, fetchRulerRulesGroup, fetchRulerRulesNamespace, setRulerRuleGroup } from './api/ruler';
import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
import { DashboardSearchHit } from 'app/features/search/types';
import { getDefaultQueries } from './utils/rule-form';
import { ExpressionEditorProps } from './components/rule-editor/ExpressionEditor';
import * as api from 'app/features/manage-dashboards/state/actions';
import { GrafanaAlertStateDecision } from 'app/types/unified-alerting-dto';

jest.mock('./components/rule-editor/ExpressionEditor', () => ({
  // eslint-disable-next-line react/display-name
  ExpressionEditor: ({ value, onChange }: ExpressionEditorProps) => (
    <input value={value} data-testid="expr" onChange={(e) => onChange(e.target.value)} />
  ),
}));

jest.mock('./api/ruler');
jest.mock('./utils/config');

// there's no angular scope in test and things go terribly wrong when trying to render the query editor row.
// lets just skip it
jest.mock('app/features/query/components/QueryEditorRow', () => ({
  // eslint-disable-next-line react/display-name
  QueryEditorRow: () => <p>hi</p>,
}));

const mocks = {
  getAllDataSources: typeAsJestMock(getAllDataSources),

  api: {
    fetchRulerRulesGroup: typeAsJestMock(fetchRulerRulesGroup),
    setRulerRuleGroup: typeAsJestMock(setRulerRuleGroup),
    fetchRulerRulesNamespace: typeAsJestMock(fetchRulerRulesNamespace),
    fetchRulerRules: typeAsJestMock(fetchRulerRules),
  },
};

function renderRuleEditor(identifier?: string) {
  const store = configureStore();

  locationService.push(identifier ? `/alerting/${identifier}/edit` : `/alerting/new`);

  return render(
    <Provider store={store}>
      <Router history={locationService.getHistory()}>
        <Route path={['/alerting/new', '/alerting/:id/edit']} component={RuleEditor} />
      </Router>
    </Provider>
  );
}

const ui = {
  inputs: {
    name: byLabelText('Rule name'),
    alertType: byTestId('alert-type-picker'),
    dataSource: byTestId('datasource-picker'),
    folder: byTestId('folder-picker'),
    namespace: byTestId('namespace-picker'),
    group: byTestId('group-picker'),
    annotationKey: (idx: number) => byTestId(`annotation-key-${idx}`),
    annotationValue: (idx: number) => byTestId(`annotation-value-${idx}`),
    labelKey: (idx: number) => byTestId(`label-key-${idx}`),
    labelValue: (idx: number) => byTestId(`label-value-${idx}`),
    expr: byTestId('expr'),
  },
  buttons: {
    save: byRole('button', { name: 'Save' }),
    addAnnotation: byRole('button', { name: /Add info/ }),
    addLabel: byRole('button', { name: /Add label/ }),
  },
};

describe('RuleEditor', () => {
  beforeEach(() => {
    jest.resetAllMocks();
    contextSrv.isEditor = true;
  });

  it('can create a new cloud alert', async () => {
    const dataSources = {
      default: mockDataSource(
        {
          type: 'prometheus',
          name: 'Prom',
          isDefault: true,
        },
        { alerting: true }
      ),
    };

    setDataSourceSrv(new MockDataSourceSrv(dataSources));
    mocks.getAllDataSources.mockReturnValue(Object.values(dataSources));
    mocks.api.setRulerRuleGroup.mockResolvedValue();
    mocks.api.fetchRulerRulesNamespace.mockResolvedValue([]);
    mocks.api.fetchRulerRulesGroup.mockResolvedValue({
      name: 'group2',
      rules: [],
    });
    mocks.api.fetchRulerRules.mockResolvedValue({
      namespace1: [
        {
          name: 'group1',
          rules: [],
        },
      ],
      namespace2: [
        {
          name: 'group2',
          rules: [],
        },
      ],
    });

    await renderRuleEditor();
    userEvent.type(await ui.inputs.name.find(), 'my great new rule');
    await clickSelectOption(ui.inputs.alertType.get(), /Cortex\/Loki managed alert/);
    const dataSourceSelect = ui.inputs.dataSource.get();
    userEvent.click(byRole('combobox').get(dataSourceSelect));
    await clickSelectOption(dataSourceSelect, 'Prom (default)');
    await waitFor(() => expect(mocks.api.fetchRulerRules).toHaveBeenCalled());
    await clickSelectOption(ui.inputs.namespace.get(), 'namespace2');
    await clickSelectOption(ui.inputs.group.get(), 'group2');

    userEvent.type(ui.inputs.expr.get(), 'up == 1');

    userEvent.type(ui.inputs.annotationValue(0).get(), 'some summary');
    userEvent.type(ui.inputs.annotationValue(1).get(), 'some description');

    // TODO remove skipPointerEventsCheck once https://github.com/jsdom/jsdom/issues/3232 is fixed
    userEvent.click(ui.buttons.addLabel.get(), undefined, { skipPointerEventsCheck: true });

    userEvent.type(ui.inputs.labelKey(0).get(), 'severity');
    userEvent.type(ui.inputs.labelValue(0).get(), 'warn');
    userEvent.type(ui.inputs.labelKey(1).get(), 'team');
    userEvent.type(ui.inputs.labelValue(1).get(), 'the a-team');

    // save and check what was sent to backend
    userEvent.click(ui.buttons.save.get());
    await waitFor(() => expect(mocks.api.setRulerRuleGroup).toHaveBeenCalled());
    expect(mocks.api.setRulerRuleGroup).toHaveBeenCalledWith('Prom', 'namespace2', {
      name: 'group2',
      rules: [
        {
          alert: 'my great new rule',
          annotations: { description: 'some description', summary: 'some summary' },
          labels: { severity: 'warn', team: 'the a-team' },
          expr: 'up == 1',
          for: '1m',
        },
      ],
    });
  });

  it('can create new grafana managed alert', async () => {
    const searchFolderMock = jest.spyOn(api, 'searchFolders').mockResolvedValue([
      {
        title: 'Folder A',
        id: 1,
      },
      {
        title: 'Folder B',
        id: 2,
      },
    ] as DashboardSearchHit[]);

    const dataSources = {
      default: mockDataSource({
        type: 'prometheus',
        name: 'Prom',
        isDefault: true,
      }),
    };

    setDataSourceSrv(new MockDataSourceSrv(dataSources));
    mocks.api.setRulerRuleGroup.mockResolvedValue();
    mocks.api.fetchRulerRulesNamespace.mockResolvedValue([]);

    // fill out the form
    await renderRuleEditor();
    userEvent.type(await ui.inputs.name.find(), 'my great new rule');
    await clickSelectOption(ui.inputs.alertType.get(), /Classic Grafana alerts based on thresholds/);
    const folderInput = await ui.inputs.folder.find();
    await waitFor(() => expect(searchFolderMock).toHaveBeenCalled());
    await clickSelectOption(folderInput, 'Folder A');

    userEvent.type(ui.inputs.annotationValue(0).get(), 'some summary');
    userEvent.type(ui.inputs.annotationValue(1).get(), 'some description');

    // TODO remove skipPointerEventsCheck once https://github.com/jsdom/jsdom/issues/3232 is fixed
    userEvent.click(ui.buttons.addLabel.get(), undefined, { skipPointerEventsCheck: true });

    userEvent.type(ui.inputs.labelKey(0).get(), 'severity');
    userEvent.type(ui.inputs.labelValue(0).get(), 'warn');
    userEvent.type(ui.inputs.labelKey(1).get(), 'team');
    userEvent.type(ui.inputs.labelValue(1).get(), 'the a-team');

    // save and check what was sent to backend
    userEvent.click(ui.buttons.save.get());
    await waitFor(() => expect(mocks.api.setRulerRuleGroup).toHaveBeenCalled());
    expect(mocks.api.setRulerRuleGroup).toHaveBeenCalledWith(GRAFANA_RULES_SOURCE_NAME, 'Folder A', {
      interval: '1m',
      name: 'my great new rule',
      rules: [
        {
          annotations: { description: 'some description', summary: 'some summary' },
          labels: { severity: 'warn', team: 'the a-team' },
          for: '5m',
          grafana_alert: {
            condition: 'B',
            data: getDefaultQueries(),
            exec_err_state: 'Alerting',
            no_data_state: 'NoData',
            title: 'my great new rule',
          },
        },
      ],
    });
  });

  it('can create a new cloud recording rule', async () => {
    const dataSources = {
      default: mockDataSource(
        {
          type: 'prometheus',
          name: 'Prom',
          isDefault: true,
        },
        { alerting: true }
      ),
    };

    setDataSourceSrv(new MockDataSourceSrv(dataSources));
    mocks.getAllDataSources.mockReturnValue(Object.values(dataSources));
    mocks.api.setRulerRuleGroup.mockResolvedValue();
    mocks.api.fetchRulerRulesNamespace.mockResolvedValue([]);
    mocks.api.fetchRulerRulesGroup.mockResolvedValue({
      name: 'group2',
      rules: [],
    });
    mocks.api.fetchRulerRules.mockResolvedValue({
      namespace1: [
        {
          name: 'group1',
          rules: [],
        },
      ],
      namespace2: [
        {
          name: 'group2',
          rules: [],
        },
      ],
    });

    await renderRuleEditor();
    userEvent.type(await ui.inputs.name.find(), 'my great new recording rule');
    await clickSelectOption(ui.inputs.alertType.get(), /Cortex\/Loki managed recording rule/);
    const dataSourceSelect = ui.inputs.dataSource.get();
    userEvent.click(byRole('combobox').get(dataSourceSelect));
    await clickSelectOption(dataSourceSelect, 'Prom (default)');
    await waitFor(() => expect(mocks.api.fetchRulerRules).toHaveBeenCalled());
    await clickSelectOption(ui.inputs.namespace.get(), 'namespace2');
    await clickSelectOption(ui.inputs.group.get(), 'group2');

    userEvent.type(ui.inputs.expr.get(), 'up == 1');

    // TODO remove skipPointerEventsCheck once https://github.com/jsdom/jsdom/issues/3232 is fixed
    userEvent.click(ui.buttons.addLabel.get(), undefined, { skipPointerEventsCheck: true });

    userEvent.type(ui.inputs.labelKey(1).get(), 'team');
    userEvent.type(ui.inputs.labelValue(1).get(), 'the a-team');

    // try to save, find out that recording rule name is invalid
    userEvent.click(ui.buttons.save.get());
    await waitFor(() =>
      expect(
        byText(
          'Recording rule name must be valid metric name. It may only contain letters, numbers, and colons. It may not contain whitespace.'
        ).get()
      ).toBeInTheDocument()
    );
    expect(mocks.api.setRulerRuleGroup).not.toBeCalled();

    // fix name and re-submit
    userEvent.type(await ui.inputs.name.find(), '{selectall}{del}my:great:new:recording:rule');
    userEvent.click(ui.buttons.save.get());

    // save and check what was sent to backend
    userEvent.click(ui.buttons.save.get());
    await waitFor(() => expect(mocks.api.setRulerRuleGroup).toHaveBeenCalled());
    expect(mocks.api.setRulerRuleGroup).toHaveBeenCalledWith('Prom', 'namespace2', {
      name: 'group2',
      rules: [
        {
          record: 'my:great:new:recording:rule',
          labels: { team: 'the a-team' },
          expr: 'up == 1',
        },
      ],
    });
  });

  it('can edit grafana managed rule', async () => {
    const uid = 'FOOBAR123';
    const folder = {
      title: 'Folder A',
      uid: 'abcd',
      id: 1,
    };
    const searchFolderMock = jest.spyOn(api, 'searchFolders').mockResolvedValue([folder] as DashboardSearchHit[]);
    const getFolderByUid = jest.fn().mockResolvedValue({
      ...folder,
      canSave: true,
    });
    const dataSources = {
      default: mockDataSource({
        type: 'prometheus',
        name: 'Prom',
        isDefault: true,
      }),
    };

    const backendSrv = {
      getFolderByUid,
    } as any as BackendSrv;
    setBackendSrv(backendSrv);
    setDataSourceSrv(new MockDataSourceSrv(dataSources));
    mocks.api.setRulerRuleGroup.mockResolvedValue();
    mocks.api.fetchRulerRulesNamespace.mockResolvedValue([]);
    mocks.api.fetchRulerRules.mockResolvedValue({
      [folder.title]: [
        {
          interval: '1m',
          name: 'my great new rule',
          rules: [
            {
              annotations: { description: 'some description', summary: 'some summary' },
              labels: { severity: 'warn', team: 'the a-team' },
              for: '5m',
              grafana_alert: {
                uid,
                namespace_uid: 'abcd',
                namespace_id: 1,
                condition: 'B',
                data: getDefaultQueries(),
                exec_err_state: GrafanaAlertStateDecision.Alerting,
                no_data_state: GrafanaAlertStateDecision.NoData,
                title: 'my great new rule',
              },
            },
          ],
        },
      ],
    });

    await renderRuleEditor(uid);
    await waitFor(() => expect(searchFolderMock).toHaveBeenCalled());

    // check that it's filled in
    const nameInput = await ui.inputs.name.find();
    expect(nameInput).toHaveValue('my great new rule');
    expect(ui.inputs.folder.get()).toHaveTextContent(new RegExp(folder.title));
    expect(ui.inputs.annotationValue(0).get()).toHaveValue('some description');
    expect(ui.inputs.annotationValue(1).get()).toHaveValue('some summary');

    // add an annotation
    await clickSelectOption(ui.inputs.annotationKey(2).get(), /Add new/);
    userEvent.type(byRole('textbox').get(ui.inputs.annotationKey(2).get()), 'custom');
    userEvent.type(ui.inputs.annotationValue(2).get(), 'value');

    //add a label
    userEvent.type(ui.inputs.labelKey(2).get(), 'custom');
    userEvent.type(ui.inputs.labelValue(2).get(), 'value');

    // save and check what was sent to backend
    userEvent.click(ui.buttons.save.get());
    await waitFor(() => expect(mocks.api.setRulerRuleGroup).toHaveBeenCalled());

    expect(mocks.api.setRulerRuleGroup).toHaveBeenCalledWith(GRAFANA_RULES_SOURCE_NAME, 'Folder A', {
      interval: '1m',
      name: 'my great new rule',
      rules: [
        {
          annotations: { description: 'some description', summary: 'some summary', custom: 'value' },
          labels: { severity: 'warn', team: 'the a-team', custom: 'value' },
          for: '5m',
          grafana_alert: {
            uid,
            condition: 'B',
            data: getDefaultQueries(),
            exec_err_state: 'Alerting',
            no_data_state: 'NoData',
            title: 'my great new rule',
          },
        },
      ],
    });
  });

  it('for cloud alerts, should only allow to select editable rules sources', async () => {
    const dataSources: Record<string, DataSourceInstanceSettings<any>> = {
      // can edit rules
      loki: mockDataSource(
        {
          type: DataSourceType.Loki,
          name: 'loki with ruler',
        },
        { alerting: true }
      ),
      loki_disabled: mockDataSource(
        {
          type: DataSourceType.Loki,
          name: 'loki disabled for alerting',
          jsonData: {
            manageAlerts: false,
          },
        },
        { alerting: true }
      ),
      // can edit rules
      prom: mockDataSource(
        {
          type: DataSourceType.Prometheus,
          name: 'cortex with ruler',
        },
        { alerting: true }
      ),
      // cannot edit rules
      loki_local_rule_store: mockDataSource(
        {
          type: DataSourceType.Loki,
          name: 'loki with local rule store',
        },
        { alerting: true }
      ),
      // cannot edit rules
      prom_no_ruler_api: mockDataSource(
        {
          type: DataSourceType.Loki,
          name: 'cortex without ruler api',
        },
        { alerting: true }
      ),
      // not a supported datasource type
      splunk: mockDataSource(
        {
          type: 'splunk',
          name: 'splunk',
        },
        { alerting: true }
      ),
    };

    mocks.api.fetchRulerRulesGroup.mockImplementation(async (dataSourceName: string) => {
      if (dataSourceName === 'loki with ruler' || dataSourceName === 'cortex with ruler') {
        return null;
      }
      if (dataSourceName === 'loki with local rule store') {
        throw {
          status: 400,
          data: {
            message: 'GetRuleGroup unsupported in rule local store',
          },
        };
      }
      if (dataSourceName === 'cortex without ruler api') {
        throw new Error('404 from rules config endpoint. Perhaps ruler API is not enabled?');
      }
      return null;
    });

    setDataSourceSrv(new MockDataSourceSrv(dataSources));
    mocks.getAllDataSources.mockReturnValue(Object.values(dataSources));

    // render rule editor, select cortex/loki managed alerts
    await renderRuleEditor();
    await ui.inputs.name.find();
    await clickSelectOption(ui.inputs.alertType.get(), /Cortex\/Loki managed alert/);

    // wait for ui theck each datasource if it supports rule editing
    await waitFor(() => expect(mocks.api.fetchRulerRulesGroup).toHaveBeenCalledTimes(4));

    // check that only rules sources that have ruler available are there
    const dataSourceSelect = ui.inputs.dataSource.get();
    userEvent.click(byRole('combobox').get(dataSourceSelect));
    expect(await byText('loki with ruler').query()).toBeInTheDocument();
    expect(byText('cortex with ruler').query()).toBeInTheDocument();
    expect(byText('loki with local rule store').query()).not.toBeInTheDocument();
    expect(byText('prom without ruler api').query()).not.toBeInTheDocument();
    expect(byText('splunk').query()).not.toBeInTheDocument();
    expect(byText('loki disabled for alerting').query()).not.toBeInTheDocument();
  });
});

const clickSelectOption = async (selectElement: HTMLElement, optionText: Matcher): Promise<void> => {
  userEvent.click(byRole('combobox').get(selectElement));
  await selectOptionInTest(selectElement, optionText as string);
};
