import { renderHook } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import { PAGE_ENGAGEMENT } from '@/lib/event-catalog';

// ── Mock analytics so we can spy on trackTypedEvent ─────────────────────────
const { mockTrackTypedEvent, mockHasAnalyticsConsent } = vi.hoisted(() => ({
  mockTrackTypedEvent: vi.fn(),
  mockHasAnalyticsConsent: vi.fn().mockReturnValue(true),
}));
vi.mock('@/lib/analytics', () => ({
  trackTypedEvent: (...args: unknown[]) => mockTrackTypedEvent(...args),
  hasAnalyticsConsent: () => mockHasAnalyticsConsent(),
}));

// ── Mock cookieConsent (required by analytics module) ────────────────────────
vi.mock('@/lib/cookieConsent', () => ({
  getCategoryConsent: vi.fn().mockReturnValue(true),
}));

import { usePageEngagement } from './usePageEngagement';

// ── Helpers ──────────────────────────────────────────────────────────────────

/** Simulate scrolling to a given percentage of the page. */
function simulateScrollTo(pct: number) {
  // document.documentElement.scrollHeight = 2000, window.innerHeight = 500 in jsdom default
  // scrollable = 2000 - 500 = 1500; scrollY = pct * 1500 / 100
  const scrollable = document.documentElement.scrollHeight - window.innerHeight;
  Object.defineProperty(window, 'scrollY', {
    value: (pct / 100) * Math.max(scrollable, 1),
    configurable: true,
  });
  window.dispatchEvent(new Event('scroll'));
}

function dispatchVisibilityChange(hidden: boolean) {
  Object.defineProperty(document, 'hidden', { value: hidden, configurable: true });
  document.dispatchEvent(new Event('visibilitychange'));
}

function dispatchPageHide() {
  window.dispatchEvent(new Event('pagehide'));
}

// ── Setup ────────────────────────────────────────────────────────────────────

beforeEach(() => {
  vi.clearAllMocks();
  vi.useFakeTimers();

  // Set a scrollable page: scrollHeight=2000, innerHeight=500 → scrollable=1500
  Object.defineProperty(document.documentElement, 'scrollHeight', {
    value: 2000,
    configurable: true,
  });
  Object.defineProperty(window, 'innerHeight', { value: 500, configurable: true });
  Object.defineProperty(window, 'scrollY', { value: 0, configurable: true });

  // Ensure tab starts as visible
  Object.defineProperty(document, 'hidden', { value: false, configurable: true });
});

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

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

describe('usePageEngagement', () => {
  describe('fires PAGE_ENGAGEMENT on unmount', () => {
    it('fires with zero scroll depth when user never scrolled', () => {
      const { unmount } = renderHook(() => usePageEngagement());
      vi.advanceTimersByTime(2000);
      unmount();

      expect(mockTrackTypedEvent).toHaveBeenCalledTimes(1);
      expect(mockTrackTypedEvent).toHaveBeenCalledWith({
        event: PAGE_ENGAGEMENT,
        properties: {
          scroll_depth_pct: 0,
          active_time_seconds: 2,
        },
      });
    });

    it('fires with the highest scroll milestone reached', () => {
      const { unmount } = renderHook(() => usePageEngagement());

      simulateScrollTo(30); // crosses 25%
      simulateScrollTo(60); // crosses 50%

      unmount();

      expect(mockTrackTypedEvent).toHaveBeenCalledWith({
        event: PAGE_ENGAGEMENT,
        properties: {
          scroll_depth_pct: 50,
          active_time_seconds: 0,
        },
      });
    });

    it('records 100% scroll depth when user scrolls to the very bottom', () => {
      const { unmount } = renderHook(() => usePageEngagement());

      simulateScrollTo(100);

      unmount();

      expect(mockTrackTypedEvent).toHaveBeenCalledWith(
        expect.objectContaining({
          event: PAGE_ENGAGEMENT,
          properties: expect.objectContaining({ scroll_depth_pct: 100 }),
        }),
      );
    });
  });

  describe('scroll milestone tracking', () => {
    it('only advances to each milestone once (monotonic)', () => {
      const { unmount } = renderHook(() => usePageEngagement());

      simulateScrollTo(80); // crosses 25, 50, 75
      simulateScrollTo(10); // scroll back up — depth should not decrease

      unmount();

      expect(mockTrackTypedEvent).toHaveBeenCalledWith(
        expect.objectContaining({
          properties: expect.objectContaining({ scroll_depth_pct: 75 }),
        }),
      );
    });

    it('treats short pages (no scrollable area) as fully scrolled', () => {
      // Make page non-scrollable: scrollHeight <= innerHeight
      Object.defineProperty(document.documentElement, 'scrollHeight', {
        value: 400,
        configurable: true,
      });
      // innerHeight is already 500 from beforeEach
      // Trigger scroll event to re-evaluate
      window.dispatchEvent(new Event('scroll'));

      const { unmount } = renderHook(() => usePageEngagement());
      unmount();

      expect(mockTrackTypedEvent).toHaveBeenCalledWith(
        expect.objectContaining({
          properties: expect.objectContaining({ scroll_depth_pct: 100 }),
        }),
      );
    });

    it('records correct milestone at exactly 25% boundary', () => {
      const { unmount } = renderHook(() => usePageEngagement());

      // scrollable = 1500; 25% = 375px
      simulateScrollTo(25);

      unmount();

      expect(mockTrackTypedEvent).toHaveBeenCalledWith(
        expect.objectContaining({
          properties: expect.objectContaining({ scroll_depth_pct: 25 }),
        }),
      );
    });
  });

  describe('active time tracking', () => {
    it('measures active time while tab is visible', () => {
      const { unmount } = renderHook(() => usePageEngagement());
      vi.advanceTimersByTime(5000);
      unmount();

      expect(mockTrackTypedEvent).toHaveBeenCalledWith(
        expect.objectContaining({
          properties: expect.objectContaining({ active_time_seconds: 5 }),
        }),
      );
    });

    it('pauses active time while tab is hidden', () => {
      const { unmount } = renderHook(() => usePageEngagement());

      vi.advanceTimersByTime(3000); // 3s visible
      dispatchVisibilityChange(true); // tab hidden
      vi.advanceTimersByTime(10000); // 10s hidden — should NOT count
      dispatchVisibilityChange(false); // tab visible again
      vi.advanceTimersByTime(2000); // 2s visible
      unmount();

      // Independently computed: 3s + 2s = 5s active
      expect(mockTrackTypedEvent).toHaveBeenCalledWith(
        expect.objectContaining({
          properties: expect.objectContaining({ active_time_seconds: 5 }),
        }),
      );
    });

    it('rounds active time to nearest second', () => {
      const { unmount } = renderHook(() => usePageEngagement());
      vi.advanceTimersByTime(1700); // 1.7s → rounds to 2s
      unmount();

      expect(mockTrackTypedEvent).toHaveBeenCalledWith(
        expect.objectContaining({
          properties: expect.objectContaining({ active_time_seconds: 2 }),
        }),
      );
    });

    it('starts with zero active time when tab starts hidden', () => {
      // Simulate tab starting hidden
      Object.defineProperty(document, 'hidden', { value: true, configurable: true });

      const { unmount } = renderHook(() => usePageEngagement());
      vi.advanceTimersByTime(5000); // all time hidden
      unmount();

      expect(mockTrackTypedEvent).toHaveBeenCalledWith(
        expect.objectContaining({
          properties: expect.objectContaining({ active_time_seconds: 0 }),
        }),
      );
    });
  });

  describe('double-fire prevention', () => {
    it('does not fire twice on pagehide followed by unmount', () => {
      const { unmount } = renderHook(() => usePageEngagement());

      dispatchPageHide();
      unmount();

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

    it('fires on pagehide even before unmount', () => {
      renderHook(() => usePageEngagement());

      vi.advanceTimersByTime(3000);
      dispatchPageHide();

      expect(mockTrackTypedEvent).toHaveBeenCalledWith({
        event: PAGE_ENGAGEMENT,
        properties: {
          scroll_depth_pct: 0,
          active_time_seconds: 3,
        },
      });
    });
  });

  describe('event listener cleanup', () => {
    it('removes scroll, visibilitychange, and pagehide listeners on unmount', () => {
      const removeEventListenerSpy = vi.spyOn(window, 'removeEventListener');
      const docRemoveListenerSpy = vi.spyOn(document, 'removeEventListener');

      const { unmount } = renderHook(() => usePageEngagement());
      unmount();

      expect(removeEventListenerSpy).toHaveBeenCalledWith(
        'scroll',
        expect.any(Function),
      );
      expect(removeEventListenerSpy).toHaveBeenCalledWith(
        'pagehide',
        expect.any(Function),
      );
      expect(docRemoveListenerSpy).toHaveBeenCalledWith(
        'visibilitychange',
        expect.any(Function),
      );
    });

    it('does not fire again when scroll event occurs after unmount', () => {
      const { unmount } = renderHook(() => usePageEngagement());
      unmount();

      const callCountAfterUnmount = mockTrackTypedEvent.mock.calls.length;
      simulateScrollTo(80);
      window.dispatchEvent(new Event('scroll'));

      expect(mockTrackTypedEvent).toHaveBeenCalledTimes(callCountAfterUnmount);
    });
  });
});
