/**
 * Tests for utils/log-helpers.js
 *
 * The log panel filters and dedups thousands of log entries during a
 * research run. These helpers power that — an off-by-one in
 * checkLogVisibility means warnings disappear; a collision in
 * hashString means dedupe drops real entries.
 */

import '@js/utils/log-helpers.js';

const {
    checkLogVisibility,
    hashString,
    normalizeMessage,
    normalizeTimestamps,
} = window.LdrLogHelpers;

describe('checkLogVisibility', () => {
    it("filter 'all' shows every type", () => {
        for (const type of ['info', 'milestone', 'warning', 'error']) {
            expect(checkLogVisibility(type, 'all')).toBe(true);
        }
    });

    it("filter 'info' shows info, milestone, warning, and error", () => {
        expect(checkLogVisibility('info', 'info')).toBe(true);
        expect(checkLogVisibility('milestone', 'info')).toBe(true);
        expect(checkLogVisibility('warning', 'info')).toBe(true);
        expect(checkLogVisibility('error', 'info')).toBe(true);
    });

    it("filter 'milestone' only shows milestones", () => {
        expect(checkLogVisibility('milestone', 'milestone')).toBe(true);
        expect(checkLogVisibility('info', 'milestone')).toBe(false);
        expect(checkLogVisibility('warning', 'milestone')).toBe(false);
        expect(checkLogVisibility('error', 'milestone')).toBe(false);
    });

    it("accepts plural 'milestones' as an alias", () => {
        expect(checkLogVisibility('milestone', 'milestones')).toBe(true);
        expect(checkLogVisibility('info', 'milestones')).toBe(false);
    });

    it("filter 'warning' shows warnings and errors", () => {
        expect(checkLogVisibility('warning', 'warning')).toBe(true);
        expect(checkLogVisibility('error', 'warning')).toBe(true);
        expect(checkLogVisibility('info', 'warning')).toBe(false);
        expect(checkLogVisibility('milestone', 'warning')).toBe(false);
    });

    it("accepts plural 'warnings' as an alias", () => {
        expect(checkLogVisibility('warning', 'warnings')).toBe(true);
        expect(checkLogVisibility('error', 'warnings')).toBe(true);
        expect(checkLogVisibility('info', 'warnings')).toBe(false);
    });

    it("filter 'error' only shows errors", () => {
        expect(checkLogVisibility('error', 'error')).toBe(true);
        expect(checkLogVisibility('warning', 'error')).toBe(false);
        expect(checkLogVisibility('info', 'error')).toBe(false);
        expect(checkLogVisibility('milestone', 'error')).toBe(false);
    });

    it("accepts plural 'errors' as an alias", () => {
        expect(checkLogVisibility('error', 'errors')).toBe(true);
        expect(checkLogVisibility('warning', 'errors')).toBe(false);
    });

    it('unknown filter defaults to showing everything', () => {
        expect(checkLogVisibility('info', 'bogus')).toBe(true);
        expect(checkLogVisibility('error', '')).toBe(true);
        expect(checkLogVisibility('milestone', undefined)).toBe(true);
    });
});

describe('hashString', () => {
    it('returns "0" for empty/null/undefined', () => {
        expect(hashString('')).toBe('0');
        expect(hashString(null)).toBe('0');
        expect(hashString(undefined)).toBe('0');
    });

    it('is deterministic: same input → same output', () => {
        const a = hashString('log entry one');
        const b = hashString('log entry one');
        expect(a).toBe(b);
    });

    it('produces different hashes for different inputs', () => {
        expect(hashString('log entry one')).not.toBe(hashString('log entry two'));
        expect(hashString('a')).not.toBe(hashString('b'));
    });

    it('handles long strings', () => {
        const long = 'x'.repeat(10000);
        expect(hashString(long)).toMatch(/^-?\d+$/);
    });

    it('handles unicode characters', () => {
        // Should not throw and should differ from its ASCII analogue
        expect(hashString('café')).toMatch(/^-?\d+$/);
        expect(hashString('café')).not.toBe(hashString('cafe'));
    });
});

describe('normalizeMessage', () => {
    it('returns empty string for falsy input', () => {
        expect(normalizeMessage('')).toBe('');
        expect(normalizeMessage(null)).toBe('');
        expect(normalizeMessage(undefined)).toBe('');
    });

    it('trims whitespace', () => {
        expect(normalizeMessage('  hello  ')).toBe('hello');
        expect(normalizeMessage('\t\nhello\r\n')).toBe('hello');
    });

    it('lowercases', () => {
        expect(normalizeMessage('HELLO World')).toBe('hello world');
    });

    it('trims and lowercases in combination', () => {
        expect(normalizeMessage('  HELLO  ')).toBe('hello');
    });
});

describe('normalizeTimestamps', () => {
    it('is a no-op when all logs share the same date', () => {
        const logs = [
            {time: '2026-04-01T10:00:00Z', message: 'a'},
            {time: '2026-04-01T11:00:00Z', message: 'b'},
            {time: '2026-04-01T12:00:00Z', message: 'c'},
        ];
        const before = JSON.parse(JSON.stringify(logs));
        normalizeTimestamps(logs);
        expect(logs).toEqual(before);
    });

    it('re-stamps an outlier date to the majority date', () => {
        const logs = [
            {time: '2026-04-01T10:00:00.000Z', message: 'a'},
            {time: '2026-04-01T11:00:00.000Z', message: 'b'},
            {time: '2026-04-01T12:00:00.000Z', message: 'c'},
            {time: '2026-04-02T15:30:00.000Z', message: 'outlier'},
        ];
        const originalTime = logs[3].time;
        normalizeTimestamps(logs);
        const outlier = logs[3];
        // The outlier's date portion should now be 2026-04-01
        expect(outlier.time.startsWith('2026-04-01')).toBe(true);
        // The time should have changed from the original
        expect(outlier.time).not.toBe(originalTime);
        // ID should be regenerated from the new timestamp + hash
        expect(outlier.id).toMatch(/^2026-04-01.*-/);
    });

    it('does nothing when logs is empty', () => {
        const logs = [];
        expect(() => normalizeTimestamps(logs)).not.toThrow();
        expect(logs).toEqual([]);
    });

    it('does nothing when every log has an unparseable time', () => {
        const logs = [
            {time: 'not-a-date', message: 'a'},
            {time: 'also-not-a-date', message: 'b'},
        ];
        // Should not throw; mostCommonDate would remain null
        expect(() => normalizeTimestamps(logs)).not.toThrow();
    });

    it('handles a tie by picking one date deterministically (first seen wins via iteration order)', () => {
        // Two days with equal count — either is acceptable, just shouldn't crash
        const logs = [
            {time: '2026-04-01T10:00:00Z', message: 'a'},
            {time: '2026-04-02T10:00:00Z', message: 'b'},
        ];
        expect(() => normalizeTimestamps(logs)).not.toThrow();
        // Both should end up on the same day
        const d1 = logs[0].time.split('T')[0];
        const d2 = logs[1].time.split('T')[0];
        expect(d1).toBe(d2);
    });
});
