import React from 'react';
import { renderHook, act } from '@testing-library/react-hooks';
import { History } from 'history';
import { Observable } from 'rxjs';
import { AppProvider } from '../../components1/AppProvider';
import { Resolve } from '../../services/resolve.service';
import { useViewList } from './useViewList';

const oldWindowLocation = window.location;
let eventMap: any = {};

beforeEach(() => {
   delete window.location;
   delete window.addEventListener;
   window.location = {} as any;
   window.location.pathname = '/';
   window.location.href = '';

   eventMap = {};
   window.addEventListener = function (event, cb): void {
      eventMap[event] = cb;
   };
});

afterEach(() => {
   window.location = oldWindowLocation;
});

const resolveDelay: Resolve<any> = (params: any) =>
   new Observable((obs) => {
      setTimeout(() => {
         obs.next();
         obs.complete();
      }, 1);
   });

const resolveDelayWithData = (response: any): any => (params: any) =>
   new Observable((obs) => {
      setTimeout(() => {
         obs.next(response);
         obs.complete();
      }, 1);
   });

const resolveWithData: Resolve<any> = (params: any) =>
   new Observable((obs) => {
      setTimeout(() => {
         obs.next({
            results: [{ id: 1 }, { id: 2 }, { id: 3 }],
            count: 20,
         });
         obs.complete();
      }, 1);
   });

const resolveErrorDelay: Resolve<any> = (params: any) =>
   new Observable((obs) => {
      setTimeout(() => {
         obs.error('Error on load');
         obs.complete();
      }, 1);
   });

const resolveError: Resolve<any> = (params: any) =>
   new Observable((obs) => {
      obs.error('Error on load');
      obs.complete();
   });

test('useViewList - verificar status info com sucesso nos resolves', async () => {
   const { result, waitForNextUpdate } = renderHook(() =>
      useViewList({
         resolves: {
            items: resolveDelay,
         },
      }),
   );

   // Inicial
   expect(result.current.starting).toBe(true);
   expect(result.current.searching).toBe(true);
   expect(result.current.searchingMore).toBe(false);
   expect(result.current.lastPage).toBe(false);
   expect(result.current.errorLoadData).toBe(false);
   expect(result.current.criticalError).toBe(false);

   await waitForNextUpdate();

   // Esperar load resolves
   expect(result.current.starting).toBe(false);
   expect(result.current.searching).toBe(false);
   expect(result.current.searchingMore).toBe(false);
   expect(result.current.lastPage).toBe(false);
   expect(result.current.errorLoadData).toBe(false);
   expect(result.current.criticalError).toBe(false);
});

test('useViewList - verificar status info com erro nos resolves', async () => {
   const { result, waitForNextUpdate } = renderHook(() =>
      useViewList({
         resolves: {
            items: resolveErrorDelay,
         },
      }),
   );

   // Inicial
   expect(result.current.starting).toBe(true);
   expect(result.current.searching).toBe(true);
   expect(result.current.searchingMore).toBe(false);
   expect(result.current.lastPage).toBe(false);
   expect(result.current.errorLoadData).toBe(false);
   expect(result.current.criticalError).toBe(false);

   await waitForNextUpdate();

   // Esperar load resolves
   expect(result.current.starting).toBe(false);
   expect(result.current.searching).toBe(false);
   expect(result.current.searchingMore).toBe(false);
   expect(result.current.lastPage).toBe(false);
   expect(result.current.errorLoadData).toBe(true);
   expect(result.current.criticalError).toBe(false);
});

test('useViewList - com firstLoad=false não deve alterar status info searching', async () => {
   const { result, waitForNextUpdate } = renderHook(() =>
      useViewList({
         resolves: {
            items: resolveErrorDelay,
         },
         firstLoad: false,
      }),
   );

   // Inicial
   expect(result.current.starting).toBe(false);
   expect(result.current.searching).toBe(false);
   expect(result.current.searchingMore).toBe(false);
   expect(result.current.lastPage).toBe(false);
   expect(result.current.errorLoadData).toBe(false);
   expect(result.current.criticalError).toBe(false);
});

test('useViewList - resultado do resolve com a key items, deve ser definida para resources', async () => {
   const resolveItem = resolveDelayWithData({
      results: [{ id: 1 }, { id: 2 }, { id: 3 }],
      count: 20,
   });

   const { result, waitForNextUpdate } = renderHook(() =>
      useViewList({
         resolves: {
            items: resolveItem,
         },
      }),
   );

   await waitForNextUpdate();

   // Items
   expect(result.current.resources).toEqual([{ id: 1 }, { id: 2 }, { id: 3 }]);
   expect(result.current.resourcesTotal).toEqual(20);
});

test('useViewList - se não houver resolve com key item NÃO deve definir resources', async () => {
   const resolveItem = resolveDelayWithData({
      results: [{ id: 1 }, { id: 2 }, { id: 3 }],
      count: 20,
   });

   const { result, waitForNextUpdate } = renderHook(() =>
      useViewList({
         resolves: {
            items2: resolveItem,
         },
      }),
   );

   await waitForNextUpdate();

   // Items
   expect(result.current.resources).toEqual([]);
   expect(result.current.resourcesTotal).toEqual(0);
});

test('useViewList - removeResource com softDelete=true, NÃO deve remove item da array e deve decrementar 1 do total', async () => {
   const resolveItem = resolveDelayWithData({
      results: [{ id: 1 }, { id: 2 }, { id: 3 }],
      count: 20,
   });

   const { result, waitForNextUpdate } = renderHook(() =>
      useViewList({
         resolves: {
            items: resolveItem,
         },
      }),
   );

   await waitForNextUpdate();

   // Items
   expect(result.current.resourcesTotal).toEqual(20);

   act(() => {
      result.current.removeResource(2, true);
   });

   expect(result.current.resources[1].deleted).toEqual(true);
   expect(result.current.resourcesTotal).toEqual(19);
});

test('useViewList - removeResource com softDelete=false, DEVE remove item da array e deve decrementar 1 do total', async () => {
   const resolveItem = resolveDelayWithData({
      results: [{ id: 1 }, { id: 2 }, { id: 3 }],
      count: 20,
   });

   const { result, waitForNextUpdate } = renderHook(() =>
      useViewList({
         resolves: {
            items: resolveItem,
         },
      }),
   );

   await waitForNextUpdate();

   // Items
   expect(result.current.resourcesTotal).toEqual(20);

   act(() => {
      result.current.removeResource(2, false);
   });

   expect(result.current.resources.length).toEqual(2);
   expect(result.current.resourcesTotal).toEqual(19);
});

test('useViewList - removeManyResources com softDelete=true, NÃO deve remover itens da array e deve decrementar do total', async () => {
   const resolveItem = resolveDelayWithData({
      results: [{ id: 1 }, { id: 2 }, { id: 3 }],
      count: 20,
   });

   const { result, waitForNextUpdate } = renderHook(() =>
      useViewList({
         resolves: {
            items: resolveItem,
         },
      }),
   );

   await waitForNextUpdate();

   // Items
   expect(result.current.resourcesTotal).toEqual(20);

   act(() => {
      result.current.removeManyResources([2, 3], true);
   });

   expect(result.current.resources[1].deleted).toEqual(true);
   expect(result.current.resources[2].deleted).toEqual(true);
   expect(result.current.resourcesTotal).toEqual(18);
});

test('useViewList - removeManyResources com softDelete=false, DEVE remover itens da array e deve decrementar do total', async () => {
   const resolveItem = resolveDelayWithData({
      results: [{ id: 1 }, { id: 2 }, { id: 3 }],
      count: 20,
   });

   const { result, waitForNextUpdate } = renderHook(() =>
      useViewList({
         resolves: {
            items: resolveItem,
         },
      }),
   );

   await waitForNextUpdate();

   // Items
   expect(result.current.resourcesTotal).toEqual(20);

   act(() => {
      result.current.removeManyResources([2, 3], false);
   });

   expect(result.current.resources.length).toEqual(1);
   expect(result.current.resourcesTotal).toEqual(18);
});

test('useViewList - query string inicial deve ser passado para filtro', async () => {
   window.location.pathname = '/route';
   window.location.href = 'https://example.com.br/route?name=Veronica&gender=female';

   const resolveItem = resolveDelayWithData({
      results: [{ id: 1 }, { id: 2 }, { id: 3 }],
      count: 20,
   });

   const { result, waitForNextUpdate } = renderHook(() =>
      useViewList({
         resolves: {
            items: resolveItem,
         },
      }),
   );

   await waitForNextUpdate();

   expect(result.current.filters).toEqual({
      name: 'Veronica',
      gender: 'female',
   });
});

test('useViewList - query string inicial deve ser passado para filtro com handleQueryToFilter', async () => {
   window.location.pathname = '/route';
   window.location.href = 'https://example.com.br/route?name=Veronica&gender=female';

   const resolveItem = resolveDelayWithData({
      results: [{ id: 1 }, { id: 2 }, { id: 3 }],
      count: 20,
   });

   function handleQueryToFilter(query: any): any {
      return { ...query, newValue: true };
   }

   const { result, waitForNextUpdate } = renderHook(() =>
      useViewList({
         resolves: {
            items: resolveItem,
         },
         handleQueryToFilter,
      }),
   );

   await waitForNextUpdate();

   expect(result.current.filters).toEqual({
      name: 'Veronica',
      gender: 'female',
      newValue: true,
   });
});

test('useViewList - query string inicial não deve passar keys de ignoreQueryInFilters para filtro', async () => {
   window.location.pathname = '/route';
   window.location.href = 'https://example.com.br/route?name=Veronica&gender=female';

   const resolveItem = resolveDelayWithData({
      results: [{ id: 1 }, { id: 2 }, { id: 3 }],
      count: 20,
   });

   const { result, waitForNextUpdate } = renderHook(() =>
      useViewList({
         resolves: {
            items: resolveItem,
         },
         ignoreQueryInFilters: ['name'],
      }),
   );

   await waitForNextUpdate();

   expect(result.current.filters).toEqual({
      gender: 'female',
   });
});

test('useViewList - mudanças na query string deve mudar filtro', async () => {
   window.location.pathname = '/route';
   window.location.href = 'https://example.com.br/route?name=Veronica&gender=female';

   const resolveItem = resolveDelayWithData({
      results: [{ id: 1 }, { id: 2 }, { id: 3 }],
      count: 20,
   });

   const { result, waitForNextUpdate } = renderHook(() =>
      useViewList({
         resolves: {
            items: resolveItem,
         },
      }),
   );
   await waitForNextUpdate();

   window.location.pathname = '/route';
   window.location.href = 'https://example.com.br/route?name=John&age=18';
   act(() => {
      eventMap.popstate();
   });

   await waitForNextUpdate();

   /**
    * Atualizar Filtros
    */
   expect(result.current.filters).toEqual({
      age: '18',
      name: 'John',
   });
});

test('useViewList - mudanças na query string deve mudar filtro com ignoreQueryInFilters', async () => {
   window.location.pathname = '/route';
   window.location.href = 'https://example.com.br/route?name=Veronica&gender=female';

   const resolveItem = resolveDelayWithData({
      results: [{ id: 1 }, { id: 2 }, { id: 3 }],
      count: 20,
   });

   const { result, waitForNextUpdate } = renderHook(() =>
      useViewList({
         resolves: {
            items: resolveItem,
         },
         ignoreQueryInFilters: ['age'],
      }),
   );
   await waitForNextUpdate();

   window.location.pathname = '/route';
   window.location.href = 'https://example.com.br/route?name=John&age=18';
   act(() => {
      eventMap.popstate();
   });

   await waitForNextUpdate();

   /**
    * Atualizar Filtros
    */
   expect(result.current.filters).toEqual({
      name: 'John',
   });
});

test('useViewList - mudanças na query string deve mudar filtro com handleQueryToFilter', async () => {
   window.location.pathname = '/route';
   window.location.href = 'https://example.com.br/route?name=Veronica&gender=female';

   const resolveItem = resolveDelayWithData({
      results: [{ id: 1 }, { id: 2 }, { id: 3 }],
      count: 20,
   });

   function handleQueryToFilter(query: any): any {
      return { ...query, newValue: true };
   }

   const { result, waitForNextUpdate } = renderHook(() =>
      useViewList({
         resolves: {
            items: resolveItem,
         },
         handleQueryToFilter,
      }),
   );
   await waitForNextUpdate();

   window.location.pathname = '/route';
   window.location.href = 'https://example.com.br/route?name=John&age=18';
   act(() => {
      eventMap.popstate();
   });

   await waitForNextUpdate();

   /**
    * Atualizar Filtros
    */
   expect(result.current.filters).toEqual({
      age: '18',
      name: 'John',
      newValue: true,
   });
});

test('useViewList - resolve com key items inicial deve receber query string', async (done) => {
   window.location.pathname = '/route';
   window.location.href = 'https://example.com.br/route?name=Veronica&gender=female';

   const resolveItem: Resolve<any> = (params: any) =>
      new Observable((obs) => {
         // Parametro do resolve Item, deve ser filtro
         expect(params).toEqual({
            name: 'Veronica',
            gender: 'female',
         });

         done();
      });

   const { result, waitForNextUpdate } = renderHook(() => {
      const viewList = useViewList({
         resolves: {
            items: resolveItem,
         },
      });

      return viewList;
   });
});

test('useViewList - resolve com key items inicial deve receber query string com filtersDefault', async (done) => {
   window.location.pathname = '/route';
   window.location.href = 'https://example.com.br/route?name=Veronica&gender=female';

   const resolveItem: Resolve<any> = (params: any) =>
      new Observable((obs) => {
         // Parametro do resolve Item, deve ser filtro
         expect(params).toEqual({
            name: 'Veronica',
            gender: 'female',
            default: true,
         });

         done();
      });

   const { result, waitForNextUpdate } = renderHook(() => {
      const viewList = useViewList({
         resolves: {
            items: resolveItem,
         },
         filtersDefault: {
            default: true,
         },
      });

      return viewList;
   });
});

test('useViewList - resolve com key items inicial deve receber deve receber query string com handleRequestBody', async (done) => {
   window.location.pathname = '/route';
   window.location.href = 'https://example.com.br/route?name=Veronica&gender=female';

   const resolveItem: Resolve<any> = (params: any) =>
      new Observable((obs) => {
         // Parametro do resolve Item, deve ser filtro
         expect(params).toEqual({
            name: 'Veronica',
            gender: 'female',
            newValue: true,
         });

         done();
      });

   function handleRequestBody(query: any): any {
      return { ...query, newValue: true };
   }

   const { result, waitForNextUpdate } = renderHook(() =>
      useViewList({
         resolves: {
            items: resolveItem,
         },
         handleRequestBody,
      }),
   );
});

test('useViewList - atualizar filtro DEVE atualizar url query', async () => {
   const history: any = {
      push: jest.fn(),
   };

   const wrapper = ({ children }): any => <AppProvider initialData={{ history }}>{children}</AppProvider>;
   const { result, waitForNextUpdate } = renderHook(() => useViewList({}), { wrapper });

   act(() => {
      result.current.setFilters(() => ({
         name: 'Veronica',
         gender: 'female',
         a: 'b',
      }));
   });

   expect(history.push).toHaveBeenCalledTimes(1);
   expect(history.push).toHaveBeenCalledWith({ search: '?a=b&gender=female&name=Veronica' });
});

test('useViewList - atualizar filtro DEVE atualizar url query com filtersDefault', async () => {
   const history: any = {
      push: jest.fn(),
   };

   const wrapper = ({ children }): any => <AppProvider initialData={{ history }}>{children}</AppProvider>;
   const { result, waitForNextUpdate } = renderHook(
      () =>
         useViewList({
            filtersDefault: {
               default: true,
            },
         }),
      { wrapper },
   );

   act(() => {
      result.current.setFilters(() => ({
         name: 'Veronica',
         gender: 'female',
         a: 'b',
      }));
   });

   expect(history.push).toHaveBeenCalledTimes(1);
   expect(history.push).toHaveBeenCalledWith({ search: '?a=b&default=true&gender=female&name=Veronica' });
});

test('useViewList - atualizar filtro DEVE atualizar url query com handleFilterToQuery', async () => {
   const history: any = {
      push: jest.fn(),
   };

   function handleFilterToQuery(query: any): any {
      return { ...query, newValue: true };
   }

   const wrapper = ({ children }): any => <AppProvider initialData={{ history }}>{children}</AppProvider>;
   const { result } = renderHook(() => useViewList({ handleFilterToQuery }), { wrapper });

   act(() => {
      result.current.setFilters(() => ({
         name: 'Veronica',
         gender: 'female',
         a: 'b',
      }));
   });

   expect(history.push).toHaveBeenCalledTimes(1);
   expect(history.push).toHaveBeenCalledWith({ search: '?a=b&gender=female&name=Veronica&newValue=true' });
});

test('useViewList - atualizar filtro DEVE atualizar url query ignorando ignoreFiltersInQuery', async () => {
   const history: any = {
      push: jest.fn(),
   };

   const wrapper = ({ children }): any => <AppProvider initialData={{ history }}>{children}</AppProvider>;
   const { result } = renderHook(() => useViewList({ ignoreFiltersInQuery: ['a', 'gender'] }), { wrapper });

   act(() => {
      result.current.setFilters(() => ({
         name: 'Veronica',
         gender: 'female',
         a: 'b',
      }));
   });

   expect(history.push).toHaveBeenCalledTimes(1);
   expect(history.push).toHaveBeenCalledWith({ search: '?name=Veronica' });
});

test('useViewList - atualizar filtro NÃO deve atualizar url query se disableQueryChange = true', async () => {
   const history: any = {
      push: jest.fn(),
   };

   const wrapper = ({ children }): any => <AppProvider initialData={{ history }}>{children}</AppProvider>;
   const { result, waitForNextUpdate } = renderHook(() => useViewList({ disableQueryChange: true }), { wrapper });

   act(() => {
      result.current.setFilters(() => ({
         name: 'Veronica',
         gender: 'female',
         a: 'b',
      }));
   });

   expect(history.push).toHaveBeenCalledTimes(0);
});

test('useViewList - atualizar filtro DEVE fazer uma nova requisição para resolve Items', async () => {
   const mockCall = jest.fn();

   const resolveItems: Resolve<any> = (params: any) =>
      new Observable((obs) => {
         setTimeout(() => {
            mockCall(params);
            obs.next({
               results: [{ id: 1 }, { id: 2 }, { id: 3 }],
               count: 20,
            });
            obs.complete();
         }, 1);
      });

   const { result, waitForNextUpdate } = renderHook(() =>
      useViewList({
         resolves: {
            items: resolveItems,
         },
      }),
   );

   act(() => {
      result.current.setFilters(() => ({
         name: 'Veronica',
         gender: 'female',
         a: 'b',
      }));
   });

   await waitForNextUpdate();

   expect(mockCall).toHaveBeenCalledTimes(2);
   expect(mockCall).toHaveBeenCalledWith({});
   expect(mockCall).toHaveBeenCalledWith({
      name: 'Veronica',
      gender: 'female',
      a: 'b',
   });
});

test('useViewList - atualizar filtro DEVE fazer uma nova requisição para resolve Items com filtersDefault', async () => {
   const mockCall = jest.fn();

   const resolveItems: Resolve<any> = (params: any) =>
      new Observable((obs) => {
         setTimeout(() => {
            mockCall(params);
            obs.next({
               results: [{ id: 1 }, { id: 2 }, { id: 3 }],
               count: 20,
            });
            obs.complete();
         }, 1);
      });

   const { result, waitForNextUpdate } = renderHook(() =>
      useViewList({
         resolves: {
            items: resolveItems,
         },
         filtersDefault: {
            default: true,
         },
      }),
   );

   act(() => {
      result.current.setFilters(() => ({
         name: 'Veronica',
         gender: 'female',
         a: 'b',
      }));
   });

   await waitForNextUpdate();

   expect(mockCall).toHaveBeenCalledTimes(2);
   // Initial Default
   expect(mockCall).toHaveBeenCalledWith({
      default: true,
   });
   expect(mockCall).toHaveBeenCalledWith({
      a: 'b',
      default: true,
      gender: 'female',
      name: 'Veronica',
   });
});

test('useViewList - atualizar filtro DEVE fazer atualizar items com nova requisição', async () => {
   const resolveItems: Resolve<any> = (params: any) =>
      new Observable((obs) => {
         setTimeout(() => {
            if (params.offset === 3) {
               obs.next({
                  results: [{ id: 4 }, { id: 5 }, { id: 6 }],
                  count: 20,
               });
            } else {
               obs.next({
                  results: [{ id: 1 }, { id: 2 }, { id: 3 }],
                  count: 20,
               });
            }

            obs.complete();
         }, 1);
      });

   const { result, waitForNextUpdate, rerender } = renderHook((initial) => useViewList(initial), {
      initialProps: {
         resolves: {
            items: resolveItems,
         },
      },
   });

   await waitForNextUpdate();

   // Página 1
   expect(result.current.resources).toEqual([{ id: 1 }, { id: 2 }, { id: 3 }]);
   expect(result.current.resourcesTotal).toEqual(20);

   act(() => {
      result.current.setFilters(() => ({
         offset: 3,
      }));
   });

   await waitForNextUpdate();

   // Página 2
   expect(result.current.resources).toEqual([{ id: 4 }, { id: 5 }, { id: 6 }]);
   expect(result.current.resourcesTotal).toEqual(20);
});

test('useViewList - atualizar filtro DEVE fazer incrementar items com nova requisição quando infiniteScroll = true', async () => {
   const resolveItems: Resolve<any> = (params: any) =>
      new Observable((obs) => {
         setTimeout(() => {
            if (params.offset === 3) {
               obs.next({
                  results: [{ id: 4 }, { id: 5 }, { id: 6 }],
                  count: 20,
               });
            } else {
               obs.next({
                  results: [{ id: 1 }, { id: 2 }, { id: 3 }],
                  count: 20,
               });
            }

            obs.complete();
         }, 1);
      });

   const { result, waitForNextUpdate } = renderHook((initial) => useViewList(initial), {
      initialProps: {
         resolves: {
            items: resolveItems,
         },
         infiniteScroll: true,
      },
   });

   await waitForNextUpdate();

   // Página 1
   expect(result.current.resources).toEqual([{ id: 1 }, { id: 2 }, { id: 3 }]);
   expect(result.current.resourcesTotal).toEqual(20);

   act(() => {
      result.current.setFilters(() => ({
         offset: 3,
      }));
   });

   await waitForNextUpdate();

   // Página 2
   expect(result.current.resources).toEqual([{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }, { id: 6 }]);
   expect(result.current.resourcesTotal).toEqual(20);
});

test('useViewList - atualizar filtro com offset = 0 DEVE sobreescrever items quando infiniteScroll = true', async () => {
   const resolveItems: Resolve<any> = (params: any) =>
      new Observable((obs) => {
         setTimeout(() => {
            if (params.offset === 0) {
               obs.next({
                  results: [{ id: 4 }, { id: 5 }, { id: 6 }],
                  count: 20,
               });
            } else {
               obs.next({
                  results: [{ id: 1 }, { id: 2 }, { id: 3 }],
                  count: 20,
               });
            }

            obs.complete();
         }, 1);
      });

   const { result, waitForNextUpdate } = renderHook((initial) => useViewList(initial), {
      initialProps: {
         resolves: {
            items: resolveItems,
         },
         infiniteScroll: true,
      },
   });

   await waitForNextUpdate();

   // Página 1
   expect(result.current.resources).toEqual([{ id: 1 }, { id: 2 }, { id: 3 }]);
   expect(result.current.resourcesTotal).toEqual(20);

   act(() => {
      result.current.setFilters(() => ({
         offset: 0,
      }));
   });

   await waitForNextUpdate();

   // Offset 0, deve substituir items
   expect(result.current.resources).toEqual([{ id: 4 }, { id: 5 }, { id: 6 }]);
   expect(result.current.resourcesTotal).toEqual(20);
});

test('useViewList - atualizar filtro com page = 0 DEVE sobreescrever items quando infiniteScroll = true', async () => {
   const resolveItems: Resolve<any> = (params: any) =>
      new Observable((obs) => {
         setTimeout(() => {
            if (params.page <= 1) {
               obs.next({
                  results: [{ id: 4 }, { id: 5 }, { id: 6 }],
                  count: 20,
               });
            } else {
               obs.next({
                  results: [{ id: 1 }, { id: 2 }, { id: 3 }],
                  count: 20,
               });
            }

            obs.complete();
         }, 1);
      });

   const { result, waitForNextUpdate } = renderHook((initial) => useViewList(initial), {
      initialProps: {
         resolves: {
            items: resolveItems,
         },
         infiniteScroll: true,
      },
   });

   await waitForNextUpdate();

   // Página 1
   expect(result.current.resources).toEqual([{ id: 1 }, { id: 2 }, { id: 3 }]);
   expect(result.current.resourcesTotal).toEqual(20);

   act(() => {
      result.current.setFilters(() => ({
         page: 0,
      }));
   });

   await waitForNextUpdate();

   // Offset 0, deve substituir items
   expect(result.current.resources).toEqual([{ id: 4 }, { id: 5 }, { id: 6 }]);
   expect(result.current.resourcesTotal).toEqual(20);
});

test('useViewList - atualizar filtro com page = 1 DEVE sobreescrever items quando infiniteScroll = true', async () => {
   const resolveItems: Resolve<any> = (params: any) =>
      new Observable((obs) => {
         setTimeout(() => {
            if (params.page <= 1) {
               obs.next({
                  results: [{ id: 4 }, { id: 5 }, { id: 6 }],
                  count: 20,
               });
            } else {
               obs.next({
                  results: [{ id: 1 }, { id: 2 }, { id: 3 }],
                  count: 20,
               });
            }

            obs.complete();
         }, 1);
      });

   const { result, waitForNextUpdate } = renderHook((initial) => useViewList(initial), {
      initialProps: {
         resolves: {
            items: resolveItems,
         },
         infiniteScroll: true,
      },
   });

   await waitForNextUpdate();

   // Página 1
   expect(result.current.resources).toEqual([{ id: 1 }, { id: 2 }, { id: 3 }]);
   expect(result.current.resourcesTotal).toEqual(20);

   act(() => {
      result.current.setFilters(() => ({
         page: 1,
      }));
   });

   await waitForNextUpdate();

   // Offset 0, deve substituir items
   expect(result.current.resources).toEqual([{ id: 4 }, { id: 5 }, { id: 6 }]);
   expect(result.current.resourcesTotal).toEqual(20);
});

test('useViewList - nova requisição deve ativar searching = true', async () => {
   const resolveItems: Resolve<any> = (params: any) =>
      new Observable((obs) => {
         setTimeout(() => {
            if (params.offset === 3) {
               obs.next({
                  results: [{ id: 4 }, { id: 5 }, { id: 6 }],
                  count: 20,
               });
            } else {
               obs.next({
                  results: [{ id: 1 }, { id: 2 }, { id: 3 }],
                  count: 20,
               });
            }

            obs.complete();
         }, 1);
      });

   const { result, waitForNextUpdate } = renderHook((initial) => useViewList(initial), {
      initialProps: {
         resolves: {
            items: resolveItems,
         },
      },
   });

   await waitForNextUpdate();

   expect(result.current.searching).toEqual(false);

   act(() => {
      result.current.setFilters(() => ({
         offset: 3,
      }));
   });

   // Buscando
   expect(result.current.searching).toEqual(true);

   await waitForNextUpdate();

   // Finalizado
   expect(result.current.searching).toEqual(false);
});

test('useViewList - nova requisição deve ativar searchingMore = true se infiniteScroll = true', async () => {
   const resolveItems: Resolve<any> = (params: any) =>
      new Observable((obs) => {
         setTimeout(() => {
            if (params.offset === 3) {
               obs.next({
                  results: [{ id: 4 }, { id: 5 }, { id: 6 }],
                  count: 20,
               });
            } else {
               obs.next({
                  results: [{ id: 1 }, { id: 2 }, { id: 3 }],
                  count: 20,
               });
            }

            obs.complete();
         }, 1);
      });

   const { result, waitForNextUpdate } = renderHook((initial) => useViewList(initial), {
      initialProps: {
         resolves: {
            items: resolveItems,
         },
         infiniteScroll: true,
      },
   });

   await waitForNextUpdate();

   expect(result.current.searchingMore).toEqual(false);

   act(() => {
      result.current.setFilters(() => ({
         offset: 3,
      }));
   });

   // Buscando
   expect(result.current.searchingMore).toEqual(true);

   await waitForNextUpdate();

   // Finalizado
   expect(result.current.searchingMore).toEqual(false);
});

test('useViewList - nova requisição com falha deve ativar errorLoadData = true', async () => {
   const resolveItems: Resolve<any> = (params: any) =>
      new Observable((obs) => {
         setTimeout(() => {
            if (params.offset === 3) {
               obs.error('error');
            } else {
               obs.next({
                  results: [{ id: 1 }, { id: 2 }, { id: 3 }],
                  count: 20,
               });
            }

            obs.complete();
         }, 1);
      });

   const { result, waitForNextUpdate } = renderHook((initial) => useViewList(initial), {
      initialProps: {
         resolves: {
            items: resolveItems,
         },
      },
   });

   await waitForNextUpdate();

   expect(result.current.errorLoadData).toEqual(false);

   act(() => {
      result.current.setFilters(() => ({
         offset: 3,
      }));
   });

   // Buscando
   expect(result.current.errorLoadData).toEqual(false);

   await waitForNextUpdate();

   // Finalizado
   expect(result.current.errorLoadData).toEqual(true);
});

test('useViewList - nova requisição com falha deve ativar errorLoadMore = true se infiniteScroll = true', async () => {
   const resolveItems: Resolve<any> = (params: any) =>
      new Observable((obs) => {
         setTimeout(() => {
            if (params.offset === 3) {
               obs.error('error');
            } else {
               obs.next({
                  results: [{ id: 1 }, { id: 2 }, { id: 3 }],
                  count: 20,
               });
            }

            obs.complete();
         }, 1);
      });

   const { result, waitForNextUpdate } = renderHook((initial) => useViewList(initial), {
      initialProps: {
         resolves: {
            items: resolveItems,
         },
         infiniteScroll: true,
      },
   });

   await waitForNextUpdate();

   expect(result.current.errorLoadMore).toEqual(false);

   act(() => {
      result.current.setFilters(() => ({
         offset: 3,
      }));
   });

   // Buscando
   expect(result.current.errorLoadMore).toEqual(false);

   await waitForNextUpdate();

   // Finalizado
   expect(result.current.errorLoadMore).toEqual(true);
});

test('useViewList - atualizar filtros com noRequest = true, não deve fazer proxima requisição', async () => {
   const resolveItems: Resolve<any> = (params: any) =>
      new Observable((obs) => {
         setTimeout(() => {
            if (params.offset === 3) {
               obs.next({
                  results: [{ id: 4 }, { id: 5 }, { id: 6 }],
                  count: 20,
               });
            } else {
               obs.next({
                  results: [{ id: 1 }, { id: 2 }, { id: 3 }],
                  count: 20,
               });
            }

            obs.complete();
         }, 1);
      });

   const { result, waitForNextUpdate, rerender } = renderHook((initial) => useViewList(initial), {
      initialProps: {
         resolves: {
            items: resolveItems,
         },
      },
   });

   await waitForNextUpdate();

   // Página 1
   expect(result.current.resources).toEqual([{ id: 1 }, { id: 2 }, { id: 3 }]);
   expect(result.current.resourcesTotal).toEqual(20);

   act(() => {
      result.current.setFilters(
         () => ({
            offset: 3,
         }),
         {
            noRequest: true,
         },
      );
   });

   // Não deve carregar pagina 2
   expect(result.current.resources).toEqual([{ id: 1 }, { id: 2 }, { id: 3 }]);
   expect(result.current.resourcesTotal).toEqual(20);
});

test('useViewList - atualizar filtros com noRequest = true, não deve fazer APENAS a proxima requisição', async () => {
   const resolveItems: Resolve<any> = (params: any) =>
      new Observable((obs) => {
         setTimeout(() => {
            if (params.offset === 3) {
               obs.next({
                  results: [{ id: 4 }, { id: 5 }, { id: 6 }],
                  count: 20,
               });
            } else {
               obs.next({
                  results: [{ id: 1 }, { id: 2 }, { id: 3 }],
                  count: 20,
               });
            }

            obs.complete();
         }, 1);
      });

   const { result, waitForNextUpdate } = renderHook((initial) => useViewList(initial), {
      initialProps: {
         resolves: {
            items: resolveItems,
         },
      },
   });

   await waitForNextUpdate();

   // Página 1
   expect(result.current.resources).toEqual([{ id: 1 }, { id: 2 }, { id: 3 }]);
   expect(result.current.resourcesTotal).toEqual(20);

   act(() => {
      result.current.setFilters(
         () => ({
            offset: 3,
         }),
         {
            noRequest: true,
         },
      );
   });

   // Não deve carregar pagina 2
   expect(result.current.resources).toEqual([{ id: 1 }, { id: 2 }, { id: 3 }]);
   expect(result.current.resourcesTotal).toEqual(20);

   act(() => {
      result.current.setFilters(() => ({
         offset: 3,
         name: 'john',
      }));
   });

   await waitForNextUpdate();

   // Deve carregar pagina 2
   expect(result.current.resources).toEqual([{ id: 4 }, { id: 5 }, { id: 6 }]);
   expect(result.current.resourcesTotal).toEqual(20);
});

test('useViewList - se reverseItems = true deve inverter items', async () => {
   const resolveItems: Resolve<any> = (params: any) =>
      new Observable((obs) => {
         setTimeout(() => {
            obs.next({
               results: [{ id: 1 }, { id: 2 }, { id: 3 }],
               count: 20,
            });
            obs.complete();
         }, 1);
      });

   const { result, waitForNextUpdate } = renderHook(() =>
      useViewList({
         resolves: {
            items: resolveItems,
         },
         reverseItems: true,
      }),
   );

   await waitForNextUpdate();

   expect(result.current.resources).toEqual([{ id: 3 }, { id: 2 }, { id: 1 }]);
   expect(result.current.resourcesTotal).toEqual(20);
});

test('useViewList - se reverseItems = true deve inverter items de novas requisições', async () => {
   const resolveItems: Resolve<any> = (params: any) =>
      new Observable((obs) => {
         setTimeout(() => {
            if (params.offset === 3) {
               obs.next({
                  results: [{ id: 4 }, { id: 5 }, { id: 6 }],
                  count: 20,
               });
            } else {
               obs.next({
                  results: [{ id: 1 }, { id: 2 }, { id: 3 }],
                  count: 20,
               });
            }

            obs.complete();
         }, 1);
      });

   const { result, waitForNextUpdate } = renderHook((initial) => useViewList(initial), {
      initialProps: {
         resolves: {
            items: resolveItems,
         },
         reverseItems: true,
      },
   });

   await waitForNextUpdate();

   // Página 1
   expect(result.current.resources).toEqual([{ id: 3 }, { id: 2 }, { id: 1 }]);
   expect(result.current.resourcesTotal).toEqual(20);

   act(() => {
      result.current.setFilters(() => ({
         offset: 3,
      }));
   });

   await waitForNextUpdate();

   // Página 2
   expect(result.current.resources).toEqual([{ id: 6 }, { id: 5 }, { id: 4 }]);
   expect(result.current.resourcesTotal).toEqual(20);
});

test('useViewList - se reverseItems = true & infiniteScroll = true deve inverter push nos items das novas requisições', async () => {
   const resolveItems: Resolve<any> = (params: any) =>
      new Observable((obs) => {
         setTimeout(() => {
            if (params.offset === 3) {
               obs.next({
                  results: [{ id: 4 }, { id: 5 }, { id: 6 }],
                  count: 20,
               });
            } else {
               obs.next({
                  results: [{ id: 1 }, { id: 2 }, { id: 3 }],
                  count: 20,
               });
            }

            obs.complete();
         }, 1);
      });

   const { result, waitForNextUpdate } = renderHook((initial) => useViewList(initial), {
      initialProps: {
         resolves: {
            items: resolveItems,
         },
         reverseItems: true,
         infiniteScroll: true,
      },
   });

   await waitForNextUpdate();

   // Página 1
   expect(result.current.resources).toEqual([{ id: 3 }, { id: 2 }, { id: 1 }]);
   expect(result.current.resourcesTotal).toEqual(20);

   act(() => {
      result.current.setFilters(() => ({
         offset: 3,
      }));
   });

   await waitForNextUpdate();

   // Página 2
   expect(result.current.resources).toEqual([{ id: 6 }, { id: 5 }, { id: 4 }, { id: 3 }, { id: 2 }, { id: 1 }]);
   expect(result.current.resourcesTotal).toEqual(20);
});

test('useViewList - função setPage deve definir offset correto', async () => {
   const resolveItems: Resolve<any> = (params: any) =>
      new Observable((obs) => {
         setTimeout(() => {
            // Para página 3 o offset deve ser 40
            if (params.offset === 6) {
               obs.next({
                  results: [{ id: 4 }, { id: 5 }, { id: 6 }],
                  count: 20,
               });
            } else {
               obs.next({
                  results: [{ id: 1 }, { id: 2 }, { id: 3 }],
                  count: 20,
               });
            }

            obs.complete();
         }, 1);
      });

   const { result, waitForNextUpdate } = renderHook((initial) => useViewList(initial), {
      initialProps: {
         resolves: {
            items: resolveItems,
         },
         limit: 3,
      },
   });

   await waitForNextUpdate();

   // Página 1
   expect(result.current.resources).toEqual([{ id: 1 }, { id: 2 }, { id: 3 }]);
   expect(result.current.resourcesTotal).toEqual(20);

   // Para página 3 o offset deve ser 6
   act(() => {
      result.current.setPage(3);
   });

   await waitForNextUpdate();

   // Página 2
   expect(result.current.resources).toEqual([{ id: 4 }, { id: 5 }, { id: 6 }]);
   expect(result.current.resourcesTotal).toEqual(20);
});

test('useViewList - função setOffset deve definir offset correto', async () => {
   const resolveItems: Resolve<any> = (params: any) =>
      new Observable((obs) => {
         setTimeout(() => {
            // Offset para 16
            if (params.offset === 16) {
               obs.next({
                  results: [{ id: 4 }, { id: 5 }, { id: 6 }],
                  count: 20,
               });
            } else {
               obs.next({
                  results: [{ id: 1 }, { id: 2 }, { id: 3 }],
                  count: 20,
               });
            }

            obs.complete();
         }, 1);
      });

   const { result, waitForNextUpdate } = renderHook((initial) => useViewList(initial), {
      initialProps: {
         resolves: {
            items: resolveItems,
         },
         limit: 3,
      },
   });

   await waitForNextUpdate();

   // Página 1
   expect(result.current.resources).toEqual([{ id: 1 }, { id: 2 }, { id: 3 }]);
   expect(result.current.resourcesTotal).toEqual(20);

   // Offset para 16
   act(() => {
      result.current.setOffset(16);
   });

   await waitForNextUpdate();

   // Página 2
   expect(result.current.resources).toEqual([{ id: 4 }, { id: 5 }, { id: 6 }]);
   expect(result.current.resourcesTotal).toEqual(20);
});

test('useViewList - função setSort deve definir offset correto', async (done) => {
   const resolveItems: Resolve<any> = (params: any) =>
      new Observable((obs) => {
         setTimeout(() => {
            /**
             * Sort deve ser -id
             */
            if (params.sort === '-id') {
               done();
            }

            obs.next({
               results: [{ id: 1 }, { id: 2 }, { id: 3 }],
               count: 20,
            });

            obs.complete();
         }, 1);
      });

   const { result, waitForNextUpdate } = renderHook((initial) => useViewList(initial), {
      initialProps: {
         resolves: {
            items: resolveItems,
         },
         limit: 3,
      },
   });

   await waitForNextUpdate();

   // Página 1
   expect(result.current.resources).toEqual([{ id: 1 }, { id: 2 }, { id: 3 }]);
   expect(result.current.resourcesTotal).toEqual(20);

   // Offset para 16
   act(() => {
      result.current.setSort('-id');
   });

   await waitForNextUpdate();
});

test('useViewList - função loadNextPage deve definir offset corretamente', async (done) => {
   const resolveItems: Resolve<any> = (params: any) =>
      new Observable((obs) => {
         setTimeout(() => {
            /**
             * Sort deve ser -id
             */
            // Três chamadas para função loadNextPage 3(limit) x 3(calls)
            if (params.offset === 9) {
               done();
            }

            obs.next({
               results: [{ id: 1 }, { id: 2 }, { id: 3 }],
               count: 20,
            });

            obs.complete();
         }, 1);
      });

   const { result, waitForNextUpdate } = renderHook((initial) => useViewList(initial), {
      initialProps: {
         resolves: {
            items: resolveItems,
         },
         limit: 3,
      },
   });

   await waitForNextUpdate();

   // Página 1
   expect(result.current.resources).toEqual([{ id: 1 }, { id: 2 }, { id: 3 }]);
   expect(result.current.resourcesTotal).toEqual(20);

   act(() => {
      result.current.loadNextPage();
   });
   await waitForNextUpdate();

   act(() => {
      result.current.loadNextPage();
   });
   await waitForNextUpdate();

   act(() => {
      result.current.loadNextPage();
   });
   await waitForNextUpdate();
});

test('useViewList - função loadNextPage deve refazer ultima request quando infiniteScroll=true', async () => {
   const mockCall = jest.fn();

   const resolveItems: Resolve<any> = (params: any) =>
      new Observable((obs) => {
         mockCall(params);
         setTimeout(() => {
            const { offset = 0 } = params;
            obs.next({
               results: [{ id: offset + 1 }, { id: offset + 2 }, { id: offset + 3 }],
               count: 20,
            });

            obs.complete();
         }, 1);
      });

   const resolveItemsError: Resolve<any> = (params: any) =>
      new Observable((obs) => {
         setTimeout(() => {
            obs.error({});
            obs.complete();
         }, 1);
      });

   const { result, waitForNextUpdate, rerender } = renderHook((initial) => useViewList(initial), {
      initialProps: {
         resolves: {
            items: resolveItems,
         },
         limit: 3,
         infiniteScroll: true,
      },
   });

   await waitForNextUpdate();

   expect(result.current.errorLoadMore).toEqual(false);

   rerender({
      resolves: {
         items: resolveItemsError,
      },
      limit: 3,
      infiniteScroll: true,
   });

   /**
    * Buscar proxima página com erro
    */
   act(() => {
      result.current.loadNextPage();
   });
   await waitForNextUpdate();
   act(() => {
      result.current.loadNextPage();
   });
   await waitForNextUpdate();
   act(() => {
      result.current.loadNextPage();
   });
   await waitForNextUpdate();
   expect(result.current.errorLoadMore).toEqual(true);
   expect(result.current.resources.length).toEqual(3);

   /**
    * Refazer request
    */
   rerender({
      resolves: {
         items: resolveItems,
      },
      limit: 3,
      infiniteScroll: true,
   });

   act(() => {
      result.current.loadNextPage();
   });
   await waitForNextUpdate();

   expect(result.current.errorLoadMore).toEqual(false);
   expect(result.current.resources.length).toEqual(6);
   expect(result.current.resources).toEqual([{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }, { id: 6 }]);
});

test('useViewList - função loadNextPage NÃO deve refazer ultima request quando infiniteScroll=false', async () => {
   const mockCall = jest.fn();

   const resolveItems: Resolve<any> = (params: any) =>
      new Observable((obs) => {
         mockCall(params);
         setTimeout(() => {
            const { offset = 0 } = params;
            obs.next({
               results: [{ id: offset + 1 }, { id: offset + 2 }, { id: offset + 3 }],
               count: 20,
            });

            obs.complete();
         }, 1);
      });

   const resolveItemsError: Resolve<any> = (params: any) =>
      new Observable((obs) => {
         setTimeout(() => {
            obs.error({});
            obs.complete();
         }, 1);
      });

   const { result, waitForNextUpdate, rerender } = renderHook((initial) => useViewList(initial), {
      initialProps: {
         resolves: {
            items: resolveItems,
         },
         limit: 3,
      },
   });

   await waitForNextUpdate();

   expect(result.current.errorLoadData).toEqual(false);

   rerender({
      resolves: {
         items: resolveItemsError,
      },
      limit: 3,
   });

   /**
    * Buscar proxima página com erro
    */
   act(() => {
      result.current.loadNextPage();
   });
   await waitForNextUpdate();
   act(() => {
      result.current.loadNextPage();
   });
   await waitForNextUpdate();
   act(() => {
      result.current.loadNextPage();
   });
   await waitForNextUpdate();
   expect(result.current.errorLoadData).toEqual(true);
   expect(result.current.resources.length).toEqual(3);

   /**
    * Refazer request
    */
   rerender({
      resolves: {
         items: resolveItems,
      },
      limit: 3,
   });

   // Deve carregar Página 5
   act(() => {
      result.current.loadNextPage();
   });
   await waitForNextUpdate();

   expect(result.current.errorLoadData).toEqual(false);
   expect(result.current.resources.length).toEqual(3);
   expect(result.current.resources).toEqual([{ id: 13 }, { id: 14 }, { id: 15 }]);
});

test('useViewList - função reloadResources deve refazer ultima request', async () => {
   const mockCall = jest.fn();

   const resolveItems: Resolve<any> = (params: any) =>
      new Observable((obs) => {
         mockCall(params);
         setTimeout(() => {
            const { offset = 0 } = params;
            obs.next({
               results: [{ id: offset + 1 }, { id: offset + 2 }, { id: offset + 3 }],
               count: 20,
            });

            obs.complete();
         }, 1);
      });

   const resolveItemsError: Resolve<any> = (params: any) =>
      new Observable((obs) => {
         setTimeout(() => {
            obs.error({});
            obs.complete();
         }, 1);
      });

   const { result, waitForNextUpdate, rerender } = renderHook((initial) => useViewList(initial), {
      initialProps: {
         resolves: {
            items: resolveItems,
         },
         limit: 3,
      },
   });

   await waitForNextUpdate();

   expect(result.current.errorLoadData).toEqual(false);

   rerender({
      resolves: {
         items: resolveItemsError,
      },
      limit: 3,
   });

   /**
    * Buscar proxima página com erro
    */
   act(() => {
      result.current.loadNextPage();
   });
   await waitForNextUpdate();
   act(() => {
      result.current.loadNextPage();
   });
   await waitForNextUpdate();
   expect(result.current.errorLoadData).toEqual(true);
   expect(result.current.resources.length).toEqual(3);

   /**
    * Refazer request
    */
   rerender({
      resolves: {
         items: resolveItems,
      },
      limit: 3,
   });

   /**
    * Deve recarregar Página 3
    */
   act(() => {
      result.current.reloadResources();
   });
   await waitForNextUpdate();

   expect(result.current.errorLoadData).toEqual(false);
   expect(result.current.resources.length).toEqual(3);
   expect(result.current.resources).toEqual([{ id: 7 }, { id: 8 }, { id: 9 }]);
});

test('useViewList - testar registerOnSearch', async (done) => {
   const mockCall = jest.fn();

   const resolveItems: Resolve<any> = (params: any) =>
      new Observable((obs) => {
         mockCall(params);
         setTimeout(() => {
            const { offset = 0 } = params;
            obs.next({
               results: [{ id: offset + 1 }, { id: offset + 2 }, { id: offset + 3 }],
               count: 20,
            });

            obs.complete();
         }, 1);
      });

   const { result, waitForNextUpdate, rerender } = renderHook((initial) => useViewList(initial), {
      initialProps: {
         resolves: {
            items: resolveItems,
         },
         limit: 3,
      },
   });

   await waitForNextUpdate();

   result.current.registerOnSearch(() => {
      done();
   });

   /**
    * Buscar proxima página com erro
    */
   act(() => {
      result.current.setFilters(() => ({
         name: 'john',
      }));
   });

   await waitForNextUpdate();
});
