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

import {useVirtualizedTree} from 'sentry/utils/profiling/hooks/useVirtualizedTree/useVirtualizedTree';

const n = d => {
  return {...d, children: []};
};

// Creates a tree with N nodes where each node has only one child
const chain = (prefix: string, depth: number) => {
  let node = n({id: `${prefix}-0`});
  let start = 1;
  // Keep a root reference so we can return it
  const root = node;

  // Build a tree of nodes with each node having only one child
  while (start < depth) {
    const child = n({id: `${prefix}-${start}`});
    node.children = [child];
    // Swap the current node
    node = child;
    start++;
  }

  return root;
};

class ResizeObserver {
  observe() {}
  unobserve() {}
  disconnect() {}
}

window.ResizeObserver = ResizeObserver;
window.requestAnimationFrame = (cb: Function) => cb();

const makeScrollContainerMock = ({height}: {height: number}) => {
  return {
    getBoundingClientRect: () => {
      return {height};
    },
    addEventListener: () => {},
    removeEventListener: () => {},
  } as unknown as HTMLElement;
};

describe('useVirtualizedTree', () => {
  it('returns a tree', () => {
    const results = reactHooks.renderHook(useVirtualizedTree, {
      initialProps: {
        overscroll: 0,
        rowHeight: 10,
        tree: [],
        scrollContainer: null,
        renderRow: () => <div />,
      },
    });

    expect(results.result.current.items).toEqual([]);
  });

  it('expands tree', () => {
    const mockScrollContainer = makeScrollContainerMock({height: 100});

    const tree = [chain('child', 5)];

    const {result} = reactHooks.renderHook(useVirtualizedTree, {
      initialProps: {
        expanded: true,
        rowHeight: 10,
        scrollContainer: mockScrollContainer,
        overscroll: 0,
        tree,
        renderRow: () => <div />,
      },
    });

    expect(result.current.items.length).toBe(5);
  });

  it('shows first 10 items', () => {
    const mockScrollContainer = makeScrollContainerMock({height: 100});

    const tree = [chain('child', 10)];

    const {result} = reactHooks.renderHook(useVirtualizedTree, {
      initialProps: {
        rowHeight: 10,
        scrollContainer: mockScrollContainer,
        overscroll: 0,
        tree,
        renderRow: () => <div />,
      },
    });

    reactHooks.act(() => {
      result.current.handleExpandTreeNode(result.current.tree.roots[0], {
        expandChildren: true,
      });
    });

    for (let i = 0; i < 10; i++) {
      expect(result.current.items[i].item.node.id).toEqual(`child-${i}`);
    }
    expect(result.current.items.length).toBe(10);
  });

  it('shows 5-15 items', async () => {
    const mockScrollContainer = makeScrollContainerMock({height: 100});

    const tree = [chain('child', 20)];

    const {result} = reactHooks.renderHook(useVirtualizedTree, {
      initialProps: {
        rowHeight: 10,
        scrollContainer: mockScrollContainer,
        overscroll: 0,
        tree,
        renderRow: () => <div />,
      },
    });

    reactHooks.act(() => {
      result.current.handleExpandTreeNode(result.current.tree.roots[0], {
        expandChildren: true,
      });
      result.current.dispatch({type: 'set scroll top', payload: 50});
    });

    await waitFor(() => {
      expect(result.current.items.length).toBe(10);
    });
    for (let i = 0; i < 10; i++) {
      expect(result.current.items[i].item.node.id).toEqual(`child-${i + 5}`);
    }
    expect(result.current.items.length).toBe(10);
  });

  it('shows last 10 items', async () => {
    const mockScrollContainer = makeScrollContainerMock({height: 100});

    const tree = [chain('child', 20)];

    const {result} = reactHooks.renderHook(useVirtualizedTree, {
      initialProps: {
        rowHeight: 10,
        scrollContainer: mockScrollContainer,
        overscroll: 0,
        tree,
        renderRow: () => <div />,
      },
    });

    reactHooks.act(() => {
      result.current.handleExpandTreeNode(result.current.tree.roots[0], {
        expandChildren: true,
      });
      result.current.dispatch({type: 'set scroll top', payload: 100});
    });

    await waitFor(() => {
      expect(result.current.items.length).toBe(10);
    });
    for (let i = 0; i < 10; i++) {
      expect(result.current.items[i].item.node.id).toEqual(`child-${i + 10}`);
    }
    expect(result.current.items.length).toBe(10);
  });

  it('shows overscroll items', () => {
    const mockScrollContainer = makeScrollContainerMock({height: 100});

    const tree = [chain('child', 20)];

    const {result} = reactHooks.renderHook(useVirtualizedTree, {
      initialProps: {
        rowHeight: 10,
        scrollContainer: mockScrollContainer,
        overscroll: 2,
        tree,
        renderRow: () => <div />,
      },
    });

    reactHooks.act(() => {
      result.current.handleExpandTreeNode(result.current.tree.roots[0], {
        expandChildren: true,
      });
      result.current.dispatch({type: 'set scroll top', payload: 50});
    });

    for (let i = 3; i < 17; i++) {
      // Should display nodes 5-15, but since we use overscroll, it should display nodes 3-17
      expect(result.current.items[i - 3].item.node.id).toEqual(`child-${i}`);
    }
    expect(result.current.items.length).toBe(14);
  });

  it('items have a stable key', () => {
    const mockScrollContainer = makeScrollContainerMock({height: 100});

    const tree = [chain('child', 20)];

    const {result} = reactHooks.renderHook(useVirtualizedTree, {
      initialProps: {
        rowHeight: 10,
        scrollContainer: mockScrollContainer,
        overscroll: 0,
        tree,
        renderRow: () => <div />,
      },
    });

    reactHooks.act(() => {
      result.current.handleExpandTreeNode(result.current.tree.roots[0], {
        expandChildren: true,
      });
      result.current.dispatch({type: 'set scroll top', payload: 50});
    });

    const stableKeys = result.current.items.map(item => item.key);

    reactHooks.act(() => {
      result.current.dispatch({type: 'set scroll top', payload: 60});
    });

    // First 9 items should be the same, the last item should be different
    for (let i = 1; i < stableKeys.length; i++) {
      expect(stableKeys[i]).toBe(result.current.items[i - 1].key);
    }

    // Last item should be different
    expect(result.current.items[result.current.items.length - 1].key).toBe(
      stableKeys[stableKeys.length - 1] + 1
    );
  });
});
