// Mock logger module first to prevent winston initialization issues
const mockLoggerWarn = jest.fn();
const mockLoggerError = jest.fn();
const mockLoggerInfo = jest.fn();

jest.mock('../../../utils/logger', () => ({
  withRequestId: jest.fn(() => ({
    warn: mockLoggerWarn,
    error: mockLoggerError,
    info: mockLoggerInfo
  }))
}));

// Mock crypto module with both randomBytes and createHash
jest.mock('crypto', () => ({
  randomBytes: jest.fn(() => ({
    toString: jest.fn(() => 'mocked-hex-string-1234567890abcdef')
  })),
  createHash: jest.fn(() => ({
    update: jest.fn().mockReturnThis(),
    digest: jest.fn(() => 'mocked-hash')
  }))
}));

const {
  enforceHTTPS,
  securityHeaders,
  addRequestId,
  logSecurityEvent,
  sanitizeError
} = require('../../../middleware/security');

describe('Security Middleware', () => {
  let req, res, next, consoleSpy, consoleWarnSpy, consoleErrorSpy;

  beforeEach(() => {
    req = {
      secure: false,
      headers: {},
      protocol: 'http',
      url: '/test-path',
      ip: '127.0.0.1',
      connection: { remoteAddress: '127.0.0.1' },
      get: jest.fn(),
      user: null
    };
    res = {
      redirect: jest.fn(),
      setHeader: jest.fn(),
      status: jest.fn().mockReturnThis(),
      json: jest.fn()
    };
    next = jest.fn();

    // Mock console methods
    consoleSpy = jest.spyOn(console, 'log').mockImplementation();
    consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
    consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();

    jest.clearAllMocks();
  });

  afterEach(() => {
    consoleSpy.mockRestore();
    consoleWarnSpy.mockRestore();
    consoleErrorSpy.mockRestore();
  });

  describe('enforceHTTPS', () => {
    describe('Development environment', () => {
      it('should skip HTTPS enforcement in dev environment', () => {
        process.env.NODE_ENV = 'dev';

        enforceHTTPS(req, res, next);

        expect(next).toHaveBeenCalled();
        expect(res.redirect).not.toHaveBeenCalled();
        expect(res.setHeader).not.toHaveBeenCalled();
      });

      it('should skip HTTPS enforcement in development environment', () => {
        process.env.NODE_ENV = 'development';

        enforceHTTPS(req, res, next);

        expect(next).toHaveBeenCalled();
        expect(res.redirect).not.toHaveBeenCalled();
        expect(res.setHeader).not.toHaveBeenCalled();
      });
    });

    describe('Production environment', () => {
      beforeEach(() => {
        process.env.NODE_ENV = 'production';
        process.env.FRONTEND_URL = 'example.com';
      });

      describe('HTTPS detection', () => {
        it('should detect HTTPS from req.secure', () => {
          req.secure = true;

          enforceHTTPS(req, res, next);

          expect(next).toHaveBeenCalled();
          expect(res.redirect).not.toHaveBeenCalled();
          expect(res.setHeader).toHaveBeenCalledWith(
            'Strict-Transport-Security',
            'max-age=31536000; includeSubDomains; preload'
          );
        });

        it('should detect HTTPS from x-forwarded-proto header', () => {
          req.headers['x-forwarded-proto'] = 'https';

          enforceHTTPS(req, res, next);

          expect(next).toHaveBeenCalled();
          expect(res.redirect).not.toHaveBeenCalled();
          expect(res.setHeader).toHaveBeenCalledWith(
            'Strict-Transport-Security',
            'max-age=31536000; includeSubDomains; preload'
          );
        });

        it('should detect HTTPS from req.protocol', () => {
          req.protocol = 'https';

          enforceHTTPS(req, res, next);

          expect(next).toHaveBeenCalled();
          expect(res.redirect).not.toHaveBeenCalled();
          expect(res.setHeader).toHaveBeenCalledWith(
            'Strict-Transport-Security',
            'max-age=31536000; includeSubDomains; preload'
          );
        });
      });

      describe('HTTP to HTTPS redirect', () => {
        it('should redirect HTTP requests to HTTPS', () => {
          req.headers.host = 'example.com';

          enforceHTTPS(req, res, next);

          expect(res.redirect).toHaveBeenCalledWith(301, 'https://example.com/test-path');
          expect(next).not.toHaveBeenCalled();
        });

        it('should handle requests with query parameters', () => {
          req.url = '/test-path?param=value';
          req.headers.host = 'example.com';

          enforceHTTPS(req, res, next);

          expect(res.redirect).toHaveBeenCalledWith(301, 'https://example.com/test-path?param=value');
        });

        it('should log warning for host header mismatch', () => {
          req.headers.host = 'malicious.com';
          req.ip = '192.168.1.1';

          enforceHTTPS(req, res, next);

          expect(mockLoggerWarn).toHaveBeenCalledWith(
            'Host header mismatch during HTTPS redirect',
            {
              requestHost: 'malicious.com',
              allowedHost: 'example.com',
              ip: '192.168.1.1',
              url: '/test-path',
              eventType: 'SECURITY_HOST_MISMATCH'
            }
          );
          expect(res.redirect).toHaveBeenCalledWith(301, 'https://example.com/test-path');
        });

        it('should not log warning when host matches allowed host', () => {
          req.headers.host = 'example.com';

          enforceHTTPS(req, res, next);

          expect(mockLoggerWarn).not.toHaveBeenCalled();
          expect(res.redirect).toHaveBeenCalledWith(301, 'https://example.com/test-path');
        });

        it('should use FRONTEND_URL as allowed host', () => {
          process.env.FRONTEND_URL = 'secure-site.com';
          req.headers.host = 'different.com';

          enforceHTTPS(req, res, next);

          expect(res.redirect).toHaveBeenCalledWith(301, 'https://secure-site.com/test-path');
        });
      });

      describe('Edge cases', () => {
        it('should handle missing host header', () => {
          delete req.headers.host;

          enforceHTTPS(req, res, next);

          expect(res.redirect).toHaveBeenCalledWith(301, 'https://example.com/test-path');
        });

        it('should handle empty URL', () => {
          req.url = '';
          req.headers.host = 'example.com';

          enforceHTTPS(req, res, next);

          expect(res.redirect).toHaveBeenCalledWith(301, 'https://example.com');
        });

        it('should handle root path', () => {
          req.url = '/';
          req.headers.host = 'example.com';

          enforceHTTPS(req, res, next);

          expect(res.redirect).toHaveBeenCalledWith(301, 'https://example.com/');
        });
      });
    });
  });

  describe('securityHeaders', () => {
    it('should set X-Content-Type-Options header', () => {
      securityHeaders(req, res, next);

      expect(res.setHeader).toHaveBeenCalledWith('X-Content-Type-Options', 'nosniff');
    });

    it('should set X-Frame-Options header', () => {
      securityHeaders(req, res, next);

      expect(res.setHeader).toHaveBeenCalledWith('X-Frame-Options', 'DENY');
    });

    it('should set Referrer-Policy header', () => {
      securityHeaders(req, res, next);

      expect(res.setHeader).toHaveBeenCalledWith('Referrer-Policy', 'strict-origin-when-cross-origin');
    });

    it('should set Permissions-Policy header', () => {
      securityHeaders(req, res, next);

      expect(res.setHeader).toHaveBeenCalledWith(
        'Permissions-Policy',
        'camera=(), microphone=(), geolocation=(self)'
      );
    });

    it('should call next after setting headers', () => {
      securityHeaders(req, res, next);

      expect(next).toHaveBeenCalled();
    });

    it('should set all security headers in one call', () => {
      securityHeaders(req, res, next);

      expect(res.setHeader).toHaveBeenCalledTimes(4);
      expect(res.setHeader).toHaveBeenNthCalledWith(1, 'X-Content-Type-Options', 'nosniff');
      expect(res.setHeader).toHaveBeenNthCalledWith(2, 'X-Frame-Options', 'DENY');
      expect(res.setHeader).toHaveBeenNthCalledWith(3, 'Referrer-Policy', 'strict-origin-when-cross-origin');
      expect(res.setHeader).toHaveBeenNthCalledWith(4, 'Permissions-Policy', 'camera=(), microphone=(), geolocation=(self)');
    });
  });

  describe('addRequestId', () => {
    const crypto = require('crypto');

    it('should generate and set request ID', () => {
      addRequestId(req, res, next);

      expect(crypto.randomBytes).toHaveBeenCalledWith(16);
      expect(req.id).toBe('mocked-hex-string-1234567890abcdef');
      expect(res.setHeader).toHaveBeenCalledWith('X-Request-ID', 'mocked-hex-string-1234567890abcdef');
      expect(next).toHaveBeenCalled();
    });

    it('should generate unique IDs for different requests', () => {
      const mockRandomBytes = require('crypto').randomBytes;

      // First call
      mockRandomBytes.mockReturnValueOnce({
        toString: jest.fn(() => 'first-request-id')
      });

      addRequestId(req, res, next);
      expect(req.id).toBe('first-request-id');

      // Reset for second call
      jest.clearAllMocks();
      const req2 = { ...req };
      const res2 = { ...res, setHeader: jest.fn() };
      const next2 = jest.fn();

      // Second call
      mockRandomBytes.mockReturnValueOnce({
        toString: jest.fn(() => 'second-request-id')
      });

      addRequestId(req2, res2, next2);
      expect(req2.id).toBe('second-request-id');
    });

    it('should call toString with hex parameter', () => {
      const mockToString = jest.fn(() => 'hex-string');
      require('crypto').randomBytes.mockReturnValueOnce({
        toString: mockToString
      });

      addRequestId(req, res, next);

      expect(mockToString).toHaveBeenCalledWith('hex');
    });
  });

  describe('logSecurityEvent', () => {
    beforeEach(() => {
      req.id = 'test-request-id';
      req.ip = '192.168.1.1';
      req.get = jest.fn((header) => {
        if (header === 'user-agent') return 'Mozilla/5.0 Test Browser';
        return null;
      });
    });

    describe('Production environment', () => {
      beforeEach(() => {
        process.env.NODE_ENV = 'production';
      });

      it('should log security event with structured data', () => {
        const eventType = 'LOGIN_ATTEMPT';
        const details = { username: 'testuser', success: false };

        logSecurityEvent(eventType, details, req);

        expect(mockLoggerWarn).toHaveBeenCalledWith(
          'Security event: LOGIN_ATTEMPT',
          {
            eventType: 'LOGIN_ATTEMPT',
            ip: '192.168.1.1',
            userAgent: 'Mozilla/5.0 Test Browser',
            userId: 'anonymous',
            username: 'testuser',
            success: false
          }
        );
      });

      it('should include user ID when user is authenticated', () => {
        req.user = { id: 123 };
        const eventType = 'DATA_ACCESS';
        const details = { resource: '/api/users' };

        logSecurityEvent(eventType, details, req);

        expect(mockLoggerWarn).toHaveBeenCalledWith(
          'Security event: DATA_ACCESS',
          expect.objectContaining({
            userId: 123,
            resource: '/api/users'
          })
        );
      });

      it('should handle missing request ID', () => {
        delete req.id;
        const eventType = 'SUSPICIOUS_ACTIVITY';
        const details = { reason: 'Multiple failed attempts' };

        logSecurityEvent(eventType, details, req);

        expect(mockLoggerWarn).toHaveBeenCalledWith(
          'Security event: SUSPICIOUS_ACTIVITY',
          expect.objectContaining({
            reason: 'Multiple failed attempts'
          })
        );
      });

      it('should handle missing IP address', () => {
        delete req.ip;
        req.connection.remoteAddress = '10.0.0.1';
        const eventType = 'IP_CHECK';
        const details = { status: 'blocked' };

        logSecurityEvent(eventType, details, req);

        expect(mockLoggerWarn).toHaveBeenCalledWith(
          'Security event: IP_CHECK',
          expect.objectContaining({
            ip: '10.0.0.1',
            status: 'blocked'
          })
        );
      });

      it('should call logger with event type and details', () => {
        const eventType = 'TEST_EVENT';
        const details = {};

        logSecurityEvent(eventType, details, req);

        expect(mockLoggerWarn).toHaveBeenCalledWith(
          'Security event: TEST_EVENT',
          expect.objectContaining({
            eventType: 'TEST_EVENT',
            ip: '192.168.1.1'
          })
        );
      });
    });

    describe('Non-production environment', () => {
      beforeEach(() => {
        process.env.NODE_ENV = 'development';
      });

      it('should log security event using logger', () => {
        const eventType = 'LOGIN_ATTEMPT';
        const details = { username: 'testuser', success: false };

        logSecurityEvent(eventType, details, req);

        expect(mockLoggerWarn).toHaveBeenCalledWith(
          'Security event: LOGIN_ATTEMPT',
          expect.objectContaining({
            eventType: 'LOGIN_ATTEMPT',
            username: 'testuser',
            success: false
          })
        );
      });

      it('should use structured logging in development', () => {
        const eventType = 'TEST_EVENT';
        const details = { test: true };

        logSecurityEvent(eventType, details, req);

        expect(mockLoggerWarn).toHaveBeenCalledWith(
          'Security event: TEST_EVENT',
          expect.objectContaining({
            eventType: 'TEST_EVENT',
            test: true
          })
        );
      });
    });

    describe('Edge cases', () => {
      it('should handle missing user-agent header', () => {
        req.get.mockReturnValue(null);
        process.env.NODE_ENV = 'production';

        logSecurityEvent('TEST', {}, req);

        expect(mockLoggerWarn).toHaveBeenCalledWith(
          'Security event: TEST',
          expect.objectContaining({
            userAgent: null
          })
        );
      });

      it('should handle empty details object', () => {
        process.env.NODE_ENV = 'production';

        logSecurityEvent('EMPTY_DETAILS', {}, req);

        expect(mockLoggerWarn).toHaveBeenCalledWith(
          'Security event: EMPTY_DETAILS',
          expect.objectContaining({
            eventType: 'EMPTY_DETAILS'
          })
        );
      });
    });
  });

  describe('sanitizeError', () => {
    beforeEach(() => {
      req.id = 'test-request-id';
      req.user = { id: 123 };
    });

    describe('Client error responses (4xx)', () => {
      it('should handle 400 Bad Request errors', () => {
        const error = new Error('Invalid input data');
        error.status = 400;

        sanitizeError(error, req, res, next);

        expect(res.status).toHaveBeenCalledWith(400);
        expect(res.json).toHaveBeenCalledWith({
          error: 'Invalid input data',
          requestId: 'test-request-id'
        });
      });

      it('should handle 400 errors with default message', () => {
        const error = new Error();
        error.status = 400;

        sanitizeError(error, req, res, next);

        expect(res.json).toHaveBeenCalledWith({
          error: 'Bad Request',
          requestId: 'test-request-id'
        });
      });

      it('should handle 401 Unauthorized errors', () => {
        const error = new Error('Token expired');
        error.status = 401;

        sanitizeError(error, req, res, next);

        expect(res.status).toHaveBeenCalledWith(401);
        expect(res.json).toHaveBeenCalledWith({
          error: 'Unauthorized',
          requestId: 'test-request-id'
        });
      });

      it('should handle 403 Forbidden errors', () => {
        const error = new Error('Access denied');
        error.status = 403;

        sanitizeError(error, req, res, next);

        expect(res.status).toHaveBeenCalledWith(403);
        expect(res.json).toHaveBeenCalledWith({
          error: 'Forbidden',
          requestId: 'test-request-id'
        });
      });

      it('should handle 404 Not Found errors', () => {
        const error = new Error('User not found');
        error.status = 404;

        sanitizeError(error, req, res, next);

        expect(res.status).toHaveBeenCalledWith(404);
        expect(res.json).toHaveBeenCalledWith({
          error: 'Not Found',
          requestId: 'test-request-id'
        });
      });
    });

    describe('Server error responses (5xx)', () => {
      describe('Development environment', () => {
        beforeEach(() => {
          process.env.NODE_ENV = 'development';
        });

        it('should include detailed error message and stack trace', () => {
          const error = new Error('Database connection failed');
          error.status = 500;
          error.stack = 'Error: Database connection failed\n    at /app/db.js:10:5';

          sanitizeError(error, req, res, next);

          expect(res.status).toHaveBeenCalledWith(500);
          expect(res.json).toHaveBeenCalledWith({
            error: 'Database connection failed',
            requestId: 'test-request-id',
            stack: 'Error: Database connection failed\n    at /app/db.js:10:5'
          });
        });

        it('should handle dev environment check', () => {
          process.env.NODE_ENV = 'dev';
          const error = new Error('Test error');
          error.status = 500;

          sanitizeError(error, req, res, next);

          expect(res.json).toHaveBeenCalledWith({
            error: 'Test error',
            requestId: 'test-request-id',
            stack: error.stack
          });
        });

        it('should use default status 500 when not specified', () => {
          const error = new Error('Unhandled error');

          sanitizeError(error, req, res, next);

          expect(res.status).toHaveBeenCalledWith(500);
          expect(res.json).toHaveBeenCalledWith({
            error: 'Unhandled error',
            requestId: 'test-request-id',
            stack: error.stack
          });
        });

        it('should handle custom error status codes', () => {
          const error = new Error('Service unavailable');
          error.status = 503;

          sanitizeError(error, req, res, next);

          expect(res.status).toHaveBeenCalledWith(503);
          expect(res.json).toHaveBeenCalledWith({
            error: 'Service unavailable',
            requestId: 'test-request-id',
            stack: error.stack
          });
        });
      });

      describe('Production environment', () => {
        beforeEach(() => {
          process.env.NODE_ENV = 'production';
        });

        it('should return generic error message', () => {
          const error = new Error('Database connection failed');
          error.status = 500;

          sanitizeError(error, req, res, next);

          expect(res.status).toHaveBeenCalledWith(500);
          expect(res.json).toHaveBeenCalledWith({
            error: 'Internal Server Error',
            requestId: 'test-request-id'
          });
        });

        it('should not include stack trace in production', () => {
          const error = new Error('Database error');
          error.status = 500;
          error.stack = 'Error: Database error\n    at /app/db.js:10:5';

          sanitizeError(error, req, res, next);

          const response = res.json.mock.calls[0][0];
          expect(response).not.toHaveProperty('stack');
        });

        it('should handle custom status codes in production', () => {
          const error = new Error('Service down');
          error.status = 502;

          sanitizeError(error, req, res, next);

          expect(res.status).toHaveBeenCalledWith(502);
          expect(res.json).toHaveBeenCalledWith({
            error: 'Internal Server Error',
            requestId: 'test-request-id'
          });
        });

        it('should use default status 500 in production', () => {
          const error = new Error('Unknown error');

          sanitizeError(error, req, res, next);

          expect(res.status).toHaveBeenCalledWith(500);
          expect(res.json).toHaveBeenCalledWith({
            error: 'Internal Server Error',
            requestId: 'test-request-id'
          });
        });
      });
    });

    describe('Edge cases', () => {
      it('should handle error without message', () => {
        const error = new Error();
        error.status = 400;

        sanitizeError(error, req, res, next);

        expect(res.json).toHaveBeenCalledWith({
          error: 'Bad Request',
          requestId: 'test-request-id'
        });
      });

      it('should handle missing request ID', () => {
        delete req.id;
        const error = new Error('Test error');
        error.status = 400;

        sanitizeError(error, req, res, next);

        expect(res.json).toHaveBeenCalledWith({
          error: 'Test error',
          requestId: undefined
        });
      });

      it('should handle error without status property', () => {
        const error = new Error('No status error');

        sanitizeError(error, req, res, next);

        expect(res.status).toHaveBeenCalledWith(500);
      });

      it('should not call next() - error handling middleware', () => {
        const error = new Error('Test error');

        sanitizeError(error, req, res, next);

        expect(next).not.toHaveBeenCalled();
      });
    });
  });

  describe('Module exports', () => {
    it('should export all required functions', () => {
      const securityModule = require('../../../middleware/security');

      expect(securityModule).toHaveProperty('enforceHTTPS');
      expect(securityModule).toHaveProperty('securityHeaders');
      expect(securityModule).toHaveProperty('addRequestId');
      expect(securityModule).toHaveProperty('logSecurityEvent');
      expect(securityModule).toHaveProperty('sanitizeError');
    });

    it('should export functions with correct types', () => {
      const securityModule = require('../../../middleware/security');

      expect(typeof securityModule.enforceHTTPS).toBe('function');
      expect(typeof securityModule.securityHeaders).toBe('function');
      expect(typeof securityModule.addRequestId).toBe('function');
      expect(typeof securityModule.logSecurityEvent).toBe('function');
      expect(typeof securityModule.sanitizeError).toBe('function');
    });
  });
});