import { lastValueFrom } from 'rxjs';

import { AlertState, getDefaultTimeRange, type TimeRange } from '@grafana/data';
import { config } from '@grafana/runtime';
import { backendSrv } from 'app/core/services/backend_srv';
import {
  grantUserPermissions,
  mockPromAlertingRule,
  mockPromRuleGroup,
  mockPromRuleNamespace,
} from 'app/features/alerting/unified/mocks';
import { Annotation } from 'app/features/alerting/unified/utils/constants';
import { createDashboardModelFixture } from 'app/features/dashboard/state/__fixtures__/dashboardFixtures';
import { AccessControlAction } from 'app/types/accessControl';
import { PromAlertingRuleState } from 'app/types/unified-alerting-dto';

import { silenceConsoleOutput } from '../../../../../test/core/utils/silenceConsoleOutput';
import * as store from '../../../../store/store';

import { UnifiedAlertStatesWorker } from './UnifiedAlertStatesWorker';
import { type DashboardQueryRunnerOptions } from './types';

jest.mock('@grafana/runtime', () => ({
  ...jest.requireActual('@grafana/runtime'),
  getBackendSrv: () => backendSrv,
}));

function getDefaultOptions(): DashboardQueryRunnerOptions {
  const dashboard = createDashboardModelFixture(
    {
      id: 12345,
      uid: 'a uid',
    },
    {}
  );
  const range = getDefaultTimeRange();

  return { dashboard, range };
}

function getTestContext() {
  jest.clearAllMocks();
  const dispatchMock = jest.spyOn(store, 'dispatch');
  const options = getDefaultOptions();

  return { options, dispatchMock };
}

describe('UnifiedAlertStatesWorker', () => {
  const worker = new UnifiedAlertStatesWorker();

  beforeEach(() => {
    config.publicDashboardAccessToken = '';
    grantUserPermissions(Object.values(AccessControlAction));
  });

  describe('when canWork is called with correct props', () => {
    it('then it should return true', () => {
      const options = getDefaultOptions();

      expect(worker.canWork(options)).toBe(true);
    });
  });

  describe('when canWork is called on a public dashboard view', () => {
    it('then it should return false', () => {
      const options = getDefaultOptions();
      config.publicDashboardAccessToken = 'abc123';

      expect(worker.canWork(options)).toBe(false);
    });
  });

  describe('when canWork is called with no dashboard id', () => {
    it('then it should return false', () => {
      const dashboard = createDashboardModelFixture({});
      const options = { ...getDefaultOptions(), dashboard };

      expect(worker.canWork(options)).toBe(false);
    });
  });

  describe('when canWork is called with wrong range', () => {
    it('then it should return false', () => {
      const defaultRange = getDefaultTimeRange();
      const range: TimeRange = { ...defaultRange, raw: { ...defaultRange.raw, to: 'now-6h' } };
      const options = { ...getDefaultOptions(), range };

      expect(worker.canWork(options)).toBe(false);
    });
  });

  describe('when run is called with incorrect props', () => {
    it('then it should return the correct results', async () => {
      const { options } = getTestContext();
      const dashboard = createDashboardModelFixture({});

      await expect(worker.work({ ...options, dashboard })).toEmitValuesWith((received) => {
        expect(received).toHaveLength(1);
        const results = received[0];
        expect(results).toEqual({ alertStates: [], annotations: [] });
      });
    });
  });

  describe('when run repeatedly for the same dashboard and no alert rules are found', () => {
    const nameSpaces = [mockPromRuleNamespace({ groups: [] })];
    const { dispatchMock, options } = getTestContext();
    dispatchMock.mockResolvedValue(nameSpaces);
    it('then canWork should start returning false', async () => {
      const worker = new UnifiedAlertStatesWorker();
      expect(worker.canWork(options)).toBe(true);
      await lastValueFrom(worker.work(options));
      expect(worker.canWork(options)).toBe(false);
    });
  });

  describe('when run is called with correct props and request is successful', () => {
    it('then it should return the correct results', async () => {
      const nameSpaces = [
        mockPromRuleNamespace({
          groups: [
            mockPromRuleGroup({
              name: 'group1',
              rules: [
                mockPromAlertingRule({
                  name: 'alert1',
                  state: PromAlertingRuleState.Firing,
                  annotations: {
                    [Annotation.dashboardUID]: 'a uid',
                    [Annotation.panelID]: '1',
                  },
                }),
              ],
            }),
            mockPromRuleGroup({
              name: 'group2',
              rules: [
                mockPromAlertingRule({
                  name: 'alert2',
                  state: PromAlertingRuleState.Inactive,
                  annotations: {
                    [Annotation.dashboardUID]: 'a uid',
                    [Annotation.panelID]: '2',
                  },
                }),
              ],
            }),
            mockPromRuleGroup({
              name: 'group3',
              rules: [
                mockPromAlertingRule({
                  name: 'alert3',
                  state: PromAlertingRuleState.Pending,
                  annotations: {
                    [Annotation.dashboardUID]: 'a uid',
                    [Annotation.panelID]: '2',
                  },
                }),
              ],
            }),
          ],
        }),
      ];
      const { dispatchMock, options } = getTestContext();
      dispatchMock.mockResolvedValue({ data: nameSpaces });

      await expect(worker.work(options)).toEmitValuesWith((received) => {
        expect(received).toHaveLength(1);
        const results = received[0];
        expect(results).toEqual({
          alertStates: [
            { id: 0, state: AlertState.Alerting, dashboardUID: 'a uid', panelId: 1 },
            { id: 1, state: AlertState.Pending, dashboardUID: 'a uid', panelId: 2 },
          ],
          annotations: [],
        });
      });

      expect(dispatchMock).toHaveBeenCalledTimes(1);
    });
  });

  describe('when run is called with correct props and request fails', () => {
    silenceConsoleOutput();
    it('then it should return the correct results', async () => {
      const { options, dispatchMock } = getTestContext();
      dispatchMock.mockResolvedValue({ error: 'An error' });

      await expect(worker.work(options)).toEmitValuesWith((received) => {
        expect(received).toHaveLength(1);
        const results = received[0];
        expect(results).toEqual({ alertStates: [], annotations: [] });
      });
    });
  });
  describe('when run is called with correct props and request is cancelled', () => {
    silenceConsoleOutput();
    it('then it should return the correct results', async () => {
      const { options, dispatchMock } = getTestContext();
      dispatchMock.mockResolvedValue({ error: { message: 'Get error' } });

      await expect(worker.work(options)).toEmitValuesWith((received) => {
        expect(received).toHaveLength(1);
        const results = received[0];
        expect(results).toEqual({ alertStates: [], annotations: [] });
      });
    });
  });
});

describe('UnifiedAlertStateWorker with RBAC', () => {
  beforeAll(() => {
    grantUserPermissions([]);
  });

  it('should not do work with insufficient permissions', () => {
    const worker = new UnifiedAlertStatesWorker();
    const options = getDefaultOptions();

    expect(worker.canWork(options)).toBe(false);
  });

  it('should do work with correct permissions', () => {
    grantUserPermissions([AccessControlAction.AlertingRuleRead, AccessControlAction.AlertingRuleExternalRead]);
    const workerWithPermissions = new UnifiedAlertStatesWorker();

    const options = getDefaultOptions();
    expect(workerWithPermissions.canWork(options)).toBe(true);
  });
});
