// @vitest-environment jsdom
import { render, act } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import GradientBlinds from './GradientBlinds';

// Mock ogl
vi.mock('ogl', () => {
  return {
    Renderer: class {
      gl = {
        // Use a real canvas element so appendChild works
        canvas: document.createElement('canvas'),
        drawingBufferWidth: 100,
        drawingBufferHeight: 100,
      };
      setSize() { }
      render() { }
    },
    Program: class {
      remove() { }
    },
    Mesh: class {
      remove() { }
    },
    Geometry: class {
      remove() { }
    },
    Triangle: class {
      remove() { }
    },

  };
});

// Mock ResizeObserver
globalThis.ResizeObserver = class ResizeObserver {
  observe() { }
  unobserve() { }
  disconnect() { }
};

describe('GradientBlinds', () => {
  let rafSpy: any;
  let cancelRafSpy: any;
  let addEventListenerSpy: any;
  let removeEventListenerSpy: any;
  let ioCallback: (entries: IntersectionObserverEntry[]) => void;
  let observeSpy: any;
  let disconnectSpy: any;

  beforeEach(() => {
    rafSpy = vi.spyOn(window, 'requestAnimationFrame').mockImplementation((_cb: any) => {
      return 123;
    });
    cancelRafSpy = vi.spyOn(window, 'cancelAnimationFrame').mockImplementation((_id: number) => {
    });
    addEventListenerSpy = vi.spyOn(window, 'addEventListener');
    removeEventListenerSpy = vi.spyOn(window, 'removeEventListener');

    // Mock IntersectionObserver
    observeSpy = vi.fn();
    disconnectSpy = vi.fn();
    (globalThis as any).IntersectionObserver = class IntersectionObserver {
      constructor(cb: any) {
        ioCallback = cb;
      }
      observe = observeSpy;
      unobserve = vi.fn();
      disconnect = disconnectSpy;
    };
  });

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

  it('does not start animation loop on mount until visible', () => {
    const { unmount } = render(<GradientBlinds />);
    expect(rafSpy).not.toHaveBeenCalled();

    // Simulate on-screen
    act(() => {
      if (ioCallback) {
        ioCallback([{ isIntersecting: true } as IntersectionObserverEntry]);
      }
    });

    expect(rafSpy).toHaveBeenCalled();
    unmount();
    expect(cancelRafSpy).toHaveBeenCalled();
  });

  it('pauses animation loop when off-screen and resumes when on-screen', () => {
    const { unmount } = render(<GradientBlinds />);

    // Should not start initially
    expect(rafSpy).not.toHaveBeenCalled();

    // Simulate on-screen (start)
    act(() => {
      if (ioCallback) {
        ioCallback([{ isIntersecting: true } as IntersectionObserverEntry]);
      }
    });
    expect(rafSpy).toHaveBeenCalledTimes(1);

    // Reset spies to check for subsequent calls
    rafSpy.mockClear();
    cancelRafSpy.mockClear();

    // Simulate off-screen
    act(() => {
      if (ioCallback) {
        ioCallback([{ isIntersecting: false } as IntersectionObserverEntry]);
      }
    });

    // Should cancel animation frame
    expect(cancelRafSpy).toHaveBeenCalled();

    // Should NOT request new frame (loop stopped)
    expect(rafSpy).not.toHaveBeenCalled();

    // Reset spies
    cancelRafSpy.mockClear();

    // Simulate on-screen
    act(() => {
      if (ioCallback) {
        ioCallback([{ isIntersecting: true } as IntersectionObserverEntry]);
      }
    });

    // Should restart animation loop
    expect(rafSpy).toHaveBeenCalled();

    unmount();
    expect(disconnectSpy).toHaveBeenCalled();
  });

  it('toggles pointer event listener based on visibility', () => {
    const { unmount } = render(<GradientBlinds />);

    // Should not listen initially (not visible)
    // Initially not called
    expect(addEventListenerSpy).not.toHaveBeenCalledWith('pointermove', expect.any(Function));

    // Simulate on-screen
    act(() => {
      if (ioCallback) {
        ioCallback([{ isIntersecting: true } as IntersectionObserverEntry]);
      }
    });
    expect(addEventListenerSpy).toHaveBeenCalledWith('pointermove', expect.any(Function));

    // Reset
    addEventListenerSpy.mockClear();
    removeEventListenerSpy.mockClear();

    // Simulate off-screen
    act(() => {
      if (ioCallback) {
        ioCallback([{ isIntersecting: false } as IntersectionObserverEntry]);
      }
    });
    expect(removeEventListenerSpy).toHaveBeenCalledWith('pointermove', expect.any(Function));

    // Simulate on-screen again
    act(() => {
      if (ioCallback) {
        ioCallback([{ isIntersecting: true } as IntersectionObserverEntry]);
      }
    });
    expect(addEventListenerSpy).toHaveBeenCalledWith('pointermove', expect.any(Function));

    unmount();
    expect(removeEventListenerSpy).toHaveBeenCalledWith('pointermove', expect.any(Function));
  });

  it('minimizes getBoundingClientRect calls during pointer move', () => {
    const { unmount } = render(<GradientBlinds />);

    const spy = vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect');
    spy.mockClear();

    act(() => {
      const event = new PointerEvent('pointermove', {
        clientX: 200,
        clientY: 200,
        bubbles: true,
      });
      window.dispatchEvent(event);
    });

    expect(spy).not.toHaveBeenCalled();

    unmount();
  });

  it('avoids expensive DOM reads (scrollX/Y) in pointermove handler when visible', () => {
    const { unmount } = render(<GradientBlinds />);

    // Spy on scrollX/scrollY getters
    // Note: In jsdom, these are properties on window.
    const scrollSpy = vi.spyOn(window, 'scrollX', 'get');

    // Make visible to attach listener
    act(() => {
      if (ioCallback) {
        ioCallback([{ isIntersecting: true } as IntersectionObserverEntry]);
      }
    });

    scrollSpy.mockClear();

    // Trigger pointer move
    act(() => {
       window.dispatchEvent(new PointerEvent('pointermove', { clientX: 100, clientY: 100 }));
    });

    // With the optimization (moving to RAF loop), this should be 0.
    // Without optimization, this will be > 0.
    // Since we are mocking RAF and not running the loop, if it's in the loop, it won't be called.
    expect(scrollSpy).not.toHaveBeenCalled();

    unmount();
  });
});
