import { produce } from 'immer';
import { Route, Routes } from 'react-router-dom-v5-compat';
import { clickSelectOption } from 'test/helpers/selectOptionInTest';
import { render, screen, userEvent, within } from 'test/test-utils';
import { byLabelText, byRole, byTestId } from 'testing-library-selector';

import { config } from '@grafana/runtime';
import { AppNotificationList } from 'app/core/components/AppNotifications/AppNotificationList';
import { PERMISSIONS_NOTIFICATION_POLICIES } from 'app/features/alerting/unified/components/notification-policies/permissions';
import { setupMswServer } from 'app/features/alerting/unified/mockApi';
import {
  getErrorResponse,
  makeAllAlertmanagerConfigFetchFail,
  makeAllK8sGetEndpointsFail,
} from 'app/features/alerting/unified/mocks/server/configure';
import { MIMIR_DATASOURCE_UID } from 'app/features/alerting/unified/mocks/server/constants';
import {
  getAlertmanagerConfig,
  setAlertmanagerConfig,
  setAlertmanagerStatus,
} from 'app/features/alerting/unified/mocks/server/entities/alertmanagers';
import {
  TIME_INTERVAL_NAME_FILE_PROVISIONED,
  TIME_INTERVAL_NAME_HAPPY_PATH,
} from 'app/features/alerting/unified/mocks/server/handlers/k8s/timeIntervals.k8s';
import { setupDataSources } from 'app/features/alerting/unified/testSetup/datasources';
import {
  AlertManagerDataSourceJsonData,
  AlertManagerImplementation,
  MatcherOperator,
  RouteWithID,
} from 'app/plugins/datasource/alertmanager/types';
import { AccessControlAction } from 'app/types/accessControl';

import NotificationPolicies from './NotificationPoliciesPage';
import { findRoutesMatchingFilters } from './components/notification-policies/PoliciesTree';
import PolicyPage from './components/notification-policies/PolicyPage';
import {
  createKubernetesRoutingTreeSpec,
  k8sRouteToRoute,
} from './components/notification-policies/useNotificationPolicyRoute';
import {
  grantUserPermissions,
  mockDataSource,
  someCloudAlertManagerConfig,
  someCloudAlertManagerStatus,
} from './mocks';
import {
  deleteRoutingTree,
  getRoutingTree,
  resetRoutingTreeMap,
  setRoutingTree,
} from './mocks/server/entities/k8s/routingtrees';
import { ALERTMANAGER_NAME_QUERY_KEY } from './utils/constants';
import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
import { ROOT_ROUTE_NAME } from './utils/k8s/constants';

jest.mock('./useRouteGroupsMatcher');

setupMswServer();

const updateTiming = async (selectElement: HTMLElement, value: string): Promise<void> => {
  const user = userEvent.setup();
  const input = byRole('textbox').get(selectElement);
  await user.clear(input);
  await user.type(input, value);
};

const openDefaultPolicyEditModal = async () => {
  const user = userEvent.setup();
  await user.click(await ui.moreActionsDefaultPolicy.find());
  await user.click(await ui.editButton.find());
};

const openEditModal = async (
  /** (zero-based) Index of the policy in the list to open the edit modal for  */
  index: number
) => {
  const user = userEvent.setup();
  await user.click((await ui.moreActions.findAll())[index]);
  await user.click(await ui.editButton.find());
};

// This is the page for the default policy when alertingMultiplePolicies is disabled.
const renderNotificationPolicies = (alertManagerSourceName: string = GRAFANA_RULES_SOURCE_NAME) =>
  render(
    <>
      <AppNotificationList />
      <NotificationPolicies />
    </>,
    {
      historyOptions: {
        initialEntries: [
          '/alerting/routes' +
            (alertManagerSourceName ? `?${ALERTMANAGER_NAME_QUERY_KEY}=${alertManagerSourceName}` : ''),
        ],
      },
    }
  );

// This is the page for the default policy when alertingMultiplePolicies is enabled.
const renderPolicyPage = (routeName: string) => () =>
  render(
    <Routes>
      <Route
        path="/alerting/routes/policy/:name/edit"
        element={
          <>
            <AppNotificationList />
            <PolicyPage />
          </>
        }
      />
    </Routes>,
    {
      historyOptions: { initialEntries: [`/alerting/routes/policy/${routeName}/edit`] },
    }
  );

const dataSources = {
  am: mockDataSource({
    name: 'Alertmanager',
    type: DataSourceType.Alertmanager,
  }),
  promAlertManager: mockDataSource<AlertManagerDataSourceJsonData>({
    name: 'PromManager',
    type: DataSourceType.Alertmanager,
    uid: 'prometheusAlertManager',
    jsonData: {
      implementation: AlertManagerImplementation.prometheus,
    },
  }),
  mimir: mockDataSource<AlertManagerDataSourceJsonData>({
    name: 'MimirAlertmanager',
    type: DataSourceType.Alertmanager,
    uid: MIMIR_DATASOURCE_UID,
    jsonData: {
      implementation: AlertManagerImplementation.mimir,
    },
  }),
};

const ui = {
  /** Row of policy tree containing default policy */
  rootRouteContainer: byTestId('am-root-route-container'),
  /** (deeply) Nested rows of policies under the default/root policy */
  row: byTestId('am-route-container'),

  newChildPolicyButton: byRole('button', { name: /New child policy/ }),
  newSiblingPolicyButton: byRole('button', { name: /Add new policy/ }),

  moreActionsDefaultPolicy: byLabelText(/more actions for default policy/i),
  moreActions: byLabelText(/more actions for policy/i),
  editButton: byRole('menuitem', { name: 'Edit' }),

  saveButton: byRole('button', { name: /update (default )?policy/i }),
  deleteRouteButton: byRole('menuitem', { name: 'Delete' }),

  receiverSelect: byTestId('am-receiver-select'),
  groupSelect: byTestId('am-group-select'),
  muteTimingSelect: byTestId('am-mute-timing-select'),

  groupWaitContainer: byTestId('am-group-wait'),
  groupIntervalContainer: byTestId('am-group-interval'),
  groupRepeatContainer: byTestId('am-repeat-interval'),

  confirmDeleteModal: byRole('dialog'),
  confirmDeleteButton: byRole('button', { name: /yes, delete policy/i }),
};

const getRootRoute = async () => {
  return ui.rootRouteContainer.find();
};

const OtherPolicyName = 'Some Other Policy Name';

describe.each([
  { testName: 'NotificationPoliciesPage', renderPage: renderNotificationPolicies, routeName: ROOT_ROUTE_NAME },
  { testName: 'PolicyPage', renderPage: renderPolicyPage(ROOT_ROUTE_NAME), routeName: ROOT_ROUTE_NAME },
  { testName: 'PolicyPage', renderPage: renderPolicyPage(OtherPolicyName), routeName: OtherPolicyName },
])('$testName - Policy: $routeName', ({ testName, renderPage, routeName }) => {
  // combobox hack :/
  beforeAll(() => {
    const mockGetBoundingClientRect = jest.fn(() => ({
      width: 120,
      height: 120,
      top: 0,
      left: 0,
      bottom: 0,
      right: 0,
    }));

    Object.defineProperty(Element.prototype, 'getBoundingClientRect', {
      value: mockGetBoundingClientRect,
    });
  });

  beforeEach(() => {
    setupDataSources(...Object.values(dataSources));
    grantUserPermissions([
      AccessControlAction.AlertingInstanceRead,
      AccessControlAction.AlertingInstanceCreate,
      AccessControlAction.AlertingInstanceUpdate,
      AccessControlAction.AlertingInstancesExternalRead,
      AccessControlAction.AlertingInstancesExternalWrite,
      AccessControlAction.AlertingNotificationsRead,
      AccessControlAction.AlertingNotificationsWrite,
      AccessControlAction.AlertingNotificationsExternalRead,
      AccessControlAction.AlertingNotificationsExternalWrite,
      ...PERMISSIONS_NOTIFICATION_POLICIES,
    ]);
    resetRoutingTreeMap();
    // Copy default config to other policy name and clear the default, so we guarantee the tests are validating against
    // the custom route.
    const defaultRoute = getRoutingTree(ROOT_ROUTE_NAME)!;
    defaultRoute.metadata.name = routeName;
    deleteRoutingTree(ROOT_ROUTE_NAME);
    setRoutingTree(routeName, defaultRoute);
  });

  afterAll(() => {
    resetRoutingTreeMap();
  });

  it('loads and shows routes', async () => {
    const defaultRoute = k8sRouteToRoute(getRoutingTree(routeName)!);

    renderPage();
    const rootRouteEl = await getRootRoute();

    expect(rootRouteEl).toHaveTextContent(new RegExp(`delivered to ${defaultRoute?.receiver}`, 'i'));
    expect(rootRouteEl).toHaveTextContent(new RegExp(`grouped by ${defaultRoute?.group_by?.join(', ')}`, 'i'));
    expect(rootRouteEl).toHaveTextContent(/wait 30s to group/i);
    expect(rootRouteEl).toHaveTextContent(/wait 5m before sending/i);
    expect(rootRouteEl).toHaveTextContent(/repeated every 4h/i);

    const rows = await ui.row.findAll();
    expect(rows).toHaveLength(5);

    defaultRoute?.routes?.forEach((route) => {
      Object.entries(route.match ?? {}).forEach(([label, value]) => {
        expect(screen.getByText(`${label} = ${value}`)).toBeInTheDocument();
      });

      Object.entries(route.match_re ?? {}).forEach(([label, value]) => {
        expect(screen.getByText(`${label} =~ ${value}`)).toBeInTheDocument();
      });

      if (route.group_by) {
        expect(rows.some((row) => row?.textContent?.includes(`Grouped by ${route.group_by?.join(', ')}`))).toBe(true);
      }

      if (route.receiver) {
        expect(rows.some((row) => row?.textContent?.includes(`Delivered to ${route.receiver}`))).toBe(true);
      }
    });
  });

  it('can edit root route if one is already defined', async () => {
    const { user } = renderPage();
    let rootRoute = await getRootRoute();

    expect(rootRoute).toHaveTextContent('default policy');
    expect(rootRoute).toHaveTextContent(/delivered to grafana-default-email/i);
    expect(rootRoute).toHaveTextContent(/grouped by alertname/i);

    await openDefaultPolicyEditModal();

    // The contact points are fetched from the k8s API, which we aren't overriding here
    // when we use a different
    const receiverSelect = ui.receiverSelect.get();
    await clickSelectOption(receiverSelect, 'lotsa-emails');

    const groupSelect = ui.groupSelect.get();
    await user.type(byRole('combobox').get(groupSelect), 'namespace{enter}');

    // configure timing intervals
    await user.click(screen.getByText(/timing options/i));

    await updateTiming(ui.groupWaitContainer.get(), '1m');
    await updateTiming(ui.groupIntervalContainer.get(), '4m');
    await updateTiming(ui.groupRepeatContainer.get(), '5h');

    //save
    await user.click(await screen.findByRole('button', { name: /update default policy/i }));

    // wait for it to go out of edit mode
    expect(await screen.findByText(/updated notification policies/i)).toBeInTheDocument();

    // check that new config values are rendered
    rootRoute = await getRootRoute();
    expect(rootRoute).toHaveTextContent(/delivered to lotsa-emails/i);
    expect(rootRoute).toHaveTextContent(/grouped by alertname, namespace/i);
  });

  it('can edit root route if one is not defined yet', async () => {
    setRoutingTree(
      routeName,
      createKubernetesRoutingTreeSpec({
        name: routeName,
        routes: [],
      })
    );
    const { user } = renderPage();

    // Sanity check to make sure we actually have an undefined root route.
    const rootRouteEl = await getRootRoute();
    expect(rootRouteEl).not.toHaveTextContent(new RegExp(`delivered to`, 'i'));
    expect(rootRouteEl).not.toHaveTextContent(new RegExp(`grouped by`, 'i'));

    await openDefaultPolicyEditModal();

    // configure receiver & group by
    const receiverSelect = await ui.receiverSelect.find();
    await clickSelectOption(receiverSelect, 'lotsa-emails');

    const groupSelect = ui.groupSelect.get();
    await user.type(byRole('combobox').get(groupSelect), 'severity{enter}');
    await user.type(byRole('combobox').get(groupSelect), 'namespace{enter}');
    //save
    await user.click(await screen.findByRole('button', { name: /update default policy/i }));

    expect(await screen.findByText(/updated notification policies/i)).toBeInTheDocument();

    const rootRoute = await getRootRoute();
    expect(rootRoute).toHaveTextContent(/delivered to lotsa-emails/i);
    expect(rootRoute).toHaveTextContent(/grouped by severity, namespace/i);
  });

  it('hides create and edit button if user does not have permission', async () => {
    grantUserPermissions([
      AccessControlAction.AlertingInstanceRead,
      AccessControlAction.AlertingInstancesExternalRead,
      AccessControlAction.AlertingNotificationsRead,
      AccessControlAction.AlertingNotificationsExternalRead,
    ]);

    const { user } = renderPage();

    expect(ui.newChildPolicyButton.query()).not.toBeInTheDocument();
    expect(ui.newSiblingPolicyButton.query()).not.toBeInTheDocument();

    await user.click(await ui.moreActionsDefaultPolicy.find());
    expect(ui.editButton.query()).not.toBeInTheDocument();
  });

  it('Show error message if loading Alertmanager config fails', async () => {
    const errMessage = "Alertmanager has exploded. it's gone. Forget about it.";
    makeAllAlertmanagerConfigFetchFail(getErrorResponse(errMessage));
    makeAllK8sGetEndpointsFail('alerting.config.notfound', errMessage);

    renderPage();
    const alert = await screen.findByRole('alert', { name: /error loading alertmanager config/i });
    expect(await within(alert).findByText(new RegExp(errMessage))).toBeInTheDocument();
    expect(ui.rootRouteContainer.query()).not.toBeInTheDocument();
  });

  it('allows user to reload and update policies if its been changed by another user', async () => {
    const { user } = renderPage();
    const NEW_INTERVAL = '12h';

    await getRootRoute();

    const existingConfig = getRoutingTree(routeName)!;
    const modifiedConfig = produce(existingConfig, (draft) => {
      draft.spec.defaults.group_interval = NEW_INTERVAL;
    });
    setRoutingTree(routeName, modifiedConfig);

    await openDefaultPolicyEditModal();
    await user.click(await screen.findByRole('button', { name: /update default policy/i }));

    expect(
      (await screen.findAllByText(/the notification policy tree has been updated by another user/i))[0]
    ).toBeInTheDocument();

    await user.click(screen.getByRole('button', { name: /cancel/i }));
    await user.click(screen.getByRole('button', { name: /reload policies/i }));

    expect((await screen.findAllByTestId('timing-options'))[0]).toHaveTextContent(NEW_INTERVAL);
  });

  it('Should be able to delete an empty route', async () => {
    setRoutingTree(
      routeName,
      createKubernetesRoutingTreeSpec({
        name: routeName,
        routes: [{}],
      })
    );

    const { user } = renderPage();

    await user.click(await ui.moreActions.find());
    const deleteButtons = await ui.deleteRouteButton.find();

    await user.click(deleteButtons);

    const confirmDeleteButton = ui.confirmDeleteButton.get(ui.confirmDeleteModal.get());
    expect(confirmDeleteButton).toBeInTheDocument();

    await user.click(confirmDeleteButton);

    expect(await screen.findByRole('status')).toHaveTextContent(/updated notification policies/i);

    expect(ui.row.query()).not.toBeInTheDocument();
  });

  it('Can add a mute timing to a route', async () => {
    const { user } = renderPage();

    await openEditModal(0);

    const muteTimingSelect = ui.muteTimingSelect.get();
    await clickSelectOption(muteTimingSelect, TIME_INTERVAL_NAME_HAPPY_PATH);
    await clickSelectOption(muteTimingSelect, TIME_INTERVAL_NAME_FILE_PROVISIONED);

    await user.click(ui.saveButton.get());

    expect(await screen.findByRole('status')).toHaveTextContent(/updated notification policies/i);

    const policy = (await ui.row.findAll())[0];
    expect(policy).toHaveTextContent(
      `Muted when ${TIME_INTERVAL_NAME_HAPPY_PATH}, ${TIME_INTERVAL_NAME_FILE_PROVISIONED}`
    );
  });
});

describe('Non-Grafana alertmanagers', () => {
  beforeAll(() => {
    setupDataSources(...Object.values(dataSources));
    grantUserPermissions([
      AccessControlAction.AlertingNotificationsExternalRead,
      AccessControlAction.AlertingNotificationsExternalWrite,
      ...PERMISSIONS_NOTIFICATION_POLICIES,
    ]);
  });

  it.skip('Shows an empty config when config returns an error and the AM supports lazy config initialization', async () => {
    makeAllAlertmanagerConfigFetchFail(getErrorResponse('alertmanager storage object not found'));
    setAlertmanagerStatus(dataSources.mimir.uid, someCloudAlertManagerStatus);
    renderNotificationPolicies(dataSources.mimir.name);

    expect(await ui.rootRouteContainer.find()).toBeInTheDocument();
  });

  it('Keeps matchers for non-grafana alertmanager sources', async () => {
    setAlertmanagerConfig(dataSources.am.uid, {
      alertmanager_config: {
        receivers: [{ name: 'default' }, { name: 'critical' }],
        route: {
          continue: false,
          receiver: 'default',
          group_by: ['alertname'],
          routes: [
            {
              receiver: 'simple-receiver',
              matchers: ['hello=world', 'foo!=bar'],
            },
          ],
          group_interval: '4m',
          group_wait: '1m',
          repeat_interval: '5h',
        },
        templates: [],
      },
      template_files: {},
    });

    const { user } = renderNotificationPolicies(dataSources.am.name);

    const policyIndex = 0;
    await openEditModal(policyIndex);

    // Save policy to test that format is NOT converted
    await user.click(await ui.saveButton.find());

    const updatedConfig = getAlertmanagerConfig(dataSources.am.uid);
    expect(updatedConfig.alertmanager_config.route?.routes?.[policyIndex].matchers).toMatchSnapshot();
  });

  it('Prometheus Alertmanager routes cannot be edited', async () => {
    setAlertmanagerStatus(dataSources.promAlertManager.uid, {
      ...someCloudAlertManagerStatus,
      config: someCloudAlertManagerConfig.alertmanager_config,
    });
    renderNotificationPolicies(dataSources.promAlertManager.name);

    expect(await ui.rootRouteContainer.find()).toBeInTheDocument();

    const rows = await ui.row.findAll();
    expect(rows).toHaveLength(2);

    expect(ui.moreActions.query()).not.toBeInTheDocument();
    expect(ui.moreActionsDefaultPolicy.query()).not.toBeInTheDocument();
  });

  it('Prometheus Alertmanager has no CTA button if there are no specific policies', async () => {
    setAlertmanagerStatus(dataSources.promAlertManager.uid, {
      ...someCloudAlertManagerStatus,
      config: {
        ...someCloudAlertManagerConfig.alertmanager_config,
        route: {
          ...someCloudAlertManagerConfig.alertmanager_config.route,
          routes: undefined,
        },
      },
    });

    renderNotificationPolicies(dataSources.promAlertManager.name);

    expect(await ui.rootRouteContainer.find()).toBeInTheDocument();

    expect(ui.newChildPolicyButton.query()).not.toBeInTheDocument();
    expect(ui.newSiblingPolicyButton.query()).not.toBeInTheDocument();
  });
});

describe('findRoutesMatchingFilters', () => {
  const simpleRouteTree: RouteWithID = {
    id: '0',
    receiver: 'default-receiver',
    routes: [
      {
        id: '1',
        receiver: 'simple-receiver',
        matchers: ['hello=world', 'foo!=bar'],
        routes: [
          {
            id: '2',
            matchers: ['bar=baz'],
          },
        ],
      },
    ],
  };

  it('should not filter when we do not have any valid filters', () => {
    expect(findRoutesMatchingFilters(simpleRouteTree, {})).toHaveProperty('filtersApplied', false);
  });

  it('should not match non-existing', () => {
    expect(
      findRoutesMatchingFilters(simpleRouteTree, {
        labelMatchersFilter: [['foo', MatcherOperator.equal, 'bar']],
      }).matchedRoutesWithPath.size
    ).toBe(0);

    const matchingRoutes = findRoutesMatchingFilters(simpleRouteTree, {
      contactPointFilter: 'does-not-exist',
    });

    expect(matchingRoutes).toMatchSnapshot();
  });

  it('should work with only label matchers', () => {
    const matchingRoutes = findRoutesMatchingFilters(simpleRouteTree, {
      labelMatchersFilter: [['hello', MatcherOperator.equal, 'world']],
    });

    expect(matchingRoutes).toMatchSnapshot();
  });

  it('should work with only contact point and inheritance', () => {
    const matchingRoutes = findRoutesMatchingFilters(simpleRouteTree, {
      contactPointFilter: 'simple-receiver',
    });

    expect(matchingRoutes).toMatchSnapshot();
  });

  it('should work with non-intersecting filters', () => {
    const matchingRoutes = findRoutesMatchingFilters(simpleRouteTree, {
      labelMatchersFilter: [['hello', MatcherOperator.equal, 'world']],
      contactPointFilter: 'does-not-exist',
    });

    expect(matchingRoutes).toMatchSnapshot();
  });

  it('should work with all filters', () => {
    const matchingRoutes = findRoutesMatchingFilters(simpleRouteTree, {
      labelMatchersFilter: [['hello', MatcherOperator.equal, 'world']],
      contactPointFilter: 'simple-receiver',
    });

    expect(matchingRoutes).toMatchSnapshot();
  });
});

const uiMultiRoute = {
  /** Policy table row by name */
  routeContainer: (name: string) => byTestId(`routing-tree_${name}`),
  /** Search box for routing policies */
  policyFilter: byRole('textbox', { name: /search routing trees/ }),
};

describe('alertingMultiplePolicies Feature Flag', () => {
  const originalFeatureToggle = config.featureToggles.alertingMultiplePolicies;

  afterAll(() => {
    config.featureToggles.alertingMultiplePolicies = originalFeatureToggle;
  });

  beforeAll(() => {
    setupDataSources(...Object.values(dataSources));
    grantUserPermissions([AccessControlAction.AlertingNotificationsExternalRead, ...PERMISSIONS_NOTIFICATION_POLICIES]);
  });

  it('Should render PoliciesList when alertingMultiplePolicies feature flag is enabled', async () => {
    config.featureToggles.alertingMultiplePolicies = true;

    renderNotificationPolicies();
    await uiMultiRoute.routeContainer('user-defined').find();

    expect(uiMultiRoute.policyFilter.get()).toBeInTheDocument();
    // This is rendered only when displaying the full policy, it shouldn't appear in the List view.
    expect(ui.rootRouteContainer.query()).not.toBeInTheDocument();
  });

  it('Should not render PoliciesList when alertingMultiplePolicies feature flag is disabled', async () => {
    config.featureToggles.alertingMultiplePolicies = false;

    renderNotificationPolicies();
    await getRootRoute();

    expect(uiMultiRoute.policyFilter.query()).not.toBeInTheDocument();
  });

  it('Should not render PoliciesList when alertmanager is external', async () => {
    config.featureToggles.alertingMultiplePolicies = true;

    setAlertmanagerStatus(dataSources.promAlertManager.uid, {
      ...someCloudAlertManagerStatus,
      config: someCloudAlertManagerConfig.alertmanager_config,
    });
    renderNotificationPolicies(dataSources.promAlertManager.name);
    await getRootRoute();

    expect(uiMultiRoute.policyFilter.query()).not.toBeInTheDocument();
  });
});
