import { renderHook, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { first, noop } from 'lodash';
import type { JSX } from 'react';
import { Route, Routes } from 'react-router-dom-v5-compat';
import { render } from 'test/test-utils';

import { contextSrv } from 'app/core/services/context_srv';
import { type ReceiversState } from 'app/features/alerting/unified/types/alerting';
import {
  type AlertmanagerGroup,
  MatcherOperator,
  type ObjectMatcher,
  type RouteWithID,
} from 'app/plugins/datasource/alertmanager/types';

import { useNotificationPolicyAbility } from '../../hooks/abilities/alertmanager/useNotificationPolicyAbility';
import {
  type Ability,
  Granted,
  InsufficientPermissions,
  NotSupported,
  NotificationPolicyAction,
} from '../../hooks/abilities/types';
import { mockReceiversState } from '../../mocks';
import { AlertmanagerProvider } from '../../state/AlertmanagerContext';
import { KnownProvenance } from '../../types/knownProvenance';
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';

import {
  AUTOGENERATED_ROOT_LABEL_NAME,
  Policy,
  TimingOptionsMeta,
  isAutoGeneratedRoot,
  useCreateDropdownMenuActions,
} from './Policy';

jest.mock('../../hooks/abilities/alertmanager/useNotificationPolicyAbility', () => ({
  ...jest.requireActual('../../hooks/abilities/alertmanager/useNotificationPolicyAbility'),
  useNotificationPolicyAbility: jest.fn(),
}));

const useNotificationPolicyAbilityMock = jest.mocked(useNotificationPolicyAbility);

function toAbility([supported, allowed]: [boolean, boolean]): Ability {
  if (!supported) {
    return NotSupported;
  }
  return allowed ? Granted : InsufficientPermissions([]);
}

describe('Policy', () => {
  beforeAll(() => {
    jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(true);
    useNotificationPolicyAbilityMock.mockReturnValue(toAbility([true, true]));
  });

  it('should render a policy tree', async () => {
    const onEditPolicy = jest.fn();
    const onAddPolicy = jest.fn();
    const onDeletePolicy = jest.fn();
    const onShowAlertInstances = jest.fn(
      (alertGroups: AlertmanagerGroup[], matchers?: ObjectMatcher[] | undefined) => {}
    );

    const routeTree = mockRoutes;
    const user = userEvent.setup();

    renderPolicy(
      <Policy
        receivers={[{ name: 'grafana-default-email' }]}
        isDefaultPolicy
        currentRoute={routeTree}
        alertManagerSourceName={GRAFANA_RULES_SOURCE_NAME}
        onEditPolicy={onEditPolicy}
        onAddPolicy={onAddPolicy}
        onDeletePolicy={onDeletePolicy}
        onShowAlertInstances={onShowAlertInstances}
      />
    );

    // should have default policy
    const defaultPolicy = screen.getByTestId('am-root-route-container');
    expect(defaultPolicy).toBeInTheDocument();
    expect(within(defaultPolicy).getByText('Default policy')).toBeVisible();

    // click "more actions" and check if we can edit and delete
    expect(within(defaultPolicy).getByTestId('more-actions')).toBeInTheDocument();
    await user.click(within(defaultPolicy).getByTestId('more-actions'));

    // should be editable
    const editDefaultPolicy = screen.getByRole('menuitem', { name: 'Edit' });
    expect(editDefaultPolicy).toBeInTheDocument();
    expect(editDefaultPolicy).toBeEnabled();
    await user.click(editDefaultPolicy);
    expect(onEditPolicy).toHaveBeenCalledWith(routeTree, true);

    // should not be deletable
    expect(screen.queryByRole('menuitem', { name: 'Delete' })).not.toBeInTheDocument();

    // default policy should show the metadata

    // no continue matching
    expect(within(defaultPolicy).queryByTestId('continue-matching')).not.toBeInTheDocument();

    // for matching instances
    // expect(within(defaultPolicy).getByTestId('matching-instances')).toHaveTextContent('0instances');

    // for contact point
    expect(within(defaultPolicy).getByTestId('contact-point')).toHaveTextContent('grafana-default-email');
    expect(within(defaultPolicy).getByRole('link', { name: 'grafana-default-email' })).toBeInTheDocument();

    // for grouping
    expect(within(defaultPolicy).getByTestId('grouping')).toHaveTextContent('grafana_folder, alertname');

    // no timings
    expect(within(defaultPolicy).queryByTestId('mute-timings')).not.toBeInTheDocument();
    expect(within(defaultPolicy).queryByTestId('active-timings')).not.toBeInTheDocument();

    // for timing options
    expect(within(defaultPolicy).getByTestId('timing-options')).toHaveTextContent(
      'Wait 30s to group instances · Wait 5m before sending updates · Repeated every 4h'
    );

    // should have custom policies (1 direct child + 2 grandchildren, all expanded by default)
    const customPolicies = screen.getAllByTestId('am-route-container');
    expect(customPolicies).toHaveLength(3);

    // all policies should be editable and deletable
    for (const container of customPolicies) {
      const policy = within(container);

      // click "more actions" and check if we can delete
      await user.click(policy.getByTestId('more-actions'));
      expect(screen.queryByRole('menuitem', { name: 'Edit' })).toBeEnabled();
      expect(screen.queryByRole('menuitem', { name: 'Delete' })).toBeEnabled();

      await user.click(screen.getByRole('menuitem', { name: 'Delete' }));
      expect(onDeletePolicy).toHaveBeenCalled();
    }

    // first custom policy should have the correct information
    const firstPolicy = customPolicies[0];
    expect(within(firstPolicy).getByTestId('label-matchers')).toHaveTextContent(/^team \= operations$/);
    expect(within(firstPolicy).getByTestId('continue-matching')).toBeInTheDocument();
    // expect(within(firstPolicy).getByTestId('matching-instances')).toHaveTextContent('0instances');
    expect(within(firstPolicy).getByTestId('contact-point')).toHaveTextContent('provisioned-contact-point');
    expect(within(firstPolicy).getByTestId('mute-timings')).toHaveTextContent('Muted when mt-1');
    expect(within(firstPolicy).getByTestId('active-timings')).toHaveTextContent('Active when mt-2');
    expect(within(firstPolicy).getByTestId('inherited-properties')).toHaveTextContent('Inherited4 properties');

    // second custom policy should be correct
    const secondPolicy = customPolicies[1];
    expect(within(secondPolicy).getByTestId('label-matchers')).toHaveTextContent(/^region \= EMEA$/);
    expect(within(secondPolicy).queryByTestId('continue-matching')).not.toBeInTheDocument();
    expect(within(secondPolicy).queryByTestId('mute-timings')).not.toBeInTheDocument();
    expect(within(secondPolicy).queryByTestId('active-timings')).not.toBeInTheDocument();
    expect(within(secondPolicy).getByTestId('inherited-properties')).toHaveTextContent('Inherited5 properties');

    // third custom policy should be correct
    const thirdPolicy = customPolicies[2];
    expect(within(thirdPolicy).getByTestId('label-matchers')).toHaveTextContent(
      /^foo = barbar = bazbaz = quxasdf = asdftype = diskand 1 more$/
    );
  });

  it('should show export option when export is allowed and supported returns true', async () => {
    const onEditPolicy = jest.fn();
    const onAddPolicy = jest.fn();
    const onDeletePolicy = jest.fn();
    const onShowAlertInstances = jest.fn(
      (alertGroups: AlertmanagerGroup[], matchers?: ObjectMatcher[] | undefined) => {}
    );

    const routeTree = mockRoutes;
    const user = userEvent.setup();

    renderPolicy(
      <Policy
        isDefaultPolicy
        currentRoute={routeTree}
        alertManagerSourceName={GRAFANA_RULES_SOURCE_NAME}
        onEditPolicy={onEditPolicy}
        onAddPolicy={onAddPolicy}
        onDeletePolicy={onDeletePolicy}
        onShowAlertInstances={onShowAlertInstances}
        isAutoGenerated={false}
      />
    );
    // should have default policy
    const defaultPolicy = screen.getByTestId('am-root-route-container');
    // click "more actions"
    expect(within(defaultPolicy).getByTestId('more-actions')).toBeInTheDocument();
    await user.click(within(defaultPolicy).getByTestId('more-actions'));
    expect(screen.getByRole('menuitem', { name: 'Export' })).toBeInTheDocument();
  });

  it('should not show export option when is not supported', async () => {
    const onEditPolicy = jest.fn();
    const onAddPolicy = jest.fn();
    const onDeletePolicy = jest.fn();
    const onShowAlertInstances = jest.fn(
      (alertGroups: AlertmanagerGroup[], matchers?: ObjectMatcher[] | undefined) => {}
    );

    const routeTree = mockRoutes;

    useNotificationPolicyAbilityMock.mockImplementation(({ action }) =>
      action === NotificationPolicyAction.Export ? toAbility([false, true]) : toAbility([true, true])
    );

    const user = userEvent.setup();

    renderPolicy(
      <Policy
        isDefaultPolicy
        currentRoute={routeTree}
        alertManagerSourceName={GRAFANA_RULES_SOURCE_NAME}
        onEditPolicy={onEditPolicy}
        onAddPolicy={onAddPolicy}
        onDeletePolicy={onDeletePolicy}
        onShowAlertInstances={onShowAlertInstances}
      />
    );
    // should have default policy
    const defaultPolicy = screen.getByTestId('am-root-route-container');
    // click "more actions"
    expect(within(defaultPolicy).getByTestId('more-actions')).toBeInTheDocument();
    await user.click(within(defaultPolicy).getByTestId('more-actions'));
    expect(screen.queryByRole('menuitem', { name: 'Export' })).not.toBeInTheDocument();
  });

  it('should not show export option when is not allowed', async () => {
    const onEditPolicy = jest.fn();
    const onAddPolicy = jest.fn();
    const onDeletePolicy = jest.fn();
    const onShowAlertInstances = jest.fn(
      (alertGroups: AlertmanagerGroup[], matchers?: ObjectMatcher[] | undefined) => {}
    );

    const routeTree = mockRoutes;

    useNotificationPolicyAbilityMock.mockImplementation(({ action }) =>
      action === NotificationPolicyAction.Export ? toAbility([true, false]) : toAbility([true, true])
    );

    const user = userEvent.setup();

    renderPolicy(
      <Policy
        isDefaultPolicy
        currentRoute={routeTree}
        alertManagerSourceName={GRAFANA_RULES_SOURCE_NAME}
        onEditPolicy={onEditPolicy}
        onAddPolicy={onAddPolicy}
        onDeletePolicy={onDeletePolicy}
        onShowAlertInstances={onShowAlertInstances}
      />
    );
    // should have default policy
    const defaultPolicy = screen.getByTestId('am-root-route-container');
    // click "more actions"
    expect(within(defaultPolicy).getByTestId('more-actions')).toBeInTheDocument();
    await user.click(within(defaultPolicy).getByTestId('more-actions'));
    expect(screen.queryByRole('menuitem', { name: 'Export' })).not.toBeInTheDocument();
  });

  it('should not allow editing readOnly policy tree', () => {
    const routeTree: RouteWithID = { id: '0', routes: [{ id: '1' }] };

    renderPolicy(
      <Policy
        readOnly
        currentRoute={routeTree}
        alertManagerSourceName={GRAFANA_RULES_SOURCE_NAME}
        onEditPolicy={noop}
        onAddPolicy={noop}
        onDeletePolicy={noop}
        onShowAlertInstances={noop}
      />
    );

    expect(screen.queryByRole('button', { name: 'Edit' })).not.toBeInTheDocument();
  });

  it.skip('should show matching instances', () => {
    const routeTree: RouteWithID = {
      id: '0',
      routes: [{ id: '1', object_matchers: [['foo', eq, 'bar']] }],
    };

    renderPolicy(
      <Policy
        readOnly
        isDefaultPolicy
        currentRoute={routeTree}
        alertManagerSourceName={GRAFANA_RULES_SOURCE_NAME}
        onEditPolicy={noop}
        onAddPolicy={noop}
        onDeletePolicy={noop}
        onShowAlertInstances={noop}
      />
    );

    const defaultPolicy = screen.getByTestId('am-root-route-container');
    expect(within(defaultPolicy).getByTestId('matching-instances')).toHaveTextContent('1instance');
    const customPolicy = screen.getByTestId('am-route-container');
    expect(within(customPolicy).getByTestId('matching-instances')).toHaveTextContent('2instances');
  });

  it('should show warnings and errors', () => {
    const routeTree: RouteWithID = {
      id: '0', // this one should show an error
      receiver: 'broken-receiver',
      routes: [{ id: '1', object_matchers: [] }], // this one should show a warning
    };

    const receiversState: ReceiversState = mockReceiversState();

    renderPolicy(
      <Policy
        readOnly
        isDefaultPolicy
        currentRoute={routeTree}
        contactPointsState={receiversState}
        alertManagerSourceName={GRAFANA_RULES_SOURCE_NAME}
        onEditPolicy={noop}
        onAddPolicy={noop}
        onDeletePolicy={noop}
        onShowAlertInstances={noop}
      />
    );

    const defaultPolicy = screen.getByTestId('am-root-route-container');
    expect(within(defaultPolicy).queryByTestId('matches-all')).not.toBeInTheDocument();
    // Errors are now shown as an icon in the gutter
    expect(within(defaultPolicy).getByTestId('policy-errors')).toBeInTheDocument();

    const customPolicy = screen.getByTestId('am-route-container');
    expect(within(customPolicy).getByTestId('matches-all')).toBeInTheDocument();
  });

  it('shows correct badge when policy has file provenance', () => {
    const mockRoute: RouteWithID = {
      id: 'test-route',
      receiver: 'test-receiver',
      routes: [],
    };

    renderPolicy(
      <Policy
        readOnly
        isDefaultPolicy
        currentRoute={mockRoute}
        contactPointsState={mockReceiversState()}
        alertManagerSourceName={GRAFANA_RULES_SOURCE_NAME}
        onEditPolicy={noop}
        onAddPolicy={noop}
        onDeletePolicy={noop}
        onShowAlertInstances={noop}
        provenance={KnownProvenance.File}
      />
    );

    const badge = screen.getByText('Provisioned');
    expect(badge).toBeInTheDocument();
  });

  it('shows correct badge when policy has converted_prometheus provenance', () => {
    const mockRoute: RouteWithID = {
      id: 'test-route',
      receiver: 'test-receiver',
      routes: [],
    };

    renderPolicy(
      <Policy
        readOnly
        isDefaultPolicy
        currentRoute={mockRoute}
        contactPointsState={mockReceiversState()}
        alertManagerSourceName={GRAFANA_RULES_SOURCE_NAME}
        onEditPolicy={noop}
        onAddPolicy={noop}
        onDeletePolicy={noop}
        onShowAlertInstances={noop}
        provenance={KnownProvenance.ConvertedPrometheus}
      />
    );

    const badge = screen.getByText('Imported');
    expect(badge).toBeInTheDocument();
  });
});

// Doesn't matter which path the routes use, it just needs to match the initialEntries history entry to render the element
const renderPolicy = (element: JSX.Element) =>
  render(
    <Routes>
      <Route path={'/'} element={<AlertmanagerProvider accessType="notification">{element}</AlertmanagerProvider>} />
    </Routes>,
    {
      historyOptions: {
        initialEntries: ['/'],
      },
    }
  );

const eq = MatcherOperator.equal;

const mockRoutes: RouteWithID = {
  id: '0',
  receiver: 'grafana-default-email',
  group_by: ['grafana_folder', 'alertname'],
  routes: [
    {
      id: '1',
      receiver: 'provisioned-contact-point',
      object_matchers: [['team', eq, 'operations']],
      mute_time_intervals: ['mt-1'],
      active_time_intervals: ['mt-2'],
      continue: true,
      routes: [
        {
          id: '2',
          object_matchers: [['region', eq, 'EMEA']],
        },
        {
          id: '3',
          receiver: 'grafana-default-email',
          object_matchers: [
            ['foo', eq, 'bar'],
            ['bar', eq, 'baz'],
            ['baz', eq, 'qux'],
            ['asdf', eq, 'asdf'],
            ['type', eq, 'disk'],
            ['severity', eq, 'critical'],
          ],
        },
      ],
    },
  ],
  group_wait: '30s',
  group_interval: '5m',
  repeat_interval: '4h',
};

describe('isAutoGeneratedRoot', () => {
  it('returns false when object_matchers is not defined', () => {
    const route: RouteWithID = {
      id: '1',
    };
    expect(isAutoGeneratedRoot(route)).toBe(false);
  });

  it('returns true when object_matchers contains AUTOGENERATED_ROOT_LABEL_NAME', () => {
    const route: RouteWithID = {
      id: '1',
      object_matchers: [[AUTOGENERATED_ROOT_LABEL_NAME, MatcherOperator.equal, 'true']],
    };
    expect(isAutoGeneratedRoot(route)).toBe(true);
  });

  it('returns false when object_matchers does not contain AUTOGENERATED_ROOT_LABEL_NAME', () => {
    const route: RouteWithID = {
      id: '1',
      object_matchers: [['label', MatcherOperator.equal, 'true']],
    };
    expect(isAutoGeneratedRoot(route)).toBe(false);
  });
});

describe('useCreateDropdownMenuActions', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });
  const openDetailModal = jest.fn();
  const currentRoute: RouteWithID = { id: '0', routes: [{ id: '1' }] };
  const toggleShowExportDrawer = jest.fn();
  const onDeletePolicy = jest.fn();
  const testCases = [
    {
      isAutoGenerated: false,
      isDefaultPolicy: true,
      provenance: undefined,
      expectedMenu: ['edit-policy', 'export-policy'],
    },
    {
      isAutoGenerated: false,
      isDefaultPolicy: true,
      provenance: KnownProvenance.File,
      expectedMenu: ['edit-policy', 'export-policy'],
    },
    {
      isAutoGenerated: false,
      isDefaultPolicy: false,
      provenance: undefined,
      expectedMenu: ['edit-policy', 'delete-policy'],
    },
    {
      isAutoGenerated: false,
      isDefaultPolicy: false,
      provenance: KnownProvenance.File,
      expectedMenu: ['edit-policy', 'delete-policy'],
    },
    { isAutoGenerated: true, isDefaultPolicy: true, provenance: KnownProvenance.File, expectedMenu: ['edit-policy'] },
    { isAutoGenerated: true, isDefaultPolicy: false, provenance: undefined, expectedMenu: ['edit-policy'] },
    { isAutoGenerated: true, isDefaultPolicy: true, provenance: undefined, expectedMenu: ['edit-policy'] },
    { isAutoGenerated: true, isDefaultPolicy: false, provenance: KnownProvenance.File, expectedMenu: ['edit-policy'] },
  ];

  testCases.forEach(({ isAutoGenerated, isDefaultPolicy, provenance, expectedMenu }) => {
    const provisionedStatus = provenance ? 'provisioned' : 'not provisioned';
    it(`Having all the permissions returns ${expectedMenu.length} menu items for isAutoGenerated=${isAutoGenerated}, isDefaultPolicy=${isDefaultPolicy}, ${provisionedStatus}`, () => {
      useNotificationPolicyAbilityMock.mockReturnValue(toAbility([true, true]));
      const { result } = renderHook(() =>
        useCreateDropdownMenuActions(
          isAutoGenerated,
          isDefaultPolicy,
          true,
          provenance,
          openDetailModal,
          currentRoute,
          toggleShowExportDrawer,
          onDeletePolicy
        )
      );

      const menuItemsKeys = result.current.map((item) => item.key ?? '');
      expect(menuItemsKeys).toEqual(expectedMenu);
    });
  });
});

describe('TimingOptionsMeta', () => {
  it('should render nothing without options', () => {
    render(<TimingOptionsMeta timingOptions={{}} />);
    expect(screen.queryByText(/wait/i)).not.toBeInTheDocument();
  });

  it('should render only repeat interval', () => {
    render(<TimingOptionsMeta timingOptions={{ repeat_interval: '5h' }} />);
    expect(screen.getByText(/repeated every/i)).toBeInTheDocument();
    expect(screen.getByText('5h')).toBeInTheDocument();
  });

  it('should render all options', () => {
    render(<TimingOptionsMeta timingOptions={{ group_wait: '30s', group_interval: '5m', repeat_interval: '4h' }} />);
    expect(
      first(
        screen.getAllByText(
          (_, element) =>
            element?.textContent === 'Wait 30s to group instances · Wait 5m before sending updates · Repeated every 4h',
          { collapseWhitespace: false, trim: false, exact: true }
        )
      )
    ).toBeInTheDocument();
  });
});
