import { render } from '@testing-library/react';
import React from 'react';
import type { Mock } from 'vitest';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { Column } from '../List.model';
import { ListRowRenderer } from './ListRowRenderer';

interface TestData {
  id: string;
  title: string;
  desc: string;
}

const mockColumns: Column<TestData>[] = [
  {
    propertyName: 'id',
    size: '1fr',
    label: 'ID',
  },
  {
    propertyName: 'title',
    size: '2fr',
    label: 'Title',
  },
  {
    propertyName: 'desc',
    size: '1fr',
    label: 'Description',
  },
];

const mockTestData: TestData = {
  id: '1',
  title: 'Test Item',
  desc: 'Test Description',
};

const mockListRowProps = {
  columns: mockColumns,
  data: mockTestData,
  itemSelected: false,
  isTrigger: false,
  isRowDisabled: false,
  columnSizes: '1fr 2fr 1fr',
  columnGap: '5px',
  rowHeight: '44px',
  actionSize: '50px',
  horizontalTextAlign: 'left' as const,
  verticalTextAlign: 'center' as const,
  textWrap: false,
  selectionMode: undefined,
  showActionButton: false,
  showCheckMark: false,
  showItemCheckbox: false,
  onItemClicked: vi.fn(),
  onTriggered: vi.fn(),
  onItemSelected: vi.fn(),
};

describe('ListRowRenderer', () => {
  const renderRowRenderer = (overrideProps = {}) => {
    const defaultProps = {
      listRowProps: mockListRowProps,
      ...overrideProps,
    };

    return render(<ListRowRenderer {...defaultProps} />);
  };

  afterEach(() => {
    vi.clearAllMocks();
  });

  describe('default behavior', () => {
    it('should render default ListRow when no custom renderer provided', () => {
      const { container } = renderRowRenderer();

      const listRow = container.querySelector('[data-test-id="list-entry"]');
      expect(listRow).toBeInTheDocument();
    });

    it('should pass all props to default ListRow', () => {
      const { container } = renderRowRenderer();

      const listRow = container.querySelector('[data-test-id="list-entry"]');
      expect(listRow).toBeInTheDocument();
    });
  });

  describe('custom row renderer', () => {
    it('should use custom renderer when provided', () => {
      const customRenderer = vi.fn(() => (
        <div data-test-id="custom-row">Custom Content</div>
      ));

      const { container } = renderRowRenderer({
        customRowRenderer: customRenderer,
      });

      expect(customRenderer).toHaveBeenCalledTimes(1);
      expect(
        container.querySelector('[data-test-id="custom-row"]'),
      ).toBeInTheDocument();
      expect(
        container.querySelector('[data-test-id="list-entry"]'),
      ).not.toBeInTheDocument();
      expect(container.textContent).toContain('Custom Content');
    });

    it('should pass complete ListRowProps to custom renderer', () => {
      const customRenderer = vi.fn(() => (
        <div data-test-id="custom-row">Custom</div>
      ));

      const customListRowProps = {
        ...mockListRowProps,
        itemSelected: true,
        isRowDisabled: true,
      };

      renderRowRenderer({
        customRowRenderer: customRenderer,
        listRowProps: customListRowProps,
      });

      expect(customRenderer).toHaveBeenCalledWith(customListRowProps);
    });

    it('should fallback to default ListRow when custom renderer returns null', () => {
      const customRenderer = vi.fn(() => null);

      const { container } = renderRowRenderer({
        customRowRenderer: customRenderer,
      });

      expect(customRenderer).toHaveBeenCalledTimes(1);
      expect(
        container.querySelector('[data-test-id="list-entry"]'),
      ).toBeInTheDocument();
      expect(
        container.querySelector('[data-test-id="custom-row"]'),
      ).not.toBeInTheDocument();
    });

    it('should fallback to default ListRow when custom renderer returns undefined', () => {
      const customRenderer = vi.fn(() => undefined as any);

      const { container } = renderRowRenderer({
        customRowRenderer: customRenderer,
      });

      expect(customRenderer).toHaveBeenCalledTimes(1);
      expect(
        container.querySelector('[data-test-id="list-entry"]'),
      ).toBeInTheDocument();
    });
  });

  describe('pagination trigger', () => {
    const windowIntersectionObserver = window.IntersectionObserver;
    const observe = vi.fn();
    const disconnect = vi.fn();

    beforeEach(() => {
      window.IntersectionObserver = vi.fn().mockImplementation(function () {
        return { observe, disconnect };
      }) as unknown as typeof IntersectionObserver;
    });

    afterEach(() => {
      window.IntersectionObserver = windowIntersectionObserver;
      vi.clearAllMocks();
    });

    describe('default ListRow renderer', () => {
      it('creates the IntersectionObserver when isTrigger===true', () => {
        const onTriggeredSpy = vi.fn();

        const { container } = renderRowRenderer({
          listRowProps: mockListRowProps,
          isTrigger: true,
          onTriggered: onTriggeredSpy,
        });

        expect(window.IntersectionObserver).toHaveBeenCalledTimes(1);
        expect(observe).toHaveBeenCalledWith(container.firstChild);
        expect(onTriggeredSpy).not.toHaveBeenCalled();
      });

      it('does not create IntersectionObserver when isTrigger===false', () => {
        const onTriggeredSpy = vi.fn();

        renderRowRenderer({
          listRowProps: mockListRowProps,
          isTrigger: false,
          onTriggered: onTriggeredSpy,
        });

        expect(window.IntersectionObserver).not.toHaveBeenCalled();
        expect(onTriggeredSpy).not.toHaveBeenCalled();
      });

      it('triggers onTriggered when IntersectionObserver observes entry', () => {
        let intersectionFn: IntersectionObserverCallback = () => null;
        const onTriggeredSpy = vi.fn();

        (window.IntersectionObserver as Mock).mockImplementation(function (fn) {
          intersectionFn = fn;
          return { observe, disconnect };
        });

        renderRowRenderer({
          listRowProps: mockListRowProps,
          isTrigger: true,
          onTriggered: onTriggeredSpy,
        });

        // Manually trigger the intersection callback
        intersectionFn(
          [{ isIntersecting: true } as IntersectionObserverEntry],
          {} as IntersectionObserver,
        );

        expect(onTriggeredSpy).toHaveBeenCalledTimes(1);
      });

      it('does not trigger when entry is not intersecting', () => {
        let intersectionFn: IntersectionObserverCallback = () => null;
        const onTriggeredSpy = vi.fn();

        (window.IntersectionObserver as Mock).mockImplementation(function (fn) {
          intersectionFn = fn;
          return { observe, disconnect };
        });

        renderRowRenderer({
          listRowProps: mockListRowProps,
          isTrigger: true,
          onTriggered: onTriggeredSpy,
        });

        // Manually trigger the intersection callback with non-intersecting entry
        intersectionFn(
          [{ isIntersecting: false } as IntersectionObserverEntry],
          {} as IntersectionObserver,
        );

        expect(onTriggeredSpy).not.toHaveBeenCalled();
      });

      it.todo('sets ref if row is trigger'); // https://github.com/airbnb/enzyme/issues/2215
    });

    describe('custom row renderer', () => {
      it('creates the IntersectionObserver when isTrigger===true for custom renderer', () => {
        const customRenderer = vi.fn(() => (
          <div data-test-id="custom-row">Custom Content</div>
        ));
        const onTriggeredSpy = vi.fn();

        const { container } = renderRowRenderer({
          listRowProps: mockListRowProps,
          isTrigger: true,
          onTriggered: onTriggeredSpy,
          customRowRenderer: customRenderer,
        });

        expect(window.IntersectionObserver).toHaveBeenCalledTimes(1);
        expect(observe).toHaveBeenCalledWith(container.firstChild);
        expect(onTriggeredSpy).not.toHaveBeenCalled();
      });

      it('does not create IntersectionObserver when isTrigger===false for custom renderer', () => {
        const customRenderer = vi.fn(() => (
          <div data-test-id="custom-row">Custom Content</div>
        ));
        const onTriggeredSpy = vi.fn();

        renderRowRenderer({
          listRowProps: mockListRowProps,
          isTrigger: false,
          onTriggered: onTriggeredSpy,
          customRowRenderer: customRenderer,
        });

        expect(window.IntersectionObserver).not.toHaveBeenCalled();
        expect(onTriggeredSpy).not.toHaveBeenCalled();
      });

      it('triggers onTriggered when IntersectionObserver observes entry for custom renderer', () => {
        let intersectionFn: IntersectionObserverCallback = () => null;
        const customRenderer = vi.fn(() => (
          <div data-test-id="custom-row">Custom Content</div>
        ));
        const onTriggeredSpy = vi.fn();

        (window.IntersectionObserver as Mock).mockImplementation(function (fn) {
          // Remember the callback function
          intersectionFn = fn;
          // Return the mocked object
          return { observe, disconnect };
        });

        renderRowRenderer({
          listRowProps: mockListRowProps,
          isTrigger: true,
          onTriggered: onTriggeredSpy,
          customRowRenderer: customRenderer,
        });

        // Manually trigger the intersection callback
        intersectionFn(
          [{ isIntersecting: true } as IntersectionObserverEntry],
          {} as IntersectionObserver,
        );

        expect(onTriggeredSpy).toHaveBeenCalledTimes(1);
      });

      it.todo('sets ref if row is trigger'); // https://github.com/airbnb/enzyme/issues/2215
    });

    describe('observer cleanup', () => {
      it('disconnects observer when isTrigger changes from true to false', () => {
        const onTriggeredSpy = vi.fn();

        const { rerender } = renderRowRenderer({
          listRowProps: mockListRowProps,
          isTrigger: true,
          onTriggered: onTriggeredSpy,
        });

        expect(window.IntersectionObserver).toHaveBeenCalledTimes(1);

        // Clear the disconnect mock to only count calls from the rerender
        disconnect.mockClear();

        // Change isTrigger to false
        rerender(
          <ListRowRenderer
            listRowProps={mockListRowProps}
            isTrigger={false}
            onTriggered={onTriggeredSpy}
          />,
        );

        expect(disconnect).toHaveBeenCalledTimes(1);
      });

      it('disconnects observer on component unmount', () => {
        const onTriggeredSpy = vi.fn();

        const { unmount } = renderRowRenderer({
          listRowProps: mockListRowProps,
          isTrigger: true,
          onTriggered: onTriggeredSpy,
        });

        expect(window.IntersectionObserver).toHaveBeenCalledTimes(1);

        unmount();

        expect(disconnect).toHaveBeenCalledTimes(1);
      });
    });
  });
});
