import { expect } from 'chai';
import * as sinon from 'sinon';
import { ConsoleLogger, Logger, LoggerService, LogLevel } from '../../services';

describe('Logger', () => {
  describe('[static methods]', () => {
    describe('when the default logger is used', () => {
      let processStdoutWriteSpy: sinon.SinonSpy;
      let processStderrWriteSpy: sinon.SinonSpy;

      beforeEach(() => {
        processStdoutWriteSpy = sinon.spy(process.stdout, 'write');
        processStderrWriteSpy = sinon.spy(process.stderr, 'write');
      });

      afterEach(() => {
        processStdoutWriteSpy.restore();
        processStderrWriteSpy.restore();
      });

      it('should print one message to the console', () => {
        const message = 'random message';
        const context = 'RandomContext';

        Logger.log(message, context);

        expect(processStdoutWriteSpy.calledOnce).to.be.true;
        expect(processStdoutWriteSpy.firstCall.firstArg).to.include(
          `[${context}]`,
        );
        expect(processStdoutWriteSpy.firstCall.firstArg).to.include(message);
      });

      it('should print one message without context to the console', () => {
        const message = 'random message without context';

        Logger.log(message);

        expect(processStdoutWriteSpy.calledOnce).to.be.true;
        expect(processStdoutWriteSpy.firstCall.firstArg).to.include(message);
      });

      it('should print multiple messages to the console', () => {
        const messages = ['message 1', 'message 2', 'message 3'];
        const context = 'RandomContext';

        Logger.log(messages[0], messages[1], messages[2], context);

        expect(processStdoutWriteSpy.calledThrice).to.be.true;
        expect(processStdoutWriteSpy.firstCall.firstArg).to.include(
          `[${context}]`,
        );
        expect(processStdoutWriteSpy.firstCall.firstArg).to.include(
          messages[0],
        );

        expect(processStdoutWriteSpy.secondCall.firstArg).to.include(
          `[${context}]`,
        );
        expect(processStdoutWriteSpy.secondCall.firstArg).to.include(
          messages[1],
        );

        expect(processStdoutWriteSpy.thirdCall.firstArg).to.include(
          `[${context}]`,
        );
        expect(processStdoutWriteSpy.thirdCall.firstArg).to.include(
          messages[2],
        );
      });

      it('should print one error to the console with context', () => {
        const message = 'random error';
        const context = 'RandomContext';

        Logger.error(message, context);

        expect(processStderrWriteSpy.calledOnce).to.be.true;
        expect(processStderrWriteSpy.firstCall.firstArg).to.include(
          `[${context}]`,
        );
        expect(processStderrWriteSpy.firstCall.firstArg).to.include(message);
      });

      it('should print one error to the console with stacktrace', () => {
        const message = 'random error';
        const stacktrace = 'Error: message\n    at <anonymous>:1:2';

        Logger.error(message, stacktrace);

        expect(processStderrWriteSpy.calledTwice).to.be.true;
        expect(processStderrWriteSpy.firstCall.firstArg).to.not.include(`[]`);
        expect(processStderrWriteSpy.firstCall.firstArg).to.include(message);
        expect(processStderrWriteSpy.secondCall.firstArg).to.equal(
          stacktrace + '\n',
        );
      });

      it('should print one error without context to the console', () => {
        const message = 'random error without context';

        Logger.error(message);

        expect(processStderrWriteSpy.calledOnce).to.be.true;
        expect(processStderrWriteSpy.firstCall.firstArg).to.include(message);
      });

      it('should print error object without context to the console', () => {
        const error = new Error('Random text here');

        Logger.error(error);

        expect(processStderrWriteSpy.calledOnce).to.be.true;
        expect(processStderrWriteSpy.firstCall.firstArg).to.include(
          `Error: Random text here`,
        );
      });

      it('should serialise a plain JS object (as a message) without context to the console', () => {
        const error = {
          randomError: true,
        };

        Logger.error(error);

        expect(processStderrWriteSpy.calledOnce).to.be.true;

        expect(processStderrWriteSpy.firstCall.firstArg).to.include(
          `Object(${Object.keys(error).length})`,
        );
        expect(processStderrWriteSpy.firstCall.firstArg).to.include(
          `randomError: \x1b[33mtrue`,
        );
      });

      it('should print one error with stacktrace and context to the console', () => {
        const message = 'random error with context';
        const stacktrace = 'stacktrace';
        const context = 'ErrorContext';

        Logger.error(message, stacktrace, context);

        expect(processStderrWriteSpy.calledTwice).to.be.true;

        expect(processStderrWriteSpy.firstCall.firstArg).to.include(
          `[${context}]`,
        );
        expect(processStderrWriteSpy.firstCall.firstArg).to.include(message);

        expect(processStderrWriteSpy.secondCall.firstArg).to.equal(
          stacktrace + '\n',
        );
        expect(processStderrWriteSpy.secondCall.firstArg).to.not.include(
          context,
        );
      });

      it('should print multiple 2 errors and one stacktrace to the console', () => {
        const messages = ['message 1', 'message 2'];
        const stack = 'stacktrace';
        const context = 'RandomContext';

        Logger.error(messages[0], messages[1], stack, context);

        expect(processStderrWriteSpy.calledThrice).to.be.true;
        expect(processStderrWriteSpy.firstCall.firstArg).to.include(
          `[${context}]`,
        );
        expect(processStderrWriteSpy.firstCall.firstArg).to.include(
          messages[0],
        );

        expect(processStderrWriteSpy.secondCall.firstArg).to.include(
          `[${context}]`,
        );
        expect(processStderrWriteSpy.secondCall.firstArg).to.include(
          messages[1],
        );

        expect(processStderrWriteSpy.thirdCall.firstArg).to.not.include(
          `[${context}]`,
        );
        expect(processStderrWriteSpy.thirdCall.firstArg).to.equal(stack + '\n');
      });
    });

    describe('when the default logger is used and json mode is enabled', () => {
      const logger = new ConsoleLogger({ json: true });

      let processStdoutWriteSpy: sinon.SinonSpy;
      let processStderrWriteSpy: sinon.SinonSpy;

      beforeEach(() => {
        processStdoutWriteSpy = sinon.spy(process.stdout, 'write');
        processStderrWriteSpy = sinon.spy(process.stderr, 'write');
      });

      afterEach(() => {
        processStdoutWriteSpy.restore();
        processStderrWriteSpy.restore();
      });

      it('should print error with stack as JSON to the console', () => {
        const errorMessage = 'error message';
        const error = new Error(errorMessage);

        logger.error(error.message, error.stack);

        const json = JSON.parse(processStderrWriteSpy.firstCall?.firstArg);

        expect(json.pid).to.equal(process.pid);
        expect(json.level).to.equal('error');
        expect(json.message).to.equal(errorMessage);
      });
      it('should log out to stdout as JSON', () => {
        const message = 'message 1';

        logger.log(message);

        const json = JSON.parse(processStdoutWriteSpy.firstCall?.firstArg);

        expect(json.pid).to.equal(process.pid);
        expect(json.level).to.equal('log');
        expect(json.message).to.equal(message);
      });
      it('should log out an error to stderr as JSON', () => {
        const message = 'message 1';

        logger.error(message);

        const json = JSON.parse(processStderrWriteSpy.firstCall?.firstArg);

        expect(json.pid).to.equal(process.pid);
        expect(json.level).to.equal('error');
        expect(json.message).to.equal(message);
      });
      it('should log Map object', () => {
        const map = new Map([
          ['key1', 'value1'],
          ['key2', 'value2'],
        ]);

        logger.log(map);

        const json = JSON.parse(processStdoutWriteSpy.firstCall?.firstArg);

        expect(json.pid).to.equal(process.pid);
        expect(json.level).to.equal('log');
        expect(json.message).to.equal(
          `Map(2) { 'key1' => 'value1', 'key2' => 'value2' }`,
        );
      });
      it('should log Set object', () => {
        const set = new Set(['value1', 'value2']);

        logger.log(set);

        const json = JSON.parse(processStdoutWriteSpy.firstCall?.firstArg);

        expect(json.pid).to.equal(process.pid);
        expect(json.level).to.equal('log');
        expect(json.message).to.equal(`Set(2) { 'value1', 'value2' }`);
      });
      it('should log bigint', () => {
        const bigInt = BigInt(9007199254740991);

        logger.log(bigInt);

        const json = JSON.parse(processStdoutWriteSpy.firstCall?.firstArg);

        expect(json.pid).to.equal(process.pid);
        expect(json.level).to.equal('log');
        expect(json.message).to.equal('9007199254740991');
      });
      it('should log symbol', () => {
        const symbol = Symbol('test');

        logger.log(symbol);

        const json = JSON.parse(processStdoutWriteSpy.firstCall?.firstArg);

        expect(json.pid).to.equal(process.pid);
        expect(json.level).to.equal('log');
        expect(json.message).to.equal('Symbol(test)');
      });
    });

    describe('when the default logger is used, json mode is enabled and compact is false (utils.inspect)', () => {
      const logger = new ConsoleLogger({ json: true, compact: false });

      let processStdoutWriteSpy: sinon.SinonSpy;
      let processStderrWriteSpy: sinon.SinonSpy;

      beforeEach(() => {
        processStdoutWriteSpy = sinon.spy(process.stdout, 'write');
        processStderrWriteSpy = sinon.spy(process.stderr, 'write');
      });

      afterEach(() => {
        processStdoutWriteSpy.restore();
        processStderrWriteSpy.restore();
      });

      it('should log out to stdout as JSON (utils.inspect)', () => {
        const message = 'message 1';

        logger.log(message);

        const json = convertInspectToJSON(
          processStdoutWriteSpy.firstCall?.firstArg,
        );

        expect(json.pid).to.equal(process.pid);
        expect(json.level).to.equal('log');
        expect(json.message).to.equal(message);
      });

      it('should log out an error to stderr as JSON (utils.inspect)', () => {
        const message = 'message 1';

        logger.error(message);

        const json = convertInspectToJSON(
          processStderrWriteSpy.firstCall?.firstArg,
        );

        expect(json.pid).to.equal(process.pid);
        expect(json.level).to.equal('error');
        expect(json.message).to.equal(message);
      });
    });

    describe('when logging is disabled', () => {
      let processStdoutWriteSpy: sinon.SinonSpy;
      let previousLoggerRef: LoggerService;

      beforeEach(() => {
        processStdoutWriteSpy = sinon.spy(process.stdout, 'write');

        previousLoggerRef =
          Logger['localInstanceRef'] || Logger['staticInstanceRef'];
        Logger.overrideLogger(false);
      });

      afterEach(() => {
        processStdoutWriteSpy.restore();

        Logger.overrideLogger(previousLoggerRef);
      });

      it('should not print any message to the console', () => {
        const message = 'random message';
        const context = 'RandomContext';

        Logger.log(message, context);

        expect(processStdoutWriteSpy.called).to.be.false;
      });
    });
    describe('when custom logger is being used', () => {
      class CustomLogger implements LoggerService {
        log(message: any, context?: string) {}
        error(message: any, trace?: string, context?: string) {}
        warn(message: any, context?: string) {}
      }

      const customLogger = new CustomLogger();
      let previousLoggerRef: LoggerService;

      beforeEach(() => {
        previousLoggerRef =
          Logger['localInstanceRef'] || Logger['staticInstanceRef'];
        Logger.overrideLogger(customLogger);
      });

      afterEach(() => {
        Logger.overrideLogger(previousLoggerRef);
      });

      it('should call custom logger "#log()" method', () => {
        const message = 'random message';
        const context = 'RandomContext';

        const customLoggerLogSpy = sinon.spy(customLogger, 'log');

        Logger.log(message, context);

        expect(customLoggerLogSpy.called).to.be.true;
        expect(customLoggerLogSpy.calledWith(message, context)).to.be.true;
      });

      it('should call custom logger "#error()" method', () => {
        const message = 'random message';
        const context = 'RandomContext';

        const customLoggerErrorSpy = sinon.spy(customLogger, 'error');

        Logger.error(message, context);

        expect(customLoggerErrorSpy.called).to.be.true;
        expect(customLoggerErrorSpy.calledWith(message, context)).to.be.true;
      });
    });
  });

  describe('ConsoleLogger', () => {
    it('should allow setting and resetting of context', () => {
      const logger = new ConsoleLogger();
      expect(logger['context']).to.be.undefined;
      logger.setContext('context');
      expect(logger['context']).to.equal('context');
      logger.resetContext();
      expect(logger['context']).to.be.undefined;

      const loggerWithContext = new ConsoleLogger('context');
      expect(loggerWithContext['context']).to.equal('context');
      loggerWithContext.setContext('other');
      expect(loggerWithContext['context']).to.equal('other');
      loggerWithContext.resetContext();
      expect(loggerWithContext['context']).to.equal('context');
    });

    describe('functions for message', () => {
      let processStdoutWriteSpy: sinon.SinonSpy;
      const logger = new ConsoleLogger();
      const message = 'Hello World';

      beforeEach(() => {
        processStdoutWriteSpy = sinon.spy(process.stdout, 'write');
      });
      afterEach(() => {
        processStdoutWriteSpy.restore();
      });

      it('works', () => {
        logger.log(() => message);

        expect(processStdoutWriteSpy.calledOnce).to.be.true;
        expect(processStdoutWriteSpy.firstCall.firstArg).to.include(message);
        // Ensure we didn't serialize the function itself.
        expect(processStdoutWriteSpy.firstCall.firstArg).not.to.include(' => ');
        expect(processStdoutWriteSpy.firstCall.firstArg).not.to.include(
          'function',
        );
        expect(processStdoutWriteSpy.firstCall.firstArg).not.to.include(
          'Function',
        );
      });
    });

    describe('classes for message', () => {
      let processStdoutWriteSpy: sinon.SinonSpy;

      beforeEach(() => {
        processStdoutWriteSpy = sinon.spy(process.stdout, 'write');
      });
      afterEach(() => {
        processStdoutWriteSpy.restore();
      });

      it("should display class's name or empty for anonymous classes", () => {
        const logger = new ConsoleLogger();

        // in-line anonymous class
        logger.log(class {});

        // named class
        class Test {
          publicField = 'public field';
        }
        logger.log(Test);

        expect(processStdoutWriteSpy.firstCall.firstArg).to.include('');
        expect(processStdoutWriteSpy.secondCall.firstArg).to.include(Test.name);
      });
    });

    describe('forceConsole option', () => {
      let consoleLogSpy: sinon.SinonSpy;
      let consoleErrorSpy: sinon.SinonSpy;
      let processStdoutWriteStub: sinon.SinonStub;
      let processStderrWriteStub: sinon.SinonStub;

      beforeEach(() => {
        // Stub process.stdout.write to prevent actual output and track calls
        processStdoutWriteStub = sinon.stub(process.stdout, 'write');
        processStderrWriteStub = sinon.stub(process.stderr, 'write');
        consoleLogSpy = sinon.spy(console, 'log');
        consoleErrorSpy = sinon.spy(console, 'error');
      });

      afterEach(() => {
        processStdoutWriteStub.restore();
        processStderrWriteStub.restore();
        consoleLogSpy.restore();
        consoleErrorSpy.restore();
      });

      it('should use console.log instead of process.stdout.write when forceConsole is true', () => {
        const logger = new ConsoleLogger({ forceConsole: true });
        const message = 'test message';

        logger.log(message);

        // When forceConsole is true, console.log should be called
        expect(consoleLogSpy.called).to.be.true;
        expect(consoleLogSpy.firstCall.firstArg).to.include(message);
      });

      it('should use console.error instead of process.stderr.write when forceConsole is true', () => {
        const logger = new ConsoleLogger({ forceConsole: true });
        const message = 'error message';

        logger.error(message);

        expect(consoleErrorSpy.called).to.be.true;
        expect(consoleErrorSpy.firstCall.firstArg).to.include(message);
      });

      it('should use console.error for stack traces when forceConsole is true', () => {
        const logger = new ConsoleLogger({ forceConsole: true });
        const message = 'error with stack';
        const stack = 'Error: test\n    at <anonymous>:1:1';

        logger.error(message, stack);

        expect(consoleErrorSpy.calledTwice).to.be.true;
        expect(consoleErrorSpy.firstCall.firstArg).to.include(message);
        expect(consoleErrorSpy.secondCall.firstArg).to.equal(stack);
      });

      it('should use process.stdout.write when forceConsole is false', () => {
        const logger = new ConsoleLogger({ forceConsole: false });
        const message = 'test message';

        logger.log(message);

        expect(processStdoutWriteStub.called).to.be.true;
        expect(processStdoutWriteStub.firstCall.firstArg).to.include(message);
        expect(consoleLogSpy.called).to.be.false;
      });

      it('should work with JSON mode and forceConsole', () => {
        const logger = new ConsoleLogger({ json: true, forceConsole: true });
        const message = 'json message';

        logger.log(message);

        expect(consoleLogSpy.called).to.be.true;

        const output = consoleLogSpy.firstCall.firstArg;
        const json = JSON.parse(output);
        expect(json.message).to.equal(message);
      });
    });
  });

  describe('[instance methods]', () => {
    describe('when the default logger is used', () => {
      const logger = new Logger();

      let processStdoutWriteSpy: sinon.SinonSpy;
      let processStderrWriteSpy: sinon.SinonSpy;

      beforeEach(() => {
        processStdoutWriteSpy = sinon.spy(process.stdout, 'write');
        processStderrWriteSpy = sinon.spy(process.stderr, 'write');
      });

      afterEach(() => {
        processStdoutWriteSpy.restore();
        processStderrWriteSpy.restore();
      });

      it('should print one message to the console', () => {
        const message = 'random message';
        const context = 'RandomContext';

        logger.log(message, context);

        expect(processStdoutWriteSpy.calledOnce).to.be.true;
        expect(processStdoutWriteSpy.firstCall.firstArg).to.include(
          `[${context}]`,
        );
        expect(processStdoutWriteSpy.firstCall.firstArg).to.include(message);
      });

      it('should print one message without context to the console', () => {
        const message = 'random message without context';

        logger.log(message);

        expect(processStdoutWriteSpy.calledOnce).to.be.true;
        expect(processStdoutWriteSpy.firstCall.firstArg).to.include(message);
      });

      it('should print multiple messages to the console', () => {
        const messages = ['message 1', 'message 2', 'message 3'];
        const context = 'RandomContext';

        logger.log(messages[0], messages[1], messages[2], context);

        expect(processStdoutWriteSpy.calledThrice).to.be.true;
        expect(processStdoutWriteSpy.firstCall.firstArg).to.include(
          `[${context}]`,
        );
        expect(processStdoutWriteSpy.firstCall.firstArg).to.include(
          messages[0],
        );

        expect(processStdoutWriteSpy.secondCall.firstArg).to.include(
          `[${context}]`,
        );
        expect(processStdoutWriteSpy.secondCall.firstArg).to.include(
          messages[1],
        );

        expect(processStdoutWriteSpy.thirdCall.firstArg).to.include(
          `[${context}]`,
        );
        expect(processStdoutWriteSpy.thirdCall.firstArg).to.include(
          messages[2],
        );
      });

      it('should print one error to the console with context', () => {
        const message = 'random error';
        const context = 'RandomContext';

        logger.error(message, context);

        expect(processStderrWriteSpy.calledOnce).to.be.true;
        expect(processStderrWriteSpy.firstCall.firstArg).to.include(
          `[${context}]`,
        );
        expect(processStderrWriteSpy.firstCall.firstArg).to.include(message);
      });

      it('should print one error to the console with stacktrace', () => {
        const message = 'random error';
        const stacktrace = new Error('err').stack;

        logger.error(message, stacktrace);

        expect(processStderrWriteSpy.calledTwice).to.be.true;
        expect(processStderrWriteSpy.firstCall.firstArg).to.not.include(`[]`);
        expect(processStderrWriteSpy.firstCall.firstArg).to.include(message);
        expect(processStderrWriteSpy.secondCall.firstArg).to.equal(
          stacktrace + '\n',
        );
      });

      it('should print one error without context to the console', () => {
        const message = 'random error without context';

        logger.error(message);

        expect(processStderrWriteSpy.calledOnce).to.be.true;
        expect(processStderrWriteSpy.firstCall.firstArg).to.include(message);
      });

      it('should print one error with stacktrace and context to the console', () => {
        const message = 'random error with context';
        const stacktrace = 'stacktrace';
        const context = 'ErrorContext';

        logger.error(message, stacktrace, context);

        expect(processStderrWriteSpy.calledTwice).to.be.true;

        expect(processStderrWriteSpy.firstCall.firstArg).to.include(
          `[${context}]`,
        );
        expect(processStderrWriteSpy.firstCall.firstArg).to.include(message);

        expect(processStderrWriteSpy.secondCall.firstArg).to.equal(
          stacktrace + '\n',
        );
      });

      it('should print 2 errors and one stacktrace to the console', () => {
        const messages = ['message 1', 'message 2'];
        const stack = 'stacktrace';
        const context = 'RandomContext';

        logger.error(messages[0], messages[1], stack, context);

        expect(processStderrWriteSpy.calledThrice).to.be.true;
        expect(processStderrWriteSpy.firstCall.firstArg).to.include(
          `[${context}]`,
        );
        expect(processStderrWriteSpy.firstCall.firstArg).to.include(
          messages[0],
        );

        expect(processStderrWriteSpy.secondCall.firstArg).to.include(
          `[${context}]`,
        );
        expect(processStderrWriteSpy.secondCall.firstArg).to.include(
          messages[1],
        );

        expect(processStderrWriteSpy.thirdCall.firstArg).to.not.include(
          `[${context}]`,
        );
        expect(processStderrWriteSpy.thirdCall.firstArg).to.equal(stack + '\n');
      });
    });

    describe('when the default logger is used and global context is set and timestamp enabled', () => {
      const globalContext = 'GlobalContext';
      const logger = new Logger(globalContext, { timestamp: true });

      let processStdoutWriteSpy: sinon.SinonSpy;
      let processStderrWriteSpy: sinon.SinonSpy;

      beforeEach(() => {
        processStdoutWriteSpy = sinon.spy(process.stdout, 'write');
        processStderrWriteSpy = sinon.spy(process.stderr, 'write');
      });

      afterEach(() => {
        processStdoutWriteSpy.restore();
        processStderrWriteSpy.restore();
      });

      it('should print multiple messages to the console and append global context', () => {
        const messages = ['message 1', 'message 2', 'message 3'];

        logger.log(messages[0], messages[1], messages[2]);

        expect(processStdoutWriteSpy.calledThrice).to.be.true;
        expect(processStdoutWriteSpy.firstCall.firstArg).to.include(
          `[${globalContext}]`,
        );
        expect(processStdoutWriteSpy.firstCall.firstArg).to.include(
          messages[0],
        );

        expect(processStdoutWriteSpy.secondCall.firstArg).to.include(
          `[${globalContext}]`,
        );
        expect(processStdoutWriteSpy.secondCall.firstArg).to.include(
          messages[1],
        );
        expect(processStdoutWriteSpy.secondCall.firstArg).to.include('ms');

        expect(processStdoutWriteSpy.thirdCall.firstArg).to.include(
          `[${globalContext}]`,
        );
        expect(processStdoutWriteSpy.thirdCall.firstArg).to.include(
          messages[2],
        );
        expect(processStdoutWriteSpy.thirdCall.firstArg).to.include('ms');
      });
      it('should log out an error to stderr but not include an undefined log', () => {
        const message = 'message 1';

        logger.error(message);

        expect(processStderrWriteSpy.calledOnce).to.be.true;
        expect(processStderrWriteSpy.firstCall.firstArg).to.include(
          `[${globalContext}]`,
        );
        expect(processStderrWriteSpy.firstCall.firstArg).to.include(message);
      });
    });

    describe('when logging is disabled', () => {
      const logger = new Logger();

      let processStdoutWriteSpy: sinon.SinonSpy;
      let previousLoggerRef: LoggerService;

      beforeEach(() => {
        processStdoutWriteSpy = sinon.spy(process.stdout, 'write');

        previousLoggerRef =
          Logger['localInstanceRef'] || Logger['staticInstanceRef'];
        Logger.overrideLogger(false);
      });

      afterEach(() => {
        processStdoutWriteSpy.restore();

        Logger.overrideLogger(previousLoggerRef);
      });

      it('should not print any message to the console', () => {
        const message = 'random message';
        const context = 'RandomContext';

        logger.log(message, context);

        expect(processStdoutWriteSpy.called).to.be.false;
      });
    });

    describe('when custom logger is being used', () => {
      class CustomLogger implements LoggerService {
        log(message: any, context?: string) {}
        error(message: any, trace?: string, context?: string) {}
        warn(message: any, context?: string) {}
      }

      describe('with global context', () => {
        const customLogger = new CustomLogger();
        const globalContext = 'RandomContext';
        const originalLogger = new Logger(globalContext);

        let previousLoggerRef: LoggerService;

        beforeEach(() => {
          previousLoggerRef =
            Logger['localInstanceRef'] || Logger['staticInstanceRef'];
          Logger.overrideLogger(customLogger);
        });

        afterEach(() => {
          Logger.overrideLogger(previousLoggerRef);
        });

        it('should call custom logger "#log()" method with context as second argument', () => {
          const message = 'random log message with global context';

          const customLoggerLogSpy = sinon.spy(customLogger, 'log');

          originalLogger.log(message);

          expect(customLoggerLogSpy.called).to.be.true;
          expect(customLoggerLogSpy.calledWith(message, globalContext)).to.be
            .true;
        });
        it('should call custom logger "#error()" method with context as third argument', () => {
          const message = 'random error message with global context';

          const customLoggerErrorSpy = sinon.spy(customLogger, 'error');

          originalLogger.error(message);

          expect(customLoggerErrorSpy.called).to.be.true;
          expect(
            customLoggerErrorSpy.calledWith(message, undefined, globalContext),
          ).to.be.true;
        });
      });
      describe('without global context', () => {
        const customLogger = new CustomLogger();
        const originalLogger = new Logger();

        let previousLoggerRef: LoggerService;

        beforeEach(() => {
          previousLoggerRef =
            Logger['localInstanceRef'] || Logger['staticInstanceRef'];
          Logger.overrideLogger(customLogger);
        });

        afterEach(() => {
          Logger.overrideLogger(previousLoggerRef);
        });

        it('should call custom logger "#log()" method', () => {
          const message = 'random message';
          const context = 'RandomContext';

          const customLoggerLogSpy = sinon.spy(customLogger, 'log');

          originalLogger.log(message, context);

          expect(customLoggerLogSpy.called).to.be.true;
          expect(customLoggerLogSpy.calledWith(message, context)).to.be.true;
        });

        it('should call custom logger "#error()" method', () => {
          const message = 'random message';
          const context = 'RandomContext';

          const customLoggerErrorSpy = sinon.spy(customLogger, 'error');

          originalLogger.error(message, undefined, context);

          expect(customLoggerErrorSpy.called).to.be.true;
          expect(customLoggerErrorSpy.calledWith(message, undefined, context))
            .to.be.true;
        });
      });
    });
  });
  describe('ConsoleLogger', () => {
    let processStdoutWriteSpy: sinon.SinonSpy;

    beforeEach(() => {
      processStdoutWriteSpy = sinon.spy(process.stdout, 'write');
    });
    afterEach(() => {
      processStdoutWriteSpy.restore();
    });

    it('should respect maxStringLength when set to 0', () => {
      const consoleLogger = new ConsoleLogger({
        colors: false,
        compact: false,
        maxStringLength: 0,
      });

      consoleLogger.log({ name: 'abcdef' });

      expect(processStdoutWriteSpy.calledOnce).to.be.true;
      expect(processStdoutWriteSpy.firstCall.firstArg).to.include(
        "''... 6 more characters",
      );
    });

    it('should respect maxArrayLength when set to 0', () => {
      const consoleLogger = new ConsoleLogger({
        colors: false,
        compact: false,
        maxArrayLength: 0,
      });

      consoleLogger.log({ items: ['a', 'b', 'c'] });

      expect(processStdoutWriteSpy.calledOnce).to.be.true;
      expect(processStdoutWriteSpy.firstCall.firstArg).to.include(
        '... 3 more items',
      );
    });

    it('should support custom formatter', () => {
      class CustomConsoleLogger extends ConsoleLogger {
        protected formatMessage(
          logLevel: LogLevel,
          message: unknown,
          pidMessage: string,
          formattedLogLevel: string,
          contextMessage: string,
          timestampDiff: string,
        ) {
          return `Prefix: ${message as string}`;
        }
      }

      const consoleLogger = new CustomConsoleLogger();
      consoleLogger.debug('test');

      expect(processStdoutWriteSpy.firstCall.firstArg).to.equal(`Prefix: test`);
    });

    it('should support custom formatter and colorizer', () => {
      class CustomConsoleLogger extends ConsoleLogger {
        protected formatMessage(
          logLevel: LogLevel,
          message: unknown,
          pidMessage: string,
          formattedLogLevel: string,
          contextMessage: string,
          timestampDiff: string,
        ) {
          const strMessage = this.stringifyMessage(message, logLevel);
          return `Prefix: ${strMessage}`;
        }

        protected colorize(message: string, logLevel: LogLevel): string {
          return `~~~${message}~~~`;
        }
      }

      const consoleLogger = new CustomConsoleLogger();
      consoleLogger.debug('test');

      expect(processStdoutWriteSpy.firstCall.firstArg).to.equal(
        `Prefix: ~~~test~~~`,
      );
    });

    it('should stringify messages', () => {
      class CustomConsoleLogger extends ConsoleLogger {
        protected colorize(message: string, _: LogLevel): string {
          return message;
        }
      }

      const consoleLogger = new CustomConsoleLogger({ colors: false });
      const consoleLoggerSpy = sinon.spy(
        consoleLogger,
        'stringifyMessage' as keyof ConsoleLogger,
      );
      consoleLogger.debug(
        'str1',
        { key: 'str2' },
        ['str3'],
        [{ key: 'str4' }],
        null,
        1,
      );

      expect(consoleLoggerSpy.getCall(0).returnValue).to.equal('str1');
      expect(consoleLoggerSpy.getCall(1).returnValue).to.equal(
        `Object(1) {
  key: 'str2'
}`,
      );
      expect(consoleLoggerSpy.getCall(2).returnValue).to.equal(
        `Array(1) [
  'str3'
]`,
      );
      expect(consoleLoggerSpy.getCall(3).returnValue).to.equal(
        `Array(1) [
  {
    key: 'str4'
  }
]`,
      );
      expect(consoleLoggerSpy.getCall(4).returnValue).to.equal('null');
      expect(consoleLoggerSpy.getCall(5).returnValue).to.equal('1');
    });
  });
});

function convertInspectToJSON(inspectOutput: string) {
  const jsonLikeString = inspectOutput
    .replace(/'([^']+)'/g, '"$1"') // single-quoted strings
    .replace(/([a-zA-Z0-9_]+):/g, '"$1":') // unquoted object keys
    .replace(/\bundefined\b/g, 'null')
    .replace(/\[Function(: [^\]]+)?\]/g, '"[Function]"')
    .replace(/\[Circular\]/g, '"[Circular]"');

  try {
    return JSON.parse(jsonLikeString);
  } catch (error) {
    console.error('Error parsing the modified inspect output:', error);
    throw error;
  }
}
