import { ScalarTypes } from '@roostorg/coop-types';

import getBottle, { type Dependencies } from '../../iocContainer/index.js';
import { getBottleContainerWithIOMocks } from '../../test/setupMockedServer.js';
import { SignalType, type SignalsService } from '../signalsService/index.js';
import makeGetTransientRunSignalWithCache from './signalExecutionService.js';

describe('Signal Execution Service', () => {
  let container: Dependencies;
  let signalsService: SignalsService;
  let getPolicyActionPenalties: Dependencies['getPolicyActionPenaltiesEventuallyConsistent'];
  const mockLocationsLoader = async () => [];
  const mockTextBankStringsLoader = jest.fn(async ({ bankId }) =>
    bankId === '1' ? ['a', 'b', 'c'] : bankId === '2' ? ['d', 'e', 'f'] : [],
  );
  const mockGetImageBank = jest.fn(async ({ bankId }) =>
    bankId === 'test-bank'
      ? {
          id: 1,
          name: 'test-bank',
          hma_name: 'org_test-bank',
          description: null,
          enabled_ratio: 1.0,
          org_id: 'test-org',
          created_at: new Date(),
          updated_at: new Date(),
        }
      : null,
  );

  // eslint-disable-next-line functional/immutable-data
  mockLocationsLoader.close = jest.fn();
  /* eslint-disable functional/immutable-data, @typescript-eslint/no-explicit-any */
  (mockTextBankStringsLoader as any).close = jest.fn();
  (mockGetImageBank as any).close = jest.fn();
  /* eslint-enable functional/immutable-data, @typescript-eslint/no-explicit-any */

  describe('getTransientRunSignalWithCache', () => {
    beforeAll(async () => {
      // No mutation rule here is a false positive, since this is more initial
      // setup (we're never gonna reassign again later) that simply has to use
      // `let` vars to defer the work until the test suite is actually running
      // (so we don't bother w/ it if this test suite is skipped, e.g., in which
      // case cleanup wouldn't happen).

      container = await getBottleContainerWithIOMocks();
      signalsService = container.SignalsService;
      getPolicyActionPenalties =
        container.getPolicyActionPenaltiesEventuallyConsistent;
    });

    afterAll(async () => {
      await container.closeSharedResourcesForShutdown();
    }, 20_000);

    beforeEach(() => {
      // This is only safe while we're not running tests concurrently.
      // Consider using the `makeTestWithFixture` helper instead to make
      // a local copy of this state for each test.
      jest.clearAllMocks();
    });

    test('should batch textBank loads w/i a single tick', async () => {
      const runSignal = makeGetTransientRunSignalWithCache(
        mockLocationsLoader,
        mockTextBankStringsLoader,
        getPolicyActionPenalties,
        mockGetImageBank,
        signalsService,
        container.Tracer,
      )();

      const signalInputs = [
        {
          signal: { type: SignalType.TEXT_MATCHING_CONTAINS_REGEX },
          value: { type: ScalarTypes.STRING, value: 'a' },
          matchingValues: {
            textBankIds: ['1'],
          },
          threshold: null,
          comparator: null,
          userId: 'dummy',
          orgId: 'dummmy',
        },
        {
          signal: { type: SignalType.TEXT_MATCHING_CONTAINS_TEXT },
          value: { type: ScalarTypes.STRING, value: 'a' },
          matchingValues: {
            textBankIds: ['2'],
          },
          threshold: null,
          comparator: null,
          userId: 'dummy',
          orgId: 'dummmy',
        },
      ] as const;

      const results = await Promise.all(signalInputs.map(runSignal));
      expect(mockTextBankStringsLoader).toHaveBeenCalledTimes(2);
      expect(mockTextBankStringsLoader.mock.calls).toMatchInlineSnapshot(`
        [
          [
            {
              "bankId": "1",
              "orgId": "dummmy",
            },
          ],
          [
            {
              "bankId": "2",
              "orgId": "dummmy",
            },
          ],
        ]
      `);

      expect(results).toEqual([
        {
          score: true,
          outputType: { scalarType: ScalarTypes.BOOLEAN },
          matchedValue: 'a',
        },
        {
          score: false,
          outputType: { scalarType: ScalarTypes.BOOLEAN },
          matchedValue: undefined,
        },
      ]);
    });

    test('should not re-run the same signal with the same input', async () => {
      // NB: this test actually does run the signals, but that's fine; these
      // signals don't hit the network. (The point of the mock is just so we can
      // spy on how many times runSignal was called.)
      const signalsServiceSpy = (await getBottle()).container.SignalsService;
      /* eslint-disable functional/immutable-data, @typescript-eslint/no-explicit-any --
         jest.fn doesn't preserve the original method's overloads, so we need
         a cast to assign back onto the typed "runSignal" property. */
      signalsServiceSpy.runSignal = jest.fn(
        signalsServiceSpy.runSignal.bind(signalsServiceSpy),
      ) as any;
      /* eslint-enable functional/immutable-data, @typescript-eslint/no-explicit-any */

      const runSignal = makeGetTransientRunSignalWithCache(
        mockLocationsLoader,
        mockTextBankStringsLoader,
        getPolicyActionPenalties,
        mockGetImageBank,
        signalsServiceSpy,
        container.Tracer,
      )();

      const signalInputs = [
        {
          signal: { type: SignalType.TEXT_MATCHING_CONTAINS_REGEX },
          value: { type: ScalarTypes.STRING, value: 'a' },
          matchingValues: {
            textBankIds: ['1'],
          },
          threshold: null,
          comparator: null,
          userId: 'dummy',
          orgId: 'dummmy',
        },
        {
          signal: { type: SignalType.TEXT_MATCHING_CONTAINS_REGEX },
          value: { type: ScalarTypes.STRING, value: 'a' },
          matchingValues: {
            textBankIds: ['1'],
          },
          threshold: null,
          comparator: null,
          userId: 'dummy',
          orgId: 'dummmy',
        },
      ] as const;

      const results = await Promise.all(signalInputs.map(runSignal));
      expect(signalsServiceSpy.runSignal).toHaveBeenCalledTimes(1);
      expect(results).toEqual([
        {
          matchedValue: 'a',
          score: true,
          outputType: { scalarType: ScalarTypes.BOOLEAN },
        },
        {
          matchedValue: 'a',
          score: true,
          outputType: { scalarType: ScalarTypes.BOOLEAN },
        },
      ]);
    });
  });
});
