import React, { ReactElement } from 'react';
import { render, RenderOptions } from '@testing-library/react-native';
import { DragStateProvider } from '../../contexts/DragStateContext';
import { ListAnimationProvider } from '../../contexts/ListAnimationContext';
import type { DragConfig } from '../../types';

/**
 * Props for the AllProviders wrapper
 */
interface AllProvidersProps {
  children: React.ReactNode;
  dragConfig?: Partial<DragConfig>;
  entryAnimationTimeout?: number;
  layoutAnimationDuration?: number;
}

/**
 * Wrapper component that provides all required contexts for testing
 */
const AllProviders: React.FC<AllProvidersProps> = ({
  children,
  dragConfig,
  entryAnimationTimeout,
  layoutAnimationDuration,
}) => {
  return (
    <ListAnimationProvider
      entryAnimationTimeout={entryAnimationTimeout}
      layoutAnimationDuration={layoutAnimationDuration}
    >
      <DragStateProvider config={dragConfig}>{children}</DragStateProvider>
    </ListAnimationProvider>
  );
};

/**
 * Custom render options with context configuration
 */
interface CustomRenderOptions extends Omit<RenderOptions, 'wrapper'> {
  dragConfig?: Partial<DragConfig>;
  entryAnimationTimeout?: number;
  layoutAnimationDuration?: number;
}

/**
 * Custom render function that wraps components with all necessary providers
 */
const customRender = async (
  ui: ReactElement,
  options?: CustomRenderOptions,
): Promise<ReturnType<typeof render>> => {
  const {
    dragConfig,
    entryAnimationTimeout,
    layoutAnimationDuration,
    ...renderOptions
  } = options ?? {};

  const Wrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
    <AllProviders
      dragConfig={dragConfig}
      entryAnimationTimeout={entryAnimationTimeout}
      layoutAnimationDuration={layoutAnimationDuration}
    >
      {children}
    </AllProviders>
  );

  return render(ui, { wrapper: Wrapper, ...renderOptions });
};

// Re-export everything
export * from '@testing-library/react-native';

// Override render with our custom version
export { customRender as render };

/**
 * Create a mock list item with required id field
 */
export interface MockListItem {
  id: string;
  title?: string;
  [key: string]: unknown;
}

/**
 * Factory function to create a single mock item
 */
export const createMockItem = (
  id: string,
  overrides: Partial<MockListItem> = {},
): MockListItem => ({
  id,
  title: `Item ${id}`,
  ...overrides,
});

/**
 * Factory function to create multiple mock items
 */
export const createMockItems = (count: number): MockListItem[] =>
  Array.from({ length: count }, (_, i) => createMockItem(`item-${i + 1}`));

/**
 * Create a mock SharedValue-like object for testing
 */
export const createMockSharedValue = <T,>(initialValue: T) => ({
  value: initialValue,
  addListener: jest.fn(),
  removeListener: jest.fn(),
  modify: jest.fn((modifier: (val: T) => T) => modifier(initialValue)),
});

/**
 * Create a mock animated ref for testing
 */
export const createMockAnimatedRef = <T,>(current: T | null = null) => ({
  current,
});

/**
 * Wait for animations to complete (mock version)
 */
export const waitForAnimations = async (ms = 300): Promise<void> => {
  await new Promise((resolve) => setTimeout(resolve, ms));
};

/**
 * Mock gesture event data for testing drag operations
 */
export const createMockGestureEvent = (overrides: Record<string, unknown> = {}) => ({
  translationX: 0,
  translationY: 0,
  velocityX: 0,
  velocityY: 0,
  absoluteX: 0,
  absoluteY: 0,
  x: 0,
  y: 0,
  numberOfPointers: 1,
  state: 4, // ACTIVE
  ...overrides,
});

/**
 * Mock FlashList ref for testing
 */
export const createMockFlashListRef = () => ({
  scrollToOffset: jest.fn(),
  scrollToIndex: jest.fn(),
  scrollToItem: jest.fn(),
  scrollToEnd: jest.fn(),
  getScrollOffset: jest.fn(() => 0),
  getScrollableNode: jest.fn(() => null),
  recordInteraction: jest.fn(),
  flashScrollIndicators: jest.fn(),
  prepareForLayoutAnimationRender: jest.fn(),
});

/**
 * Drag simulation parameters
 */
interface DragSimulationParams {
  from: { x: number; y: number };
  to: { x: number; y: number };
  duration?: number;
  steps?: number;
}

/**
 * Create a sequence of gesture events to simulate a drag operation.
 * Returns an array of gesture event objects that can be used to test drag behavior.
 *
 * @param params - Drag simulation parameters
 * @returns Array of gesture events representing the drag sequence
 */
export const simulateDrag = (params: DragSimulationParams) => {
  const { from, to, duration = 300, steps = 10 } = params;

  const events = [];

  // Calculate deltas per step
  const deltaX = (to.x - from.x) / steps;
  const deltaY = (to.y - from.y) / steps;
  const timeStep = duration / steps;

  // Start event
  events.push(
    createMockGestureEvent({
      translationX: 0,
      translationY: 0,
      absoluteX: from.x,
      absoluteY: from.y,
      x: from.x,
      y: from.y,
      state: 2, // BEGAN
    }),
  );

  // Update events
  for (let i = 1; i <= steps; i++) {
    const progress = i / steps;
    const currentX = from.x + deltaX * i;
    const currentY = from.y + deltaY * i;

    events.push(
      createMockGestureEvent({
        translationX: deltaX * i,
        translationY: deltaY * i,
        absoluteX: currentX,
        absoluteY: currentY,
        x: currentX,
        y: currentY,
        state: 4, // ACTIVE
        time: timeStep * i,
      }),
    );
  }

  // End event
  events.push(
    createMockGestureEvent({
      translationX: to.x - from.x,
      translationY: to.y - from.y,
      absoluteX: to.x,
      absoluteY: to.y,
      x: to.x,
      y: to.y,
      velocityX: (to.x - from.x) / (duration / 1000),
      velocityY: (to.y - from.y) / (duration / 1000),
      state: 5, // END
    }),
  );

  return events;
};

/**
 * Generate a large list of mock items for performance testing.
 *
 * @param count - Number of items to generate
 * @returns Array of mock list items
 */
export const generateLargeList = (count: number): MockListItem[] =>
  Array.from({ length: count }, (_, i) => ({
    id: `item-${i}`,
    title: `Item ${i}`,
    index: i,
  }));

/**
 * Animation timing helper - waits for a specific SharedValue to reach target.
 * Useful for testing animation completion.
 *
 * @param sharedValue - The SharedValue to observe
 * @param targetValue - The target value to wait for
 * @param timeout - Maximum wait time in ms (default: 1000)
 * @returns Promise that resolves when target is reached or rejects on timeout
 */
export const waitForSharedValue = async <T,>(
  sharedValue: { value: T },
  targetValue: T,
  timeout = 1000,
): Promise<T> => {
  const startTime = Date.now();

  return new Promise((resolve, reject) => {
    const checkValue = () => {
      if (sharedValue.value === targetValue) {
        resolve(sharedValue.value);
        return;
      }

      if (Date.now() - startTime > timeout) {
        reject(
          new Error(
            `Timeout waiting for SharedValue to reach ${targetValue}. Current value: ${sharedValue.value}`,
          ),
        );
        return;
      }

      setTimeout(checkValue, 16); // Check every frame (~60fps)
    };

    checkValue();
  });
};

/**
 * Performance measurement helper for tests.
 * Wraps an async function and returns execution time.
 *
 * @param fn - Async function to measure
 * @returns Tuple of [result, executionTimeMs]
 */
export const measurePerformance = async <T,>(
  fn: () => Promise<T>,
): Promise<[T, number]> => {
  const startTime = performance.now();
  const result = await fn();
  const endTime = performance.now();
  return [result, endTime - startTime];
};
