import {
  type Dependencies,
  type PublicInterface,
} from '../../iocContainer/index.js';
import { type NotificationsService } from '../../services/notificationsService/notificationsService.js';
import { type GetCurrentPeriodRuleAlarmStatuses } from '../../services/ruleAnomalyDetectionService/getCurrentPeriodRuleAlarmStatuses.js';
import createOrg from '../../test/fixtureHelpers/createOrg.js';
import createRule from '../../test/fixtureHelpers/createRule.js';
import createUser from '../../test/fixtureHelpers/createUser.js';
import { makeTransactionalTestWithFixture } from '../../test/harness/transactionalTest.js';
import { type Mocked } from '../../test/mockHelpers/jestMocks.js';
import { RuleAlarmStatus } from '../moderationConfigService/index.js';
import DetectRulePassRateAnomaliesJob from './detectRulePassRateAnomaliesJob.js';

function makeMockKyselyForRules(
  fakeRules: Array<{
    id: string;
    orgId: string;
    creatorId: string;
    name: string;
    alarmStatus: RuleAlarmStatus;
    statusIfUnexpired: string;
  }>,
  orgRows: Array<{ id: string; on_call_alert_email: string | null }>,
) {
  const updateExecute = jest.fn().mockResolvedValue(undefined);
  const mockDb = {
    selectFrom: jest.fn((table: string) => {
      const chain: {
        select: jest.Mock;
        where: jest.Mock;
        execute: jest.Mock;
      } = {
        select: jest.fn(),
        where: jest.fn(),
        execute: jest.fn(),
      };
      chain.select.mockReturnValue(chain);
      chain.where.mockReturnValue(chain);
      chain.execute.mockImplementation(async () => {
        if (table === 'public.rules') {
          return fakeRules.map((r) => ({
            id: r.id,
            org_id: r.orgId,
            creator_id: r.creatorId,
            name: r.name,
            alarm_status: r.alarmStatus,
            status_if_unexpired: r.statusIfUnexpired,
          }));
        }
        if (table === 'public.orgs') {
          return orgRows;
        }
        return [];
      });
      return chain;
    }),
    updateTable: jest.fn(() => ({
      set: jest.fn().mockReturnValue({
        where: jest.fn().mockReturnValue({
          execute: updateExecute,
        }),
      }),
    })),
    __updateExecute: updateExecute,
  };
  return mockDb;
}

describe('Detect Rule Anomalies', () => {
  describe('worker', () => {
    const testWithFixture = makeTransactionalTestWithFixture(
      async ({ deps }) => {
        const { ModerationConfigService, ApiKeyService, KyselyPg } = deps;

        // Make some fake rules (w/ stable ids so we can match them in a snapshot)
        // in different initial alarm statuses, to test all 9 combinations [i.e.,
        // starting and ending at one of (OK, ALARM, or INSUFFICENT_DATA), where
        // the start and end states can be the same]. The rows are written inside
        // the test transaction and rolled back afterward, so the stable ids never
        // leak across runs.
        const { org } = await createOrg({
          KyselyPg,
          ModerationConfigService,
          ApiKeyService,
        });
        const { org: org2 } = await createOrg(
          {
            KyselyPg,
            ModerationConfigService,
            ApiKeyService,
          },
          undefined,
          { onCallAlertEmail: 'test@gmail.com' },
        );
        const { user: ruleOwner } = await createUser(KyselyPg, org.id, {
          id: 'cb34377bcc3',
        });
        const { user: ruleOwner2 } = await createUser(KyselyPg, org.id, {
          id: 'cb34377bcc4',
        });
        const fakeRules = await Promise.all([
          createRule(KyselyPg, org.id, {
            alarmStatus: RuleAlarmStatus.ALARM,
            id: '9d237a650c1',
            creator: ruleOwner,
          }),
          createRule(KyselyPg, org.id, {
            alarmStatus: RuleAlarmStatus.ALARM,
            id: '386da8abc3b',
            creator: ruleOwner,
          }),
          createRule(KyselyPg, org.id, {
            alarmStatus: RuleAlarmStatus.ALARM,
            id: 'd237a650c13',
            creator: ruleOwner,
          }),

          createRule(KyselyPg, org.id, {
            alarmStatus: RuleAlarmStatus.OK,
            id: '86da8abc3b6',
            creator: ruleOwner,
          }),
          createRule(KyselyPg, org.id, {
            alarmStatus: RuleAlarmStatus.OK,
            id: 'fdb4ee86f93',
            creator: ruleOwner,
          }),
          createRule(KyselyPg, org.id, {
            alarmStatus: RuleAlarmStatus.OK,
            id: '237a650c134',
            creator: ruleOwner,
          }),

          createRule(KyselyPg, org2.id, {
            alarmStatus: RuleAlarmStatus.INSUFFICIENT_DATA,
            id: 'db4ee86f938',
            creator: ruleOwner2,
          }),
          createRule(KyselyPg, org2.id, {
            alarmStatus: RuleAlarmStatus.INSUFFICIENT_DATA,
            id: '37a650c1342',
            creator: ruleOwner2,
          }),
          createRule(KyselyPg, org2.id, {
            alarmStatus: RuleAlarmStatus.INSUFFICIENT_DATA,
            id: 'b4ee86f9386',
            creator: ruleOwner2,
          }),
        ]);

        const mockDummyRules = fakeRules.map((r) => ({
          id: r.id,
          orgId: r.orgId,
          creatorId: r.creatorId,
          name: r.name,
          alarmStatus: r.alarmStatus,
          statusIfUnexpired: r.statusIfUnexpired,
        }));

        const mockGetCurrentPeriodRuleAlarmStatuses: GetCurrentPeriodRuleAlarmStatuses =
          async () => {
            const newAlarmStatusByRule =
              // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
              {} as Awaited<ReturnType<GetCurrentPeriodRuleAlarmStatuses>>;

            fakeRules.forEach((rule, i) => {
              // eslint-disable-next-line functional/immutable-data
              newAlarmStatusByRule[rule.id] = {
                status:
                  i % 3 === 0
                    ? RuleAlarmStatus.ALARM
                    : i % 3 === 1
                      ? RuleAlarmStatus.OK
                      : RuleAlarmStatus.INSUFFICIENT_DATA,
                meta: {
                  lastPeriodPassRate: 0.5,
                  secondToLastPeriodPassRate: 0.4,
                },
              };
            });
            return newAlarmStatusByRule;
          };

        const mockNotificationsService = {
          createNotifications: jest.fn(),
          getNotificationsForUser: jest.fn(),
        } as unknown as Mocked<
          PublicInterface<NotificationsService>,
          'createNotifications'
        >;

        const mockKysely = makeMockKyselyForRules(mockDummyRules, [
          { id: org.id, on_call_alert_email: null },
          { id: org2.id, on_call_alert_email: 'test@gmail.com' },
        ]);

        return {
          mockKysely,
          mockNotificationsService,
          mockGetCurrentPeriodRuleAlarmStatuses,
          mockDummyRules,
        };
      },
    );

    testWithFixture(
      'should generate the proper notifications + update rules',
      async ({
        mockKysely,
        mockNotificationsService,
        mockGetCurrentPeriodRuleAlarmStatuses,
        mockDummyRules,
      }) => {
        const worker = DetectRulePassRateAnomaliesJob(
          mockKysely as unknown as Dependencies['KyselyPg'],
          mockNotificationsService,
          mockGetCurrentPeriodRuleAlarmStatuses,
          jest.fn<() => Promise<void>>(),
        );
        await worker.run();

        const mockCreateNotifications =
          mockNotificationsService.createNotifications;

        expect(mockCreateNotifications).toHaveBeenCalledTimes(1);
        expect(
          mockCreateNotifications.mock.calls[0][0]
            .slice(0)
            .sort((a, b) => a.data.ruleId.localeCompare(b.data.ruleId)),
        ).toMatchInlineSnapshot(`
        [
          {
            "data": {
              "lastPeriodPassRate": 0.5,
              "ruleId": "386da8abc3b",
              "ruleName": "Dummy_Rule_Name_386da8abc3b",
              "secondToLastPeriodPassRate": 0.4,
            },
            "message": "[Alarm Cleared - Live Rule] Dummy_Rule_Name_386da8abc3b has stopped passing at an anomalous rate.",
            "recipients": [
              {
                "type": "user_id",
                "value": "cb34377bcc3",
              },
            ],
            "type": "RULE_PASS_RATE_INCREASE_ANOMALY_END",
          },
          {
            "data": {
              "lastPeriodPassRate": 0.5,
              "ruleId": "86da8abc3b6",
              "ruleName": "Dummy_Rule_Name_86da8abc3b6",
              "secondToLastPeriodPassRate": 0.4,
            },
            "message": "[Alarm Triggered - Live Rule] Dummy_Rule_Name_86da8abc3b6 has started passing at an anomalous rate.",
            "recipients": [
              {
                "type": "user_id",
                "value": "cb34377bcc3",
              },
            ],
            "type": "RULE_PASS_RATE_INCREASE_ANOMALY_START",
          },
          {
            "data": {
              "lastPeriodPassRate": 0.5,
              "ruleId": "d237a650c13",
              "ruleName": "Dummy_Rule_Name_d237a650c13",
              "secondToLastPeriodPassRate": 0.4,
            },
            "message": "[Alarm Cleared - Live Rule] Dummy_Rule_Name_d237a650c13 has stopped passing at an anomalous rate.",
            "recipients": [
              {
                "type": "user_id",
                "value": "cb34377bcc3",
              },
            ],
            "type": "RULE_PASS_RATE_INCREASE_ANOMALY_END",
          },
          {
            "data": {
              "lastPeriodPassRate": 0.5,
              "ruleId": "db4ee86f938",
              "ruleName": "Dummy_Rule_Name_db4ee86f938",
              "secondToLastPeriodPassRate": 0.4,
            },
            "message": "[Alarm Triggered - Live Rule] Dummy_Rule_Name_db4ee86f938 has started passing at an anomalous rate.",
            "recipients": [
              {
                "type": "user_id",
                "value": "cb34377bcc4",
              },
              {
                "type": "email_address",
                "value": "test@gmail.com",
              },
            ],
            "type": "RULE_PASS_RATE_INCREASE_ANOMALY_START",
          },
        ]
      `);

        const expectedUpdates = mockDummyRules.filter(
          (_rule, i) => ![0, 4, 8].includes(i),
        ).length;
        expect(mockKysely.updateTable).toHaveBeenCalledTimes(expectedUpdates);
      },
    );
  });
});
