import { createAction } from '@reduxjs/toolkit';
import { omit } from 'lodash';

import { type GrafanaRuleIdentifier } from 'app/types/unified-alerting';
import { type PostableRulerRuleGroupDTO } from 'app/types/unified-alerting-dto';

import { mockGrafanaRulerRule, mockRulerAlertingRule, mockRulerGrafanaRule, mockRulerRecordingRule } from '../../mocks';
import { fromRulerRule } from '../../utils/rule-id';

import {
  type SwapOperation,
  addRuleAction,
  deleteRuleAction,
  moveRuleGroupAction,
  pauseRuleAction,
  renameRuleGroupAction,
  reorder,
  reorderRulesInRuleGroupAction,
  ruleGroupReducer,
  swapItems,
  updateRuleAction,
  updateRuleGroupAction,
} from './ruleGroups';

describe('pausing rules', () => {
  // pausing only works for Grafana managed rules
  it('should pause a Grafana managed rule in a group', () => {
    const initialGroup: PostableRulerRuleGroupDTO = {
      name: 'group-1',
      interval: '5m',
      rules: [
        mockRulerGrafanaRule({}, { uid: '1' }),
        mockRulerGrafanaRule({}, { uid: '2' }),
        mockRulerGrafanaRule({}, { uid: '3' }),
      ],
    };

    // we will pause rule with UID "2"
    const action = pauseRuleAction({ uid: '2', pause: true });
    const output = ruleGroupReducer(initialGroup, action);

    expect(output).toHaveProperty('rules');
    expect(output.rules).toHaveLength(initialGroup.rules.length);

    expect(output).toHaveProperty('rules.1.grafana_alert.is_paused', true);
    expect(output.rules[0]).toStrictEqual(initialGroup.rules[0]);
    expect(output.rules[2]).toStrictEqual(initialGroup.rules[2]);

    expect(output).toMatchSnapshot();
  });

  it('should throw if the uid does not exist in the group', () => {
    const group: PostableRulerRuleGroupDTO = {
      name: 'group-1',
      interval: '5m',
      rules: [mockRulerGrafanaRule({}, { uid: '1' })],
    };

    const action = pauseRuleAction({ uid: '2', pause: true });

    expect(() => {
      ruleGroupReducer(group, action);
    }).toThrow();
  });
});

describe('removing a rule', () => {
  it('should remove a Grafana managed ruler rule without touching other rules', () => {
    const ruleToDelete = mockRulerGrafanaRule({}, { uid: '2' });

    const initialGroup: PostableRulerRuleGroupDTO = {
      name: 'group-1',
      interval: '5m',
      rules: [mockRulerGrafanaRule({}, { uid: '1' }), ruleToDelete, mockRulerGrafanaRule({}, { uid: '3' })],
    };
    const ruleIdentifier = fromRulerRule('my-datasource', 'my-namespace', 'group-1', ruleToDelete);

    const action = deleteRuleAction({ identifier: ruleIdentifier });
    const output = ruleGroupReducer(initialGroup, action);

    expect(output).toHaveProperty('rules');
    expect(output.rules).toHaveLength(2);
    expect(output.rules[0]).toStrictEqual(initialGroup.rules[0]);
    expect(output.rules[1]).toStrictEqual(initialGroup.rules[2]);

    expect(output).toMatchSnapshot();
  });

  it('should remove a Data source managed ruler rule without touching other rules', () => {
    const ruleToDelete = mockRulerAlertingRule({
      alert: 'delete me',
    });

    const initialGroup: PostableRulerRuleGroupDTO = {
      name: 'group-1',
      interval: '5m',
      rules: [
        mockRulerAlertingRule({ alert: 'do not delete me' }),
        ruleToDelete,
        mockRulerRecordingRule({
          record: 'do not delete me',
        }),
      ],
    };
    const ruleIdentifier = fromRulerRule('my-datasource', 'my-namespace', 'group-1', ruleToDelete);

    const action = deleteRuleAction({ identifier: ruleIdentifier });
    const output = ruleGroupReducer(initialGroup, action);

    expect(output).toHaveProperty('rules');

    expect(output.rules).toHaveLength(2);
    expect(output.rules[0]).toStrictEqual(initialGroup.rules[0]);
    expect(output.rules[1]).toStrictEqual(initialGroup.rules[2]);

    expect(output).toMatchSnapshot();
  });
});

describe('add rule', () => {
  it('should add a single rule to a rule group', () => {
    const ruleToAdd = mockGrafanaRulerRule({ uid: '3' });

    const rule1 = mockRulerGrafanaRule({}, { uid: '1' });
    const rule2 = mockRulerGrafanaRule({}, { uid: '2' });

    const initialGroup: PostableRulerRuleGroupDTO = {
      name: 'group-1',
      interval: '5m',
      rules: [rule1, rule2],
    };

    const action = addRuleAction({ rule: ruleToAdd });
    const output = ruleGroupReducer(initialGroup, action);

    expect(output).toHaveProperty('name', 'group-1');
    expect(output).toHaveProperty('interval', '5m');

    expect(output.rules).toHaveLength(3);
    expect(output.rules[0]).toBe(rule1);
    expect(output.rules[1]).toBe(rule2);
    expect(output.rules[2]).toBe(ruleToAdd);
  });

  it('should allow adding the rule to a new group with custom name and interval', () => {
    const ruleToAdd = mockGrafanaRulerRule({ uid: '1' });

    const initialGroup: PostableRulerRuleGroupDTO = {
      name: 'default',
      interval: '1m',
      rules: [],
    };

    const action = addRuleAction({ rule: ruleToAdd, groupName: 'new group', interval: '10m' });
    const output = ruleGroupReducer(initialGroup, action);

    expect(output).toHaveProperty('name', 'new group');
    expect(output).toHaveProperty('interval', '10m');

    expect(output.rules).toHaveLength(1);
    expect(output.rules[0]).toBe(ruleToAdd);
  });
});

describe('update rule', () => {
  it('should update a single rule in a rule group', () => {
    const ruleToUpdate = mockGrafanaRulerRule({ uid: '1' });
    const ruleIdentifier = fromRulerRule('datasource', 'namespace', 'group', ruleToUpdate);
    const updatedRule: typeof ruleToUpdate = { ...ruleToUpdate, labels: { foo: 'bar' } };

    const otherRule = mockRulerGrafanaRule({}, { uid: '2' });

    const initialGroup: PostableRulerRuleGroupDTO = {
      name: 'group-1',
      interval: '5m',
      rules: [ruleToUpdate, otherRule],
    };

    const action = updateRuleAction({ identifier: ruleIdentifier, rule: updatedRule });
    const output = ruleGroupReducer(initialGroup, action);

    expect(output.rules).toHaveLength(2);
    expect(output.rules[0]).toStrictEqual(updatedRule);
    expect(output.rules[1]).toBe(otherRule);
  });

  it('should throw when rule is not found', () => {
    const rule = mockGrafanaRulerRule({ uid: '1' });
    const ruleIdentifier: GrafanaRuleIdentifier = {
      uid: 'wrong one',
      ruleSourceName: 'grafana',
    };

    const initialGroup: PostableRulerRuleGroupDTO = {
      name: 'group-1',
      interval: '5m',
      rules: [rule],
    };

    const action = updateRuleAction({ identifier: ruleIdentifier, rule });
    expect(() => {
      ruleGroupReducer(initialGroup, action);
    }).toThrow('no rule matching identifier found');
  });
});

describe('re-order rules', () => {
  const r1 = mockGrafanaRulerRule({ uid: 'r1' });
  const r2 = mockGrafanaRulerRule({ uid: 'r2' });
  const r3 = mockGrafanaRulerRule({ uid: 'r3' });

  const initialGroup: PostableRulerRuleGroupDTO = {
    name: 'group-1',
    interval: '5m',
    rules: [r1, r2, r3],
  };

  const swaps: SwapOperation[] = [
    [0, 1],
    [2, 1],
  ];
  const action = reorderRulesInRuleGroupAction({ swaps });
  const output = ruleGroupReducer(initialGroup, action);

  expect(output.rules).toHaveLength(3);
});

describe('rename rule group', () => {
  const initialGroup: PostableRulerRuleGroupDTO = {
    name: 'group-1',
    interval: '5m',
    rules: [],
  };

  it('should allow updating the group name and interval', () => {
    const output = ruleGroupReducer(initialGroup, renameRuleGroupAction({ groupName: 'group-2', interval: '999m' }));
    expect(output).toHaveProperty('name', 'group-2');
    expect(output).toHaveProperty('interval', '999m');

    expect(omit(output, ['name', 'interval'])).toEqual(omit(initialGroup, ['name', 'interval']));
  });
});

describe('move rule group', () => {
  const initialGroup: PostableRulerRuleGroupDTO = {
    name: 'group-1',
    interval: '5m',
    rules: [],
  };

  it('should allow updating the group name and interval', () => {
    const output = ruleGroupReducer(
      initialGroup,
      moveRuleGroupAction({ newNamespaceName: 'doesnt-really-matter', groupName: 'group-2', interval: '999m' })
    );
    expect(output).toHaveProperty('name', 'group-2');
    expect(output).toHaveProperty('interval', '999m');

    expect(omit(output, ['name', 'interval'])).toEqual(omit(initialGroup, ['name', 'interval']));
  });
});

describe('update rule group', () => {
  const initialGroup: PostableRulerRuleGroupDTO = {
    name: 'group-1',
    interval: '5m',
    rules: [],
  };

  it('should allow updating the interval', () => {
    const output = ruleGroupReducer(initialGroup, updateRuleGroupAction({ interval: '999m' }));
    expect(output).toHaveProperty('interval', '999m');

    expect(omit(output, 'interval')).toEqual(omit(initialGroup, 'interval'));
  });
});

describe('unknown actions', () => {
  it('should throw for unknown actions', () => {
    expect(() => {
      const initialGroup: PostableRulerRuleGroupDTO = {
        name: 'group-1',
        interval: '5m',
        rules: [],
      };

      const unknownAction = createAction('unkown');

      // @ts-expect-error
      ruleGroupReducer(initialGroup, unknownAction);
    }).toThrow('Unknown action');
  });
});

describe('reorder and swap', () => {
  it('should reorder arrays', () => {
    const original = [1, 2, 3];
    const operations = [
      [1, 2],
      [0, 2],
    ] satisfies SwapOperation[];
    const expected = [3, 2, 1];

    expect(reorder(original, operations)).toEqual(expected);
    expect(original).toEqual(expected); // make sure it mutates so we can use it in produce functions
  });

  it('inverse swaps should cancel out', () => {
    const original = [1, 2, 3];
    const operations = [
      [1, 2],
      [2, 1],
    ] satisfies SwapOperation[];

    expect(reorder(original, operations)).toEqual(original);
  });

  it('should throw when swapping out of bounds', () => {
    expect(() => {
      swapItems([], [-1, 3]);
    }).toThrow('out of bounds');
  });
});
