/**
 * Tests for @bytelyst/logger package — createLogger + structured logging.
 */

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { createLogger } from '../logger.js';
import type { Logger } from '../types.js';

describe('createLogger', () => {
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  let stdoutSpy: any;
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  let stderrSpy: any;
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  let consoleSpy: any;
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  let consoleErrorSpy: any;

  beforeEach(() => {
    stdoutSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
    stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
    consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
    consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
  });

  afterEach(() => {
    vi.restoreAllMocks();
  });

  it('returns a Logger with all four log methods', () => {
    const log = createLogger({ service: 'test-service' });
    expect(typeof log.error).toBe('function');
    expect(typeof log.warn).toBe('function');
    expect(typeof log.info).toBe('function');
    expect(typeof log.debug).toBe('function');
  });

  describe('production mode (isDev: false)', () => {
    let log: Logger;

    beforeEach(() => {
      log = createLogger({ service: 'platform-service', isDev: false });
    });

    it('info writes structured JSON to stdout', () => {
      log.info('Server started', { port: 4003 });

      expect(stdoutSpy).toHaveBeenCalledTimes(1);
      const output = stdoutSpy.mock.calls[0][0] as string;
      const parsed = JSON.parse(output.trim());

      expect(parsed.level).toBe('info');
      expect(parsed.message).toBe('Server started');
      expect(parsed.service).toBe('platform-service');
      expect(parsed.port).toBe(4003);
      expect(parsed.timestamp).toBeDefined();
    });

    it('error writes structured JSON to stderr', () => {
      log.error('Connection failed', new Error('timeout'));

      expect(stderrSpy).toHaveBeenCalledTimes(1);
      const output = stderrSpy.mock.calls[0][0] as string;
      const parsed = JSON.parse(output.trim());

      expect(parsed.level).toBe('error');
      expect(parsed.message).toBe('Connection failed');
      expect(parsed.error).toBe('timeout');
      expect(parsed.stack).toBeDefined();
      expect(parsed.service).toBe('platform-service');
    });

    it('warn writes structured JSON to stderr', () => {
      log.warn('High memory usage', { usageMb: 512 });

      expect(stderrSpy).toHaveBeenCalledTimes(1);
      const output = stderrSpy.mock.calls[0][0] as string;
      const parsed = JSON.parse(output.trim());

      expect(parsed.level).toBe('warn');
      expect(parsed.message).toBe('High memory usage');
      expect(parsed.usageMb).toBe(512);
    });

    it('debug does NOT emit in production mode', () => {
      log.debug('Debug info');

      expect(stdoutSpy).not.toHaveBeenCalled();
      expect(stderrSpy).not.toHaveBeenCalled();
    });

    it('error handles non-Error objects', () => {
      log.error('Strange error', 'just a string');

      const output = stderrSpy.mock.calls[0][0] as string;
      const parsed = JSON.parse(output.trim());
      expect(parsed.error).toBe('just a string');
      expect(parsed.stack).toBeUndefined();
    });

    it('error handles undefined error', () => {
      log.error('No error object');

      const output = stderrSpy.mock.calls[0][0] as string;
      const parsed = JSON.parse(output.trim());
      expect(parsed.error).toBeUndefined();
    });

    it('includes timestamp in ISO format', () => {
      log.info('Timestamp test');

      const output = stdoutSpy.mock.calls[0][0] as string;
      const parsed = JSON.parse(output.trim());
      expect(parsed.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
    });

    it('appends newline to output', () => {
      log.info('Newline check');

      const output = stdoutSpy.mock.calls[0][0] as string;
      expect(output.endsWith('\n')).toBe(true);
    });
  });

  describe('dev mode (isDev: true)', () => {
    let log: Logger;

    beforeEach(() => {
      log = createLogger({ service: 'dev-service', isDev: true });
    });

    it('info uses console.log with prefix', () => {
      log.info('Dev message');

      expect(consoleSpy).toHaveBeenCalledTimes(1);
      const output = consoleSpy.mock.calls[0][0] as string;
      expect(output).toContain('[INFO]');
      expect(output).toContain('Dev message');
    });

    it('error uses console.error with prefix', () => {
      log.error('Dev error', new Error('boom'));

      expect(consoleErrorSpy).toHaveBeenCalledTimes(1);
      const output = consoleErrorSpy.mock.calls[0][0] as string;
      expect(output).toContain('[ERROR]');
      expect(output).toContain('Dev error');
      expect(output).toContain('boom');
    });

    it('warn uses console.error with prefix', () => {
      log.warn('Dev warning');

      expect(consoleErrorSpy).toHaveBeenCalledTimes(1);
      const output = consoleErrorSpy.mock.calls[0][0] as string;
      expect(output).toContain('[WARN]');
      expect(output).toContain('Dev warning');
    });

    it('debug emits in dev mode', () => {
      log.debug('Debug details', { key: 'value' });

      expect(consoleSpy).toHaveBeenCalledTimes(1);
      const output = consoleSpy.mock.calls[0][0] as string;
      expect(output).toContain('[DEBUG]');
      expect(output).toContain('Debug details');
    });

    it('includes extra data in dev mode', () => {
      log.info('With extras', { requestId: 'abc', userId: 'u1' });

      const output = consoleSpy.mock.calls[0][0] as string;
      expect(output).toContain('requestId');
    });
  });

  describe('config defaults', () => {
    it('defaults to dev mode when NODE_ENV is not production', () => {
      const originalEnv = process.env.NODE_ENV;
      process.env.NODE_ENV = 'development';

      const log = createLogger({ service: 'test' });
      log.debug('Should emit in dev');

      // Debug should emit in non-production
      expect(consoleSpy.mock.calls.length + stdoutSpy.mock.calls.length).toBeGreaterThan(0);

      process.env.NODE_ENV = originalEnv;
    });
  });
});
