import { renderHook } from '@testing-library/react-hooks';
import type { EnvironmentWithOptionalUrl } from './use-config';
import { useConfig, LOCAL_STORAGE_NETWORK_KEY } from './use-config';
import { Networks } from '../types';

type HostMapping = Record<string, number | Error>;

const mockHostsMap: HostMapping = {
  'https://host1.com': 300,
  'https://host2.com': 500,
  'https://host3.com': 100,
  'https://host4.com': 650,
};

const hostList = Object.keys(mockHostsMap);

const mockEnvironment: EnvironmentWithOptionalUrl = {
  VEGA_ENV: Networks.TESTNET,
  VEGA_CONFIG_URL: 'https://vega.url/config.json',
  VEGA_NETWORKS: {},
  ETHEREUM_PROVIDER_URL: 'https://ethereum.provider',
  ETHERSCAN_URL: 'https://etherscan.url',
  GIT_BRANCH: 'test',
  GIT_ORIGIN_URL: 'https://github.com/test/repo',
  GIT_COMMIT_HASH: 'abcde01234',
  GITHUB_FEEDBACK_URL: 'https://github.com/test/feedback',
};

function setupFetch(configUrl: string, hostMap: HostMapping) {
  const hostUrls = Object.keys(hostMap);
  return (url: RequestInfo) => {
    if (url === configUrl) {
      return Promise.resolve({
        ok: true,
        json: () => Promise.resolve({ hosts: hostUrls }),
      } as Response);
    }

    if (hostUrls.includes(url as string)) {
      const value = hostMap[url as string];
      return new Promise<Response>((resolve, reject) => {
        if (typeof value === 'number') {
          setTimeout(() => {
            resolve({ ok: true } as Response);
          }, value);
        } else {
          reject(value);
        }
      });
    }

    return Promise.resolve({
      ok: true,
    } as Response);
  };
}

// eslint-disable-next-line @typescript-eslint/no-empty-function
const noop = () => {};

global.fetch = jest.fn();

const mockUpdate = jest.fn();

beforeEach(() => {
  jest.useFakeTimers();
  mockUpdate.mockClear();
  window.localStorage.clear();

  // @ts-ignore typescript doesn't recognise the mocked instance
  global.fetch.mockReset();
  // @ts-ignore typescript doesn't recognise the mocked instance
  global.fetch.mockImplementation(
    setupFetch(mockEnvironment.VEGA_CONFIG_URL ?? '', mockHostsMap)
  );
});

afterAll(() => {
  // @ts-ignore: typescript doesn't recognise the mocked fetch instance
  fetch.mockRestore();
});

describe('useConfig hook', () => {
  it('updates the environment with a host url from the network configuration', async () => {
    const allowedStatuses = [
      'idle',
      'loading-config',
      'loading-node',
      'success',
    ];

    const { result, waitForNextUpdate } = renderHook(() =>
      useConfig(mockEnvironment, mockUpdate)
    );

    await waitForNextUpdate();
    jest.runAllTimers();
    await waitForNextUpdate();

    expect(result.current.status).toBe('success');
    result.all.forEach((state) => {
      expect(allowedStatuses).toContain('status' in state && state.status);
    });

    // fetches config
    expect(fetch).toHaveBeenCalledWith(mockEnvironment.VEGA_CONFIG_URL);
    // calls each node
    hostList.forEach((url) => {
      expect(fetch).toHaveBeenCalledWith(url);
    });

    // updates the environment
    expect(hostList).toContain(mockUpdate.mock.calls[0][0]({}).VEGA_URL);
  });

  it('uses the host from the configuration which responds first', async () => {
    const shortestResponseTime = Object.values(mockHostsMap).sort()[0];
    const expectedHost = hostList.find((url: keyof typeof mockHostsMap) => {
      return mockHostsMap[url] === shortestResponseTime;
    });

    const { result, waitForNextUpdate } = renderHook(() =>
      useConfig(mockEnvironment, mockUpdate)
    );

    await waitForNextUpdate();
    jest.runAllTimers();
    await waitForNextUpdate();

    expect(result.current.status).toBe('success');
    expect(mockUpdate.mock.calls[0][0]({}).VEGA_URL).toBe(expectedHost);
  });

  it('ignores failing hosts and uses one which returns a success response', async () => {
    const mockHostsMapScoped = {
      'https://host1.com': 350,
      'https://host2.com': new Error('Server error'),
      'https://host3.com': 230,
      'https://host4.com': new Error('Server error'),
    };

    // @ts-ignore typescript doesn't recognise the mocked instance
    global.fetch.mockImplementation(
      setupFetch(mockEnvironment.VEGA_CONFIG_URL ?? '', mockHostsMapScoped)
    );

    const { result, waitForNextUpdate } = renderHook(() =>
      useConfig(mockEnvironment, mockUpdate)
    );

    await waitForNextUpdate();
    jest.runAllTimers();
    await waitForNextUpdate();

    expect(result.current.status).toBe('success');
    expect(mockUpdate.mock.calls[0][0]({}).VEGA_URL).toBe('https://host3.com');
  });

  it('returns the correct error status for when the config cannot be accessed', async () => {
    // @ts-ignore typescript doesn't recognise the mocked instance
    global.fetch.mockImplementation((url: RequestInfo) => {
      if (url === mockEnvironment.VEGA_CONFIG_URL) {
        return Promise.reject(new Error('Server error'));
      }
      return Promise.resolve({ ok: true } as Response);
    });

    const { result, waitForNextUpdate } = renderHook(() =>
      useConfig(mockEnvironment, mockUpdate)
    );

    await waitForNextUpdate();

    expect(result.current.status).toBe('error-loading-config');
    expect(mockUpdate).not.toHaveBeenCalled();
  });

  it('returns the correct error status for when the config is not valid', async () => {
    // @ts-ignore typescript doesn't recognise the mocked instance
    global.fetch.mockImplementation((url: RequestInfo) => {
      if (url === mockEnvironment.VEGA_CONFIG_URL) {
        return Promise.resolve({
          ok: true,
          json: () => Promise.resolve({ some: 'data' }),
        });
      }
      return Promise.resolve({ ok: true } as Response);
    });

    const { result, waitForNextUpdate } = renderHook(() =>
      useConfig(mockEnvironment, mockUpdate)
    );

    await waitForNextUpdate();

    expect(result.current.status).toBe('error-validating-config');
    expect(mockUpdate).not.toHaveBeenCalled();
  });

  it('returns the correct error status for when no hosts can be accessed', async () => {
    const mockHostsMapScoped = {
      'https://host1.com': new Error('Server error'),
      'https://host2.com': new Error('Server error'),
      'https://host3.com': new Error('Server error'),
      'https://host4.com': new Error('Server error'),
    };

    // @ts-ignore typescript doesn't recognise the mocked instance
    global.fetch.mockImplementation(
      setupFetch(mockEnvironment.VEGA_CONFIG_URL ?? '', mockHostsMapScoped)
    );

    const { result, waitForNextUpdate } = renderHook(() =>
      useConfig(mockEnvironment, mockUpdate)
    );

    await waitForNextUpdate();

    expect(result.current.status).toBe('error-loading-node');
    expect(mockUpdate).not.toHaveBeenCalled();
  });

  it('caches the list of networks', async () => {
    const run1 = renderHook(() => useConfig(mockEnvironment, mockUpdate));

    await run1.waitForNextUpdate();
    jest.runAllTimers();
    await run1.waitForNextUpdate();

    expect(run1.result.current.status).toBe('success');
    expect(fetch).toHaveBeenCalledWith(mockEnvironment.VEGA_CONFIG_URL);

    // @ts-ignore typescript doesn't recognise the mocked instance
    fetch.mockClear();

    const run2 = renderHook(() => useConfig(mockEnvironment, mockUpdate));

    jest.runAllTimers();
    await run2.waitForNextUpdate();

    expect(run2.result.current.status).toBe('success');
    expect(fetch).not.toHaveBeenCalledWith(mockEnvironment.VEGA_CONFIG_URL);
  });

  it('caches the list of networks between runs', async () => {
    const run1 = renderHook(() => useConfig(mockEnvironment, mockUpdate));

    await run1.waitForNextUpdate();
    jest.runAllTimers();
    await run1.waitForNextUpdate();

    expect(run1.result.current.status).toBe('success');
    expect(fetch).toHaveBeenCalledWith(mockEnvironment.VEGA_CONFIG_URL);

    // @ts-ignore typescript doesn't recognise the mocked instance
    fetch.mockClear();

    const run2 = renderHook(() => useConfig(mockEnvironment, mockUpdate));

    jest.runAllTimers();
    await run2.waitForNextUpdate();

    expect(run2.result.current.status).toBe('success');
    expect(fetch).not.toHaveBeenCalledWith(mockEnvironment.VEGA_CONFIG_URL);
  });

  it('refetches the network configuration and resets the cache when malformed data found in the storage', async () => {
    window.localStorage.setItem(
      `${LOCAL_STORAGE_NETWORK_KEY}-${mockEnvironment.VEGA_ENV}`,
      '{not:{valid:{json'
    );
    const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(noop);

    const run1 = renderHook(() => useConfig(mockEnvironment, mockUpdate));

    await run1.waitForNextUpdate();
    jest.runAllTimers();
    await run1.waitForNextUpdate();

    expect(run1.result.current.status).toBe('success');
    expect(fetch).toHaveBeenCalledWith(mockEnvironment.VEGA_CONFIG_URL);
    expect(consoleWarnSpy).toHaveBeenCalled();

    consoleWarnSpy.mockRestore();
  });

  it('refetches the network configuration and resets the cache when invalid data found in the storage', async () => {
    window.localStorage.setItem(
      `${LOCAL_STORAGE_NETWORK_KEY}-${mockEnvironment.VEGA_ENV}`,
      JSON.stringify({ invalid: 'data' })
    );
    const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(noop);

    const run1 = renderHook(() => useConfig(mockEnvironment, mockUpdate));

    await run1.waitForNextUpdate();
    jest.runAllTimers();
    await run1.waitForNextUpdate();

    expect(run1.result.current.status).toBe('success');
    expect(fetch).toHaveBeenCalledWith(mockEnvironment.VEGA_CONFIG_URL);
    expect(consoleSpy).toHaveBeenCalled();

    consoleSpy.mockRestore();
  });
});
