import React from 'react';
import { StyleSheet, Text } from 'react-native';

import Animated from 'react-native-reanimated';

import * as SafeAreaContext from 'react-native-safe-area-context';

import { act, cleanup, fireEvent, render, screen } from '@testing-library/react-native';

import {
  finalizeCloseOverlay,
  openOverlay,
  overlayStore,
  setOverlayBottomH,
  setOverlayMessageH,
  setOverlayTopH,
} from '../../../state-store';
import { WithComponents } from '../../componentsContext/ComponentsContext';
import { MessageActionsProps, MessageOverlayHostLayer } from '../MessageOverlayHostLayer';

jest.mock('react-native', () => {
  const actual = jest.requireActual('react-native');

  return new Proxy(actual, {
    get(target, prop, receiver) {
      if (prop === 'useWindowDimensions') {
        return () => ({ fontScale: 1, height: 200, scale: 1, width: 320 });
      }

      return Reflect.get(target, prop, receiver);
    },
  });
});

jest.mock('react-native-reanimated', () => {
  const React = require('react');
  const actual = jest.requireActual('react-native-reanimated/mock');
  const { View } = require('react-native');

  const useStableSharedValue = (init: unknown) => {
    const ref = React.useRef(null) as React.MutableRefObject<{
      value: unknown;
    } | null>;

    if (!ref.current) {
      const value = { value: init };
      ref.current = new Proxy(value, {
        get(target, prop) {
          if (prop === 'value') {
            return target.value;
          }

          return undefined;
        },
        set(target, prop, nextValue) {
          if (prop === 'value') {
            target.value = nextValue;
            return true;
          }

          return false;
        },
      });
    }

    return ref.current;
  };

  return {
    ...actual,
    Animated: {
      ...actual.default,
      View,
    },
    default: {
      ...actual.default,
      View,
    },
    clamp: (value: number, min: number, max: number) => Math.min(Math.max(value, min), max),
    runOnJS: (fn: (...args: unknown[]) => unknown) => fn,
    useAnimatedReaction: (
      prepare: () => unknown,
      react: (current: unknown, previous: unknown) => void,
    ) => {
      react(prepare(), undefined);
    },
    useAnimatedStyle: (updater: () => unknown) => updater(),
    useDerivedValue: (updater: () => unknown) => ({ value: updater() }),
    useSharedValue: useStableSharedValue,
    withSpring: (value: unknown) => value,
  };
});

const TOP_RECT = { h: 20, w: 90, x: 5, y: 0 };
const MESSAGE_RECT = { h: 50, w: 180, x: 10, y: 0 };
const BOTTOM_RECT = { h: 30, w: 140, x: 20, y: 100 };
const NoopBackground = () => null;
const CustomMessageActions = ({
  bottomItemStyle,
  hostStyle,
  topItemStyle,
}: MessageActionsProps) => (
  <>
    <Text testID='custom-message-actions'>Custom</Text>
    <Animated.View style={topItemStyle} testID='custom-message-actions-top' />
    <Animated.View style={hostStyle} testID='custom-message-actions-message' />
    <Animated.View style={bottomItemStyle} testID='custom-message-actions-bottom' />
  </>
);

const flushAnimationFrameQueue = () => {
  act(() => {
    jest.runAllTimers();
  });
};

describe('MessageOverlayHostLayer', () => {
  let useSafeAreaInsetsSpy: jest.SpyInstance;

  beforeEach(() => {
    jest.useFakeTimers();
    useSafeAreaInsetsSpy = jest.spyOn(SafeAreaContext, 'useSafeAreaInsets').mockReturnValue({
      bottom: 15,
      left: 0,
      right: 0,
      top: 10,
    });

    act(() => {
      finalizeCloseOverlay();
      overlayStore.next({
        closing: false,
        closingPortalHostBlacklist: [],
        id: undefined,
        messageId: undefined,
      });
    });
  });

  afterEach(() => {
    cleanup();

    act(() => {
      finalizeCloseOverlay();
      overlayStore.next({
        closing: false,
        closingPortalHostBlacklist: [],
        id: undefined,
        messageId: undefined,
      });
    });

    useSafeAreaInsetsSpy.mockRestore();
    jest.clearAllTimers();
    jest.useRealTimers();
  });

  it('renders the custom background only while active and pressing the backdrop starts closing', () => {
    const CustomBackground = () => <Text testID='custom-background'>Background</Text>;
    render(
      <WithComponents overrides={{ MessageOverlayBackground: CustomBackground }}>
        <MessageOverlayHostLayer />
      </WithComponents>,
    );

    expect(screen.queryByTestId('custom-background')).toBeNull();
    expect(screen.queryByTestId('message-overlay-backdrop')).toBeNull();

    act(() => {
      openOverlay('message-1');
    });

    expect(screen.getByTestId('custom-background')).toBeTruthy();
    expect(screen.getByTestId('message-overlay-backdrop')).toBeTruthy();

    fireEvent.press(screen.getByTestId('message-overlay-backdrop'));
    flushAnimationFrameQueue();

    expect(overlayStore.getLatestValue().closing).toBe(true);
  });

  it('positions and translates the top, message, and bottom hosts using the registered rects', () => {
    const renderTree = () => (
      <WithComponents overrides={{ MessageOverlayBackground: NoopBackground }}>
        <MessageOverlayHostLayer />
      </WithComponents>
    );
    const { rerender } = render(renderTree());

    act(() => {});

    act(() => {
      setOverlayTopH(TOP_RECT);
      setOverlayMessageH(MESSAGE_RECT);
      setOverlayBottomH(BOTTOM_RECT);
      openOverlay('message-1');
    });

    rerender(renderTree());

    const topSlot = screen.getByTestId('message-overlay-top');
    const messageSlot = screen.getByTestId('message-overlay-message');
    const bottomSlot = screen.getByTestId('message-overlay-bottom');

    expect(StyleSheet.flatten(topSlot.props.style)).toMatchObject({
      height: TOP_RECT.h,
      left: TOP_RECT.x,
      position: 'absolute',
      top: TOP_RECT.y,
      transform: [{ scale: 1 }, { translateY: 38 }],
      width: TOP_RECT.w,
    });
    expect(StyleSheet.flatten(messageSlot.props.style)).toMatchObject({
      height: MESSAGE_RECT.h,
      left: MESSAGE_RECT.x,
      position: 'absolute',
      top: MESSAGE_RECT.y,
      // First transform = springed open/close position; second = scroll offset (0 here).
      transform: [{ translateY: 38 }, { translateY: 0 }],
      width: MESSAGE_RECT.w,
    });
    expect(StyleSheet.flatten(bottomSlot.props.style)).toMatchObject({
      height: BOTTOM_RECT.h,
      left: BOTTOM_RECT.x,
      position: 'absolute',
      top: BOTTOM_RECT.y,
      transform: [{ scale: 1 }, { translateY: -12 }],
      width: BOTTOM_RECT.w,
    });
  });

  it('keeps the hosts mounted but resets their geometry after finalizeCloseOverlay', () => {
    const renderTree = () => (
      <WithComponents overrides={{ MessageOverlayBackground: NoopBackground }}>
        <MessageOverlayHostLayer />
      </WithComponents>
    );
    const { rerender } = render(renderTree());

    act(() => {});

    act(() => {
      setOverlayTopH(TOP_RECT);
      setOverlayMessageH(MESSAGE_RECT);
      setOverlayBottomH(BOTTOM_RECT);
      openOverlay('message-1');
    });

    rerender(renderTree());

    expect(
      StyleSheet.flatten(screen.getByTestId('message-overlay-message').props.style),
    ).toMatchObject({
      height: MESSAGE_RECT.h,
      width: MESSAGE_RECT.w,
    });

    act(() => {
      finalizeCloseOverlay();
    });

    rerender(renderTree());

    // The hosts stay mounted (not gated on isActive) so the native teleport target is stable
    // across opens; only their geometry resets to 0. The backdrop press target is gated.
    expect(StyleSheet.flatten(screen.getByTestId('message-overlay-top').props.style)).toMatchObject(
      { height: 0 },
    );
    expect(
      StyleSheet.flatten(screen.getByTestId('message-overlay-message').props.style),
    ).toMatchObject({ height: 0 });
    expect(
      StyleSheet.flatten(screen.getByTestId('message-overlay-bottom').props.style),
    ).toMatchObject({ height: 0 });
    expect(screen.queryByTestId('message-overlay-backdrop')).toBeNull();
  });

  it('pins the top item, leaves the message unclipped (behind the items), and sticks the bottom item when the content overflows', () => {
    // window height 200, insets top 10 / bottom 15, padding 8 => minY 18, maxY 177.
    // Available height is 159; the rects below sum to 190 so scrolling kicks in.
    const SCROLL_TOP_RECT = { h: 20, w: 90, x: 5, y: 20 };
    const SCROLL_MESSAGE_RECT = { h: 140, w: 180, x: 10, y: 40 };
    const SCROLL_BOTTOM_RECT = { h: 30, w: 140, x: 20, y: 180 };

    const renderTree = () => (
      <WithComponents overrides={{ MessageOverlayBackground: NoopBackground }}>
        <MessageOverlayHostLayer />
      </WithComponents>
    );
    const { rerender } = render(renderTree());

    act(() => {});

    act(() => {
      setOverlayTopH(SCROLL_TOP_RECT);
      setOverlayMessageH(SCROLL_MESSAGE_RECT);
      setOverlayBottomH(SCROLL_BOTTOM_RECT);
      openOverlay('message-1');
    });

    rerender(renderTree());

    // Top item pinned to minY (18): 20 + (-2) = 18.
    expect(StyleSheet.flatten(screen.getByTestId('message-overlay-top').props.style)).toMatchObject(
      {
        top: SCROLL_TOP_RECT.y,
        transform: [{ scale: 1 }, { translateY: -2 }],
      },
    );

    // Message host keeps its natural height and is NOT clipped (no overflow:hidden). It is
    // pinned under the top item (top 40 + first translateY -2 = contentTop 38); the second
    // translateY is the scroll offset (0 at rest). It renders behind the opaque items.
    const messageStyle = StyleSheet.flatten(
      screen.getByTestId('message-overlay-message').props.style,
    );
    expect(messageStyle).toMatchObject({
      height: SCROLL_MESSAGE_RECT.h,
      left: SCROLL_MESSAGE_RECT.x,
      top: SCROLL_MESSAGE_RECT.y,
      transform: [{ translateY: -2 }, { translateY: 0 }],
    });
    expect(messageStyle.overflow).toBeUndefined();

    // Bottom item sticks to the bottom of the usable area (maxY - bottomH) = 147: 180 + (-33).
    expect(
      StyleSheet.flatten(screen.getByTestId('message-overlay-bottom').props.style),
    ).toMatchObject({
      top: SCROLL_BOTTOM_RECT.y,
      transform: [{ scale: 1 }, { translateY: -33 }],
    });
  });

  it('renders MessageActions override instead of the default host wrappers when provided', () => {
    const renderTree = () => (
      <WithComponents
        overrides={{
          MessageActions: CustomMessageActions,
          MessageOverlayBackground: NoopBackground,
        }}
      >
        <MessageOverlayHostLayer />
      </WithComponents>
    );
    const { rerender } = render(renderTree());

    act(() => {
      setOverlayTopH(TOP_RECT);
      setOverlayMessageH(MESSAGE_RECT);
      setOverlayBottomH(BOTTOM_RECT);
      openOverlay('message-1');
    });

    rerender(renderTree());

    expect(screen.getByTestId('custom-message-actions')).toBeTruthy();
    expect(screen.queryByTestId('message-overlay-top')).toBeNull();
    expect(screen.queryByTestId('message-overlay-message')).toBeNull();
    expect(screen.queryByTestId('message-overlay-bottom')).toBeNull();
    expect(
      StyleSheet.flatten(screen.getByTestId('custom-message-actions-top').props.style),
    ).toMatchObject({
      height: TOP_RECT.h,
      width: TOP_RECT.w,
    });
    expect(
      StyleSheet.flatten(screen.getByTestId('custom-message-actions-message').props.style),
    ).toMatchObject({
      height: MESSAGE_RECT.h,
      width: MESSAGE_RECT.w,
    });
    expect(
      StyleSheet.flatten(screen.getByTestId('custom-message-actions-bottom').props.style),
    ).toMatchObject({
      height: BOTTOM_RECT.h,
      width: BOTTOM_RECT.w,
    });
  });
});
