import { act, render, screen, waitFor } from '@testing-library/react';
import React from 'react';
import type { Mock, MockInstance } from 'vitest';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import * as idleCallbackHelpers from '../../../../../helpers/idleCallbackHelpers';
import { noop } from '../../../../../helpers/utils';
import { TagsRenderer } from './TagsRenderer';

// Wrapper component to properly render TagsRenderer
interface TagsRendererWrapperProps {
  value: unknown;
}

const TagsRendererWrapper: React.FC<TagsRendererWrapperProps> = ({ value }) => {
  return <>{TagsRenderer(value)}</>;
};

// Helper to trigger ResizeObserver callback
const triggerResizeObserver = (): void => {
  const resizeObserverCallback = (global.ResizeObserver as Mock).mock
    .calls[0]?.[0];
  if (resizeObserverCallback) {
    act(() => {
      resizeObserverCallback([]);
    });
  }
};

describe('TagsRenderer', () => {
  let mockResizeObserver: {
    observe: Mock;
    unobserve: Mock;
    disconnect: Mock;
  };
  let mockScheduleIdleCallback: MockInstance;
  let mockCancelScheduledCallback: MockInstance;

  beforeEach(() => {
    // Setup ResizeObserver mock
    mockResizeObserver = {
      observe: vi.fn(),
      unobserve: vi.fn(),
      disconnect: vi.fn(),
    };

    global.ResizeObserver = vi.fn().mockImplementation(function () {
      return mockResizeObserver;
    }) as unknown as typeof ResizeObserver;

    // Setup scheduleIdleCallback mock
    mockScheduleIdleCallback = vi
      .spyOn(idleCallbackHelpers, 'scheduleIdleCallback')
      .mockImplementation((callback) => {
        callback({
          didTimeout: false,
          timeRemaining: () => 50,
        } as IdleDeadline);
        return 1;
      });

    // Setup cancelScheduledCallback mock
    mockCancelScheduledCallback = vi
      .spyOn(idleCallbackHelpers, 'cancelScheduledCallback')
      .mockImplementation(noop);

    // Mock window.getComputedStyle for font size calculations
    window.getComputedStyle = vi.fn().mockReturnValue({
      fontSize: '14px',
    });

    // Mock offsetWidth for container
    Object.defineProperty(HTMLElement.prototype, 'offsetWidth', {
      configurable: true,
      writable: true,
      value: 300,
    });
  });

  afterEach(() => {
    // Don't use vi.clearAllMocks() as it removes mock implementations
    // Instead, manually clear just the mock call history
    mockResizeObserver.observe.mockClear();
    mockResizeObserver.unobserve.mockClear();
    mockResizeObserver.disconnect.mockClear();
    mockScheduleIdleCallback.mockClear();
    mockCancelScheduledCallback.mockClear();
  });

  describe('rendering', () => {
    it('should render empty container when value is empty array', () => {
      render(<TagsRendererWrapper value={[]} />);

      const container = screen.getByTestId('tags-container');
      expect(container).toBeInTheDocument();
      expect(container).toBeEmptyDOMElement();
    });

    it('should render container with title attribute containing all tags', () => {
      const tags = ['tag1', 'tag2', 'tag3'];

      render(<TagsRendererWrapper value={tags} />);

      const container = screen.getByTestId('tags-container');
      expect(container).toHaveAttribute('title', 'tag1, tag2, tag3');
    });

    it('should render tags when array is provided', () => {
      const tags = ['tag1', 'tag2', 'tag3'];

      render(<TagsRendererWrapper value={tags} />);

      const container = screen.getByTestId('tags-container');
      expect(container).toBeInTheDocument();

      triggerResizeObserver();

      expect(mockScheduleIdleCallback).toHaveBeenCalled();
    });

    it('should render tags with proper text content', async () => {
      const tags = ['JavaScript', 'React', 'Testing'];
      Object.defineProperty(HTMLElement.prototype, 'offsetWidth', {
        configurable: true,
        writable: true,
        value: 500,
      });

      render(<TagsRendererWrapper value={tags} />);

      triggerResizeObserver();

      await waitFor(() => {
        expect(screen.getByText('JavaScript')).toBeInTheDocument();
        expect(screen.getByText('React')).toBeInTheDocument();
        expect(screen.getByText('Testing')).toBeInTheDocument();
      });
    });
  });

  describe('overflow behavior', () => {
    it('should handle overflow with small container width', () => {
      const tags = ['tag1', 'tag2', 'tag3', 'tag4', 'tag5'];

      Object.defineProperty(HTMLElement.prototype, 'offsetWidth', {
        configurable: true,
        writable: true,
        value: 100,
      });

      render(<TagsRendererWrapper value={tags} />);

      const container = screen.getByTestId('tags-container');
      expect(container).toBeInTheDocument();

      triggerResizeObserver();

      expect(mockScheduleIdleCallback).toHaveBeenCalled();
    });

    it('should display overflow indicator when not all tags fit', async () => {
      const tags = ['tag1', 'tag2', 'tag3', 'tag4', 'tag5'];

      Object.defineProperty(HTMLElement.prototype, 'offsetWidth', {
        configurable: true,
        writable: true,
        value: 150,
      });

      render(<TagsRendererWrapper value={tags} />);

      triggerResizeObserver();

      // The overflow indicator may or may not appear depending on calculations
      // This test verifies the component handles the scenario without errors
      const container = screen.getByTestId('tags-container');
      expect(container).toBeInTheDocument();
    });

    it('should handle empty tag list gracefully', () => {
      render(<TagsRendererWrapper value={[]} />);

      const container = screen.getByTestId('tags-container');
      expect(container).toBeInTheDocument();
    });

    it('should handle non-array values gracefully', () => {
      render(<TagsRendererWrapper value={null} />);

      const container = screen.getByTestId('tags-container');
      expect(container).toBeInTheDocument();
    });
  });

  describe('ResizeObserver integration', () => {
    it('should observe container on mount', () => {
      render(<TagsRendererWrapper value={['tag1', 'tag2']} />);

      expect(mockResizeObserver.observe).toHaveBeenCalledTimes(1);
    });

    it('should trigger calculations when ResizeObserver fires', () => {
      render(<TagsRendererWrapper value={['tag1', 'tag2', 'tag3']} />);

      expect(mockScheduleIdleCallback).not.toHaveBeenCalled();

      triggerResizeObserver();

      expect(mockScheduleIdleCallback).toHaveBeenCalled();
    });
  });

  describe('cleanup', () => {
    it('should disconnect ResizeObserver on unmount', () => {
      const { unmount } = render(
        <TagsRendererWrapper value={['tag1', 'tag2']} />,
      );

      expect(mockResizeObserver.disconnect).not.toHaveBeenCalled();

      unmount();

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

    it('should cancel idle callback on unmount if pending', () => {
      const { unmount } = render(
        <TagsRendererWrapper value={['tag1', 'tag2']} />,
      );

      triggerResizeObserver();

      unmount();

      expect(mockCancelScheduledCallback).toHaveBeenCalled();
    });
  });
});
