import { describe, it, expect } from 'vitest';
import {
  snapDate,
  getPosition,
  getDateFromPosition,
  snapPixels,
  SNAP_MINUTES,
  DEFAULT_PIXELS_PER_HOUR
} from '../timelineUtils';

describe('timelineUtils', () => {
  describe('SNAP_MINUTES', () => {
    it('should be 15', () => {
      expect(SNAP_MINUTES).toBe(15);
    });
  });

  describe('DEFAULT_PIXELS_PER_HOUR', () => {
    it('should be 100', () => {
      expect(DEFAULT_PIXELS_PER_HOUR).toBe(100);
    });
  });

  describe('snapDate', () => {
    it('should leave exact 15-minute times unchanged', () => {
      const date1 = new Date(2025, 0, 1, 9, 0, 0); // 9:00 AM
      expect(snapDate(date1)).toEqual(date1);

      const date2 = new Date(2025, 0, 1, 9, 15, 0); // 9:15 AM
      expect(snapDate(date2)).toEqual(date2);

      const date3 = new Date(2025, 0, 1, 9, 30, 0); // 9:30 AM
      expect(snapDate(date3)).toEqual(date3);

      const date4 = new Date(2025, 0, 1, 9, 45, 0); // 9:45 AM
      expect(snapDate(date4)).toEqual(date4);
    });

    it('should round down to nearest 15 minutes when less than 7.5 minutes away', () => {
      // 9:07 should round down to 9:00
      const date1 = new Date(2025, 0, 1, 9, 7, 0);
      expect(snapDate(date1)).toEqual(new Date(2025, 0, 1, 9, 0, 0));

      // 9:22 should round down to 9:15
      const date2 = new Date(2025, 0, 1, 9, 22, 0);
      expect(snapDate(date2)).toEqual(new Date(2025, 0, 1, 9, 15, 0));

      // 9:37 should round down to 9:30
      const date3 = new Date(2025, 0, 1, 9, 37, 0);
      expect(snapDate(date3)).toEqual(new Date(2025, 0, 1, 9, 30, 0));
    });

    it('should round up to nearest 15 minutes when 7.5 or more minutes away', () => {
      // 9:08 should round up to 9:15
      const date1 = new Date(2025, 0, 1, 9, 8, 0);
      expect(snapDate(date1)).toEqual(new Date(2025, 0, 1, 9, 15, 0));

      // 9:23 should round up to 9:30
      const date2 = new Date(2025, 0, 1, 9, 23, 0);
      expect(snapDate(date2)).toEqual(new Date(2025, 0, 1, 9, 30, 0));

      // 9:38 should round up to 9:45
      const date3 = new Date(2025, 0, 1, 9, 38, 0);
      expect(snapDate(date3)).toEqual(new Date(2025, 0, 1, 9, 45, 0));

      // 9:53 should round up to 10:00
      const date4 = new Date(2025, 0, 1, 9, 53, 0);
      expect(snapDate(date4)).toEqual(new Date(2025, 0, 1, 10, 0, 0));
    });

    it('should handle edge case at exactly 7.5 minutes (midpoint)', () => {
      // Date-fns roundToNearestMinutes rounds midpoint up
      // 9:07:30 should round up to 9:15
      const date = new Date(2025, 0, 1, 9, 7, 30);
      expect(snapDate(date)).toEqual(new Date(2025, 0, 1, 9, 15, 0));
    });

    it('should handle seconds and milliseconds', () => {
      // 9:07:45.500 is 7 minutes 45.5 seconds, which rounds up to 9:15
      const date = new Date(2025, 0, 1, 9, 7, 45, 500);
      expect(snapDate(date)).toEqual(new Date(2025, 0, 1, 9, 15, 0));
    });
  });

  describe('getPosition', () => {
    const startTime = new Date(2025, 0, 1, 9, 0, 0); // 9:00 AM

    it('should return 0 for startTime', () => {
      const position = getPosition(startTime, startTime, DEFAULT_PIXELS_PER_HOUR);
      expect(position).toBe(0);
    });

    it('should calculate position at 30 minutes with default pixels per hour', () => {
      const time = new Date(2025, 0, 1, 9, 30, 0); // 9:30 AM
      const position = getPosition(time, startTime, DEFAULT_PIXELS_PER_HOUR);
      // 30 minutes = 0.5 hours = 50 pixels at 100 pixels/hour
      expect(position).toBe(50);
    });

    it('should calculate position at 1 hour with default pixels per hour', () => {
      const time = new Date(2025, 0, 1, 10, 0, 0); // 10:00 AM
      const position = getPosition(time, startTime, DEFAULT_PIXELS_PER_HOUR);
      expect(position).toBe(100);
    });

    it('should calculate position at 2 hours with default pixels per hour', () => {
      const time = new Date(2025, 0, 1, 11, 0, 0); // 11:00 AM
      const position = getPosition(time, startTime, DEFAULT_PIXELS_PER_HOUR);
      expect(position).toBe(200);
    });

    it('should calculate position at 15 minutes with default pixels per hour', () => {
      const time = new Date(2025, 0, 1, 9, 15, 0); // 9:15 AM
      const position = getPosition(time, startTime, DEFAULT_PIXELS_PER_HOUR);
      // 15 minutes = 0.25 hours = 25 pixels at 100 pixels/hour
      expect(position).toBe(25);
    });

    it('should handle different pixelsPerHour values', () => {
      const time = new Date(2025, 0, 1, 10, 0, 0); // 10:00 AM (1 hour after start)

      // 1 hour at 50 pixels/hour = 50 pixels
      expect(getPosition(time, startTime, 50)).toBe(50);

      // 1 hour at 200 pixels/hour = 200 pixels
      expect(getPosition(time, startTime, 200)).toBe(200);

      // 1 hour at 150 pixels/hour = 150 pixels
      expect(getPosition(time, startTime, 150)).toBe(150);
    });

    it('should handle 30 minutes with different pixelsPerHour values', () => {
      const time = new Date(2025, 0, 1, 9, 30, 0); // 9:30 AM (0.5 hours)

      // 0.5 hours at 50 pixels/hour = 25 pixels
      expect(getPosition(time, startTime, 50)).toBe(25);

      // 0.5 hours at 200 pixels/hour = 100 pixels
      expect(getPosition(time, startTime, 200)).toBe(100);
    });

    it('should return negative position for times before startTime', () => {
      const time = new Date(2025, 0, 1, 8, 30, 0); // 8:30 AM (30 minutes before start)
      const position = getPosition(time, startTime, DEFAULT_PIXELS_PER_HOUR);
      expect(position).toBe(-50);
    });

    it('should handle fractional minutes correctly', () => {
      const time = new Date(2025, 0, 1, 9, 7, 0); // 9:07:00 (7 minutes)
      const position = getPosition(time, startTime, DEFAULT_PIXELS_PER_HOUR);
      // 7 minutes = 7/60 hours = 11.666... pixels at 100 pixels/hour
      expect(position).toBeCloseTo(11.67, 1);
    });
  });

  describe('getDateFromPosition', () => {
    const startTime = new Date(2025, 0, 1, 9, 0, 0); // 9:00 AM

    it('should return startTime for position 0', () => {
      const date = getDateFromPosition(0, startTime, DEFAULT_PIXELS_PER_HOUR);
      expect(date).toEqual(startTime);
    });

    it('should calculate date from position at 50 pixels (30 minutes)', () => {
      const date = getDateFromPosition(50, startTime, DEFAULT_PIXELS_PER_HOUR);
      expect(date).toEqual(new Date(2025, 0, 1, 9, 30, 0));
    });

    it('should calculate date from position at 100 pixels (1 hour)', () => {
      const date = getDateFromPosition(100, startTime, DEFAULT_PIXELS_PER_HOUR);
      expect(date).toEqual(new Date(2025, 0, 1, 10, 0, 0));
    });

    it('should calculate date from position at 200 pixels (2 hours)', () => {
      const date = getDateFromPosition(200, startTime, DEFAULT_PIXELS_PER_HOUR);
      expect(date).toEqual(new Date(2025, 0, 1, 11, 0, 0));
    });

    it('should calculate date from position at 25 pixels (15 minutes)', () => {
      const date = getDateFromPosition(25, startTime, DEFAULT_PIXELS_PER_HOUR);
      expect(date).toEqual(new Date(2025, 0, 1, 9, 15, 0));
    });

    it('should handle different pixelsPerHour values', () => {
      // 50 pixels at 50 pixels/hour = 1 hour
      const date1 = getDateFromPosition(50, startTime, 50);
      expect(date1).toEqual(new Date(2025, 0, 1, 10, 0, 0));

      // 50 pixels at 200 pixels/hour = 0.25 hours = 15 minutes
      const date2 = getDateFromPosition(50, startTime, 200);
      expect(date2).toEqual(new Date(2025, 0, 1, 9, 15, 0));
    });

    it('should handle negative positions (times before startTime)', () => {
      const date = getDateFromPosition(-50, startTime, DEFAULT_PIXELS_PER_HOUR);
      expect(date).toEqual(new Date(2025, 0, 1, 8, 30, 0));
    });

    it('should be inverse of getPosition', () => {
      const originalDate = new Date(2025, 0, 1, 14, 37, 0); // 2:37 PM
      const pixels = getPosition(originalDate, startTime, DEFAULT_PIXELS_PER_HOUR);
      const recoveredDate = getDateFromPosition(pixels, startTime, DEFAULT_PIXELS_PER_HOUR);
      expect(recoveredDate).toEqual(originalDate);
    });

    it('should be inverse of getPosition with different pixelsPerHour', () => {
      const pixelsPerHour = 150;
      const originalDate = new Date(2025, 0, 1, 11, 22, 0); // No seconds - date-fns works with minutes
      const pixels = getPosition(originalDate, startTime, pixelsPerHour);
      const recoveredDate = getDateFromPosition(pixels, startTime, pixelsPerHour);
      expect(recoveredDate).toEqual(originalDate);
    });
  });

  describe('snapPixels', () => {
    it('should leave exact grid line positions unchanged with default pixels per hour', () => {
      // At 100 pixels/hour, 15 minutes = 25 pixels
      expect(snapPixels(0, DEFAULT_PIXELS_PER_HOUR)).toBe(0);
      expect(snapPixels(25, DEFAULT_PIXELS_PER_HOUR)).toBe(25); // 15 min
      expect(snapPixels(50, DEFAULT_PIXELS_PER_HOUR)).toBe(50); // 30 min
      expect(snapPixels(75, DEFAULT_PIXELS_PER_HOUR)).toBe(75); // 45 min
      expect(snapPixels(100, DEFAULT_PIXELS_PER_HOUR)).toBe(100); // 1 hour
    });

    it('should round to nearest grid line when less than halfway', () => {
      // At 100 pixels/hour, snap interval = 25 pixels
      // 10 pixels is less than 12.5, should round down to 0
      expect(snapPixels(10, DEFAULT_PIXELS_PER_HOUR)).toBe(0);

      // 35 pixels is less than 37.5, should round down to 25
      expect(snapPixels(35, DEFAULT_PIXELS_PER_HOUR)).toBe(25);

      // 60 pixels is less than 62.5, should round down to 50
      expect(snapPixels(60, DEFAULT_PIXELS_PER_HOUR)).toBe(50);
    });

    it('should round to nearest grid line when at or past halfway', () => {
      // At 100 pixels/hour, snap interval = 25 pixels
      // 13 pixels is >= 12.5, should round up to 25
      expect(snapPixels(13, DEFAULT_PIXELS_PER_HOUR)).toBe(25);

      // 38 pixels is >= 37.5, should round up to 50
      expect(snapPixels(38, DEFAULT_PIXELS_PER_HOUR)).toBe(50);

      // 63 pixels is >= 62.5, should round up to 75
      expect(snapPixels(63, DEFAULT_PIXELS_PER_HOUR)).toBe(75);
    });

    it('should handle different pixelsPerHour values', () => {
      // At 50 pixels/hour, 15 minutes = 12.5 pixels
      expect(snapPixels(0, 50)).toBe(0);
      expect(snapPixels(12.5, 50)).toBe(12.5);
      expect(snapPixels(25, 50)).toBe(25);
      expect(snapPixels(10, 50)).toBe(12.5); // Rounds up
      expect(snapPixels(15, 50)).toBe(12.5); // Rounds down

      // At 200 pixels/hour, 15 minutes = 50 pixels
      expect(snapPixels(0, 200)).toBe(0);
      expect(snapPixels(50, 200)).toBe(50);
      expect(snapPixels(100, 200)).toBe(100);
      expect(snapPixels(75, 200)).toBe(100); // Rounds up
      expect(snapPixels(20, 200)).toBe(0); // Rounds down
    });

    it('should handle negative pixel values', () => {
      // -10 pixels should round to 0 (Math.round(-0.4) = -0, which equals 0)
      const result = snapPixels(-10, DEFAULT_PIXELS_PER_HOUR);
      expect(Math.abs(result)).toBe(0); // Handle -0 vs 0

      // -15 pixels should round to -25
      expect(snapPixels(-15, DEFAULT_PIXELS_PER_HOUR)).toBe(-25);

      // -35 pixels should round to -25
      expect(snapPixels(-35, DEFAULT_PIXELS_PER_HOUR)).toBe(-25);

      // -40 pixels should round to -50
      expect(snapPixels(-40, DEFAULT_PIXELS_PER_HOUR)).toBe(-50);
    });

    it('should handle fractional pixel values', () => {
      // 12.7 pixels should round to 25
      expect(snapPixels(12.7, DEFAULT_PIXELS_PER_HOUR)).toBe(25);

      // 12.3 pixels should round to 0
      expect(snapPixels(12.3, DEFAULT_PIXELS_PER_HOUR)).toBe(0);

      // 37.8 pixels should round to 50
      expect(snapPixels(37.8, DEFAULT_PIXELS_PER_HOUR)).toBe(50);
    });

    it('should snap correctly at exact midpoint', () => {
      // At 100 pixels/hour, snap interval = 25 pixels
      // Midpoint at 12.5 should round to nearest even (JavaScript rounding behavior)
      const snapped = snapPixels(12.5, DEFAULT_PIXELS_PER_HOUR);
      // Math.round(0.5) = 1, so 12.5 should snap to 25
      expect(snapped).toBe(25);

      const snapped2 = snapPixels(37.5, DEFAULT_PIXELS_PER_HOUR);
      // Math.round(1.5) = 2, so 37.5 should snap to 50
      expect(snapped2).toBe(50);
    });

    it('should work in conjunction with getPosition and snapDate', () => {
      const startTime = new Date(2025, 0, 1, 9, 0, 0);
      const unsnapppedDate = new Date(2025, 0, 1, 9, 7, 0); // 9:07 AM

      // Get position of unsnapped date
      const unsnappedPixels = getPosition(unsnapppedDate, startTime, DEFAULT_PIXELS_PER_HOUR);

      // Snap the pixels
      const snappedPixels = snapPixels(unsnappedPixels, DEFAULT_PIXELS_PER_HOUR);

      // Get date from snapped pixels
      const snappedDate = getDateFromPosition(snappedPixels, startTime, DEFAULT_PIXELS_PER_HOUR);

      // Should match what snapDate would give us
      expect(snappedDate).toEqual(snapDate(unsnapppedDate));
    });
  });

  describe('integration tests', () => {
    it('should consistently round trip dates through position and back', () => {
      const startTime = new Date(2025, 0, 1, 8, 0, 0);
      const testDates = [
        new Date(2025, 0, 1, 8, 0, 0),
        new Date(2025, 0, 1, 8, 15, 0),
        new Date(2025, 0, 1, 9, 30, 0),
        new Date(2025, 0, 1, 12, 45, 0),
        new Date(2025, 0, 1, 17, 0, 0),
      ];

      testDates.forEach(date => {
        const pixels = getPosition(date, startTime, DEFAULT_PIXELS_PER_HOUR);
        const recovered = getDateFromPosition(pixels, startTime, DEFAULT_PIXELS_PER_HOUR);
        expect(recovered).toEqual(date);
      });
    });

    it('should consistently snap pixels and dates to same grid', () => {
      const startTime = new Date(2025, 0, 1, 9, 0, 0);
      const unsnappedDate = new Date(2025, 0, 1, 9, 22, 0); // 9:22 AM

      // Method 1: Snap date first, then get position
      const snappedDate = snapDate(unsnappedDate);
      const pixelsFromSnappedDate = getPosition(snappedDate, startTime, DEFAULT_PIXELS_PER_HOUR);

      // Method 2: Get position first, then snap pixels
      const unsnappedPixels = getPosition(unsnappedDate, startTime, DEFAULT_PIXELS_PER_HOUR);
      const snappedPixels = snapPixels(unsnappedPixels, DEFAULT_PIXELS_PER_HOUR);

      // Both methods should give same result
      expect(snappedPixels).toBe(pixelsFromSnappedDate);
    });

    it('should handle full day timeline correctly', () => {
      const dayStart = new Date(2025, 0, 1, 0, 0, 0);
      const dayEnd = new Date(2025, 0, 1, 23, 59, 0); // 23:59:00 (no seconds)

      // 23:59 = 1439 minutes = 2398.33... pixels at 100 pixels/hour
      const endPixels = getPosition(dayEnd, dayStart, DEFAULT_PIXELS_PER_HOUR);
      expect(endPixels).toBeCloseTo(2398.33, 1);

      // Verify we can get back close to end time
      const recovered = getDateFromPosition(endPixels, dayStart, DEFAULT_PIXELS_PER_HOUR);
      expect(recovered.getHours()).toBe(23);
      expect(recovered.getMinutes()).toBe(59);
    });
  });
});
