import { describe, it, expect } from 'bun:test';
import {
    randInt,
    randBool,
    choice,
    weightedChoice,
    shuffle,
    mapNumber,
    zip,
    enumerate,
    range,
    clamp,
    lerp,
    prevent,
    intHash,
    sinHash,
    throttle,
    callUpdater,
    formatTime,
    formatNumber,
    fuzzyMatch,
} from '@common/utils';

describe('utils', () => {
    describe('randInt', () => {
        it('should return a number within the specified range', () => {
            for (let i = 0; i < 100; i++) {
                const result = randInt(1, 10);
                expect(result).toBeGreaterThanOrEqual(1);
                expect(result).toBeLessThanOrEqual(10);
            }
        });

        it('should return integers only (no floats)', () => {
            for (let i = 0; i < 100; i++) {
                const result = randInt(0, 100);
                expect(Number.isInteger(result)).toBe(true);
            }
        });

        it('should handle single value range', () => {
            expect(randInt(5, 6)).toBe(5);
        });
    });

    describe('randBool', () => {
        it('should return boolean values', () => {
            for (let i = 0; i < 100; i++) {
                const result = randBool();
                expect(typeof result).toBe('boolean');
            }
        });

        it('should return both true and false over multiple calls', () => {
            const results = new Set();
            for (let i = 0; i < 100; i++) {
                results.add(randBool());
            }
            expect(results.has(true)).toBe(true);
            expect(results.has(false)).toBe(true);
        });
    });

    describe('choice', () => {
        it('should return an element from the array', () => {
            const array = [1, 2, 3, 4, 5];
            const result = choice(array);
            expect(array).toContain(result);
        });

        it('should work with string arrays', () => {
            const array = ['a', 'b', 'c'];
            const result = choice(array);
            expect(array).toContain(result);
        });

        it('should work with object arrays', () => {
            const array = [{ id: 1 }, { id: 2 }, { id: 3 }];
            const result = choice(array);
            expect(array).toContain(result);
        });
    });

    describe('weightedChoice', () => {
        it('should return an item based on weights (array input)', () => {
            const options: [number, number][] = [[1, 10], [2, 1], [3, 1]];
            const results = new Set<number>();
            for (let i = 0; i < 100; i++) {
                const result = weightedChoice(options);
                if (result !== null) results.add(result);
            }
            // Item 1 has much higher weight, should appear most often
            expect(results.has(1)).toBe(true);
        });

        it('should return an item based on weights (object input)', () => {
            const options = { a: 10, b: 1, c: 1 };
            const results = new Set<string>();
            for (let i = 0; i < 100; i++) {
                const result = weightedChoice(options);
                if (result !== null) results.add(result);
            }
            expect(results.has('a')).toBe(true);
        });

        it('should return null for empty options', () => {
            expect(weightedChoice([])).toBe(null);
            expect(weightedChoice({})).toBe(null);
        });
    });

    describe('shuffle', () => {
        it('should return an array of the same length', () => {
            const array = [1, 2, 3, 4, 5];
            const shuffled = shuffle(array);
            expect(shuffled.length).toBe(array.length);
        });

        it('should contain the same elements', () => {
            const array = [1, 2, 3, 4, 5];
            const shuffled = shuffle(array);
            expect(shuffled.sort((a, b) => a - b)).toEqual(array);
        });

        it('should not modify the original array', () => {
            const array = [1, 2, 3, 4, 5];
            const original = [...array];
            shuffle(array);
            expect(array).toEqual(original);
        });

        it('should handle empty arrays', () => {
            expect(shuffle([])).toEqual([]);
        });

        it('should handle single element arrays', () => {
            expect(shuffle([1])).toEqual([1]);
        });
    });

    describe('mapNumber', () => {
        it('should map a number from one range to another', () => {
            expect(mapNumber(5, 0, 10, 0, 100)).toBe(50);
        });

        it('should handle reverse ranges', () => {
            expect(mapNumber(5, 0, 10, 100, 0)).toBe(50);
        });

        it('should handle negative ranges', () => {
            expect(mapNumber(0, -10, 10, 0, 100)).toBe(50);
        });
    });

    describe('zip', () => {
        it('should zip two arrays together', () => {
            const result = [...zip([1, 2, 3], ['a', 'b', 'c'])];
            expect(result).toEqual([[1, 'a'], [2, 'b'], [3, 'c']]);
        });

        it('should stop at the shortest array', () => {
            const result = [...zip([1, 2], ['a', 'b', 'c', 'd'])];
            expect(result).toEqual([[1, 'a'], [2, 'b']]);
        });

        it('should zip three arrays', () => {
            const result = [...zip([1, 2], ['a', 'b'], [true, false])];
            expect(result).toEqual([[1, 'a', true], [2, 'b', false]]);
        });

        it('should handle empty arrays', () => {
            expect([...zip([], [])]).toEqual([]);
        });
    });

    describe('enumerate', () => {
        it('should enumerate an array with indices', () => {
            const result = [...enumerate(['a', 'b', 'c'])];
            expect(result).toEqual([[0, 'a'], [1, 'b'], [2, 'c']]);
        });

        it('should handle empty arrays', () => {
            expect([...enumerate([])]).toEqual([]);
        });
    });

    describe('range', () => {
        it('should create a range from a number', () => {
            expect(range(5)).toEqual([0, 1, 2, 3, 4]);
        });

        it('should create a range from a string number', () => {
            expect(range('5')).toEqual([0, 1, 2, 3, 4]);
        });

        it('should return empty array for 0', () => {
            expect(range(0)).toEqual([]);
        });
    });

    describe('clamp', () => {
        it('should clamp a value within range', () => {
            expect(clamp(5, 0, 10)).toBe(5);
        });

        it('should clamp values below minimum', () => {
            expect(clamp(-5, 0, 10)).toBe(0);
        });

        it('should clamp values above maximum', () => {
            expect(clamp(15, 0, 10)).toBe(10);
        });
    });

    describe('lerp', () => {
        it('should linearly interpolate between two values', () => {
            expect(lerp(0, 100, 0.5)).toBe(50);
        });

        it('should return start value at t=0', () => {
            expect(lerp(0, 100, 0)).toBe(0);
        });

        it('should return end value at t=1', () => {
            expect(lerp(0, 100, 1)).toBe(100);
        });

        it('should handle values outside 0-1 range', () => {
            expect(lerp(0, 100, -0.5)).toBe(-50);
            expect(lerp(0, 100, 1.5)).toBe(150);
        });
    });

    describe('prevent', () => {
        it('should call preventDefault and return false', () => {
            let preventDefaultCalled = false;
            const mockEvent = {
                preventDefault: () => { preventDefaultCalled = true; }
            } as Event;
            const result = prevent(mockEvent);
            expect(preventDefaultCalled).toBe(true);
            expect(result).toBe(false);
        });
    });

    describe('intHash', () => {
        it('should return a number', () => {
            expect(typeof intHash(1, 2, 3)).toBe('number');
        });

        it('should be deterministic', () => {
            const hash1 = intHash(1, 2, 3);
            const hash2 = intHash(1, 2, 3);
            expect(hash1).toBe(hash2);
        });

        it('should produce different hashes for different inputs', () => {
            const hash1 = intHash(1);
            const hash2 = intHash(2);
            expect(hash1).not.toBe(hash2);
        });
    });

    describe('sinHash', () => {
        it('should return a number between 0 and 1', () => {
            const result = sinHash(1, 2, 3);
            expect(result).toBeGreaterThanOrEqual(0);
            expect(result).toBeLessThanOrEqual(1);
        });

        it('should be deterministic', () => {
            const hash1 = sinHash(1, 2, 3);
            const hash2 = sinHash(1, 2, 3);
            expect(hash1).toBeCloseTo(hash2);
        });

        it('should produce different hashes for different inputs', () => {
            const hash1 = sinHash(1, 2, 3);
            const hash2 = sinHash(4, 5, 6);
            expect(hash1).not.toBeCloseTo(hash2);
        });
    });

    describe('throttle', () => {
        it('should call the function immediately', () => {
            let callCount = 0;
            const fn = throttle(() => { callCount++; }, 1000);
            fn();
            expect(callCount).toBe(1);
        });

        it('should throttle subsequent calls', () => {
            let callCount = 0;
            const fn = throttle(() => { callCount++; }, 1000);
            fn();
            fn();
            fn();
            expect(callCount).toBe(1);
        });
    });

    describe('callUpdater', () => {
        it('should call a function updater', () => {
            const updater = (prev: number) => prev + 1;
            expect(callUpdater(updater, 5)).toBe(6);
        });

        it('should return a direct value updater', () => {
            expect(callUpdater(10, 5)).toBe(10);
        });
    });

    describe('formatNumber', () => {
        it('should return small integer as-is', () => {
            expect(formatNumber(0)).toBe('0');
            expect(formatNumber(42)).toBe('42');
            expect(formatNumber(999)).toBe('999');
        });

        it('should return small decimals rounded to at most 2 significant digits', () => {
            expect(formatNumber(0.1)).toBe('0.10');
            expect(formatNumber(0.123)).toBe('0.12');
            expect(formatNumber(0.5)).toBe('0.50');
            expect(formatNumber(0.999)).toBe('1.00');
            expect(formatNumber(0.001)).toBe('0.0010');
            expect(formatNumber(0.0001234)).toBe('0.00012');
        });

        it('should return decimals between 100 and 1000 with 2 decimal places', () => {
            expect(formatNumber(100.5)).toBe('100.50');
            expect(formatNumber(500.75)).toBe('500.75');
            expect(formatNumber(999.9)).toBe('999.90');
        });

        it('should format thousands with k suffix', () => {
            expect(formatNumber(1_000)).toBe('1.00k');
            expect(formatNumber(5_500)).toBe('5.50k');
            expect(formatNumber(999_999)).toBe('1000.00k');
        });

        it('should format millions with M suffix', () => {
            expect(formatNumber(1_000_000)).toBe('1.00M');
            expect(formatNumber(2_500_000)).toBe('2.50M');
            expect(formatNumber(999_999_999)).toBe('1000.00M');
        });

        it('should format billions with B suffix', () => {
            expect(formatNumber(1_000_000_000)).toBe('1.00B');
            expect(formatNumber(3_750_000_000)).toBe('3.75B');
            expect(formatNumber(999_999_999_999)).toBe('1000.00B');
        });

        it('should format trillions with T suffix', () => {
            expect(formatNumber(1_000_000_000_000)).toBe('1.00T');
            expect(formatNumber(5_250_000_000_000)).toBe('5.25T');
        });

        it('should format negative numbers with a leading minus sign', () => {
            expect(formatNumber(-42)).toBe('-42');
            expect(formatNumber(-0.123)).toBe('-0.12');
            expect(formatNumber(-0.999)).toBe('-1.00');
            expect(formatNumber(-1_000)).toBe('-1.00k');
            expect(formatNumber(-1_000_000)).toBe('-1.00M');
            expect(formatNumber(-1_000_000_000)).toBe('-1.00B');
            expect(formatNumber(-1_000_000_000_000)).toBe('-1.00T');
        });
    });

    describe('fuzzyMatch', () => {
        it('returns true for exact match', () => {
            expect(fuzzyMatch('hello', 'hello')).toBe(true);
        });

        it('returns true when query chars appear in order', () => {
            expect(fuzzyMatch('Deliberate', 'dlt')).toBe(true);
        });

        it('returns true for non-contiguous subsequence', () => {
            expect(fuzzyMatch('Analog Diffusion', 'andif')).toBe(true);
        });

        it('returns false when query chars are out of order', () => {
            expect(fuzzyMatch('abc', 'ca')).toBe(false);
        });

        it('returns false when a query char is missing from target', () => {
            expect(fuzzyMatch('Deliberate', 'dltz')).toBe(false);
        });

        it('returns true for empty query', () => {
            expect(fuzzyMatch('anything', '')).toBe(true);
        });

        it('returns false for empty target with non-empty query', () => {
            expect(fuzzyMatch('', 'a')).toBe(false);
        });

        it('returns false when query is longer than target', () => {
            expect(fuzzyMatch('ab', 'abc')).toBe(false);
        });

        it('is case-insensitive', () => {
            expect(fuzzyMatch('DreamShaper', 'dreamshaper')).toBe(true);
            expect(fuzzyMatch('dreamshaper', 'DREAM')).toBe(true);
        });
    });

    describe('formatTime', () => {
        it('should return 0:00 for zero seconds', () => {
            expect(formatTime(0)).toBe('0:00');
        });

        it('should format seconds correctly', () => {
            expect(formatTime(1)).toBe('0:01');
            expect(formatTime(30)).toBe('0:30');
            expect(formatTime(59)).toBe('0:59');
        });

        it('should format minutes correctly', () => {
            expect(formatTime(60)).toBe('1:00');
            expect(formatTime(2 * 60 + 5)).toBe('2:05');
            expect(formatTime(59 * 60 + 59)).toBe('59:59');
        });

        it('should format hours correctly', () => {
            expect(formatTime(60 * 60)).toBe('1:00:00');
            expect(formatTime(2 * 60 * 60 + 60 + 5)).toBe('2:01:05');
        });

        it('should format days correctly', () => {
            expect(formatTime(24 * 60 * 60)).toBe('1d 0:00:00');
            expect(formatTime(2 * 24 * 60 * 60)).toBe('2d 0:00:00');
        });

        it('should format weeks correctly', () => {
            expect(formatTime(7 * 24 * 60 * 60)).toBe('1w 0:00:00');
        });

        it('should format months correctly', () => {
            const month = 30 * 24 * 60 * 60;
            expect(formatTime(month)).toBe('1m 0:00:00');
        });

        it('should format years correctly', () => {
            const year = 365 * 24 * 60 * 60;
            expect(formatTime(year)).toBe('1y 0:00:00');
        });

        it('should format complex durations', () => {
            const total =
                365 * 24 * 60 * 60 + // 1y
                30 * 24 * 60 * 60 +  // 1m
                7 * 24 * 60 * 60 +   // 1w
                24 * 60 * 60 +       // 1d
                60 * 60 +            // 1h
                60 +                 // 1m
                1;                   // 1s
            expect(formatTime(total)).toBe('1y 1m 1w 1d 1:01:01');
        });
    });
});
