/**
 * Tests for security/safe-logger.js
 *
 * Tests the SafeLogger's environment detection, data redaction
 * in production mode, and passthrough in development mode.
 */

// Load SafeLogger — it self-registers on window
import '@js/security/safe-logger.js';

const SL = window.SafeLogger;

describe('SafeLogger', () => {
    afterEach(() => {
        SL.resetProductionMode();
    });

    describe('environment detection', () => {
        it('detects localhost as development', () => {
            // happy-dom defaults to localhost
            SL.resetProductionMode();
            expect(SL.isProduction()).toBe(false);
        });

        it('can be forced to production mode', () => {
            SL.setProductionMode(true);
            expect(SL.isProduction()).toBe(true);
        });

        it('can be forced to development mode', () => {
            SL.setProductionMode(false);
            expect(SL.isProduction()).toBe(false);
        });

        it('returns to auto-detection after reset', () => {
            SL.setProductionMode(true);
            expect(SL.isProduction()).toBe(true);
            SL.resetProductionMode();
            // localhost → development
            expect(SL.isProduction()).toBe(false);
        });

        it('getProductionMode returns null when auto-detecting', () => {
            expect(SL.getProductionMode()).toBeNull();
        });

        it('getProductionMode returns forced value', () => {
            SL.setProductionMode(true);
            expect(SL.getProductionMode()).toBe(true);
        });
    });

    describe('development mode logging', () => {
        beforeEach(() => {
            SL.setProductionMode(false);
        });

        it('passes static message through', () => {
            const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
            SL.log('test message');
            expect(spy).toHaveBeenCalledWith('test message');
            spy.mockRestore();
        });

        it('passes dynamic data through in dev mode', () => {
            const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
            SL.log('query:', 'climate change');
            expect(spy).toHaveBeenCalledWith('query:', 'climate change');
            spy.mockRestore();
        });

        it('extracts Error properties in dev mode', () => {
            const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
            const err = new Error('test error');
            SL.error('Failed:', err);
            const secondArg = spy.mock.calls[0][1];
            expect(secondArg).toHaveProperty('name', 'Error');
            expect(secondArg).toHaveProperty('message', 'test error');
            expect(secondArg).toHaveProperty('stack');
            spy.mockRestore();
        });

        it('extracts .message from error-like objects in dev mode', () => {
            const spy = vi.spyOn(console, 'warn').mockImplementation(() => {});
            SL.warn('Issue:', { message: 'not found', code: 404 });
            expect(spy).toHaveBeenCalledWith('Issue:', 'not found');
            spy.mockRestore();
        });

        it('stringifies plain objects in dev mode', () => {
            const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
            SL.log('Data:', { error: 'not found' });
            expect(spy).toHaveBeenCalledWith('Data:', '{"error":"not found"}');
            spy.mockRestore();
        });

        it('debug messages are emitted in dev mode', () => {
            const spy = vi.spyOn(console, 'debug').mockImplementation(() => {});
            SL.debug('debug info');
            expect(spy).toHaveBeenCalledWith('debug info');
            spy.mockRestore();
        });
    });

    describe('production mode redaction', () => {
        beforeEach(() => {
            SL.setProductionMode(true);
        });

        it('passes first string argument (static message) through', () => {
            const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
            SL.log('User searched:', 'sensitive query');
            expect(spy.mock.calls[0][0]).toBe('User searched:');
            spy.mockRestore();
        });

        it('redacts string arguments after the first', () => {
            const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
            SL.log('User searched:', 'sensitive query');
            expect(spy.mock.calls[0][1]).toBe('[redacted]');
            spy.mockRestore();
        });

        it('redacts numbers', () => {
            const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
            SL.log('Research ID:', 42);
            expect(spy.mock.calls[0][1]).toBe('[redacted]');
            spy.mockRestore();
        });

        it('preserves booleans in production', () => {
            const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
            SL.log('Active:', true);
            expect(spy.mock.calls[0][1]).toBe(true);
            spy.mockRestore();
        });

        it('preserves null/undefined in production', () => {
            const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
            SL.log('Value:', null);
            expect(spy.mock.calls[0][1]).toBeNull();
            spy.mockRestore();
        });

        it('redacts Error message but keeps name in production', () => {
            const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
            SL.error('Failed:', new TypeError('secret details'));
            const secondArg = spy.mock.calls[0][1];
            expect(secondArg.name).toBe('TypeError');
            expect(secondArg.message).toBe('[redacted]');
            spy.mockRestore();
        });

        it('shows array structure but not contents in production', () => {
            const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
            SL.log('Items:', [1, 2, 3]);
            expect(spy.mock.calls[0][1]).toBe('[Array(3)]');
            spy.mockRestore();
        });

        it('shows [Object] for objects in production', () => {
            const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
            SL.log('Config:', { key: 'value' });
            expect(spy.mock.calls[0][1]).toBe('[Object]');
            spy.mockRestore();
        });

        it('suppresses debug messages entirely in production', () => {
            const spy = vi.spyOn(console, 'debug').mockImplementation(() => {});
            SL.debug('secret debug info');
            expect(spy).not.toHaveBeenCalled();
            spy.mockRestore();
        });
    });

    describe('log methods call correct console methods', () => {
        beforeEach(() => {
            SL.setProductionMode(false);
        });

        it('log → console.log', () => {
            const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
            SL.log('test');
            expect(spy).toHaveBeenCalled();
            spy.mockRestore();
        });

        it('info → console.info', () => {
            const spy = vi.spyOn(console, 'info').mockImplementation(() => {});
            SL.info('test');
            expect(spy).toHaveBeenCalled();
            spy.mockRestore();
        });

        it('warn → console.warn', () => {
            const spy = vi.spyOn(console, 'warn').mockImplementation(() => {});
            SL.warn('test');
            expect(spy).toHaveBeenCalled();
            spy.mockRestore();
        });

        it('error → console.error', () => {
            const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
            SL.error('test');
            expect(spy).toHaveBeenCalled();
            spy.mockRestore();
        });
    });

    describe('production redaction invariants (fuzz)', () => {
        // Property-based test: in production mode, no string/number argument
        // after the first should ever appear verbatim in console output.
        // This is the most critical security property of SafeLogger — if
        // it ever regresses, user search queries / API responses leak.

        beforeEach(() => {
            SL.setProductionMode(true);
        });

        const generateRandomString = () => {
            const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%';
            const len = Math.floor(Math.random() * 30) + 5;
            let s = '';
            for (let i = 0; i < len; i++) {
                s += chars[Math.floor(Math.random() * chars.length)];
            }
            return s;
        };

        it('never leaks string arguments after the first (100 random inputs)', () => {
            const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
            for (let i = 0; i < 100; i++) {
                const secret = generateRandomString();
                SL.log('Request:', secret);
                const loggedArgs = spy.mock.calls[spy.mock.calls.length - 1];
                // The static message should pass through
                expect(loggedArgs[0]).toBe('Request:');
                // The secret should NOT appear in any logged argument
                for (let j = 1; j < loggedArgs.length; j++) {
                    expect(loggedArgs[j]).not.toBe(secret);
                    expect(String(loggedArgs[j])).not.toContain(secret);
                }
            }
            spy.mockRestore();
        });

        it('never leaks number arguments after the first', () => {
            const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
            for (let i = 0; i < 50; i++) {
                const secretId = Math.floor(Math.random() * 1e9);
                SL.log('User ID:', secretId);
                const loggedArgs = spy.mock.calls[spy.mock.calls.length - 1];
                for (let j = 1; j < loggedArgs.length; j++) {
                    expect(loggedArgs[j]).not.toBe(secretId);
                }
            }
            spy.mockRestore();
        });

        it('never leaks object property values', () => {
            const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
            for (let i = 0; i < 30; i++) {
                const secret = generateRandomString();
                SL.log('Data:', { token: secret, user: secret });
                const loggedArgs = spy.mock.calls[spy.mock.calls.length - 1];
                // The serialized representation should not contain the secret
                const serialized = JSON.stringify(loggedArgs);
                expect(serialized).not.toContain(secret);
            }
            spy.mockRestore();
        });

        it('never leaks array contents', () => {
            const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
            for (let i = 0; i < 30; i++) {
                const secrets = [generateRandomString(), generateRandomString()];
                SL.log('Items:', secrets);
                const loggedArgs = spy.mock.calls[spy.mock.calls.length - 1];
                const serialized = JSON.stringify(loggedArgs);
                for (const secret of secrets) {
                    expect(serialized).not.toContain(secret);
                }
            }
            spy.mockRestore();
        });

        it('never leaks Error message contents', () => {
            const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
            for (let i = 0; i < 30; i++) {
                const secretMsg = generateRandomString();
                SL.error('Failed:', new Error(secretMsg));
                const loggedArgs = spy.mock.calls[spy.mock.calls.length - 1];
                const serialized = JSON.stringify(loggedArgs);
                expect(serialized).not.toContain(secretMsg);
            }
            spy.mockRestore();
        });
    });

    describe('edge cases', () => {
        it('handles zero arguments', () => {
            SL.setProductionMode(false);
            const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
            SL.log();
            expect(spy).toHaveBeenCalledWith();
            spy.mockRestore();
        });

        it('redacts error-like objects in production', () => {
            SL.setProductionMode(true);
            const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
            SL.log('Issue:', { message: 'secret', name: 'ApiError' });
            const arg = spy.mock.calls[0][1];
            expect(arg.name).toBe('ApiError');
            expect(arg.message).toBe('[redacted]');
            spy.mockRestore();
        });

        it('does not misclassify data objects with .message but no .name', () => {
            // Regression: a summary object like {hasLogEntry, logType, message}
            // was being treated as an Error in production, producing the
            // misleading log line "Object { name: 'Error', message: '[redacted]' }".
            SL.setProductionMode(true);
            const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
            SL.log('Calling handlers with data:', {
                hasLogEntry: true,
                logType: 'milestone',
                message: 'Phase complete...',
            });
            const arg = spy.mock.calls[0][1];
            // Should fall through to the generic-object redaction, not the
            // error-like branch.
            expect(arg).toBe('[Object]');
            spy.mockRestore();
        });
    });
});
