import userEvent from '@testing-library/user-event';
import { HttpResponse, http } from 'msw';
import { render, screen, within } from 'test/test-utils';
import { byTestId } from 'testing-library-selector';

import { config } from '@grafana/runtime';
import { AppNotificationList } from 'app/core/components/AppNotifications/AppNotificationList';
import { setupMswServer } from 'app/features/alerting/unified/mockApi';
import { setupDataSources } from 'app/features/alerting/unified/testSetup/datasources';

import { AccessControlAction } from '../../../../../types/accessControl';
import NotificationPolicies from '../../NotificationPoliciesPage';
import { useContactPointAbility } from '../../hooks/abilities/alertmanager/useContactPointAbility';
import { useNotificationPolicyAbility } from '../../hooks/abilities/alertmanager/useNotificationPolicyAbility';
import {
  type Ability,
  Granted,
  InsufficientPermissions,
  NotSupported,
  NotificationPolicyAction,
} from '../../hooks/abilities/types';
import { grantUserPermissions, mockDataSource } from '../../mocks';
import {
  getRoutingTree,
  getRoutingTreeList,
  resetRoutingTreeMap,
  setAllRoutingTreePermissions,
} from '../../mocks/server/entities/k8s/routingtrees';
import { KnownProvenance } from '../../types/knownProvenance';
import { DataSourceType } from '../../utils/datasource';
import { K8sAnnotations } from '../../utils/k8s/constants';

import { countPolicies } from './PoliciesList';
import * as analytics from './notificationPolicyAnalytics';

jest.mock('../../useRouteGroupsMatcher');

// The export drawer uses IntersectionObserver which is not available in JSDOM.
// Mock it out so we can test the tracking call without rendering the drawer.
jest.mock('../export/GrafanaPoliciesExporter', () => ({
  GrafanaPoliciesExporter: () => null,
}));

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

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

const mocks = {
  useContactPointAbility: jest.mocked(useContactPointAbility),
  useNotificationPolicyAbility: jest.mocked(useNotificationPolicyAbility),
};

const server = setupMswServer();

const renderNotificationPolicies = () =>
  render(
    <>
      <AppNotificationList />
      <NotificationPolicies />
    </>,
    {
      historyOptions: {
        initialEntries: ['/alerting/routes'],
      },
    }
  );

const dataSources = {
  am: mockDataSource({
    name: 'Alertmanager',
    type: DataSourceType.Alertmanager,
  }),
};

const ui = {
  rootRouteContainer: byTestId('am-root-route-container'),
  createPolicyButton: byTestId('create-policy-button'),
};

const allPolicyActions = [
  NotificationPolicyAction.Create,
  NotificationPolicyAction.ViewTree,
  NotificationPolicyAction.UpdateTree,
  NotificationPolicyAction.Delete,
  NotificationPolicyAction.Export,
];

const grantAlertmanagerAbilities = (allowed: readonly NotificationPolicyAction[]) => {
  // useContactPointAbility is called for View checks — always grant it
  mocks.useContactPointAbility.mockReturnValue(Granted);

  // useNotificationPolicyAbility is called with { action, context } — check action against allowed list
  mocks.useNotificationPolicyAbility.mockImplementation(({ action }) => {
    const included = (allowed as readonly string[]).includes(action);
    return toAbility(true, included);
  });
};

describe('PoliciesList', () => {
  const originalFeatureToggle = config.featureToggles.alertingMultiplePolicies;
  afterAll(() => {
    config.featureToggles.alertingMultiplePolicies = originalFeatureToggle;
  });

  beforeEach(() => {
    config.featureToggles.alertingMultiplePolicies = true;
    setupDataSources(...Object.values(dataSources));
    jest.clearAllMocks();
    jest.restoreAllMocks();
    jest.resetAllMocks();

    // MultiCombobox uses canvas measureText which isn't available in jsdom.
    // Must be re-applied after jest.resetAllMocks() clears mock return values.
    HTMLCanvasElement.prototype.getContext = jest.fn().mockReturnValue({
      measureText: jest.fn().mockReturnValue({ width: 0 }),
      font: '',
    }) as never;

    grantAlertmanagerAbilities(allPolicyActions);

    grantUserPermissions([AccessControlAction.AlertingInstanceRead, AccessControlAction.AlertingNotificationsRead]);

    resetRoutingTreeMap();
    // Strip entity-level k8s access annotations so tests start with least-privilege.
    // Tests that need to open the more-actions menu must opt in via setAllRoutingTreePermissions.
    setAllRoutingTreePermissions({ canWrite: false, canDelete: false, canAdmin: false });
  });

  describe('Route headers and metadata', () => {
    const allRoutes = getRoutingTreeList();
    expect(allRoutes).toHaveLength(5);
    it.each(allRoutes.map((route) => ({ routeName: route.metadata.name! })))(
      'policy: $routeName',
      async ({ routeName }) => {
        const route = getRoutingTree(routeName)!;

        renderNotificationPolicies();

        // Wait for all root route containers to render
        const allRoots = await ui.rootRouteContainer.findAll();
        expect(allRoots.length).toBeGreaterThanOrEqual(1);

        // Find the matching root by receiver name.
        // For duplicate receivers, also match by provenance text.
        const receiverName = route.spec.defaults.receiver!;
        const isProvisioned = route.metadata.annotations?.[K8sAnnotations.Provenance] !== KnownProvenance.None;
        const routeEl = allRoots.find((el) => {
          const text = el.textContent ?? '';
          const hasReceiver = text.includes(receiverName);
          if (!hasReceiver) {
            return false;
          }
          // If provisioned, ensure the element also has "Provisioned" text
          if (isProvisioned) {
            return text.includes('Provisioned');
          }
          // If not provisioned and receiver matches but there could be a duplicate,
          // ensure the element does NOT have "Provisioned" text
          return !text.includes('Provisioned');
        })!;
        expect(routeEl).toBeDefined();

        // Check receiver is displayed
        expect(routeEl).toHaveTextContent(new RegExp(`Delivered to`, 'i'));
        expect(routeEl).toHaveTextContent(new RegExp(receiverName, 'i'));

        // Check grouping — only shown when group_by is explicitly set as an array
        const groupBy = route?.spec.defaults.group_by;
        if (Array.isArray(groupBy) && groupBy.length > 0) {
          if (groupBy[0] === '...') {
            expect(routeEl).toHaveTextContent(/Not grouping/i);
          } else {
            expect(routeEl).toHaveTextContent(new RegExp(`Grouped by ${groupBy.join(', ')}`, 'i'));
          }
        } else if (Array.isArray(groupBy) && groupBy.length === 0) {
          expect(routeEl).toHaveTextContent(/Single group/i);
        }

        // Check provisioned badge
        if (isProvisioned) {
          expect(routeEl).toHaveTextContent(/Provisioned/i);
        }

        // Check subpolicies exist in the tree data
        const size = countPolicies(route.spec);
        expect(size).toBeGreaterThanOrEqual(0);
      }
    );
  });

  describe('Table action permissions', () => {
    describe('Create', () => {
      it('enable if user has permission', async () => {
        grantAlertmanagerAbilities([NotificationPolicyAction.Create, NotificationPolicyAction.ViewTree]);
        renderNotificationPolicies();
        expect(await ui.createPolicyButton.find()).toBeInTheDocument();
        expect(ui.createPolicyButton.query()).toBeEnabled();
      });
      it('disable if user does not have permission', async () => {
        grantAlertmanagerAbilities([NotificationPolicyAction.ViewTree]);

        renderNotificationPolicies();
        expect(await ui.createPolicyButton.find()).toBeInTheDocument();
        expect(ui.createPolicyButton.query()).toBeDisabled();
      });
    });
  });

  describe('Policy action permissions', () => {
    describe('Edit', () => {
      it('shows edit menu item if user has edit permission', async () => {
        grantAlertmanagerAbilities([NotificationPolicyAction.ViewTree, NotificationPolicyAction.UpdateTree]);
        setAllRoutingTreePermissions({ canWrite: true, canDelete: true, canAdmin: true });

        const user = userEvent.setup();
        renderNotificationPolicies();
        const allRoots = await ui.rootRouteContainer.findAll();
        const defaultPolicyEl = allRoots[0];
        await user.click(within(defaultPolicyEl).getByTestId('more-actions'));
        expect(screen.getByRole('menuitem', { name: 'Edit' })).toBeInTheDocument();
      });
      it('does not show more actions if user has no edit permission', async () => {
        grantAlertmanagerAbilities([NotificationPolicyAction.ViewTree]);

        renderNotificationPolicies();
        const allRoots = await ui.rootRouteContainer.findAll();
        const defaultPolicyEl = allRoots[0];
        // No actions available = no more-actions button rendered
        expect(within(defaultPolicyEl).queryByTestId('more-actions')).not.toBeInTheDocument();
      });
      it('shows edit as disabled if policy is provisioned', async () => {
        grantAlertmanagerAbilities([NotificationPolicyAction.ViewTree, NotificationPolicyAction.UpdateTree]);
        setAllRoutingTreePermissions({ canWrite: true, canDelete: true, canAdmin: true });

        const user = userEvent.setup();
        renderNotificationPolicies();
        const allRoots = await ui.rootRouteContainer.findAll();
        // Find the provisioned policy (has "Provisioned" text)
        const provisionedPolicy = allRoots.find((el) => el.textContent?.includes('Provisioned'));
        expect(provisionedPolicy).toBeDefined();
        await user.click(within(provisionedPolicy!).getByTestId('more-actions'));
        const editItem = screen.getByRole('menuitem', { name: 'Edit' });
        expect(editItem).toBeInTheDocument();
        expect(editItem).toBeDisabled();
      });
    });
    describe('Export', () => {
      it('enable if user has permission', async () => {
        grantAlertmanagerAbilities([NotificationPolicyAction.ViewTree, NotificationPolicyAction.Export]);
        setAllRoutingTreePermissions({ canWrite: true, canDelete: true, canAdmin: true });

        const user = userEvent.setup();
        renderNotificationPolicies();
        const allRoots = await ui.rootRouteContainer.findAll();
        const defaultPolicyEl = allRoots[0];
        await user.click(within(defaultPolicyEl).getByTestId('more-actions'));
        expect(screen.getByRole('menuitem', { name: 'Export' })).toBeInTheDocument();
      });
      it('does not show more actions if user has no export or edit permission', async () => {
        grantAlertmanagerAbilities([NotificationPolicyAction.ViewTree]);

        renderNotificationPolicies();
        const allRoots = await ui.rootRouteContainer.findAll();
        const defaultPolicyEl = allRoots[0];
        // No actions available = no more-actions button rendered
        expect(within(defaultPolicyEl).queryByTestId('more-actions')).not.toBeInTheDocument();
      });
    });

    describe('Reset', () => {
      it('enable on default policy if user has permission', async () => {
        grantAlertmanagerAbilities([NotificationPolicyAction.ViewTree, NotificationPolicyAction.Delete]);
        setAllRoutingTreePermissions({ canWrite: true, canDelete: true, canAdmin: true });

        const user = userEvent.setup();
        renderNotificationPolicies();
        const allRoots = await ui.rootRouteContainer.findAll();
        const defaultPolicyEl = allRoots[0];
        await user.click(within(defaultPolicyEl).getByTestId('more-actions'));
        const resetItem = screen.getByRole('menuitem', { name: 'Reset' });
        expect(resetItem).toBeInTheDocument();
      });
      it('does not show more actions on default policy if user has no permission', async () => {
        grantAlertmanagerAbilities([NotificationPolicyAction.ViewTree]);

        renderNotificationPolicies();
        const allRoots = await ui.rootRouteContainer.findAll();
        const defaultPolicyEl = allRoots[0];
        // No actions available = no more-actions button rendered
        expect(within(defaultPolicyEl).queryByTestId('more-actions')).not.toBeInTheDocument();
      });
    });
  });

  describe('Analytics', () => {
    beforeEach(() => {
      setAllRoutingTreePermissions({ canWrite: true, canDelete: true, canAdmin: true });
      jest.spyOn(analytics, 'trackNotificationPolicyExported');
      jest.spyOn(analytics, 'trackNotificationPoliciesToggledAll');
      // Add a handler for the export API to prevent unhandled request errors
      server.use(http.get('/api/v1/provisioning/policies/export', () => HttpResponse.text('')));
      // The measureText utility in @grafana/ui caches the canvas context at module level.
      // jest.resetAllMocks() between tests resets the cached context's measureText mock, causing
      // crashes when uncached text is measured. We patch the context directly to fix this.
      const { getCanvasContext } = require('@grafana/ui');
      const ctx = getCanvasContext();
      ctx.measureText = jest.fn().mockReturnValue({ width: 100 } as TextMetrics);
    });

    it('tracks export click for the default policy', async () => {
      grantAlertmanagerAbilities(allPolicyActions);
      const user = userEvent.setup();

      renderNotificationPolicies();

      const allRoots = await ui.rootRouteContainer.findAll();
      const defaultPolicyEl = allRoots[0];
      await user.click(within(defaultPolicyEl).getByTestId('more-actions'));
      await user.click(screen.getByRole('menuitem', { name: 'Export' }));

      expect(analytics.trackNotificationPolicyExported).toHaveBeenCalledWith({ isDefaultPolicy: true });
    });

    it('tracks export click for a custom policy', async () => {
      grantAlertmanagerAbilities(allPolicyActions);
      const user = userEvent.setup();

      renderNotificationPolicies();

      const allRoots = await ui.rootRouteContainer.findAll();
      // The second root container is a custom policy tree (not ROOT_ROUTE_NAME)
      const customPolicyEl = allRoots[1];
      await user.click(within(customPolicyEl).getByTestId('more-actions'));
      await user.click(screen.getByRole('menuitem', { name: 'Export' }));

      expect(analytics.trackNotificationPolicyExported).toHaveBeenCalledWith({ isDefaultPolicy: false });
    });

    it('tracks expand all click', async () => {
      grantAlertmanagerAbilities(allPolicyActions);
      const user = userEvent.setup();

      renderNotificationPolicies();

      // Wait for policies to load
      await ui.rootRouteContainer.findAll();

      const expandAllButton = await screen.findByRole('button', { name: /expand all/i });
      await user.click(expandAllButton);

      expect(analytics.trackNotificationPoliciesToggledAll).toHaveBeenCalledWith(
        expect.objectContaining({ action: 'expand' })
      );
    });

    it('tracks collapse all click after expanding', async () => {
      grantAlertmanagerAbilities(allPolicyActions);
      const user = userEvent.setup();

      renderNotificationPolicies();
      await ui.rootRouteContainer.findAll();

      // Expand all, then collapse all
      await user.click(await screen.findByRole('button', { name: /expand all/i }));
      await user.click(await screen.findByRole('button', { name: /collapse all/i }));

      expect(analytics.trackNotificationPoliciesToggledAll).toHaveBeenCalledWith(
        expect.objectContaining({ action: 'collapse' })
      );
    });
  });
});
