import { renderHook, act } from '@testing-library/react-hooks';
import { ApolloClient } from '@apollo/client';
import createClient from '../utils/apollo-client';
import { useNodes } from './use-nodes';
import createMockClient, {
  getMockStatisticsResult,
} from './mocks/apollo-client';

jest.mock('../utils/apollo-client');

const MOCK_DURATION = 1073;

const initialState = {
  url: '',
  initialized: false,
  responseTime: {
    isLoading: false,
    hasError: false,
    value: undefined,
  },
  block: {
    isLoading: false,
    hasError: false,
    value: undefined,
  },
  ssl: {
    isLoading: false,
    hasError: false,
    value: undefined,
  },
  chain: {
    isLoading: false,
    hasError: false,
    value: undefined,
  },
};

window.performance.getEntriesByName = jest
  .fn()
  .mockImplementation((url: string) => [
    {
      entryType: 'resource',
      name: url,
      startTime: 0,
      toJSON: () => ({}),
      duration: MOCK_DURATION,
    },
  ]);

beforeEach(() => {
  // @ts-ignore allow adding a mock return value to mocked module
  createClient.mockImplementation(() => createMockClient());
});

afterAll(() => {
  jest.clearAllMocks();
});

describe('useNodes hook', () => {
  it('returns the default state when empty config provided', () => {
    const { result } = renderHook(() => useNodes({ hosts: [] }));

    expect(result.current.state).toEqual({});
  });

  it('sets loading state while waiting for the results', async () => {
    const node = 'https://some.url';
    const { result, waitForNextUpdate } = renderHook(() =>
      useNodes({ hosts: [node] })
    );

    expect(result.current.state[node]).toEqual({
      ...initialState,
      url: node,
      initialized: true,
      responseTime: {
        ...initialState.responseTime,
        isLoading: true,
      },
      block: {
        ...initialState.block,
        isLoading: true,
      },
      chain: {
        ...initialState.chain,
        isLoading: true,
      },
      ssl: {
        ...initialState.ssl,
        isLoading: true,
      },
    });

    await waitForNextUpdate();
  });

  it('sets statistics results', async () => {
    const mockResult = getMockStatisticsResult();
    const node = 'https://some.url';
    const { result, waitFor } = renderHook(() => useNodes({ hosts: [node] }));

    await waitFor(() => {
      expect(result.current.state[node].block).toEqual({
        isLoading: false,
        hasError: false,
        value: Number(mockResult.statistics.blockHeight),
      });

      expect(result.current.state[node].chain).toEqual({
        isLoading: false,
        hasError: false,
        value: mockResult.statistics.chainId,
      });

      expect(result.current.state[node].responseTime).toEqual({
        isLoading: false,
        hasError: false,
        value: MOCK_DURATION,
      });
    });
  });

  it('sets subscription result', async () => {
    const node = 'https://some.url';
    const { result, waitFor } = renderHook(() => useNodes({ hosts: [node] }));

    await waitFor(() => {
      expect(result.current.state[node].ssl).toEqual({
        isLoading: false,
        hasError: false,
        value: true,
      });
    });
  });

  it('sets error when host in not a valid url', async () => {
    const node = 'not-url';
    const { result, waitFor } = renderHook(() => useNodes({ hosts: [node] }));

    await waitFor(() => {
      expect(result.current.state[node].block.hasError).toBe(true);
      expect(result.current.state[node].chain.hasError).toBe(true);
      expect(result.current.state[node].responseTime.hasError).toBe(true);
      expect(result.current.state[node].responseTime.hasError).toBe(true);
    });
  });

  it('sets error when statistics request fails', async () => {
    // @ts-ignore allow adding a mock return value to mocked module
    createClient.mockImplementation(() =>
      createMockClient({ statistics: { hasError: true } })
    );

    const node = 'https://some.url';
    const { result, waitFor } = renderHook(() => useNodes({ hosts: [node] }));

    await waitFor(() => {
      expect(result.current.state[node].block).toEqual({
        isLoading: false,
        hasError: true,
        value: undefined,
      });

      expect(result.current.state[node].chain).toEqual({
        isLoading: false,
        hasError: true,
        value: undefined,
      });

      expect(result.current.state[node].responseTime).toEqual({
        isLoading: false,
        hasError: true,
        value: undefined,
      });
    });
  });

  it('sets error when subscription request fails', async () => {
    // @ts-ignore allow adding a mock return value to mocked module
    createClient.mockImplementation(() =>
      createMockClient({ busEvents: { hasError: true } })
    );

    const node = 'https://some.url';
    const { result, waitFor } = renderHook(() => useNodes({ hosts: [node] }));

    await waitFor(() => {
      expect(result.current.state[node].ssl).toEqual({
        isLoading: false,
        hasError: true,
        value: undefined,
      });
    });
  });

  it('allows updating block values', async () => {
    const mockResult = getMockStatisticsResult();
    const node = 'https://some.url';
    const { result, waitFor } = renderHook(() => useNodes({ hosts: [node] }));

    await waitFor(() => {
      expect(result.current.state[node].block.value).toEqual(
        Number(mockResult.statistics.blockHeight)
      );
    });

    act(() => {
      result.current.updateNodeBlock(node, 12);
    });

    await waitFor(() => {
      expect(result.current.state[node].block.value).toEqual(12);
    });
  });

  it('does nothing when calling the block update on a non-existing node', async () => {
    const mockResult = getMockStatisticsResult();
    const node = 'https://some.url';
    const { result, waitFor } = renderHook(() => useNodes({ hosts: [node] }));

    await waitFor(() => {
      expect(result.current.state[node].block.value).toEqual(
        Number(mockResult.statistics.blockHeight)
      );
    });

    act(() => {
      result.current.updateNodeBlock('https://non-existing.url', 12);
    });

    expect(result.current.state['https://non-existing.url']).toBe(undefined);
  });

  it('adds new node', async () => {
    const node = 'custom-node-key';
    const { result, waitFor } = renderHook(() => useNodes({ hosts: [] }));

    expect(result.current.state[node]).toEqual(undefined);

    act(() => {
      result.current.addNode(node);
    });

    await waitFor(() => {
      expect(result.current.state[node]).toEqual(initialState);
    });
  });

  it('sets new url for node', async () => {
    const node = 'https://some.url';
    const newUrl = 'https://some-other.url';
    const { result, waitFor } = renderHook(() => useNodes({ hosts: [node] }));

    act(() => {
      result.current.updateNodeUrl(node, newUrl);
    });

    await waitFor(() => {
      expect(result.current.state[node].url).toBe(newUrl);
    });
  });

  it('sets error when custom node has an invalid url', async () => {
    const node = 'node-key';
    const url = 'not-url';
    const { result, waitFor } = renderHook(() => useNodes({ hosts: [] }));

    expect(result.current.state[node]).toBe(undefined);

    act(() => {
      result.current.updateNodeUrl(node, url);
    });

    await waitFor(() => {
      expect(result.current.state[node].url).toBe(url);
      expect(result.current.state[node].block.hasError).toBe(true);
      expect(result.current.state[node].chain.hasError).toBe(true);
      expect(result.current.state[node].responseTime.hasError).toBe(true);
      expect(result.current.state[node].ssl.hasError).toBe(true);
    });
  });

  it('sets error when custom node statistics request fails', async () => {
    // @ts-ignore allow adding a mock return value to mocked module
    createClient.mockImplementation(() =>
      createMockClient({ statistics: { hasError: true } })
    );

    const node = 'node-key';
    const url = 'https://some.url';
    const { result, waitFor } = renderHook(() => useNodes({ hosts: [] }));

    expect(result.current.state[node]).toBe(undefined);

    act(() => {
      result.current.updateNodeUrl(node, url);
    });

    await waitFor(() => {
      expect(result.current.state[node].block).toEqual({
        isLoading: false,
        hasError: true,
        value: undefined,
      });

      expect(result.current.state[node].chain).toEqual({
        isLoading: false,
        hasError: true,
        value: undefined,
      });

      expect(result.current.state[node].responseTime).toEqual({
        isLoading: false,
        hasError: true,
        value: undefined,
      });
    });
  });

  it('sets error when custom node subscription fails', async () => {
    // @ts-ignore allow adding a mock return value to mocked module
    createClient.mockImplementation(() =>
      createMockClient({ busEvents: { hasError: true } })
    );

    const node = 'node-key';
    const url = 'https://some.url';
    const { result, waitFor } = renderHook(() => useNodes({ hosts: [] }));

    expect(result.current.state[node]).toBe(undefined);

    act(() => {
      result.current.updateNodeUrl(node, url);
    });

    await waitFor(() => {
      expect(result.current.state[node].ssl).toEqual({
        isLoading: false,
        hasError: true,
        value: undefined,
      });
    });
  });

  it('exposes a collection of clients', async () => {
    const url1 = 'https://some.url';
    const url2 = 'https://some-other.url';
    const { result, waitFor } = renderHook(() =>
      useNodes({ hosts: [url1, url2] })
    );

    await waitFor(() => {
      expect(result.current.clients[url1]).toBeInstanceOf(ApolloClient);
      expect(result.current.clients[url2]).toBeInstanceOf(ApolloClient);
    });
  });

  it('exposes a client for the custom node', async () => {
    const node = 'node-key';
    const url = 'https://some.url';
    const { result, waitFor } = renderHook(() => useNodes({ hosts: [] }));

    act(() => {
      result.current.updateNodeUrl(node, url);
    });

    await waitFor(() => {
      expect(result.current.clients[url]).toBeInstanceOf(ApolloClient);
    });
  });
});
