/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
import { fireEvent, render } from 'spec/helpers/testing-library';

import BackgroundStyleDropdown from 'src/dashboard/components/menu/BackgroundStyleDropdown';
import IconButton from 'src/dashboard/components/IconButton';
import { DASHBOARD_GRID_ID } from 'src/dashboard/util/constants';

import { getMockStore } from 'spec/fixtures/mockStore';
import { dashboardLayout as mockLayout } from 'spec/fixtures/mockDashboardLayout';
import { initialState } from 'src/SqlLab/fixtures';
import Row from './Row';

jest.mock('@superset-ui/core', () => ({
  ...jest.requireActual('@superset-ui/core'),
  isFeatureEnabled: jest.fn(() => true),
  FeatureFlag: {
    DashboardVirtualization: 'DASHBOARD_VIRTUALIZATION',
  },
}));

jest.mock('src/utils/isBot', () => ({
  isCurrentUserBot: jest.fn(() => false),
}));

jest.mock('src/dashboard/util/isEmbedded', () => ({
  isEmbedded: jest.fn(() => false),
}));

jest.mock('src/dashboard/components/dnd/DragDroppable', () => ({
  Draggable: ({ children }) => (
    <div data-test="mock-draggable">{children({})}</div>
  ),
  Droppable: ({ children, depth }) => (
    <div data-test="mock-droppable" depth={depth}>
      {children({})}
    </div>
  ),
}));
jest.mock(
  'src/dashboard/containers/DashboardComponent',
  () =>
    ({ availableColumnCount, depth }) => (
      <div data-test="mock-dashboard-component" depth={depth}>
        {availableColumnCount}
      </div>
    ),
);

jest.mock(
  'src/dashboard/components/menu/WithPopoverMenu',
  () =>
    ({ children }) => <div data-test="mock-with-popover-menu">{children}</div>,
);

jest.mock(
  'src/dashboard/components/DeleteComponentButton',
  () =>
    ({ onDelete }) => (
      <button
        type="button"
        data-test="mock-delete-component-button"
        onClick={onDelete}
      >
        Delete
      </button>
    ),
);

const rowWithoutChildren = { ...mockLayout.present.ROW_ID, children: [] };
const props = {
  id: 'ROW_ID',
  parentId: DASHBOARD_GRID_ID,
  component: mockLayout.present.ROW_ID,
  parentComponent: mockLayout.present[DASHBOARD_GRID_ID],
  index: 0,
  depth: 2,
  editMode: false,
  availableColumnCount: 12,
  columnWidth: 50,
  occupiedColumnCount: 6,
  onResizeStart() {},
  onResize() {},
  onResizeStop() {},
  handleComponentDrop() {},
  deleteComponent() {},
  updateComponents() {},
};

function setup(overrideProps) {
  // We have to wrap provide DragDropContext for the underlying DragDroppable
  // otherwise we cannot assert on DragDroppable children
  const mockStore = getMockStore({
    ...initialState,
  });

  return render(<Row {...props} {...overrideProps} />, {
    store: mockStore,
    useDnd: true,
    useRouter: true,
  });
}

test('should render a Draggable', () => {
  // don't count child DragDroppables
  const { getByTestId, queryByTestId } = setup({
    component: rowWithoutChildren,
  });

  expect(getByTestId('mock-draggable')).toBeInTheDocument();
  expect(queryByTestId('mock-droppable')).not.toBeInTheDocument();
});

test('should skip rendering HoverMenu and DeleteComponentButton when not in editMode', () => {
  const { container, queryByTestId } = setup({
    component: rowWithoutChildren,
  });
  expect(container.querySelector('.hover-menu')).not.toBeInTheDocument();
  expect(queryByTestId('mock-delete-component-button')).not.toBeInTheDocument();
});

test('should render a WithPopoverMenu', () => {
  // don't count child DragDroppables
  const { getByTestId } = setup({ component: rowWithoutChildren });
  expect(getByTestId('mock-with-popover-menu')).toBeInTheDocument();
});

test('should render a HoverMenu in editMode', () => {
  const { container, getAllByTestId, getByTestId } = setup({
    component: rowWithoutChildren,
    editMode: true,
  });
  expect(container.querySelector('.hover-menu')).toBeInTheDocument();

  // Droppable area enabled in editMode
  expect(getAllByTestId('mock-droppable').length).toBe(1);

  // pass the same depth of its droppable area
  expect(getByTestId('mock-droppable')).toHaveAttribute(
    'depth',
    `${props.depth}`,
  );
});

test('should render a DeleteComponentButton in editMode', () => {
  const { getByTestId } = setup({
    component: rowWithoutChildren,
    editMode: true,
  });
  expect(getByTestId('mock-delete-component-button')).toBeInTheDocument();
});

test.skip('should render a BackgroundStyleDropdown when focused', () => {
  let wrapper = setup({ component: rowWithoutChildren });
  expect(wrapper.find(BackgroundStyleDropdown)).toBeFalsy();

  // we cannot set props on the Row because of the WithDragDropContext wrapper
  wrapper = setup({ component: rowWithoutChildren, editMode: true });
  wrapper
    .find(IconButton)
    .at(1) // first one is delete button
    .simulate('click');

  expect(wrapper.find(BackgroundStyleDropdown)).toBeTruthy();
});

test('should call deleteComponent when deleted', () => {
  const deleteComponent = jest.fn();
  const { getByTestId } = setup({ editMode: true, deleteComponent });
  fireEvent.click(getByTestId('mock-delete-component-button'));
  expect(deleteComponent).toHaveBeenCalledTimes(1);
});

test('should pass appropriate availableColumnCount to children', () => {
  const { getByTestId } = setup();
  expect(getByTestId('mock-dashboard-component')).toHaveTextContent(
    props.availableColumnCount - props.occupiedColumnCount,
  );
});

test('should increment the depth of its children', () => {
  const { getByTestId } = setup();
  expect(getByTestId('mock-dashboard-component')).toHaveAttribute(
    'depth',
    `${props.depth + 1}`,
  );
});

// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('visibility handling for intersection observers', () => {
  const mockIntersectionObserver = jest.fn();
  const mockObserve = jest.fn();
  const mockDisconnect = jest.fn();

  beforeAll(() => {
    mockIntersectionObserver.mockReturnValue({
      observe: mockObserve,
      unobserve: jest.fn(),
      disconnect: mockDisconnect,
    });
    window.IntersectionObserver = mockIntersectionObserver;
  });

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

  afterAll(() => {
    delete window.IntersectionObserver;
  });

  test('should handle visibility prop changes without crashing', () => {
    const { rerender } = setup({ isComponentVisible: true });

    expect(setup).not.toThrow();

    expect(() => {
      rerender(<Row {...props} isComponentVisible={false} />);
    }).not.toThrow();

    expect(() => {
      rerender(<Row {...props} isComponentVisible />);
    }).not.toThrow();
  });

  test('should create intersection observers when feature is enabled', () => {
    setup({ isComponentVisible: true });

    expect(mockIntersectionObserver).toHaveBeenCalledWith(
      expect.any(Function),
      expect.objectContaining({ rootMargin: expect.any(String) }),
    );
  });

  test('should not create intersection observers when feature is disabled', () => {
    const coreMock = jest.requireMock('@superset-ui/core');
    coreMock.isFeatureEnabled.mockReturnValue(false);

    jest.clearAllMocks();
    setup({ isComponentVisible: true });

    expect(mockIntersectionObserver).not.toHaveBeenCalled();

    coreMock.isFeatureEnabled.mockReturnValue(true);
  });

  test('intersection observer callbacks handle entries without errors', () => {
    const callback = ([entry]) => {
      if (entry.isIntersecting) return true;

      return false;
    };

    const intersectingEntry = { isIntersecting: true };
    expect(() => callback([intersectingEntry])).not.toThrow();
    expect(callback([intersectingEntry])).toBe(true);

    const nonIntersectingEntry = { isIntersecting: false };
    expect(() => callback([nonIntersectingEntry])).not.toThrow();
    expect(callback([nonIntersectingEntry])).toBe(false);
  });
});
