import { renderHook, act } from '@testing-library/react';
import React, { ReactNode, createElement } from 'react';
import usePlayerNavigation, {
  PLAYER_ANIMATION_DURATION_MS,
  SLIDE_DURATION_MS,
} from '../usePlayerNavigation';
import { StoreContext, initial } from '../../services/store';
import { IMedia } from '../../types';

// ─── Fixtures ────────────────────────────────────────────────────────────────

const makeMedia = (index: number, type: IMedia['type'] = 'image'): IMedia => ({
  id: `post-${index}`,
  type,
  caption: `Caption ${index}`,
  source: 'instagram',
  post: `https://post.com/${index}`,
  image: `https://image.com/${index}.jpg`,
  thumbnail: type === 'video' ? `https://thumb.com/${index}.jpg` : undefined,
  video: type === 'video' ? `https://video.com/${index}.mp4` : undefined,
  postIndex: index,
  productsLinked: {},
});

const medias = [makeMedia(0), makeMedia(1), makeMedia(2)];

// ─── Store mock helpers ───────────────────────────────────────────────────────

const mockTriggerEvent = jest.fn();

const makeStore = (overrides?: Partial<typeof initial>) => ({
  ...initial,
  data: {
    settings: {} as any,
    content: { medias },
    id: 'widget-1',
  },
  triggerEvent: mockTriggerEvent,
  setStoreState: jest.fn(),
  ...overrides,
});

const makeWrapper = (store: ReturnType<typeof makeStore>) =>
  ({ children }: { children: ReactNode }) =>
    createElement(StoreContext.Provider, { value: store as any }, children);

// Default render — starts on the middle post so both prev and next exist
const renderNav = (
  postIndex = 1,
  {
    onClose = jest.fn(),
    onPostChange = jest.fn(),
    store = makeStore(),
  }: {
    onClose?: jest.Mock;
    onPostChange?: jest.Mock;
    store?: ReturnType<typeof makeStore>;
  } = {},
) =>
  renderHook(
    () =>
      usePlayerNavigation({
        post: medias[postIndex],
        onClose,
        onPostChange,
      }),
    { wrapper: makeWrapper(store) },
  );

// ─── Tests ────────────────────────────────────────────────────────────────────

describe('usePlayerNavigation', () => {
  beforeEach(() => {
    jest.clearAllMocks();
    jest.useFakeTimers();
  });

  afterEach(() => {
    jest.useRealTimers();
  });

  // ─── Initial state ──────────────────────────────────────────────────────────

  describe('initial state', () => {
    it('exposes the provided post as currentDisplayPost', () => {
      const { result } = renderNav(1);
      expect(result.current.currentDisplayPost).toBe(medias[1]);
    });

    it('resolves the correct prev and next neighbours', () => {
      const { result } = renderNav(1);
      expect(result.current.prevDisplayPost).toBe(medias[0]);
      expect(result.current.nextDisplayPost).toBe(medias[2]);
    });

    it('has null prevDisplayPost for the first post', () => {
      const { result } = renderNav(0);
      expect(result.current.prevDisplayPost).toBeNull();
    });

    it('has null nextDisplayPost for the last post', () => {
      const { result } = renderNav(2);
      expect(result.current.nextDisplayPost).toBeNull();
    });

    it('starts muted', () => {
      const { result } = renderNav();
      expect(result.current.isMuted).toBe(true);
    });

    it('starts playing', () => {
      const { result } = renderNav();
      expect(result.current.isPlaying).toBe(true);
    });

    it('starts not visible (for the enter animation)', () => {
      const { result } = renderNav();
      expect(result.current.isVisible).toBe(false);
    });

    it('starts not closing', () => {
      const { result } = renderNav();
      expect(result.current.isClosing).toBe(false);
    });

    it('starts with no slide in progress', () => {
      const { result } = renderNav();
      expect(result.current.isSliding).toBe(false);
      expect(result.current.slideOffset).toBe(0);
    });
  });

  // ─── Visibility animation ───────────────────────────────────────────────────

  describe('enter animation', () => {
    it('becomes visible after the 50 ms mount delay', () => {
      const { result } = renderNav();
      expect(result.current.isVisible).toBe(false);

      act(() => { jest.advanceTimersByTime(50); });

      expect(result.current.isVisible).toBe(true);
    });
  });

  // ─── triggerEvent ───────────────────────────────────────────────────────────

  describe('analytics events', () => {
    it('fires the opened event on mount', () => {
      renderNav(1);
      expect(mockTriggerEvent).toHaveBeenCalledWith(
        'mobilePlayerOpened',
        { post: medias[1] },
        'widget-1',
      );
    });

    it('fires the closed event when handleClose is called', () => {
      const { result } = renderNav(1);

      act(() => { result.current.handleClose(); });

      expect(mockTriggerEvent).toHaveBeenCalledWith(
        'mobilePlayerClosed',
        { post: medias[1] },
        'widget-1',
      );
    });

    it('fires the navigation event when moving to the next post', () => {
      const { result } = renderNav(1);

      act(() => { result.current.handleNextPost(); });

      expect(mockTriggerEvent).toHaveBeenCalledWith(
        'mobilePlayerNavigation',
        { direction: 'right', post: medias[2] },
        'widget-1',
      );
    });

    it('fires the navigation event when moving to the previous post', () => {
      const { result } = renderNav(1);

      act(() => { result.current.handlePreviousPost(); });

      expect(mockTriggerEvent).toHaveBeenCalledWith(
        'mobilePlayerNavigation',
        { direction: 'left', post: medias[0] },
        'widget-1',
      );
    });
  });

  // ─── Body overflow ─────────────────────────────────────────────────────────

  describe('body overflow lock', () => {
    it('adds a scroll-lock class to body on mount', () => {
      const classesBefore = [...document.body.classList];
      renderNav();
      // The hook adds a typestyle class to lock scroll; inline style.overflow is not set
      expect(document.body.classList.length).toBeGreaterThan(classesBefore.length);
    });

    it('removes the scroll-lock class from body on unmount', () => {
      const classesBefore = [...document.body.classList];
      const { unmount } = renderNav();

      expect(document.body.classList.length).toBeGreaterThan(classesBefore.length);

      unmount();

      expect(document.body.classList.length).toBe(classesBefore.length);
    });
  });

  // ─── toggleMute & togglePlay ────────────────────────────────────────────────

  describe('toggleMute', () => {
    it('flips isMuted to false', () => {
      const { result } = renderNav();

      act(() => { result.current.toggleMute(); });

      expect(result.current.isMuted).toBe(false);
    });

    it('flips isMuted back to true on second call', () => {
      const { result } = renderNav();

      act(() => { result.current.toggleMute(); });
      act(() => { result.current.toggleMute(); });

      expect(result.current.isMuted).toBe(true);
    });

    it('does not unmute a force-muted video post', () => {
      const forceMutedMedias = [
        makeMedia(0),
        { ...makeMedia(1, 'video'), isMuted: true },
        makeMedia(2),
      ];
      const store = makeStore({
        data: {
          settings: {} as any,
          content: { medias: forceMutedMedias },
          id: 'widget-1',
        },
      });

      const { result } = renderHook(
        () =>
          usePlayerNavigation({
            post: forceMutedMedias[1],
            onClose: jest.fn(),
            onPostChange: jest.fn(),
          }),
        { wrapper: makeWrapper(store) },
      );

      expect(result.current.isMuted).toBe(true);

      act(() => { result.current.toggleMute(); });

      expect(result.current.isMuted).toBe(true);
    });

    it('restores the previous unmuted preference after leaving a force-muted post', () => {
      const mediasWithForceMutedMiddle = [
        makeMedia(0, 'video'),
        { ...makeMedia(1, 'video'), isMuted: true },
        makeMedia(2, 'video'),
      ];
      const store = makeStore({
        data: {
          settings: {} as any,
          content: { medias: mediasWithForceMutedMiddle },
          id: 'widget-1',
        },
      });

      const { result, rerender } = renderHook(
        ({ post }: { post: IMedia }) =>
          usePlayerNavigation({ post, onClose: jest.fn(), onPostChange: jest.fn() }),
        {
          initialProps: { post: mediasWithForceMutedMiddle[0] },
          wrapper: makeWrapper(store),
        },
      );

      act(() => { result.current.toggleMute(); });
      expect(result.current.isMuted).toBe(false);

      act(() => { rerender({ post: mediasWithForceMutedMiddle[1] }); });
      expect(result.current.isMuted).toBe(true);

      act(() => { rerender({ post: mediasWithForceMutedMiddle[2] }); });
      expect(result.current.isMuted).toBe(false);
    });

    it('restores the previous unmuted preference after navigating through a force-muted post', () => {
      const mediasWithForceMutedMiddle = [
        makeMedia(0, 'video'),
        { ...makeMedia(1, 'video'), isMuted: true },
        makeMedia(2, 'video'),
      ];
      const store = makeStore({
        data: {
          settings: {} as any,
          content: { medias: mediasWithForceMutedMiddle },
          id: 'widget-1',
        },
      });

      const { result } = renderHook(
        () =>
          usePlayerNavigation({
            post: mediasWithForceMutedMiddle[0],
            onClose: jest.fn(),
            onPostChange: jest.fn(),
          }),
        { wrapper: makeWrapper(store) },
      );

      act(() => { result.current.toggleMute(); });
      expect(result.current.isMuted).toBe(false);

      act(() => { result.current.handleNextPost(); });
      act(() => { jest.advanceTimersByTime(SLIDE_DURATION_MS); });
      expect(result.current.currentDisplayPost.id).toBe(mediasWithForceMutedMiddle[1].id);
      expect(result.current.isMuted).toBe(true);

      act(() => { result.current.handlePreviousPost(); });
      act(() => { jest.advanceTimersByTime(SLIDE_DURATION_MS); });
      expect(result.current.currentDisplayPost.id).toBe(mediasWithForceMutedMiddle[0].id);
      expect(result.current.isMuted).toBe(false);
    });
  });

  describe('togglePlay', () => {
    it('flips isPlaying to false', () => {
      const { result } = renderNav();

      act(() => { result.current.togglePlay(); });

      expect(result.current.isPlaying).toBe(false);
    });

    it('flips isPlaying back to true on second call', () => {
      const { result } = renderNav();

      act(() => { result.current.togglePlay(); });
      act(() => { result.current.togglePlay(); });

      expect(result.current.isPlaying).toBe(true);
    });
  });

  // ─── handleClose ───────────────────────────────────────────────────────────

  describe('handleClose', () => {
    it('sets isClosing to true immediately', () => {
      const { result } = renderNav();

      act(() => { result.current.handleClose(); });

      expect(result.current.isClosing).toBe(true);
    });

    it('calls onClose after PLAYER_ANIMATION_DURATION_MS', () => {
      const onClose = jest.fn();
      const { result } = renderNav(1, { onClose });

      act(() => { result.current.handleClose(); });
      expect(onClose).not.toHaveBeenCalled();

      act(() => { jest.advanceTimersByTime(PLAYER_ANIMATION_DURATION_MS); });
      expect(onClose).toHaveBeenCalledTimes(1);
    });
  });

  // ─── handleNextPost ─────────────────────────────────────────────────────────

  describe('handleNextPost', () => {
    it('starts a slide-right animation immediately', () => {
      const { result } = renderNav(1);

      act(() => { result.current.handleNextPost(); });

      expect(result.current.isSliding).toBe(true);
      expect(result.current.slideOffset).toBe(-100);
    });

    it('updates currentDisplayPost to next post after SLIDE_DURATION_MS', () => {
      const onPostChange = jest.fn();
      const { result } = renderNav(1, { onPostChange });

      act(() => { result.current.handleNextPost(); });
      act(() => { jest.advanceTimersByTime(SLIDE_DURATION_MS); });

      expect(result.current.currentDisplayPost).toBe(medias[2]);
      expect(result.current.isSliding).toBe(false);
      expect(result.current.slideOffset).toBe(0);
    });

    it('calls onPostChange with the new post after the slide', () => {
      const onPostChange = jest.fn();
      const { result } = renderNav(1, { onPostChange });

      act(() => { result.current.handleNextPost(); });
      act(() => { jest.advanceTimersByTime(SLIDE_DURATION_MS); });

      expect(onPostChange).toHaveBeenCalledWith(medias[2]);
    });

    it('resumes playing after navigating', () => {
      const { result } = renderNav(1);

      act(() => { result.current.togglePlay(); }); // pause first
      act(() => { result.current.handleNextPost(); });
      act(() => { jest.advanceTimersByTime(SLIDE_DURATION_MS); });

      expect(result.current.isPlaying).toBe(true);
    });

    it('calls handleClose when there is no next post', () => {
      const onClose = jest.fn();
      const { result } = renderNav(2, { onClose }); // last post

      act(() => { result.current.handleNextPost(); });
      act(() => { jest.advanceTimersByTime(PLAYER_ANIMATION_DURATION_MS); });

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

    it('ignores a second call while a slide is in progress', () => {
      const onPostChange = jest.fn();
      const { result } = renderNav(0, { onPostChange });

      act(() => {
        result.current.handleNextPost();
        result.current.handleNextPost(); // should be ignored
      });
      act(() => { jest.advanceTimersByTime(SLIDE_DURATION_MS); });

      // onPostChange should only be called once (for medias[1], not medias[2])
      expect(onPostChange).toHaveBeenCalledTimes(1);
      expect(onPostChange).toHaveBeenCalledWith(medias[1]);
    });
  });

  // ─── handlePreviousPost ─────────────────────────────────────────────────────

  describe('handlePreviousPost', () => {
    it('starts a slide-left animation immediately', () => {
      const { result } = renderNav(1);

      act(() => { result.current.handlePreviousPost(); });

      expect(result.current.isSliding).toBe(true);
      expect(result.current.slideOffset).toBe(100);
    });

    it('updates currentDisplayPost to prev post after SLIDE_DURATION_MS', () => {
      const onPostChange = jest.fn();
      const { result } = renderNav(1, { onPostChange });

      act(() => { result.current.handlePreviousPost(); });
      act(() => { jest.advanceTimersByTime(SLIDE_DURATION_MS); });

      expect(result.current.currentDisplayPost).toBe(medias[0]);
      expect(onPostChange).toHaveBeenCalledWith(medias[0]);
    });

    it('does nothing when there is no previous post', () => {
      const onPostChange = jest.fn();
      const { result } = renderNav(0, { onPostChange }); // first post

      act(() => { result.current.handlePreviousPost(); });

      expect(result.current.isSliding).toBe(false);
      expect(onPostChange).not.toHaveBeenCalled();
    });
  });

  // ─── showNextIndicator ──────────────────────────────────────────────────────

  describe('showNextIndicator', () => {
    it('is true initially when a next post exists and user has not interacted', () => {
      const { result } = renderNav(0); // has next
      expect(result.current.showNextIndicator).toBe(true);
    });

    it('is false when there is no next post', () => {
      const { result } = renderNav(2); // last post
      expect(result.current.showNextIndicator).toBe(false);
    });

    it('becomes false after the user navigates', () => {
      const { result } = renderNav(0);

      act(() => { result.current.handleNextPost(); });
      act(() => { jest.advanceTimersByTime(SLIDE_DURATION_MS); });

      expect(result.current.showNextIndicator).toBe(false);
    });
  });

  // ─── thumbnailImage ─────────────────────────────────────────────────────────

  describe('thumbnailImage', () => {
    it('returns the thumbnail when the current post is a video with a thumbnail', () => {
      const mediaWithThumb = makeMedia(0, 'video');
      const store = {
        ...makeStore(),
        data: {
          settings: {} as any,
          content: { medias: [mediaWithThumb] },
          id: 'widget-1',
        },
      };
      const wrapper = makeWrapper(store);

      const { result } = renderHook(
        () => usePlayerNavigation({ post: mediaWithThumb, onClose: jest.fn(), onPostChange: jest.fn() }),
        { wrapper },
      );

      expect(result.current.thumbnailImage).toBe(mediaWithThumb.thumbnail);
    });

    it('returns the image for an image post', () => {
      const { result } = renderNav(0);
      expect(result.current.thumbnailImage).toBe(medias[0].image);
    });
  });

  // ─── External post prop change ──────────────────────────────────────────────

  describe('post prop sync', () => {
    it('updates nav when the post prop changes to a different post', () => {
      const { result, rerender } = renderHook(
        ({ post }: { post: IMedia }) =>
          usePlayerNavigation({ post, onClose: jest.fn(), onPostChange: jest.fn() }),
        {
          initialProps: { post: medias[0] },
          wrapper: makeWrapper(makeStore()),
        },
      );

      expect(result.current.currentDisplayPost).toBe(medias[0]);

      act(() => {
        rerender({ post: medias[2] });
      });

      expect(result.current.currentDisplayPost).toBe(medias[2]);
    });

    it('resumes playing when the post prop changes', () => {
      const { result, rerender } = renderHook(
        ({ post }: { post: IMedia }) =>
          usePlayerNavigation({ post, onClose: jest.fn(), onPostChange: jest.fn() }),
        {
          initialProps: { post: medias[0] },
          wrapper: makeWrapper(makeStore()),
        },
      );

      act(() => { result.current.togglePlay(); }); // pause
      expect(result.current.isPlaying).toBe(false);

      act(() => { rerender({ post: medias[2] }); });

      expect(result.current.isPlaying).toBe(true);
    });

    it('preserves mute preference when the post prop changes to a non-force-muted post', () => {
      const { result, rerender } = renderHook(
        ({ post }: { post: IMedia }) =>
          usePlayerNavigation({ post, onClose: jest.fn(), onPostChange: jest.fn() }),
        {
          initialProps: { post: medias[0] },
          wrapper: makeWrapper(makeStore()),
        },
      );

      act(() => { result.current.toggleMute(); });
      expect(result.current.isMuted).toBe(false);

      act(() => { rerender({ post: medias[2] }); });

      expect(result.current.isMuted).toBe(false);
    });

    it('does not update nav when rerendered with the same post', () => {
      const onPostChange = jest.fn();
      const { result, rerender } = renderHook(
        ({ post }: { post: IMedia }) =>
          usePlayerNavigation({ post, onClose: jest.fn(), onPostChange }),
        {
          initialProps: { post: medias[1] },
          wrapper: makeWrapper(makeStore()),
        },
      );

      act(() => { rerender({ post: medias[1] }); });

      // Nav stays the same, onPostChange is not called
      expect(result.current.currentDisplayPost).toBe(medias[1]);
      expect(onPostChange).not.toHaveBeenCalled();
    });
  });
});
