import _ from 'lodash';
import {
  getRangeType,
  convertToFixedRange,
  isEqualTimeRanges,
  findTimeRange,
  timeRangeToParams,
  timeRangeFromParams,
} from '~/lib/utils/datetime_range';

const MOCK_NOW = Date.UTC(2020, 0, 23, 20);

const MOCK_NOW_ISO_STRING = new Date(MOCK_NOW).toISOString();

const mockFixedRange = {
  label: 'January 2020',
  start: '2020-01-01T00:00:00.000Z',
  end: '2020-01-31T23:59:00.000Z',
};

const mockAnchoredRange = {
  label: 'First two minutes of 2020',
  anchor: '2020-01-01T00:00:00.000Z',
  direction: 'after',
  duration: {
    seconds: 60 * 2,
  },
};

const mockRollingRange = {
  label: 'Next 2 minutes',
  direction: 'after',
  duration: {
    seconds: 60 * 2,
  },
};

const mockOpenRange = {
  label: '2020 so far',
  anchor: '2020-01-01T00:00:00.000Z',
  direction: 'after',
};

describe('Date time range utils', () => {
  describe('getRangeType', () => {
    it('infers correctly the range type from the input object', () => {
      const rangeTypes = {
        fixed: [{ start: MOCK_NOW_ISO_STRING, end: MOCK_NOW_ISO_STRING }],
        anchored: [{ anchor: MOCK_NOW_ISO_STRING, duration: { seconds: 0 } }],
        rolling: [{ duration: { seconds: 0 } }],
        open: [{ anchor: MOCK_NOW_ISO_STRING }],
        invalid: [
          {},
          { start: MOCK_NOW_ISO_STRING },
          { end: MOCK_NOW_ISO_STRING },
          { start: 'NOT_A_DATE', end: 'NOT_A_DATE' },
          { duration: { seconds: 'NOT_A_NUMBER' } },
          { duration: { seconds: Infinity } },
          { duration: { minutes: 20 } },
          { anchor: MOCK_NOW_ISO_STRING, duration: { seconds: 'NOT_A_NUMBER' } },
          { anchor: MOCK_NOW_ISO_STRING, duration: { seconds: Infinity } },
          { junk: 'exists' },
        ],
      };

      Object.entries(rangeTypes).forEach(([type, examples]) => {
        examples.forEach(example => expect(getRangeType(example)).toEqual(type));
      });
    });
  });

  describe('convertToFixedRange', () => {
    beforeEach(() => {
      jest.spyOn(Date, 'now').mockImplementation(() => MOCK_NOW);
    });

    afterEach(() => {
      Date.now.mockRestore();
    });

    describe('When a fixed range is input', () => {
      it('converts a fixed range to an equal fixed range', () => {
        expect(convertToFixedRange(mockFixedRange)).toEqual({
          start: mockFixedRange.start,
          end: mockFixedRange.end,
        });
      });

      it('throws an error when fixed range does not contain an end time', () => {
        const aFixedRangeMissingEnd = _.omit(mockFixedRange, 'end');

        expect(() => convertToFixedRange(aFixedRangeMissingEnd)).toThrow();
      });

      it('throws an error when fixed range does not contain a start time', () => {
        const aFixedRangeMissingStart = _.omit(mockFixedRange, 'start');

        expect(() => convertToFixedRange(aFixedRangeMissingStart)).toThrow();
      });

      it('throws an error when the dates cannot be parsed', () => {
        const wrongStart = { ...mockFixedRange, start: 'I_CANNOT_BE_PARSED' };
        const wrongEnd = { ...mockFixedRange, end: 'I_CANNOT_BE_PARSED' };

        expect(() => convertToFixedRange(wrongStart)).toThrow();
        expect(() => convertToFixedRange(wrongEnd)).toThrow();
      });
    });

    describe('When an anchored range is input', () => {
      it('converts to a fixed range', () => {
        expect(convertToFixedRange(mockAnchoredRange)).toEqual({
          start: '2020-01-01T00:00:00.000Z',
          end: '2020-01-01T00:02:00.000Z',
        });
      });

      it('converts to a fixed range with a `before` direction', () => {
        expect(convertToFixedRange({ ...mockAnchoredRange, direction: 'before' })).toEqual({
          start: '2019-12-31T23:58:00.000Z',
          end: '2020-01-01T00:00:00.000Z',
        });
      });

      it('converts to a fixed range without an explicit direction, defaulting to `before`', () => {
        const defaultDirectionRange = _.omit(mockAnchoredRange, 'direction');

        expect(convertToFixedRange(defaultDirectionRange)).toEqual({
          start: '2019-12-31T23:58:00.000Z',
          end: '2020-01-01T00:00:00.000Z',
        });
      });

      it('throws an error when the anchor cannot be parsed', () => {
        const wrongAnchor = { ...mockAnchoredRange, anchor: 'I_CANNOT_BE_PARSED' };

        expect(() => convertToFixedRange(wrongAnchor)).toThrow();
      });
    });

    describe('when a rolling range is input', () => {
      it('converts to a fixed range', () => {
        expect(convertToFixedRange(mockRollingRange)).toEqual({
          start: '2020-01-23T20:00:00.000Z',
          end: '2020-01-23T20:02:00.000Z',
        });
      });

      it('converts to a fixed range with an implicit `before` direction', () => {
        const noDirection = _.omit(mockRollingRange, 'direction');

        expect(convertToFixedRange(noDirection)).toEqual({
          start: '2020-01-23T19:58:00.000Z',
          end: '2020-01-23T20:00:00.000Z',
        });
      });

      it('throws an error when the duration is not in the right format', () => {
        const wrongDuration = { ...mockRollingRange, duration: { minutes: 20 } };

        expect(() => convertToFixedRange(wrongDuration)).toThrow();
      });

      it('throws an error when the anchor is not valid', () => {
        const wrongAnchor = { ...mockRollingRange, anchor: 'CAN_T_PARSE_THIS' };

        expect(() => convertToFixedRange(wrongAnchor)).toThrow();
      });
    });

    describe('when an open range is input', () => {
      it('converts to a fixed range with an `after` direction', () => {
        expect(convertToFixedRange(mockOpenRange)).toEqual({
          start: '2020-01-01T00:00:00.000Z',
          end: '2020-01-23T20:00:00.000Z',
        });
      });

      it('converts to a fixed range with the explicit `before` direction', () => {
        const beforeOpenRange = { ...mockOpenRange, direction: 'before' };

        expect(convertToFixedRange(beforeOpenRange)).toEqual({
          start: '1970-01-01T00:00:00.000Z',
          end: '2020-01-01T00:00:00.000Z',
        });
      });

      it('converts to a fixed range with the implicit `before` direction', () => {
        const noDirectionOpenRange = _.omit(mockOpenRange, 'direction');

        expect(convertToFixedRange(noDirectionOpenRange)).toEqual({
          start: '1970-01-01T00:00:00.000Z',
          end: '2020-01-01T00:00:00.000Z',
        });
      });

      it('throws an error when the anchor cannot be parsed', () => {
        const wrongAnchor = { ...mockOpenRange, anchor: 'CAN_T_PARSE_THIS' };

        expect(() => convertToFixedRange(wrongAnchor)).toThrow();
      });
    });
  });

  describe('isEqualTimeRanges', () => {
    it('equal only compares relevant properies', () => {
      expect(
        isEqualTimeRanges(
          {
            ...mockFixedRange,
            label: 'A label',
            default: true,
          },
          {
            ...mockFixedRange,
            label: 'Another label',
            default: false,
            anotherKey: 'anotherValue',
          },
        ),
      ).toBe(true);

      expect(
        isEqualTimeRanges(
          {
            ...mockAnchoredRange,
            label: 'A label',
            default: true,
          },
          {
            ...mockAnchoredRange,
            anotherKey: 'anotherValue',
          },
        ),
      ).toBe(true);
    });
  });

  describe('findTimeRange', () => {
    const timeRanges = [
      {
        label: 'Before 2020',
        anchor: '2020-01-01T00:00:00.000Z',
      },
      {
        label: 'Last 30 minutes',
        duration: { seconds: 60 * 30 },
      },
      {
        label: 'In 2019',
        start: '2019-01-01T00:00:00.000Z',
        end: '2019-12-31T12:59:59.999Z',
      },
      {
        label: 'Next 2 minutes',
        direction: 'after',
        duration: {
          seconds: 60 * 2,
        },
      },
    ];

    it('finds a time range', () => {
      const tr0 = {
        anchor: '2020-01-01T00:00:00.000Z',
      };
      expect(findTimeRange(tr0, timeRanges)).toBe(timeRanges[0]);

      const tr1 = {
        duration: { seconds: 60 * 30 },
      };
      expect(findTimeRange(tr1, timeRanges)).toBe(timeRanges[1]);

      const tr1Direction = {
        direction: 'before',
        duration: {
          seconds: 60 * 30,
        },
      };
      expect(findTimeRange(tr1Direction, timeRanges)).toBe(timeRanges[1]);

      const tr2 = {
        someOtherLabel: 'Added arbitrarily',
        start: '2019-01-01T00:00:00.000Z',
        end: '2019-12-31T12:59:59.999Z',
      };
      expect(findTimeRange(tr2, timeRanges)).toBe(timeRanges[2]);

      const tr3 = {
        direction: 'after',
        duration: {
          seconds: 60 * 2,
        },
      };
      expect(findTimeRange(tr3, timeRanges)).toBe(timeRanges[3]);
    });

    it('doesnot finds a missing time range', () => {
      const nonExistant = {
        direction: 'before',
        duration: {
          seconds: 200,
        },
      };
      expect(findTimeRange(nonExistant, timeRanges)).toBeUndefined();
    });
  });

  describe('conversion to/from params', () => {
    const mockFixedParams = {
      start: '2020-01-01T00:00:00.000Z',
      end: '2020-01-31T23:59:00.000Z',
    };

    const mockAnchoredParams = {
      anchor: '2020-01-01T00:00:00.000Z',
      direction: 'after',
      duration_seconds: '120',
    };

    const mockRollingParams = {
      direction: 'after',
      duration_seconds: '120',
    };

    describe('timeRangeToParams', () => {
      it('converts fixed ranges to params', () => {
        expect(timeRangeToParams(mockFixedRange)).toEqual(mockFixedParams);
      });

      it('converts anchored ranges to params', () => {
        expect(timeRangeToParams(mockAnchoredRange)).toEqual(mockAnchoredParams);
      });

      it('converts rolling ranges to params', () => {
        expect(timeRangeToParams(mockRollingRange)).toEqual(mockRollingParams);
      });
    });

    describe('timeRangeFromParams', () => {
      it('converts fixed ranges from params', () => {
        const params = { ...mockFixedParams, other_param: 'other_value' };
        const expectedRange = _.omit(mockFixedRange, 'label');

        expect(timeRangeFromParams(params)).toEqual(expectedRange);
      });

      it('converts anchored ranges to params', () => {
        const expectedRange = _.omit(mockRollingRange, 'label');

        expect(timeRangeFromParams(mockRollingParams)).toEqual(expectedRange);
      });

      it('converts rolling ranges from params', () => {
        const params = { ...mockRollingParams, other_param: 'other_value' };
        const expectedRange = _.omit(mockRollingRange, 'label');

        expect(timeRangeFromParams(params)).toEqual(expectedRange);
      });

      it('converts rolling ranges from params with a default direction', () => {
        const params = {
          ...mockRollingParams,
          direction: 'before',
          other_param: 'other_value',
        };
        const expectedRange = _.omit(mockRollingRange, 'label', 'direction');

        expect(timeRangeFromParams(params)).toEqual(expectedRange);
      });

      it('converts to null when for no relevant params', () => {
        const range = {
          useless_param_1: 'value1',
          useless_param_2: 'value2',
        };

        expect(timeRangeFromParams(range)).toBe(null);
      });
    });
  });
});
