import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';

import { AnnotationEvent, FieldConfigSource, getDefaultTimeRange, LoadingState } from '@grafana/data';
import { locationService } from '@grafana/runtime';

import { silenceConsoleOutput } from '../../../../test/core/utils/silenceConsoleOutput';
import { backendSrv } from '../../../core/services/backend_srv';
import { setDashboardSrv } from '../../../features/dashboard/services/DashboardSrv';

import { AnnoListPanel, Props } from './AnnoListPanel';
import { AnnoOptions } from './types';

jest.mock('@grafana/runtime', () => ({
  ...(jest.requireActual('@grafana/runtime') as unknown as object),
  getBackendSrv: () => backendSrv,
}));

const defaultOptions: AnnoOptions = {
  limit: 10,
  navigateAfter: '10m',
  navigateBefore: '10m',
  navigateToPanel: true,
  onlyFromThisDashboard: true,
  onlyInTimeRange: false,
  showTags: true,
  showTime: true,
  showUser: true,
  tags: ['tag A', 'tag B'],
};

const defaultResult: any = {
  text: 'Result text',
  userId: 1,
  login: 'Result login',
  email: 'Result email',
  avatarUrl: 'Result avatarUrl',
  tags: ['Result tag A', 'Result tag B'],
  time: Date.UTC(2021, 0, 1, 0, 0, 0, 0),
  panelId: 13,
  dashboardId: 14, // deliberately different from panelId
  id: 14,
  url: '/d/asdkjhajksd/some-dash',
};

async function setupTestContext({
  options = defaultOptions,
  results = [defaultResult],
}: { options?: AnnoOptions; results?: AnnotationEvent[] } = {}) {
  jest.clearAllMocks();

  const getMock = jest.spyOn(backendSrv, 'get');
  getMock.mockResolvedValue(results);

  const dash: any = { id: 1, formatDate: (time: number) => new Date(time).toISOString() };
  const dashSrv: any = { getCurrent: () => dash };
  setDashboardSrv(dashSrv);
  const pushSpy = jest.spyOn(locationService, 'push');

  const props: Props = {
    data: { state: LoadingState.Done, timeRange: getDefaultTimeRange(), series: [] },
    eventBus: {
      subscribe: jest.fn(),
      getStream: () =>
        ({
          subscribe: jest.fn(),
        } as any),
      publish: jest.fn(),
      removeAllListeners: jest.fn(),
      newScopedBus: jest.fn(),
    },
    fieldConfig: {} as unknown as FieldConfigSource,
    height: 400,
    id: 1,
    onChangeTimeRange: jest.fn(),
    onFieldConfigChange: jest.fn(),
    onOptionsChange: jest.fn(),
    options,
    renderCounter: 1,
    replaceVariables: (str: string) => str,
    timeRange: getDefaultTimeRange(),
    timeZone: 'utc',
    title: 'Test Title',
    transparent: false,
    width: 320,
  };
  const { rerender } = render(<AnnoListPanel {...props} />);
  await waitFor(() => expect(getMock).toHaveBeenCalledTimes(1));

  return { props, rerender, getMock, pushSpy };
}

describe('AnnoListPanel', () => {
  describe('when mounted', () => {
    it('then it should fetch annotations', async () => {
      const { getMock } = await setupTestContext();

      expect(getMock).toHaveBeenCalledWith(
        '/api/annotations',
        {
          dashboardId: 1,
          limit: 10,
          tags: ['tag A', 'tag B'],
          type: 'annotation',
        },
        'anno-list-panel-1'
      );
    });
  });

  describe('when there are no annotations', () => {
    it('then it should show a no annotations message', async () => {
      await setupTestContext({ results: [] });

      expect(screen.getByText(/no annotations found/i)).toBeInTheDocument();
    });
  });

  describe('when there are annotations', () => {
    it('then it renders the annotations correctly', async () => {
      await setupTestContext();

      expect(screen.queryByText(/no annotations found/i)).not.toBeInTheDocument();
      expect(screen.queryByText(/result email/i)).not.toBeInTheDocument();
      expect(screen.getByText(/result text/i)).toBeInTheDocument();
      expect(screen.getByRole('img')).toBeInTheDocument();
      expect(screen.getByText('Result tag A')).toBeInTheDocument();
      expect(screen.getByText('Result tag B')).toBeInTheDocument();
      expect(screen.getByText(/2021-01-01T00:00:00.000Z/i)).toBeInTheDocument();
    });

    describe('and login property is missing in annotation', () => {
      it('then it renders the annotations correctly', async () => {
        await setupTestContext({ results: [{ ...defaultResult, login: undefined }] });

        expect(screen.queryByRole('img')).not.toBeInTheDocument();
        expect(screen.getByText(/result text/i)).toBeInTheDocument();
        expect(screen.getByText('Result tag A')).toBeInTheDocument();
        expect(screen.getByText('Result tag B')).toBeInTheDocument();
        expect(screen.getByText(/2021-01-01T00:00:00.000Z/i)).toBeInTheDocument();
      });
    });

    describe('and property is missing in annotation', () => {
      it('then it renders the annotations correctly', async () => {
        await setupTestContext({ results: [{ ...defaultResult, time: undefined }] });

        expect(screen.queryByText(/2021-01-01T00:00:00.000Z/i)).not.toBeInTheDocument();
        expect(screen.getByText(/result text/i)).toBeInTheDocument();
        expect(screen.getByRole('img')).toBeInTheDocument();
        expect(screen.getByText('Result tag A')).toBeInTheDocument();
        expect(screen.getByText('Result tag B')).toBeInTheDocument();
      });
    });

    describe('and show user option is off', () => {
      it('then it renders the annotations correctly', async () => {
        await setupTestContext({
          options: { ...defaultOptions, showUser: false },
        });

        expect(screen.queryByRole('img')).not.toBeInTheDocument();
        expect(screen.getByText(/result text/i)).toBeInTheDocument();
        expect(screen.getByText('Result tag A')).toBeInTheDocument();
        expect(screen.getByText('Result tag B')).toBeInTheDocument();
        expect(screen.getByText(/2021-01-01T00:00:00.000Z/i)).toBeInTheDocument();
      });
    });

    describe('and show time option is off', () => {
      it('then it renders the annotations correctly', async () => {
        await setupTestContext({
          options: { ...defaultOptions, showTime: false },
        });

        expect(screen.queryByText(/2021-01-01T00:00:00.000Z/i)).not.toBeInTheDocument();
        expect(screen.getByText(/result text/i)).toBeInTheDocument();
        expect(screen.getByRole('img')).toBeInTheDocument();
        expect(screen.getByText('Result tag A')).toBeInTheDocument();
        expect(screen.getByText('Result tag B')).toBeInTheDocument();
      });
    });

    describe('and show tags option is off', () => {
      it('then it renders the annotations correctly', async () => {
        await setupTestContext({
          options: { ...defaultOptions, showTags: false },
        });

        expect(screen.queryByText('Result tag A')).not.toBeInTheDocument();
        expect(screen.queryByText('Result tag B')).not.toBeInTheDocument();
        expect(screen.getByText(/result text/i)).toBeInTheDocument();
        expect(screen.getByRole('img')).toBeInTheDocument();
        expect(screen.getByText(/2021-01-01T00:00:00.000Z/i)).toBeInTheDocument();
      });
    });

    describe('and the user clicks on the annotation', () => {
      it('then it should navigate to the dashboard connected to the annotation', async () => {
        const { getMock, pushSpy } = await setupTestContext();

        getMock.mockClear();
        expect(screen.getByText(/result text/i)).toBeInTheDocument();
        await userEvent.click(screen.getByText(/result text/i));
        await waitFor(() => expect(getMock).toHaveBeenCalledTimes(1));

        expect(getMock).toHaveBeenCalledWith('/api/search', { dashboardIds: 14 });
        expect(pushSpy).toHaveBeenCalledTimes(1);
        expect(pushSpy).toHaveBeenCalledWith('/d/asdkjhajksd/some-dash?from=1609458600000&to=1609459800000');
      });
    });

    describe('and the user clicks on a tag', () => {
      it('then it should navigate to the dashboard connected to the annotation', async () => {
        const { getMock } = await setupTestContext();

        getMock.mockClear();
        expect(screen.getByText('Result tag B')).toBeInTheDocument();
        await userEvent.click(screen.getByText('Result tag B'));

        expect(getMock).toHaveBeenCalledTimes(1);
        expect(getMock).toHaveBeenCalledWith(
          '/api/annotations',
          {
            dashboardId: 1,
            limit: 10,
            tags: ['tag A', 'tag B', 'Result tag B'],
            type: 'annotation',
          },
          'anno-list-panel-1'
        );
        expect(screen.getByText(/filter:/i)).toBeInTheDocument();
        expect(screen.getAllByText(/result tag b/i)).toHaveLength(2);
      });
    });

    describe('and the user clicks on the user avatar', () => {
      it('then it should filter annotations by login and the filter should show', async () => {
        const { getMock } = await setupTestContext();

        getMock.mockClear();
        expect(screen.getByRole('img')).toBeInTheDocument();
        await userEvent.click(screen.getByRole('img'));

        expect(getMock).toHaveBeenCalledTimes(1);
        expect(getMock).toHaveBeenCalledWith(
          '/api/annotations',
          {
            dashboardId: 1,
            limit: 10,
            tags: ['tag A', 'tag B'],
            type: 'annotation',
            userId: 1,
          },
          'anno-list-panel-1'
        );
        expect(screen.getByText(/filter:/i)).toBeInTheDocument();
        expect(screen.getByText(/result email/i)).toBeInTheDocument();
      });
    });

    describe('and the user hovers over the user avatar', () => {
      silenceConsoleOutput(); // Popper throws an act error, but if we add act around the hover here it doesn't matter
      it('then it should filter annotations by login', async () => {
        const { getMock } = await setupTestContext();

        getMock.mockClear();
        expect(screen.getByRole('img')).toBeInTheDocument();
      });
    });
  });
});
