import {OrganizationFixture} from 'sentry-fixture/organization';

import {render, waitFor} from 'sentry-test/reactTestingLibrary';

import {doEventsRequest} from 'sentry/actionCreators/events';
import type {EventsRequestProps} from 'sentry/components/charts/eventsRequest';
import EventsRequest from 'sentry/components/charts/eventsRequest';

const COUNT_OBJ = {
  count: 123,
};

jest.mock('sentry/actionCreators/events', () => ({
  doEventsRequest: jest.fn(),
}));

describe('EventsRequest', function () {
  const organization = OrganizationFixture();
  const mock = jest.fn(() => null);

  const DEFAULTS: EventsRequestProps = {
    api: new MockApiClient(),
    period: '24h',
    organization,
    includePrevious: false,
    interval: '24h',
    limit: 30,
    query: '',
    children: () => null,
    partial: false,
    includeTransformedData: true,
  };

  describe('with props changes', function () {
    beforeAll(function () {
      (doEventsRequest as jest.Mock).mockImplementation(() =>
        Promise.resolve({
          data: [[new Date(), [COUNT_OBJ]]],
        })
      );
    });

    it('makes requests', async function () {
      render(<EventsRequest {...DEFAULTS}>{mock}</EventsRequest>);
      expect(mock).toHaveBeenNthCalledWith(
        1,
        expect.objectContaining({
          loading: true,
        })
      );

      await waitFor(() =>
        expect(mock).toHaveBeenLastCalledWith(
          expect.objectContaining({
            loading: false,
            timeseriesData: [
              {
                seriesName: expect.anything(),
                data: [
                  expect.objectContaining({
                    name: expect.any(Number),
                    value: 123,
                  }),
                ],
              },
            ],
            originalTimeseriesData: [[expect.anything(), expect.anything()]],
          })
        )
      );

      expect(doEventsRequest).toHaveBeenCalled();
    });

    it('makes a new request if projects prop changes', async function () {
      const {rerender} = render(<EventsRequest {...DEFAULTS}>{mock}</EventsRequest>);
      (doEventsRequest as jest.Mock).mockClear();

      rerender(
        <EventsRequest {...DEFAULTS} project={[123]}>
          {mock}
        </EventsRequest>
      );
      await waitFor(() => expect(doEventsRequest).toHaveBeenCalledTimes(1));
      expect(doEventsRequest).toHaveBeenCalledWith(
        expect.anything(),
        expect.objectContaining({
          project: [123],
        })
      );
    });

    it('makes a new request if environments prop changes', async function () {
      const {rerender} = render(<EventsRequest {...DEFAULTS}>{mock}</EventsRequest>);
      (doEventsRequest as jest.Mock).mockClear();

      rerender(
        <EventsRequest {...DEFAULTS} environment={['dev']}>
          {mock}
        </EventsRequest>
      );
      await waitFor(() => expect(doEventsRequest).toHaveBeenCalledTimes(1));
      expect(doEventsRequest).toHaveBeenCalledWith(
        expect.anything(),
        expect.objectContaining({
          environment: ['dev'],
        })
      );
    });

    it('makes a new request if period prop changes', async function () {
      const {rerender} = render(<EventsRequest {...DEFAULTS}>{mock}</EventsRequest>);
      (doEventsRequest as jest.Mock).mockClear();

      rerender(
        <EventsRequest {...DEFAULTS} period="7d">
          {mock}
        </EventsRequest>
      );

      await waitFor(() => expect(doEventsRequest).toHaveBeenCalledTimes(1));
      expect(doEventsRequest).toHaveBeenCalledWith(
        expect.anything(),
        expect.objectContaining({
          period: '7d',
        })
      );
    });
  });

  describe('transforms', function () {
    beforeEach(function () {
      (doEventsRequest as jest.Mock).mockClear();
    });

    it('expands period in query if `includePrevious`', async function () {
      (doEventsRequest as jest.Mock).mockImplementation(() =>
        Promise.resolve({
          data: [
            [
              new Date(),
              [
                {...COUNT_OBJ, count: 321},
                {...COUNT_OBJ, count: 79},
              ],
            ],
            [new Date(), [COUNT_OBJ]],
          ],
        })
      );
      render(
        <EventsRequest {...DEFAULTS} includePrevious>
          {mock}
        </EventsRequest>
      );

      // actionCreator handles expanding the period when calling the API
      expect(doEventsRequest).toHaveBeenCalledWith(
        expect.anything(),
        expect.objectContaining({
          period: '24h',
        })
      );

      await waitFor(() =>
        expect(mock).toHaveBeenLastCalledWith(
          expect.objectContaining({
            loading: false,
            allTimeseriesData: [
              [
                expect.anything(),
                [
                  expect.objectContaining({count: 321}),
                  expect.objectContaining({count: 79}),
                ],
              ],
              [expect.anything(), [expect.objectContaining({count: 123})]],
            ],
            timeseriesData: [
              {
                seriesName: expect.anything(),
                data: [
                  expect.objectContaining({
                    name: expect.anything(),
                    value: 123,
                  }),
                ],
              },
            ],
            previousTimeseriesData: [
              expect.objectContaining({
                seriesName: 'Previous',
                data: [
                  expect.objectContaining({
                    name: expect.anything(),
                    value: 400,
                  }),
                ],
              }),
            ],

            originalTimeseriesData: [
              [expect.anything(), [expect.objectContaining({count: 123})]],
            ],

            originalPreviousTimeseriesData: [
              [
                expect.anything(),
                [
                  expect.objectContaining({count: 321}),
                  expect.objectContaining({count: 79}),
                ],
              ],
            ],
          })
        )
      );
    });

    it('expands multiple periods in query if `includePrevious`', async function () {
      (doEventsRequest as jest.Mock).mockImplementation(() =>
        Promise.resolve({
          'count()': {
            data: [
              [
                new Date(),
                [
                  {...COUNT_OBJ, count: 321},
                  {...COUNT_OBJ, count: 79},
                ],
              ],
              [new Date(), [COUNT_OBJ]],
            ],
          },
          'failure_count()': {
            data: [
              [
                new Date(),
                [
                  {...COUNT_OBJ, count: 421},
                  {...COUNT_OBJ, count: 79},
                ],
              ],
              [new Date(), [COUNT_OBJ]],
            ],
          },
        })
      );
      const multiYOptions = {
        yAxis: ['count()', 'failure_count()'],
        previousSeriesNames: ['previous count()', 'previous failure_count()'],
      };
      render(
        <EventsRequest {...DEFAULTS} {...multiYOptions} includePrevious>
          {mock}
        </EventsRequest>
      );

      // actionCreator handles expanding the period when calling the API
      expect(doEventsRequest).toHaveBeenCalledWith(
        expect.anything(),
        expect.objectContaining({
          period: '24h',
        })
      );

      await waitFor(() =>
        expect(mock).toHaveBeenLastCalledWith(
          expect.objectContaining({
            loading: false,
            yAxis: ['count()', 'failure_count()'],
            previousSeriesNames: ['previous count()', 'previous failure_count()'],
            results: [
              expect.objectContaining({
                data: [expect.objectContaining({name: expect.anything(), value: 123})],
                seriesName: 'count()',
              }),
              expect.objectContaining({
                data: [expect.objectContaining({name: expect.anything(), value: 123})],
                seriesName: 'failure_count()',
              }),
            ],
            previousTimeseriesData: [
              expect.objectContaining({
                data: [expect.objectContaining({name: expect.anything(), value: 400})],
                seriesName: 'previous count()',
                stack: 'previous',
              }),
              expect.objectContaining({
                data: [expect.objectContaining({name: expect.anything(), value: 500})],
                seriesName: 'previous failure_count()',
                stack: 'previous',
              }),
            ],
          })
        )
      );
    });

    it('aggregates counts per timestamp only when `includeTimeAggregation` prop is true', async function () {
      (doEventsRequest as jest.Mock).mockImplementation(() =>
        Promise.resolve({
          data: [[new Date(), [COUNT_OBJ, {...COUNT_OBJ, count: 100}]]],
        })
      );

      const {rerender} = render(<EventsRequest {...DEFAULTS}>{mock}</EventsRequest>);

      await waitFor(() =>
        expect(mock).toHaveBeenLastCalledWith(
          expect.objectContaining({
            timeAggregatedData: {},
          })
        )
      );

      rerender(
        <EventsRequest
          {...DEFAULTS}
          includeTimeAggregation
          timeAggregationSeriesName="aggregated series"
        >
          {mock}
        </EventsRequest>
      );

      await waitFor(() =>
        expect(mock).toHaveBeenLastCalledWith(
          expect.objectContaining({
            timeAggregatedData: {
              seriesName: 'aggregated series',
              data: [{name: expect.anything(), value: 223}],
            },
          })
        )
      );
    });

    it('aggregates all counts per timestamp when category name identical', async function () {
      (doEventsRequest as jest.Mock).mockImplementation(() =>
        Promise.resolve({
          data: [[new Date(), [COUNT_OBJ, {...COUNT_OBJ, count: 100}]]],
        })
      );

      const {rerender} = render(<EventsRequest {...DEFAULTS}>{mock}</EventsRequest>);

      await waitFor(() =>
        expect(mock).toHaveBeenLastCalledWith(
          expect.objectContaining({
            timeAggregatedData: {},
          })
        )
      );

      rerender(
        <EventsRequest
          {...DEFAULTS}
          includeTimeAggregation
          timeAggregationSeriesName="aggregated series"
        >
          {mock}
        </EventsRequest>
      );

      await waitFor(() =>
        expect(mock).toHaveBeenLastCalledWith(
          expect.objectContaining({
            timeAggregatedData: {
              seriesName: 'aggregated series',
              data: [{name: expect.anything(), value: 223}],
            },
          })
        )
      );
    });
  });

  describe('yAxis', function () {
    beforeEach(function () {
      (doEventsRequest as jest.Mock).mockClear();
    });

    it('supports yAxis', async function () {
      (doEventsRequest as jest.Mock).mockImplementation(() =>
        Promise.resolve({
          data: [
            [
              new Date(),
              [
                {...COUNT_OBJ, count: 321},
                {...COUNT_OBJ, count: 79},
              ],
            ],
            [new Date(), [COUNT_OBJ]],
          ],
        })
      );

      render(
        <EventsRequest {...DEFAULTS} includePrevious yAxis="apdex()">
          {mock}
        </EventsRequest>
      );

      await waitFor(() =>
        expect(mock).toHaveBeenLastCalledWith(
          expect.objectContaining({
            loading: false,
            allTimeseriesData: [
              [
                expect.anything(),
                [
                  expect.objectContaining({count: 321}),
                  expect.objectContaining({count: 79}),
                ],
              ],
              [expect.anything(), [expect.objectContaining({count: 123})]],
            ],
            timeseriesData: [
              {
                seriesName: expect.anything(),
                data: [
                  expect.objectContaining({
                    name: expect.anything(),
                    value: 123,
                  }),
                ],
              },
            ],
            previousTimeseriesData: [
              expect.objectContaining({
                seriesName: 'Previous',
                data: [
                  expect.objectContaining({
                    name: expect.anything(),
                    value: 400,
                  }),
                ],
              }),
            ],

            originalTimeseriesData: [
              [expect.anything(), [expect.objectContaining({count: 123})]],
            ],

            originalPreviousTimeseriesData: [
              [
                expect.anything(),
                [
                  expect.objectContaining({count: 321}),
                  expect.objectContaining({count: 79}),
                ],
              ],
            ],
          })
        )
      );
    });

    it('supports multiple yAxis', async function () {
      (doEventsRequest as jest.Mock).mockImplementation(() =>
        Promise.resolve({
          'epm()': {
            data: [
              [
                new Date(),
                [
                  {...COUNT_OBJ, count: 321},
                  {...COUNT_OBJ, count: 79},
                ],
              ],
              [new Date(), [COUNT_OBJ]],
            ],
          },
          'apdex()': {
            data: [
              [
                new Date(),
                [
                  {...COUNT_OBJ, count: 321},
                  {...COUNT_OBJ, count: 79},
                ],
              ],
              [new Date(), [COUNT_OBJ]],
            ],
          },
        })
      );

      render(
        <EventsRequest {...DEFAULTS} yAxis={['apdex()', 'epm()']}>
          {mock}
        </EventsRequest>
      );

      const generateExpected = name => {
        return {
          seriesName: name,
          data: [
            {name: expect.anything(), value: 400},
            {name: expect.anything(), value: 123},
          ],
        };
      };

      await waitFor(() =>
        expect(mock).toHaveBeenLastCalledWith(
          expect.objectContaining({
            loading: false,

            results: [generateExpected('epm()'), generateExpected('apdex()')],
          })
        )
      );
    });
  });

  describe('topEvents', function () {
    beforeEach(function () {
      (doEventsRequest as jest.Mock).mockClear();
    });

    it('supports topEvents parameter', async function () {
      (doEventsRequest as jest.Mock).mockImplementation(() =>
        Promise.resolve({
          'project1,error': {
            data: [
              [
                new Date(),
                [
                  {...COUNT_OBJ, count: 321},
                  {...COUNT_OBJ, count: 79},
                ],
              ],
              [new Date(), [COUNT_OBJ]],
            ],
          },
          'project1,warning': {
            data: [
              [
                new Date(),
                [
                  {...COUNT_OBJ, count: 321},
                  {...COUNT_OBJ, count: 79},
                ],
              ],
              [new Date(), [COUNT_OBJ]],
            ],
          },
        })
      );

      render(
        <EventsRequest {...DEFAULTS} field={['project', 'level']} topEvents={2}>
          {mock}
        </EventsRequest>
      );

      const generateExpected = name => {
        return {
          seriesName: name,
          data: [
            {name: expect.anything(), value: 400},
            {name: expect.anything(), value: 123},
          ],
        };
      };

      await waitFor(() =>
        expect(mock).toHaveBeenLastCalledWith(
          expect.objectContaining({
            loading: false,

            results: [
              generateExpected('project1,error'),
              generateExpected('project1,warning'),
            ],
          })
        )
      );
    });
  });

  describe('out of retention', function () {
    beforeEach(function () {
      (doEventsRequest as jest.Mock).mockClear();
    });

    it('does not make request', function () {
      render(
        <EventsRequest {...DEFAULTS} expired>
          {mock}
        </EventsRequest>
      );
      expect(doEventsRequest).not.toHaveBeenCalled();
    });

    it('errors', function () {
      render(
        <EventsRequest {...DEFAULTS} expired>
          {mock}
        </EventsRequest>
      );
      expect(mock).toHaveBeenLastCalledWith(
        expect.objectContaining({
          expired: true,
          errored: true,
        })
      );
    });
  });

  describe('timeframe', function () {
    beforeEach(function () {
      (doEventsRequest as jest.Mock).mockClear();
    });

    it('passes query timeframe start and end to the child if supplied by timeseriesData', async function () {
      (doEventsRequest as jest.Mock).mockImplementation(() =>
        Promise.resolve({
          p95: {
            data: [[new Date(), [COUNT_OBJ]]],
            start: 1627402280,
            end: 1627402398,
          },
        })
      );
      render(<EventsRequest {...DEFAULTS}>{mock}</EventsRequest>);

      await waitFor(() =>
        expect(mock).toHaveBeenLastCalledWith(
          expect.objectContaining({
            timeframe: {
              start: 1627402280000,
              end: 1627402398000,
            },
          })
        )
      );
    });
  });

  describe('custom performance metrics', function () {
    beforeEach(function () {
      (doEventsRequest as jest.Mock).mockClear();
    });

    it('passes timeseriesResultTypes to child', async function () {
      (doEventsRequest as jest.Mock).mockImplementation(() =>
        Promise.resolve({
          data: [[new Date(), [COUNT_OBJ]]],
          start: 1627402280,
          end: 1627402398,
          meta: {
            fields: {
              p95_measurements_custom: 'size',
            },
            units: {
              p95_measurements_custom: 'kibibyte',
            },
          },
        })
      );
      render(
        <EventsRequest {...DEFAULTS} yAxis="p95(measurements.custom)">
          {mock}
        </EventsRequest>
      );

      await waitFor(() =>
        expect(mock).toHaveBeenLastCalledWith(
          expect.objectContaining({
            timeseriesResultsTypes: {'p95(measurements.custom)': 'size'},
          })
        )
      );
    });

    it('scales timeseries values according to unit meta', async function () {
      (doEventsRequest as jest.Mock).mockImplementation(() =>
        Promise.resolve({
          data: [[new Date(), [COUNT_OBJ]]],
          start: 1627402280,
          end: 1627402398,
          meta: {
            fields: {
              p95_measurements_custom: 'size',
            },
            units: {
              p95_measurements_custom: 'mebibyte',
            },
          },
        })
      );
      render(
        <EventsRequest
          {...DEFAULTS}
          yAxis="p95(measurements.custom)"
          currentSeriesNames={['p95(measurements.custom)']}
        >
          {mock}
        </EventsRequest>
      );

      await waitFor(() =>
        expect(mock).toHaveBeenLastCalledWith(
          expect.objectContaining({
            timeseriesData: [
              {
                data: [{name: 1508208080000000, value: 128974848}],
                seriesName: 'p95(measurements.custom)',
              },
            ],
          })
        )
      );
    });
  });
});