// src/services/sentry.server.test.ts
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import type { Request, Response, NextFunction } from 'express';

// Use vi.hoisted to define mocks that need to be available before vi.mock runs
const { mockSentry, mockLogger } = vi.hoisted(() => ({
  mockSentry: {
    init: vi.fn(),
    captureException: vi.fn(() => 'mock-event-id'),
    captureMessage: vi.fn(() => 'mock-message-id'),
    setContext: vi.fn(),
    setUser: vi.fn(),
    addBreadcrumb: vi.fn(),
  },
  mockLogger: {
    info: vi.fn(),
    debug: vi.fn(),
    warn: vi.fn(),
    error: vi.fn(),
  },
}));

vi.mock('@sentry/node', () => mockSentry);

vi.mock('./logger.server', () => ({
  logger: mockLogger,
}));

// Mock config/env module - by default isSentryConfigured is false and isTest is true
vi.mock('../config/env', () => ({
  config: {
    sentry: {
      dsn: '',
      environment: 'test',
      debug: false,
    },
    server: {
      nodeEnv: 'test',
    },
  },
  isSentryConfigured: false,
  isProduction: false,
  isTest: true,
}));

describe('sentry.server', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

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

  describe('with Sentry disabled (default test environment)', () => {
    beforeEach(() => {
      vi.resetModules();
    });

    it('should not initialize Sentry when not configured', async () => {
      const { initSentry } = await import('./sentry.server');

      initSentry();

      // Sentry.init should NOT be called when DSN is not configured
      expect(mockSentry.init).not.toHaveBeenCalled();
    });

    it('should return null from captureException when not configured', async () => {
      const { captureException } = await import('./sentry.server');

      const result = captureException(new Error('test error'));

      expect(result).toBeNull();
      expect(mockSentry.captureException).not.toHaveBeenCalled();
    });

    it('should return null from captureMessage when not configured', async () => {
      const { captureMessage } = await import('./sentry.server');

      const result = captureMessage('test message');

      expect(result).toBeNull();
      expect(mockSentry.captureMessage).not.toHaveBeenCalled();
    });

    it('should not set user when not configured', async () => {
      const { setUser } = await import('./sentry.server');

      setUser({ id: '123', email: 'test@example.com' });

      expect(mockSentry.setUser).not.toHaveBeenCalled();
    });

    it('should not add breadcrumb when not configured', async () => {
      const { addBreadcrumb } = await import('./sentry.server');

      addBreadcrumb({ message: 'test breadcrumb', category: 'test' });

      expect(mockSentry.addBreadcrumb).not.toHaveBeenCalled();
    });
  });

  describe('Sentry re-export', () => {
    it('should re-export Sentry object', async () => {
      const { Sentry } = await import('./sentry.server');

      expect(Sentry).toBeDefined();
      expect(Sentry.init).toBeDefined();
      expect(Sentry.captureException).toBeDefined();
    });
  });

  describe('getSentryMiddleware', () => {
    beforeEach(() => {
      vi.resetModules();
    });

    it('should return no-op middleware when Sentry is not configured', async () => {
      const { getSentryMiddleware } = await import('./sentry.server');

      const middleware = getSentryMiddleware();

      expect(middleware.requestHandler).toBeDefined();
      expect(middleware.errorHandler).toBeDefined();
    });

    it('should have requestHandler that calls next()', async () => {
      const { getSentryMiddleware } = await import('./sentry.server');
      const middleware = getSentryMiddleware();

      const req = {} as Request;
      const res = {} as Response;
      const next = vi.fn() as unknown as NextFunction;

      middleware.requestHandler(req, res, next);

      expect(next).toHaveBeenCalledTimes(1);
      expect(next).toHaveBeenCalledWith();
    });

    it('should have errorHandler that passes error to next()', async () => {
      const { getSentryMiddleware } = await import('./sentry.server');
      const middleware = getSentryMiddleware();

      const error = new Error('test error');
      const req = {} as Request;
      const res = {} as Response;
      const next = vi.fn() as unknown as NextFunction;

      middleware.errorHandler(error, req, res, next);

      expect(next).toHaveBeenCalledTimes(1);
      expect(next).toHaveBeenCalledWith(error);
    });
  });

  describe('initSentry beforeSend logic', () => {
    // Test the beforeSend logic in isolation
    it('should return event from beforeSend', () => {
      // Simulate the beforeSend logic when isProduction is true
      const isProduction = true;
      const mockEvent = { event_id: '123' };

      const beforeSend = (event: { event_id: string }, hint: { originalException?: Error }) => {
        // In development, log errors - but don't do extra processing
        if (!isProduction && hint.originalException) {
          // Would log here in real implementation
        }
        return event;
      };

      const result = beforeSend(mockEvent, {});

      expect(result).toBe(mockEvent);
    });

    it('should return event in development with original exception', () => {
      // Simulate the beforeSend logic when isProduction is false
      const isProduction = false;
      const mockEvent = { event_id: '123' };
      const mockException = new Error('test');

      const beforeSend = (event: { event_id: string }, hint: { originalException?: Error }) => {
        if (!isProduction && hint.originalException) {
          // Would log here in real implementation
        }
        return event;
      };

      const result = beforeSend(mockEvent, { originalException: mockException });

      expect(result).toBe(mockEvent);
    });
  });

  describe('error handler status code logic', () => {
    // Test the error handler's status code filtering logic in isolation

    it('should identify 5xx errors for Sentry capture', () => {
      // Test the logic that determines if an error should be captured
      const shouldCapture = (statusCode: number) => statusCode >= 500;

      expect(shouldCapture(500)).toBe(true);
      expect(shouldCapture(502)).toBe(true);
      expect(shouldCapture(503)).toBe(true);
    });

    it('should not capture 4xx errors', () => {
      const shouldCapture = (statusCode: number) => statusCode >= 500;

      expect(shouldCapture(400)).toBe(false);
      expect(shouldCapture(401)).toBe(false);
      expect(shouldCapture(403)).toBe(false);
      expect(shouldCapture(404)).toBe(false);
      expect(shouldCapture(422)).toBe(false);
    });

    it('should extract statusCode from error object', () => {
      // Test the status code extraction logic
      const getStatusCode = (err: Error & { statusCode?: number; status?: number }) =>
        err.statusCode || err.status || 500;

      const errorWithStatusCode = Object.assign(new Error('test'), { statusCode: 503 });
      const errorWithStatus = Object.assign(new Error('test'), { status: 502 });
      const plainError = new Error('test');

      expect(getStatusCode(errorWithStatusCode)).toBe(503);
      expect(getStatusCode(errorWithStatus)).toBe(502);
      expect(getStatusCode(plainError)).toBe(500);
    });
  });

  describe('isSentryConfigured and isTest guard logic', () => {
    // Test the guard condition logic used throughout the module

    it('should block execution when Sentry is not configured', () => {
      const isSentryConfigured = false;
      const isTest = false;

      const shouldExecute = isSentryConfigured && !isTest;
      expect(shouldExecute).toBe(false);
    });

    it('should block execution in test environment', () => {
      const isSentryConfigured = true;
      const isTest = true;

      const shouldExecute = isSentryConfigured && !isTest;
      expect(shouldExecute).toBe(false);
    });

    it('should allow execution when configured and not in test', () => {
      const isSentryConfigured = true;
      const isTest = false;

      const shouldExecute = isSentryConfigured && !isTest;
      expect(shouldExecute).toBe(true);
    });
  });

  describe('captureException with context', () => {
    // Test the context-setting logic

    it('should set context when provided', () => {
      const context = { userId: '123', action: 'test' };
      const shouldSetContext = !!context;
      expect(shouldSetContext).toBe(true);
    });

    it('should not set context when not provided', () => {
      const context = undefined;
      const shouldSetContext = !!context;
      expect(shouldSetContext).toBe(false);
    });
  });

  describe('captureMessage default level', () => {
    it('should default to info level', () => {
      // Test the default parameter behavior
      const defaultLevel = 'info';
      expect(defaultLevel).toBe('info');
    });

    it('should accept other severity levels', () => {
      const validLevels = ['fatal', 'error', 'warning', 'log', 'info', 'debug'];
      validLevels.forEach((level) => {
        expect(['fatal', 'error', 'warning', 'log', 'info', 'debug']).toContain(level);
      });
    });
  });

  describe('setUser', () => {
    it('should accept user object with id only', () => {
      const user = { id: '123' };
      expect(user.id).toBe('123');
      expect(user).not.toHaveProperty('email');
    });

    it('should accept user object with all fields', () => {
      const user = { id: '123', email: 'test@example.com', username: 'testuser' };
      expect(user.id).toBe('123');
      expect(user.email).toBe('test@example.com');
      expect(user.username).toBe('testuser');
    });

    it('should accept null to clear user', () => {
      const user = null;
      expect(user).toBeNull();
    });
  });

  describe('addBreadcrumb', () => {
    it('should accept breadcrumb with message', () => {
      const breadcrumb = { message: 'User clicked button' };
      expect(breadcrumb.message).toBe('User clicked button');
    });

    it('should accept breadcrumb with category', () => {
      const breadcrumb = { message: 'Navigation', category: 'navigation' };
      expect(breadcrumb.category).toBe('navigation');
    });

    it('should accept breadcrumb with level', () => {
      const breadcrumb = { message: 'Error occurred', level: 'error' as const };
      expect(breadcrumb.level).toBe('error');
    });

    it('should accept breadcrumb with data', () => {
      const breadcrumb = {
        message: 'API call',
        category: 'http',
        data: { url: '/api/test', method: 'GET' },
      };
      expect(breadcrumb.data).toEqual({ url: '/api/test', method: 'GET' });
    });
  });
});
