import log from 'loglevel';
import {
  FALLBACK_LOCALE,
  I18NMessageDict,
  clearCaches,
  fetchLocale,
  getMessage,
  loadRelativeTimeFormatLocaleData,
} from './i18n';

const localeCodeMock = 'te';
const keyMock = 'testKey';
const errorLocaleMock = 'testLocaleError';
const errorMock = 'TestError';

jest.mock('loglevel');

jest.mock('./fetch-with-timeout', () =>
  jest.fn(() => (url: string) => {
    return Promise.resolve({
      json: () => {
        if (url.includes(errorLocaleMock)) {
          throw new Error(errorMock);
        }

        return { url };
      },
    });
  }),
);

describe('I18N Module', () => {
  beforeEach(() => {
    jest.resetAllMocks();
    clearCaches();
    process.env.IN_TEST = 'true';
  });

  describe('getMessage', () => {
    describe('on error', () => {
      it('returns null if no messages', () => {
        expect(
          getMessage(
            localeCodeMock,
            null as unknown as I18NMessageDict,
            keyMock,
          ),
        ).toBeNull();
      });

      describe('if missing key', () => {
        describe('if not using fallback locale', () => {
          it('logs warning', () => {
            expect(
              getMessage(
                localeCodeMock,
                {} as unknown as I18NMessageDict,
                keyMock,
              ),
            ).toBeNull();

            expect(log.warn).toHaveBeenCalledTimes(1);
            expect(log.warn).toHaveBeenCalledWith(
              `Translator - Unable to find value of key "${keyMock}" for locale "${localeCodeMock}"`,
            );
          });

          it('does not log warning if warning already created', () => {
            expect(
              getMessage(
                localeCodeMock,
                {} as unknown as I18NMessageDict,
                keyMock,
              ),
            ).toBeNull();

            expect(
              getMessage(
                localeCodeMock,
                {} as unknown as I18NMessageDict,
                keyMock,
              ),
            ).toBeNull();

            expect(log.warn).toHaveBeenCalledTimes(1);
            expect(log.warn).toHaveBeenCalledWith(
              `Translator - Unable to find value of key "${keyMock}" for locale "${localeCodeMock}"`,
            );
          });
        });

        describe('if using fallback locale', () => {
          it('logs error', () => {
            delete process.env.IN_TEST;

            expect(
              getMessage(
                FALLBACK_LOCALE,
                {} as unknown as I18NMessageDict,
                keyMock,
              ),
            ).toBeNull();

            expect(log.error).toHaveBeenCalledTimes(1);
            expect(log.error).toHaveBeenCalledWith(
              new Error(
                `Unable to find value of key "${keyMock}" for locale "${FALLBACK_LOCALE}"`,
              ),
            );
          });

          it('throws if test env set', () => {
            expect(() =>
              getMessage(
                FALLBACK_LOCALE,
                {} as unknown as I18NMessageDict,
                keyMock,
              ),
            ).toThrow(
              `Unable to find value of key "${keyMock}" for locale "${FALLBACK_LOCALE}"`,
            );
          });

          it('calls onError callback', () => {
            const onErrorMock = jest.fn();

            try {
              getMessage(
                FALLBACK_LOCALE,
                {} as unknown as I18NMessageDict,
                keyMock,
                [],
                onErrorMock,
              );
            } catch {
              // Expected
            }

            expect(onErrorMock).toHaveBeenCalledTimes(1);
            expect(onErrorMock).toHaveBeenCalledWith(
              new Error(
                `Unable to find value of key "${keyMock}" for locale "${FALLBACK_LOCALE}"`,
              ),
            );
          });

          it('does nothing if error already created', () => {
            const onErrorMock = jest.fn();

            try {
              getMessage(
                FALLBACK_LOCALE,
                {} as unknown as I18NMessageDict,
                keyMock,
                [],
                onErrorMock,
              );
            } catch {
              // Expected
            }

            getMessage(
              FALLBACK_LOCALE,
              {} as unknown as I18NMessageDict,
              keyMock,
              [],
              onErrorMock,
            );

            expect(log.error).toHaveBeenCalledTimes(1);
            expect(onErrorMock).toHaveBeenCalledTimes(1);
          });
        });
      });

      describe('if missing substitution', () => {
        it('logs error', () => {
          expect(
            getMessage(
              localeCodeMock,
              { [keyMock]: { message: 'test1 $1 test2 $2 test3' } },
              keyMock,
              ['a1'],
            ),
          ).toStrictEqual('test1 a1 test2  test3');

          expect(log.error).toHaveBeenCalledTimes(1);
          expect(log.error).toHaveBeenCalledWith(
            new Error(
              `Insufficient number of substitutions for key "${keyMock}" with locale "${localeCodeMock}"`,
            ),
          );
        });

        it('calls onError callback', () => {
          const onErrorMock = jest.fn();

          expect(
            getMessage(
              localeCodeMock,
              { [keyMock]: { message: 'test1 $1 test2 $2 test3' } },
              keyMock,
              ['a1'],
              onErrorMock,
            ),
          ).toStrictEqual('test1 a1 test2  test3');

          expect(onErrorMock).toHaveBeenCalledTimes(1);
          expect(onErrorMock).toHaveBeenCalledWith(
            new Error(
              `Insufficient number of substitutions for key "${keyMock}" with locale "${localeCodeMock}"`,
            ),
          );
        });

        it('does nothing if error already created', () => {
          const onErrorMock = jest.fn();

          expect(
            getMessage(
              localeCodeMock,
              { [keyMock]: { message: 'test1 $1 test2 $2 test3' } },
              keyMock,
              ['a1'],
              onErrorMock,
            ),
          ).toStrictEqual('test1 a1 test2  test3');

          expect(
            getMessage(
              localeCodeMock,
              { [keyMock]: { message: 'test1 $1 test2 $2 test3' } },
              keyMock,
              ['a1'],
              onErrorMock,
            ),
          ).toStrictEqual('test1 a1 test2  test3');

          expect(log.error).toHaveBeenCalledTimes(1);
          expect(onErrorMock).toHaveBeenCalledTimes(1);
        });
      });
    });

    it('returns text only if no substitutions', () => {
      expect(
        getMessage(
          localeCodeMock,
          { [keyMock]: { message: 'testValue' } },
          keyMock,
        ),
      ).toStrictEqual('testValue');
    });

    it('returns text including substitutions', () => {
      expect(
        getMessage(
          localeCodeMock,
          { [keyMock]: { message: 'test1 $1 test2 $2 test3' } },
          keyMock,
          ['a1', 'b2'],
        ),
      ).toStrictEqual('test1 a1 test2 b2 test3');
    });

    it('returns text including substitutions using custom join', () => {
      expect(
        getMessage(
          localeCodeMock,
          { [keyMock]: { message: 'test1 $1 test2 $2 test3' } },
          keyMock,
          ['a1', 'b2'],
          undefined,
          (substitutions) => substitutions.join(','),
        ),
      ).toStrictEqual('test1 ,a1, test2 ,b2, test3');
    });
  });

  describe('fetchLocale', () => {
    it('returns json from locale file', async () => {
      const result = await fetchLocale(localeCodeMock);
      expect(result).toStrictEqual({
        url: `../_locales/${localeCodeMock}/messages.json`,
      });
    });

    it('logs if fetch fails', async () => {
      await fetchLocale(errorLocaleMock);

      expect(log.error).toHaveBeenCalledTimes(1);
      expect(log.error).toHaveBeenCalledWith(
        `failed to fetch testLocaleError locale because of Error: ${errorMock}`,
      );
    });

    it('returns empty object if fetch fails', async () => {
      expect(await fetchLocale(errorLocaleMock)).toStrictEqual({});
    });
  });

  describe('loadRelativeTimeFormatLocaleData', () => {
    it('adds locale data if function exists', async () => {
      const addMock = jest.fn();

      global.Intl = {
        RelativeTimeFormat: {
          __addLocaleData: addMock,
        },
        // TODO: Replace `any` with type
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
      } as any;

      await loadRelativeTimeFormatLocaleData(`${localeCodeMock}_test`);

      expect(addMock).toHaveBeenCalledTimes(1);
      expect(addMock).toHaveBeenCalledWith({
        url: `./intl/${localeCodeMock}/relative-time-format-data.json`,
      });
    });

    it('does not add locale data if language tag already processed', async () => {
      const addMock = jest.fn();

      global.Intl = {
        RelativeTimeFormat: {
          __addLocaleData: addMock,
        },
        // TODO: Replace `any` with type
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
      } as any;

      await loadRelativeTimeFormatLocaleData(`${localeCodeMock}_test`);
      await loadRelativeTimeFormatLocaleData(`${localeCodeMock}_test`);

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