import { describe, expect, test, beforeAll, afterAll, spyOn } from 'bun:test';
import {
  leftpad,
  rightpad,
  formatPercent,
  formatTime,
  formatDate,
  formatDateTime,
  formatCurrency,
  formatNumber,
  formatPercentage,
  formatDiff,
  daysAgo,
  formatByThousands,
} from './formatters';

// Mock browser globals
const originalNavigator = globalThis.navigator;

beforeAll(() => {
  // @ts-expect-error - mocking navigator for tests
  globalThis.navigator = {
    language: 'en-US',
    languages: ['en-US'],
  };
});

afterAll(() => {
  globalThis.navigator = originalNavigator;
});

describe('leftpad', () => {
  test('pads number with leading zeros', () => {
    expect(leftpad(5, 3)).toBe('005');
    expect(leftpad(42, 4)).toBe('0042');
    expect(leftpad(123, 3)).toBe('123');
  });

  test('pads string with leading zeros', () => {
    expect(leftpad('5', 3)).toBe('005');
    expect(leftpad('ab', 4)).toBe('00ab');
  });

  test('handles value already at or exceeding size', () => {
    expect(leftpad(12345, 3)).toBe('12345');
    expect(leftpad('abc', 3)).toBe('abc');
  });

  test('handles default value of 0', () => {
    expect(leftpad(undefined, 3)).toBe('000');
  });
});

describe('rightpad', () => {
  test('pads number with trailing zeros', () => {
    expect(rightpad(5, 3)).toBe('500');
    expect(rightpad(42, 4)).toBe('4200');
  });

  test('pads string with trailing zeros', () => {
    expect(rightpad('5', 3)).toBe('500');
    expect(rightpad('ab', 4)).toBe('ab00');
  });

  test('handles value already at or exceeding size', () => {
    expect(rightpad(12345, 3)).toBe('12345');
  });

  test('handles default value of 0', () => {
    expect(rightpad(undefined, 3)).toBe('000');
  });
});

describe('formatPercent', () => {
  test('formats number with 2 decimal places', () => {
    expect(formatPercent(5.5)).toBe('05.50');
    expect(formatPercent(12.34)).toBe('12.34');
    expect(formatPercent(99.9)).toBe('99.90');
  });

  test('formats string numbers', () => {
    expect(formatPercent('5.5')).toBe('05.50');
    expect(formatPercent('12.34')).toBe('12.34');
  });

  test('handles whole numbers', () => {
    expect(formatPercent(5)).toBe('05.00');
    expect(formatPercent('12')).toBe('12.00');
  });

  test('handles zero', () => {
    expect(formatPercent(0)).toBe('00.00');
    expect(formatPercent('0')).toBe('00.00');
  });

  test('returns empty string for null/undefined', () => {
    expect(formatPercent(null as unknown as number)).toBe('');
    expect(formatPercent(undefined as unknown as number)).toBe('');
  });
});

describe('formatPercentage', () => {
  test('formats number as percentage string', () => {
    expect(formatPercentage(5.5)).toBe('5.50%');
    expect(formatPercentage(12.345)).toBe('12.35%');
    expect(formatPercentage(0.1)).toBe('0.10%');
  });

  test('formats string numbers', () => {
    expect(formatPercentage('5.5')).toBe('5.50%');
    expect(formatPercentage('99')).toBe('99.00%');
  });

  test('returns default for falsy values', () => {
    expect(formatPercentage(0)).toBe('00.00%');
    expect(formatPercentage('')).toBe('00.00%');
  });
});

describe('formatDiff', () => {
  test('formats positive difference with plus sign', () => {
    expect(formatDiff(5)).toBe('+5');
    expect(formatDiff(10.7)).toBe('+11');
  });

  test('formats negative difference without extra sign', () => {
    expect(formatDiff(-5)).toBe('-5');
    expect(formatDiff(-10.7)).toBe('-11');
  });

  test('adds percent suffix when isPercent is true', () => {
    expect(formatDiff(5, true)).toBe('+5%');
    expect(formatDiff(-3, true)).toBe('-3%');
  });

  test('shows decimals when decimals is true', () => {
    expect(formatDiff(5.55, false, true)).toBe('+5.55');
    expect(formatDiff(-3.14, true, true)).toBe('-3.14%');
  });

  test('returns empty string for falsy values', () => {
    expect(formatDiff(0)).toBe('');
    expect(formatDiff('')).toBe('');
  });
});

describe('daysAgo', () => {
  const oneDay = 1000 * 60 * 60 * 24;

  test('calculates days between two timestamps', () => {
    const now = Date.now();
    expect(daysAgo(now, now - oneDay)).toBe(1);
    expect(daysAgo(now, now - oneDay * 5)).toBe(5);
  });

  test('order of arguments does not matter (absolute difference)', () => {
    const now = Date.now();
    const past = now - oneDay * 3;
    expect(daysAgo(now, past)).toBe(3);
    expect(daysAgo(past, now)).toBe(3);
  });

  test('same timestamp returns 0', () => {
    const now = Date.now();
    expect(daysAgo(now, now)).toBe(0);
  });

  test('partial days are rounded up', () => {
    const now = Date.now();
    const halfDayAgo = now - oneDay * 0.5;
    expect(daysAgo(now, halfDayAgo)).toBe(1);
  });
});

describe('formatByThousands', () => {
  test('formats millions with M suffix', () => {
    const result = formatByThousands(1500000);
    expect(result).toContain('M');
    expect(result).toContain('1');
  });

  test('formats thousands with K suffix', () => {
    const result = formatByThousands(5000);
    expect(result).toContain('K');
    expect(result).toContain('5');
  });

  test('formats small numbers without suffix', () => {
    const result = formatByThousands(500);
    expect(result).not.toContain('K');
    expect(result).not.toContain('M');
  });

  test('handles zero', () => {
    const result = formatByThousands(0);
    expect(result).toContain('0');
  });

  test('handles exactly 1000', () => {
    const result = formatByThousands(1000);
    expect(result).toContain('K');
  });

  test('handles exactly 1000000', () => {
    const result = formatByThousands(1000000);
    expect(result).toContain('M');
  });
});

describe('formatTime', () => {
  test('formats timestamp to time string', () => {
    const mockToLocaleTimeString = spyOn(Date.prototype, 'toLocaleTimeString').mockReturnValue('10:30:00 AM');
    const result = formatTime(1706180400000);
    expect(result).toBe('10:30:00 AM');
    mockToLocaleTimeString.mockRestore();
  });

  test('returns empty string for falsy values', () => {
    expect(formatTime(0)).toBe('');
    expect(formatTime('')).toBe('');
  });
});

describe('formatDate', () => {
  test('formats timestamp to date string', () => {
    const mockToLocaleDateString = spyOn(Date.prototype, 'toLocaleDateString').mockReturnValue('1/25/2024');
    const result = formatDate(1706180400000);
    expect(result).toBe('1/25/2024');
    mockToLocaleDateString.mockRestore();
  });

  test('returns empty string for falsy values', () => {
    expect(formatDate(0)).toBe('');
    expect(formatDate('')).toBe('');
  });
});

describe('formatDateTime', () => {
  test('formats timestamp to date and time string', () => {
    const mockToLocaleDateString = spyOn(Date.prototype, 'toLocaleDateString').mockReturnValue('1/25/2024');
    const mockToLocaleTimeString = spyOn(Date.prototype, 'toLocaleTimeString').mockReturnValue('10:30:00 AM');
    const result = formatDateTime(1706180400000);
    expect(result).toBe('1/25/2024 10:30:00 AM');
    mockToLocaleDateString.mockRestore();
    mockToLocaleTimeString.mockRestore();
  });

  test('returns empty string for falsy values', () => {
    expect(formatDateTime(0)).toBe('');
    expect(formatDateTime('')).toBe('');
  });
});

describe('formatCurrency', () => {
  const OriginalNumberFormat = Intl.NumberFormat;

  afterAll(() => {
    globalThis.Intl.NumberFormat = OriginalNumberFormat;
  });

  test('formats number as currency', () => {
    // @ts-expect-error - mocking Intl.NumberFormat
    globalThis.Intl.NumberFormat = class {
      format = () => '$1,234.56';
    };
    const result = formatCurrency(1234.56, 'USD');
    expect(result).toBe('$1,234.56');
  });

  test('handles string input', () => {
    // @ts-expect-error - mocking Intl.NumberFormat
    globalThis.Intl.NumberFormat = class {
      format = () => '$99.00';
    };
    const result = formatCurrency('99', 'USD');
    expect(result).toBe('$99.00');
  });

  test('handles zero and NaN', () => {
    // @ts-expect-error - mocking Intl.NumberFormat
    globalThis.Intl.NumberFormat = class {
      format = () => '$0.00';
    };
    expect(formatCurrency(0, 'USD')).toBe('$0.00');
    expect(formatCurrency(NaN, 'USD')).toBe('$0.00');
  });

  test('respects decimals parameter', () => {
    // @ts-expect-error - mocking Intl.NumberFormat
    globalThis.Intl.NumberFormat = class {
      format = () => '€10.000';
    };
    const result = formatCurrency(10, 'EUR', 3);
    expect(result).toBe('€10.000');
  });
});

describe('formatNumber', () => {
  const OriginalNumberFormat = Intl.NumberFormat;

  afterAll(() => {
    globalThis.Intl.NumberFormat = OriginalNumberFormat;
  });

  test('formats number with locale formatting', () => {
    // @ts-expect-error - mocking Intl.NumberFormat
    globalThis.Intl.NumberFormat = class {
      format = () => '1,234,567';
    };
    const result = formatNumber(1234567);
    expect(result).toBe('1,234,567');
  });

  test('handles string input', () => {
    // @ts-expect-error - mocking Intl.NumberFormat
    globalThis.Intl.NumberFormat = class {
      format = () => '999';
    };
    const result = formatNumber('999');
    expect(result).toBe('999');
  });

  test('handles zero and NaN', () => {
    // @ts-expect-error - mocking Intl.NumberFormat
    globalThis.Intl.NumberFormat = class {
      format = () => '0';
    };
    expect(formatNumber(0)).toBe('0');
    expect(formatNumber(NaN)).toBe('0');
  });
});
