import { type PropsWithChildren } from 'react';
import { getWrapper, render, renderHook, screen, waitFor } from 'test/test-utils';

import { config } from '@grafana/runtime';
import { setupMswServer } from 'app/features/alerting/unified/mockApi';
import { setFolderAccessControl } from 'app/features/alerting/unified/mocks/server/configure';
import { MIMIR_DATASOURCE_UID } from 'app/features/alerting/unified/mocks/server/constants';
import {
  type AlertManagerDataSourceJsonData,
  AlertManagerImplementation,
} from 'app/plugins/datasource/alertmanager/types';
import { AccessControlAction } from 'app/types/accessControl';
import { type CombinedRule } from 'app/types/unified-alerting';

import { getCloudRule, getGrafanaRule, grantUserPermissions, mockDataSource } from '../mocks';
import { AlertmanagerProvider } from '../state/AlertmanagerContext';
import { grantPermissionsHelper } from '../test/test-utils';
import { setupDataSources } from '../testSetup/datasources';
import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
import * as misc from '../utils/misc';

import {
  AlertRuleAction,
  AlertmanagerAction,
  EnrichmentAction,
  useAlertmanagerAbilities,
  useAlertmanagerAbility,
  useAllAlertRuleAbilities,
  useAllAlertmanagerAbilities,
  useEnrichmentAbilities,
  useEnrichmentAbility,
} from './useAbilities';

/**
 * This test will write snapshots with a record of the current permissions assigned to actions.
 * We encourage that every change to the snapshot is inspected _very_ thoroughly!
 */
describe('alertmanager abilities', () => {
  it("should report Create / Update / Delete actions aren't supported for external vanilla alertmanager", () => {
    setupDataSources(
      mockDataSource<AlertManagerDataSourceJsonData>({
        name: GRAFANA_RULES_SOURCE_NAME,
        type: DataSourceType.Alertmanager,
        jsonData: { implementation: AlertManagerImplementation.prometheus },
      })
    );

    const { result } = renderHook(() => useAllAlertmanagerAbilities(), {
      wrapper: createAlertmanagerWrapper('does-not-exist'),
    });
    expect(result.current).toMatchSnapshot();
  });

  it('should report everything is supported for builtin alertmanager', () => {
    setupDataSources(
      mockDataSource<AlertManagerDataSourceJsonData>({
        name: GRAFANA_RULES_SOURCE_NAME,
        type: DataSourceType.Alertmanager,
      })
    );

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

    const { result } = renderHook(() => useAllAlertmanagerAbilities(), {
      wrapper: createAlertmanagerWrapper(GRAFANA_RULES_SOURCE_NAME),
    });

    Object.values(result.current).forEach(([supported]) => {
      expect(supported).toBe(true);
    });

    // since we only granted "read" permissions, only those should be allowed
    const { result: viewResult } = renderHook(() => useAlertmanagerAbility(AlertmanagerAction.ViewSilence), {
      wrapper: createAlertmanagerWrapper(GRAFANA_RULES_SOURCE_NAME),
    });

    const [viewSupported, viewAllowed] = viewResult.current;

    expect(viewSupported).toBe(true);
    expect(viewAllowed).toBe(true);

    // editing should not be allowed, but supported
    const { result: editResult } = renderHook(() => useAlertmanagerAbility(AlertmanagerAction.ViewSilence), {
      wrapper: createAlertmanagerWrapper(GRAFANA_RULES_SOURCE_NAME),
    });

    const [editSupported, editAllowed] = editResult.current;

    expect(editSupported).toBe(true);
    expect(editAllowed).toBe(true);

    // record the snapshot to prevent future regressions
    expect(result.current).toMatchSnapshot();
  });

  it('should report everything except exporting for Mimir alertmanager', () => {
    setupDataSources(
      mockDataSource<AlertManagerDataSourceJsonData>({
        name: MIMIR_DATASOURCE_UID,
        type: DataSourceType.Alertmanager,
        jsonData: {
          implementation: AlertManagerImplementation.mimir,
        },
      })
    );

    grantUserPermissions([
      AccessControlAction.AlertingNotificationsExternalRead,
      AccessControlAction.AlertingNotificationsExternalWrite,
      AccessControlAction.AlertingInstancesExternalRead,
      AccessControlAction.AlertingInstancesExternalWrite,
    ]);

    const { result } = renderHook(() => useAllAlertmanagerAbilities(), {
      wrapper: createAlertmanagerWrapper('mimir'),
    });

    expect(result.current).toMatchSnapshot();
  });

  it('should be able to return multiple abilities', () => {
    setupDataSources(
      mockDataSource<AlertManagerDataSourceJsonData>({
        name: GRAFANA_RULES_SOURCE_NAME,
        type: DataSourceType.Alertmanager,
      })
    );

    grantUserPermissions([AccessControlAction.AlertingNotificationsRead]);

    const { result } = renderHook(
      () =>
        useAlertmanagerAbilities([
          AlertmanagerAction.ViewContactPoint,
          AlertmanagerAction.CreateContactPoint,
          AlertmanagerAction.ExportContactPoint,
        ]),
      {
        wrapper: createAlertmanagerWrapper(GRAFANA_RULES_SOURCE_NAME),
      }
    );

    expect(result.current).toHaveLength(3);
    expect(result.current[0]).toStrictEqual([true, true]);
    expect(result.current[1]).toStrictEqual([true, false]);
    expect(result.current[2]).toStrictEqual([true, true]);
  });
});

describe('notification policy abilities — K8s-scoped permissions', () => {
  const setupGrafanaAlertmanager = () =>
    setupDataSources(
      mockDataSource<AlertManagerDataSourceJsonData>({
        name: GRAFANA_RULES_SOURCE_NAME,
        type: DataSourceType.Alertmanager,
      })
    );

  const renderAbility = (action: AlertmanagerAction) =>
    renderHook(() => useAlertmanagerAbility(action), {
      wrapper: createAlertmanagerWrapper(GRAFANA_RULES_SOURCE_NAME),
    });

  it('ViewNotificationPolicyTree: allows access with only ActionAlertingManagedRoutesRead', () => {
    setupGrafanaAlertmanager();
    grantUserPermissions([AccessControlAction.ActionAlertingManagedRoutesRead]);

    const { result } = renderAbility(AlertmanagerAction.ViewNotificationPolicyTree);
    const [supported, allowed] = result.current;

    expect(supported).toBe(true);
    expect(allowed).toBe(true);
  });

  it('CreateNotificationPolicy: allows access with only ActionAlertingManagedRoutesCreate', () => {
    setupGrafanaAlertmanager();
    grantUserPermissions([AccessControlAction.ActionAlertingManagedRoutesCreate]);

    const { result } = renderAbility(AlertmanagerAction.CreateNotificationPolicy);
    const [supported, allowed] = result.current;

    expect(supported).toBe(true);
    expect(allowed).toBe(true);
  });

  it('UpdateNotificationPolicyTree: allows access with only ActionAlertingManagedRoutesWrite', () => {
    setupGrafanaAlertmanager();
    grantUserPermissions([AccessControlAction.ActionAlertingManagedRoutesWrite]);

    const { result } = renderAbility(AlertmanagerAction.UpdateNotificationPolicyTree);
    const [supported, allowed] = result.current;

    expect(supported).toBe(true);
    expect(allowed).toBe(true);
  });

  it('DeleteNotificationPolicy: allows access with only ActionAlertingManagedRoutesDelete', () => {
    setupGrafanaAlertmanager();
    grantUserPermissions([AccessControlAction.ActionAlertingManagedRoutesDelete]);

    const { result } = renderAbility(AlertmanagerAction.DeleteNotificationPolicy);
    const [supported, allowed] = result.current;

    expect(supported).toBe(true);
    expect(allowed).toBe(true);
  });

  it.each([
    [AlertmanagerAction.ViewNotificationPolicyTree, AccessControlAction.AlertingNotificationsRead],
    [AlertmanagerAction.ViewNotificationPolicyTree, AccessControlAction.AlertingRoutesRead],
    // For write actions, alert.notifications.routes:write (not alert.notifications:write) is the
    // correct standalone permission — it must be included in PERMISSIONS_NOTIFICATION_POLICIES to
    // make the Grafana alertmanager available in getAlertManagerDataSourcesByPermission.
    [AlertmanagerAction.CreateNotificationPolicy, AccessControlAction.AlertingRoutesWrite],
    [AlertmanagerAction.UpdateNotificationPolicyTree, AccessControlAction.AlertingRoutesWrite],
    [AlertmanagerAction.DeleteNotificationPolicy, AccessControlAction.AlertingRoutesWrite],
  ] as const)(
    '%s is still allowed with legacy permission %s',
    (action: AlertmanagerAction, permission: AccessControlAction) => {
      setupGrafanaAlertmanager();
      grantUserPermissions([permission]);

      const { result } = renderAbility(action);
      const [, allowed] = result.current;
      expect(allowed).toBe(true);
    }
  );
});

setupMswServer();

const wrapper = () => getWrapper({ renderWithRouter: true });

/**
 * Render the hook result in a component so we can more reliably check that the result has settled
 * after API requests. Without this approach, the hook might return `[false, false]` whilst
 * API requests are still loading
 */
const RenderActionPermissions = ({ rule, action }: { rule: CombinedRule; action: AlertRuleAction }) => {
  const result = useAllAlertRuleAbilities(rule);
  const [isSupported, isAllowed] = result[action];
  return (
    <>
      {isSupported && 'supported'}
      {isAllowed && 'allowed'}
    </>
  );
};

describe('AlertRule abilities', () => {
  it('should report that all actions are supported for a Grafana Managed alert rule', async () => {
    const rule = getGrafanaRule();

    const { result } = renderHook(() => useAllAlertRuleAbilities(rule), { wrapper: wrapper() });

    await waitFor(() => {
      const results = Object.values(result.current);

      for (const [supported, _allowed] of results) {
        expect(supported).toBe(true);
      }
    });

    expect(result.current).toMatchSnapshot();
  });

  it('grants correct silence permissions for folder with silence create permission', async () => {
    setFolderAccessControl({ [AccessControlAction.AlertingSilenceCreate]: true });

    const rule = getGrafanaRule();

    render(<RenderActionPermissions rule={rule} action={AlertRuleAction.Silence} />);

    expect(await screen.findByText(/supported/)).toBeInTheDocument();
    expect(await screen.findByText(/allowed/)).toBeInTheDocument();
  });

  it('does not grant silence permissions for folder without silence create permission', async () => {
    setFolderAccessControl({ [AccessControlAction.AlertingSilenceCreate]: false });

    const rule = getGrafanaRule();

    render(<RenderActionPermissions rule={rule} action={AlertRuleAction.Silence} />);

    expect(await screen.findByText(/supported/)).toBeInTheDocument();
    expect(screen.queryByText(/allowed/)).not.toBeInTheDocument();
  });

  it('should report no permissions while we are loading data for cloud rule', async () => {
    const mimirDs = mockDataSource({ uid: 'mimir', name: 'Mimir' });
    setupDataSources(mimirDs);

    const rule = getCloudRule({}, { rulesSource: mimirDs });

    const { result } = renderHook(() => useAllAlertRuleAbilities(rule), { wrapper: wrapper() });

    await waitFor(() => {
      expect(result.current).not.toBeUndefined();
    });

    expect(result.current).toMatchSnapshot();
  });

  it('should allow editing/deleting rules with plugin origin label when plugin is not installed', async () => {
    // Create a rule with a plugin origin label for a plugin that doesn't exist
    const rule = getGrafanaRule({
      labels: { __grafana_origin: 'plugin/non-existent-plugin' },
    });

    const { result } = renderHook(() => useAllAlertRuleAbilities(rule), { wrapper: wrapper() });

    await waitFor(() => {
      // Wait for the abilities to settle - update should be supported (not loading)
      const [updateSupported] = result.current[AlertRuleAction.Update];
      expect(updateSupported).toBe(true);
    });

    // When plugin is not installed, these actions should be supported
    const [updateSupported] = result.current[AlertRuleAction.Update];
    const [deleteSupported] = result.current[AlertRuleAction.Delete];

    expect(updateSupported).toBe(true);
    expect(deleteSupported).toBe(true);
  });
});

describe('enrichment abilities', () => {
  setupMswServer();

  const originalFeatureToggle = config.featureToggles.alertEnrichment;

  beforeEach(() => {
    // Default to feature toggle enabled
    config.featureToggles.alertEnrichment = true;
  });

  afterEach(() => {
    config.featureToggles.alertEnrichment = originalFeatureToggle;
  });

  it('should grant read and write permissions to admin users when feature is enabled', () => {
    grantPermissionsHelper([]);
    jest.spyOn(misc, 'isAdmin').mockReturnValue(true);

    const { result } = renderHook(() => useEnrichmentAbilities(), { wrapper: wrapper() });

    const [readSupported, readAllowed] = result.current[EnrichmentAction.Read];
    const [writeSupported, writeAllowed] = result.current[EnrichmentAction.Write];

    expect(readSupported).toBe(true);
    expect(readAllowed).toBe(true);
    expect(writeSupported).toBe(true);
    expect(writeAllowed).toBe(true);
  });

  it('should grant read permission when user has enrichments:read permission', () => {
    jest.spyOn(misc, 'isAdmin').mockReturnValue(false);
    grantPermissionsHelper([AccessControlAction.AlertingEnrichmentsRead]);

    const { result } = renderHook(() => useEnrichmentAbilities(), { wrapper: wrapper() });

    const [readSupported, readAllowed] = result.current[EnrichmentAction.Read];
    const [writeSupported, writeAllowed] = result.current[EnrichmentAction.Write];

    expect(readSupported).toBe(true);
    expect(readAllowed).toBe(true);
    expect(writeSupported).toBe(true);
    expect(writeAllowed).toBe(false);
  });

  it('should grant write permission when user has enrichments:write permission', () => {
    jest.spyOn(misc, 'isAdmin').mockReturnValue(false);
    grantPermissionsHelper([AccessControlAction.AlertingEnrichmentsWrite]);

    const { result } = renderHook(() => useEnrichmentAbilities(), { wrapper: wrapper() });

    const [readSupported, readAllowed] = result.current[EnrichmentAction.Read];
    const [writeSupported, writeAllowed] = result.current[EnrichmentAction.Write];

    expect(readSupported).toBe(true);
    expect(readAllowed).toBe(false);
    expect(writeSupported).toBe(true);
    expect(writeAllowed).toBe(true);
  });

  it('should grant both read and write permissions when user has both permissions', () => {
    jest.spyOn(misc, 'isAdmin').mockReturnValue(false);
    grantPermissionsHelper([AccessControlAction.AlertingEnrichmentsRead, AccessControlAction.AlertingEnrichmentsWrite]);

    const { result } = renderHook(() => useEnrichmentAbilities(), { wrapper: wrapper() });

    const [readSupported, readAllowed] = result.current[EnrichmentAction.Read];
    const [writeSupported, writeAllowed] = result.current[EnrichmentAction.Write];

    expect(readSupported).toBe(true);
    expect(readAllowed).toBe(true);
    expect(writeSupported).toBe(true);
    expect(writeAllowed).toBe(true);
  });

  it('should deny all permissions when user is not admin and has no permissions', () => {
    jest.spyOn(misc, 'isAdmin').mockReturnValue(false);
    grantPermissionsHelper([]);

    const { result } = renderHook(() => useEnrichmentAbilities(), { wrapper: wrapper() });

    const [readSupported, readAllowed] = result.current[EnrichmentAction.Read];
    const [writeSupported, writeAllowed] = result.current[EnrichmentAction.Write];

    expect(readSupported).toBe(true);
    expect(readAllowed).toBe(false);
    expect(writeSupported).toBe(true);
    expect(writeAllowed).toBe(false);
  });

  it('should return correct ability for specific action using useEnrichmentAbility', () => {
    jest.spyOn(misc, 'isAdmin').mockReturnValue(false);
    grantPermissionsHelper([AccessControlAction.AlertingEnrichmentsRead]);

    const { result } = renderHook(() => useEnrichmentAbility(EnrichmentAction.Read), { wrapper: wrapper() });

    const [supported, allowed] = result.current;

    expect(supported).toBe(true);
    expect(allowed).toBe(true);
  });

  it('should report enrichments as not supported when feature toggle is disabled', () => {
    config.featureToggles.alertEnrichment = false;
    jest.spyOn(misc, 'isAdmin').mockReturnValue(true);
    grantPermissionsHelper([AccessControlAction.AlertingEnrichmentsRead, AccessControlAction.AlertingEnrichmentsWrite]);

    const { result } = renderHook(() => useEnrichmentAbilities(), { wrapper: wrapper() });

    const [readSupported, readAllowed] = result.current[EnrichmentAction.Read];
    const [writeSupported, writeAllowed] = result.current[EnrichmentAction.Write];

    // Enrichments not supported when feature toggle is off
    expect(readSupported).toBe(false);
    expect(writeSupported).toBe(false);
    // Permissions would be granted if it were supported
    expect(readAllowed).toBe(true);
    expect(writeAllowed).toBe(true);
  });
});

function createAlertmanagerWrapper(alertmanagerSourceName: string) {
  const ProviderWrapper = getWrapper({ renderWithRouter: true });
  const wrapperComponent = (props: PropsWithChildren) => (
    <ProviderWrapper>
      <AlertmanagerProvider accessType={'notification'} alertmanagerSourceName={alertmanagerSourceName}>
        {props.children}
      </AlertmanagerProvider>
    </ProviderWrapper>
  );

  return wrapperComponent;
}
