// src/services/logger.server.test.ts
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';

// Unmock the module we are testing to override the global mock from setupFiles.
vi.unmock('./logger.server');

// Mock fs to prevent actual file system operations
vi.mock('fs', () => ({
  default: {
    existsSync: vi.fn(() => true),
    mkdirSync: vi.fn(),
  },
  existsSync: vi.fn(() => true),
  mkdirSync: vi.fn(),
}));

// Create mock objects for pino's multistream functionality
const mockDestinationStream = { write: vi.fn() };
const mockMultistream = { write: vi.fn() };

// Mock pino before importing the logger
// The new logger uses pino.destination() and pino.multistream() for production/test
const mockLoggerInstance = {
  info: vi.fn(),
  warn: vi.fn(),
  error: vi.fn(),
  debug: vi.fn(),
  level: 'info',
  child: vi.fn(() => mockLoggerInstance),
};

// Create a properly typed mock that includes pino's static methods
const mockDestination = vi.fn(() => mockDestinationStream);
const mockMultistreamFn = vi.fn(() => mockMultistream);

const pinoMock = Object.assign(
  vi.fn(() => mockLoggerInstance),
  {
    destination: mockDestination,
    multistream: mockMultistreamFn,
  },
);

vi.mock('pino', () => ({ default: pinoMock }));

describe('Server Logger', () => {
  beforeEach(() => {
    // Reset modules to ensure process.env changes are applied to new module instances
    vi.resetModules();
    vi.clearAllMocks();
    vi.unstubAllEnvs();
  });

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

  it('should initialize pino with multistream for production (stdout + file)', async () => {
    vi.stubEnv('NODE_ENV', 'production');
    await import('./logger.server');

    // Production uses pino.destination for file output
    expect(mockDestination).toHaveBeenCalledWith(
      expect.objectContaining({
        dest: expect.stringContaining('app.log'),
        sync: false,
        mkdir: true,
      }),
    );

    // Production uses pino.multistream to combine stdout and file streams
    expect(mockMultistreamFn).toHaveBeenCalledWith(
      expect.arrayContaining([
        expect.objectContaining({ stream: process.stdout }),
        expect.objectContaining({ stream: mockDestinationStream }),
      ]),
    );

    // pino is called with level 'info' for production
    expect(pinoMock).toHaveBeenCalledWith(
      expect.objectContaining({ level: 'info' }),
      mockMultistream,
    );
  });

  it('should initialize pino with pretty-print transport for development', async () => {
    vi.stubEnv('NODE_ENV', 'development');
    await import('./logger.server');

    // Development does NOT use destination or multistream
    expect(mockDestination).not.toHaveBeenCalled();
    expect(mockMultistreamFn).not.toHaveBeenCalled();

    // Development uses pino-pretty transport
    expect(pinoMock).toHaveBeenCalledWith(
      expect.objectContaining({
        level: 'debug',
        transport: expect.objectContaining({
          target: 'pino-pretty',
        }),
      }),
    );
  });

  it('should initialize pino with multistream for test (stdout + file)', async () => {
    // This is the default for vitest, but we stub it for clarity.
    vi.stubEnv('NODE_ENV', 'test');
    await import('./logger.server');

    // Test env also uses file logging like production
    expect(mockDestination).toHaveBeenCalledWith(
      expect.objectContaining({
        dest: expect.stringContaining('app.log'),
        sync: false,
        mkdir: true,
      }),
    );

    expect(mockMultistreamFn).toHaveBeenCalled();

    // Test uses debug level
    expect(pinoMock).toHaveBeenCalledWith(
      expect.objectContaining({ level: 'debug' }),
      mockMultistream,
    );
  });

  it('should use LOG_DIR environment variable when set', async () => {
    vi.stubEnv('NODE_ENV', 'production');
    vi.stubEnv('LOG_DIR', '/custom/log/dir');
    await import('./logger.server');

    // Should use the custom LOG_DIR in the file path
    expect(mockDestination).toHaveBeenCalledWith(
      expect.objectContaining({
        dest: '/custom/log/dir/app.log',
      }),
    );
  });

  it('should fall back to stdout only when log directory creation fails', async () => {
    vi.stubEnv('NODE_ENV', 'production');

    // Mock fs.existsSync to return false (dir doesn't exist)
    // and mkdirSync to throw an error
    const fs = await import('fs');
    vi.mocked(fs.default.existsSync).mockReturnValue(false);
    vi.mocked(fs.default.mkdirSync).mockImplementation(() => {
      throw new Error('Permission denied');
    });

    // Suppress console.error during this test
    const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});

    await import('./logger.server');

    // Should have tried to create directory
    expect(fs.default.mkdirSync).toHaveBeenCalled();

    // Should log error to console
    expect(consoleErrorSpy).toHaveBeenCalledWith(
      expect.stringContaining('Failed to create log directory'),
      expect.any(Error),
    );

    // Should fall back to stdout-only logger (no multistream)
    // When logDir is null, pino is called without multistream
    expect(pinoMock).toHaveBeenCalledWith(expect.objectContaining({ level: 'info' }));

    consoleErrorSpy.mockRestore();
  });

  describe('createScopedLogger', () => {
    it('should create a child logger with module name', async () => {
      vi.stubEnv('NODE_ENV', 'production');
      const { createScopedLogger } = await import('./logger.server');

      const scopedLogger = createScopedLogger('test-module');

      expect(mockLoggerInstance.child).toHaveBeenCalledWith(
        expect.objectContaining({ module: 'test-module' }),
      );
      expect(scopedLogger).toBeDefined();
    });

    it('should enable debug level when DEBUG_MODULES includes module name', async () => {
      vi.stubEnv('NODE_ENV', 'production');
      vi.stubEnv('DEBUG_MODULES', 'test-module,other-module');
      const { createScopedLogger } = await import('./logger.server');

      createScopedLogger('test-module');

      expect(mockLoggerInstance.child).toHaveBeenCalledWith(
        expect.objectContaining({
          module: 'test-module',
          level: 'debug',
        }),
      );
    });

    it('should enable debug level when DEBUG_MODULES includes wildcard', async () => {
      vi.stubEnv('NODE_ENV', 'production');
      vi.stubEnv('DEBUG_MODULES', '*');
      const { createScopedLogger } = await import('./logger.server');

      createScopedLogger('any-module');

      expect(mockLoggerInstance.child).toHaveBeenCalledWith(
        expect.objectContaining({
          module: 'any-module',
          level: 'debug',
        }),
      );
    });

    it('should use default level when module not in DEBUG_MODULES', async () => {
      vi.stubEnv('NODE_ENV', 'production');
      vi.stubEnv('DEBUG_MODULES', 'other-module');
      const { createScopedLogger } = await import('./logger.server');

      createScopedLogger('test-module');

      expect(mockLoggerInstance.child).toHaveBeenCalledWith(
        expect.objectContaining({
          module: 'test-module',
          level: 'info', // Uses logger.level which is 'info'
        }),
      );
    });

    it('should handle empty DEBUG_MODULES', async () => {
      vi.stubEnv('NODE_ENV', 'production');
      vi.stubEnv('DEBUG_MODULES', '');
      const { createScopedLogger } = await import('./logger.server');

      createScopedLogger('test-module');

      expect(mockLoggerInstance.child).toHaveBeenCalledWith(
        expect.objectContaining({
          module: 'test-module',
          level: 'info',
        }),
      );
    });
  });

  describe('redaction configuration', () => {
    it('should configure redaction for sensitive fields', async () => {
      // Reset fs mock to ensure directory creation succeeds
      const fs = await import('fs');
      vi.mocked(fs.default.existsSync).mockReturnValue(true);

      vi.stubEnv('NODE_ENV', 'production');
      await import('./logger.server');

      // Verify redact configuration is passed to pino
      // When log directory exists, pino is called with config and multistream
      expect(pinoMock).toHaveBeenCalledWith(
        expect.objectContaining({
          redact: expect.objectContaining({
            paths: expect.arrayContaining([
              'req.headers.authorization',
              'req.headers.cookie',
              '*.body.password',
              '*.body.newPassword',
              '*.body.currentPassword',
              '*.body.confirmPassword',
              '*.body.refreshToken',
              '*.body.token',
            ]),
            censor: '[REDACTED]',
          }),
        }),
        expect.anything(),
      );
    });
  });

  describe('environment detection', () => {
    it('should treat undefined NODE_ENV as development', async () => {
      vi.stubEnv('NODE_ENV', '');
      await import('./logger.server');

      // Development uses pino-pretty transport
      expect(pinoMock).toHaveBeenCalledWith(
        expect.objectContaining({
          transport: expect.objectContaining({
            target: 'pino-pretty',
          }),
        }),
      );
    });
  });
});
