// tests/flyTo.test.ts
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import flyTo from '../src/flyTo';

describe('flyTo', () => {
  let flyingEl: HTMLElement;
  let targetEl: HTMLElement;
  let cloneEl: HTMLElement;
  let mockGetElementById: any;
  let mockAppendChild: any;
  let mockRemove: any;
  let mockAddEventListener: any;
  let mockGetBoundingClientRect: any;
  
  beforeEach(() => {
    // Clean up the document
    document.body.innerHTML = '';
    
    // Create actual DOM elements with happy-dom
    flyingEl = document.createElement('div');
    flyingEl.id = 'flying-element';
    document.body.appendChild(flyingEl);
    
    targetEl = document.createElement('div');
    targetEl.id = 'target-element';
    document.body.appendChild(targetEl);
    
    // Create clone element
    cloneEl = document.createElement('div');
    
    // Mock getBoundingClientRect for all elements
    mockGetBoundingClientRect = vi.fn();
    flyingEl.getBoundingClientRect = vi.fn().mockReturnValue({
      top: 100,
      left: 100,
      right: 200,
      bottom: 200,
      width: 100,
      height: 100
    });
    
    targetEl.getBoundingClientRect = vi.fn().mockReturnValue({
      top: 300,
      left: 300,
      right: 400,
      bottom: 400,
      width: 100,
      height: 100
    });
    
    cloneEl.getBoundingClientRect = vi.fn();
    
    // Mock other methods
    mockAddEventListener = vi.fn();
    cloneEl.addEventListener = mockAddEventListener;
    
    mockRemove = vi.fn();
    cloneEl.remove = mockRemove;
    
    // Mock cloneNode to return our controlled clone
    flyingEl.cloneNode = vi.fn().mockReturnValue(cloneEl);
    
    // Mock document methods
    mockGetElementById = vi.spyOn(document, 'getElementById');
    mockGetElementById.mockImplementation((id: string) => {
      if (id === 'flying-element') return flyingEl;
      if (id === 'target-element') return targetEl;
      return null;
    });
    
    mockAppendChild = vi.spyOn(document.body, 'appendChild');
    mockAppendChild.mockImplementation(() => cloneEl);
  });
  
  afterEach(() => {
    vi.restoreAllMocks();
  });
  
  it('should do nothing if flying element is not found', () => {
    mockGetElementById.mockImplementation((id: string) => {
      if (id === 'flying-element') return null;
      if (id === 'target-element') return targetEl;
      return null;
    });
    
    flyTo('flying-element', 'target-element');
    
    // Should not be called when flying element is not found
    expect(mockAppendChild).not.toHaveBeenCalled();
  });
  
  it('should do nothing if target element is not found', () => {
    mockGetElementById.mockImplementation((id: string) => {
      if (id === 'flying-element') return flyingEl;
      if (id === 'target-element') return null;
      return null;
    });
    
    flyTo('flying-element', 'target-element');
    
    // Should not be called when target element is not found
    expect(mockAppendChild).not.toHaveBeenCalled();
  });
  
  it('should clone and append flying element to document body', () => {
    flyTo('flying-element', 'target-element');
    
    expect(flyingEl.cloneNode).toHaveBeenCalledWith(true);
    expect(mockAppendChild).toHaveBeenCalled();
  });
  
  it('should set correct styles on cloned element', () => {
    flyTo('flying-element', 'target-element');
    
    expect(cloneEl.style.position).toBe('absolute');
    expect(cloneEl.style.top).toBe('100px');
    expect(cloneEl.style.left).toBe('100px');
    expect(cloneEl.style.width).toBe('100px');
    expect(cloneEl.style.height).toBe('100px');
    expect(cloneEl.style.zIndex).toBe('99');
    expect(cloneEl.style.opacity).toBe('1');
    expect(cloneEl.style.animation).toBe('none');
  });
  
  it('should set transform and transition properties with default durations', () => {
    flyTo('flying-element', 'target-element');
    
    // The deltaX should be (350 - 150) = 200, deltaY should be (350 - 150) = 200
    expect(cloneEl.style.transform).toBe('translate(200px, 200px) scale(0.1)');
    
    // Use a regular expression to match the transition value, avoiding floating-point precision issues
    expect(cloneEl.style.transition).toMatch(/transform 0\.1\d+s ease, opacity 0\.4s ease/);
  });
  
  it('should set custom durations when provided in options', () => {
    flyTo('flying-element', 'target-element', { duration: 1.0 });
    
    // Use a regular expression to match the transition value, avoiding floating-point precision issues
    expect(cloneEl.style.transition).toMatch(/transform 0\.6\d*s ease, opacity 1s ease/);
  });
  
  it('should add transitionend event listener', () => {
    flyTo('flying-element', 'target-element');
    
    expect(mockAddEventListener).toHaveBeenCalledWith('transitionend', expect.any(Function));
  });
  
  it('should use custom scale value when provided', () => {
    flyTo('flying-element', 'target-element', { scale: 0.5 });
    
    expect(cloneEl.style.transform).toBe('translate(200px, 200px) scale(0.5)');
  });
  
  it('should not move along X axis when moveX is false', () => {
    flyTo('flying-element', 'target-element', { moveX: false });
    
    expect(cloneEl.style.transform).toBe('translate(0px, 200px) scale(0.1)');
  });
  
  it('should not move along Y axis when moveY is false', () => {
    flyTo('flying-element', 'target-element', { moveY: false });
    
    expect(cloneEl.style.transform).toBe('translate(200px, 0px) scale(0.1)');
  });
  
  it('should not move along any axis when both moveX and moveY are false', () => {
    flyTo('flying-element', 'target-element', { moveX: false, moveY: false });
    
    expect(cloneEl.style.transform).toBe('translate(0px, 0px) scale(0.1)');
  });
  
  it('should remove original element by default', () => {
    // Mock the remove method
    const originalRemove = vi.fn();
    flyingEl.remove = originalRemove;
    
    flyTo('flying-element', 'target-element');
    
    // Verify the original element was removed
    expect(originalRemove).toHaveBeenCalled();
  });
  
  it('should not remove original element when removeOriginal is false', () => {
    // Mock the remove method
    const originalRemove = vi.fn();
    flyingEl.remove = originalRemove;
    
    flyTo('flying-element', 'target-element', { removeOriginal: false });
    
    // Verify the original element was not removed
    expect(originalRemove).not.toHaveBeenCalled();
  });
  
  it('should set transformOrigin when resetTransformation is true', () => {
    // Set an initial transform on the cloned element
    cloneEl.style.transform = 'rotate(45deg) scale(0.8)';
    cloneEl.style.transformOrigin = 'top left';
    
    flyTo('flying-element', 'target-element', { resetTransformation: true });
    
    // Verify the transformOrigin was reset
    // Note: We can't verify transform is 'none' because it's immediately overwritten
    // with the animation transform
    expect(cloneEl.style.transformOrigin).toBe('center center');
  });
  
  it('should not set transformOrigin when resetTransformation is false', () => {
    // Set an initial transform on the cloned element
    cloneEl.style.transform = 'rotate(45deg) scale(0.8)';
    cloneEl.style.transformOrigin = 'top left';
    
    flyTo('flying-element', 'target-element', { resetTransformation: false });
    
    // Verify the transformOrigin was not changed
    expect(cloneEl.style.transformOrigin).not.toBe('center center');
  });
  
  it('should add html-trajectory-cloned class to cloned element', () => {
    flyTo('flying-element', 'target-element');
    
    // Verify the html-trajectory-cloned class was added to the clone
    expect(cloneEl.classList.contains('html-trajectory-cloned')).toBe(true);
    
    // Verify the transformation styles are still applied
    expect(cloneEl.style.transform).toBe('translate(200px, 200px) scale(0.1)');
  });
  
  it('should call onTransitionEnd callback and remove clone when transition ends', () => {
    const onTransitionEnd = vi.fn();
    flyTo('flying-element', 'target-element', { onTransitionEnd });
    
    // Get the callback function passed to addEventListener
    const transitionEndCallback = mockAddEventListener.mock.calls[0][1];
    
    // Simulate transition end
    transitionEndCallback();
    
    expect(onTransitionEnd).toHaveBeenCalled();
    expect(mockRemove).toHaveBeenCalled();
  });
  
  it('should work with default onTransitionEnd callback and remove clone when transition ends', () => {
    // Call flyTo without providing the onTransitionEnd callback
    flyTo('flying-element', 'target-element');
    
    // Get the callback function passed to addEventListener
    const transitionEndCallback = mockAddEventListener.mock.calls[0][1];
    
    // Verify that mockAddEventListener was called
    expect(mockAddEventListener).toHaveBeenCalledWith('transitionend', expect.any(Function));
    
    // Simulate transition end
    transitionEndCallback();
    
    // The element should still be removed even with the default callback
    expect(mockRemove).toHaveBeenCalled();
  });
  
  it('should only call onTransitionEnd once even if multiple transition events fire', () => {
    const onTransitionEnd = vi.fn();
    flyTo('flying-element', 'target-element', { onTransitionEnd });
    
    // Get the callback function passed to addEventListener
    const transitionEndCallback = mockAddEventListener.mock.calls[0][1];
    
    // Mock the remove method to track calls and also clear the element reference
    // This helps prevent multiple calls to onTransitionEnd
    let removed = false;
    mockRemove.mockImplementation(() => {
      if (!removed) {
        removed = true;
      }
    });

    // First call - should execute onTransitionEnd and remove
    transitionEndCallback();
    
    // Reset the mock to verify the second call doesn't trigger it again
    onTransitionEnd.mockClear();
    
    // Second call - should not execute onTransitionEnd again
    transitionEndCallback();
    
    // Verify onTransitionEnd was only called once in total
    expect(onTransitionEnd).not.toHaveBeenCalled();
  });
  
  it('should remove clone even when onTransitionEnd throws an error', () => {
    // Create a callback that throws an error
    const onTransitionEnd = vi.fn().mockImplementation(() => {
      throw new Error('Intentional test error');
    });
    
    // Spy on console.error to prevent actual error output during tests
    const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
    
    flyTo('flying-element', 'target-element', { onTransitionEnd });
    
    // Get the callback function passed to addEventListener
    const transitionEndCallback = mockAddEventListener.mock.calls[0][1];
    
    // Simulate transition end
    transitionEndCallback();
    
    // Verify that onTransitionEnd was called and threw an error
    expect(onTransitionEnd).toHaveBeenCalled();
    
    // Verify the clone was still removed despite the error
    expect(mockRemove).toHaveBeenCalled();
    
    // Clean up the console.error spy
    consoleErrorSpy.mockRestore();
  });
});
