const jwt = require('jsonwebtoken');
const cookie = require('cookie');

// Mock dependencies
jest.mock('jsonwebtoken');
jest.mock('cookie');
jest.mock('../../../models', () => ({
  User: {
    findByPk: jest.fn(),
  },
}));

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

const { User } = require('../../../models');
const { authenticateSocket } = require('../../../sockets/socketAuth');

describe('Socket Authentication', () => {
  let mockSocket;
  let mockNext;
  const originalEnv = process.env;

  beforeEach(() => {
    mockSocket = {
      id: 'socket-123',
      handshake: {
        headers: {},
        auth: {},
        address: '127.0.0.1',
      },
    };
    mockNext = jest.fn();
    jest.clearAllMocks();
    process.env = { ...originalEnv, JWT_ACCESS_SECRET: 'test-secret' };
  });

  afterEach(() => {
    process.env = originalEnv;
  });

  describe('Token extraction', () => {
    it('should extract token from cookie', async () => {
      mockSocket.handshake.headers.cookie = 'accessToken=cookie-token';
      cookie.parse.mockReturnValue({ accessToken: 'cookie-token' });
      jwt.verify.mockReturnValue({ id: 'user-123', jwtVersion: 1 });
      User.findByPk.mockResolvedValue({
        id: 'user-123',
        email: 'test@example.com',
        firstName: 'Test',
        lastName: 'User',
        jwtVersion: 1,
      });

      await authenticateSocket(mockSocket, mockNext);

      expect(jwt.verify).toHaveBeenCalledWith('cookie-token', 'test-secret');
      expect(mockNext).toHaveBeenCalledWith();
    });

    it('should extract token from auth object when cookie not present', async () => {
      mockSocket.handshake.auth.token = 'auth-token';
      jwt.verify.mockReturnValue({ id: 'user-123', jwtVersion: 1 });
      User.findByPk.mockResolvedValue({
        id: 'user-123',
        email: 'test@example.com',
        firstName: 'Test',
        lastName: 'User',
        jwtVersion: 1,
      });

      await authenticateSocket(mockSocket, mockNext);

      expect(jwt.verify).toHaveBeenCalledWith('auth-token', 'test-secret');
      expect(mockNext).toHaveBeenCalledWith();
    });

    it('should prefer cookie token over auth object token', async () => {
      mockSocket.handshake.headers.cookie = 'accessToken=cookie-token';
      mockSocket.handshake.auth.token = 'auth-token';
      cookie.parse.mockReturnValue({ accessToken: 'cookie-token' });
      jwt.verify.mockReturnValue({ id: 'user-123', jwtVersion: 1 });
      User.findByPk.mockResolvedValue({
        id: 'user-123',
        email: 'test@example.com',
        firstName: 'Test',
        lastName: 'User',
        jwtVersion: 1,
      });

      await authenticateSocket(mockSocket, mockNext);

      expect(jwt.verify).toHaveBeenCalledWith('cookie-token', 'test-secret');
    });

    it('should reject when no token provided', async () => {
      await authenticateSocket(mockSocket, mockNext);

      expect(mockNext).toHaveBeenCalledWith(expect.any(Error));
      expect(mockNext.mock.calls[0][0].message).toBe('Authentication required');
    });
  });

  describe('Token verification', () => {
    it('should authenticate user with valid token', async () => {
      mockSocket.handshake.auth.token = 'valid-token';
      jwt.verify.mockReturnValue({ id: 'user-123', jwtVersion: 1 });
      User.findByPk.mockResolvedValue({
        id: 'user-123',
        email: 'test@example.com',
        firstName: 'Test',
        lastName: 'User',
        jwtVersion: 1,
      });

      await authenticateSocket(mockSocket, mockNext);

      expect(mockSocket.userId).toBe('user-123');
      expect(mockSocket.user).toMatchObject({
        id: 'user-123',
        email: 'test@example.com',
        firstName: 'Test',
        lastName: 'User',
      });
      expect(mockNext).toHaveBeenCalledWith();
    });

    it('should reject token without user id', async () => {
      mockSocket.handshake.auth.token = 'invalid-token';
      jwt.verify.mockReturnValue({ someOtherField: 'value' });

      await authenticateSocket(mockSocket, mockNext);

      expect(mockNext).toHaveBeenCalledWith(expect.any(Error));
      expect(mockNext.mock.calls[0][0].message).toBe('Invalid token format');
    });

    it('should reject when user not found', async () => {
      mockSocket.handshake.auth.token = 'valid-token';
      jwt.verify.mockReturnValue({ id: 'nonexistent-user', jwtVersion: 1 });
      User.findByPk.mockResolvedValue(null);

      await authenticateSocket(mockSocket, mockNext);

      expect(mockNext).toHaveBeenCalledWith(expect.any(Error));
      expect(mockNext.mock.calls[0][0].message).toBe('User not found');
    });

    it('should reject when JWT version mismatch', async () => {
      mockSocket.handshake.auth.token = 'old-token';
      jwt.verify.mockReturnValue({ id: 'user-123', jwtVersion: 1 });
      User.findByPk.mockResolvedValue({
        id: 'user-123',
        jwtVersion: 2, // Different version
      });

      await authenticateSocket(mockSocket, mockNext);

      expect(mockNext).toHaveBeenCalledWith(expect.any(Error));
      expect(mockNext.mock.calls[0][0].message).toContain('password change');
    });

    it('should handle expired token', async () => {
      mockSocket.handshake.auth.token = 'expired-token';
      const error = new Error('jwt expired');
      error.name = 'TokenExpiredError';
      jwt.verify.mockImplementation(() => { throw error; });

      await authenticateSocket(mockSocket, mockNext);

      expect(mockNext).toHaveBeenCalledWith(expect.any(Error));
      expect(mockNext.mock.calls[0][0].message).toBe('Token expired');
    });

    it('should handle generic authentication errors', async () => {
      mockSocket.handshake.auth.token = 'invalid-token';
      jwt.verify.mockImplementation(() => { throw new Error('Invalid token'); });

      await authenticateSocket(mockSocket, mockNext);

      expect(mockNext).toHaveBeenCalledWith(expect.any(Error));
      expect(mockNext.mock.calls[0][0].message).toBe('Authentication failed');
    });
  });
});
