const request = require('supertest');
const express = require('express');
const messagesRouter = require('../../../routes/messages');

// Mock all dependencies
jest.mock('../../../models', () => ({
  Message: {
    findAll: jest.fn(),
    findOne: jest.fn(),
    findByPk: jest.fn(),
    create: jest.fn(),
    count: jest.fn(),
  },
  User: {
    findByPk: jest.fn(),
  },
}));

jest.mock('../../../middleware/auth', () => ({
  authenticateToken: jest.fn((req, res, next) => {
    req.user = { id: 1 };
    next();
  }),
}));

jest.mock('sequelize', () => ({
  Op: {
    or: Symbol('or'),
  },
}));

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

jest.mock('../../../sockets/messageSocket', () => ({
  emitNewMessage: jest.fn(),
  emitMessageRead: jest.fn(),
}));

jest.mock('../../../services/email', () => ({
  messaging: {
    sendNewMessageNotification: jest.fn().mockResolvedValue(),
  },
}));

const { Message, User } = require('../../../models');

// Create express app with the router
const app = express();
app.use(express.json());
app.use('/messages', messagesRouter);

// Add error handler middleware
app.use((err, req, res, next) => {
  res.status(500).json({ error: err.message });
});

// Mock models
const mockMessageFindAll = Message.findAll;
const mockMessageFindOne = Message.findOne;
const mockMessageFindByPk = Message.findByPk;
const mockMessageCreate = Message.create;
const mockMessageCount = Message.count;
const mockUserFindByPk = User.findByPk;

describe('Messages Routes', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  describe('GET /', () => {
    it('should get inbox messages for authenticated user', async () => {
      const mockMessages = [
        {
          id: 1,
          senderId: 2,
          receiverId: 1,
          content: 'Hello there!',
          isRead: false,
          createdAt: '2024-01-15T10:00:00.000Z',
          sender: {
            id: 2,
            firstName: 'Jane',
            lastName: 'Smith',
            imageFilename: 'jane.jpg'
          }
        },
        {
          id: 2,
          senderId: 3,
          receiverId: 1,
          content: 'Hi!',
          isRead: true,
          createdAt: '2024-01-14T10:00:00.000Z',
          sender: {
            id: 3,
            firstName: 'Bob',
            lastName: 'Johnson',
            imageFilename: null
          }
        }
      ];

      mockMessageFindAll.mockResolvedValue(mockMessages);

      const response = await request(app)
        .get('/messages');

      expect(response.status).toBe(200);
      expect(response.body).toEqual(mockMessages);
      expect(mockMessageFindAll).toHaveBeenCalledWith({
        where: { receiverId: 1 },
        include: [
          {
            model: User,
            as: 'sender',
            attributes: ['id', 'firstName', 'lastName', 'imageFilename']
          }
        ],
        order: [['createdAt', 'DESC']]
      });
    });

    it('should handle database errors', async () => {
      mockMessageFindAll.mockRejectedValue(new Error('Database error'));

      const response = await request(app)
        .get('/messages');

      expect(response.status).toBe(500);
      expect(response.body).toEqual({ error: 'Database error' });
    });
  });

  describe('GET /sent', () => {
    it('should get sent messages for authenticated user', async () => {
      const mockSentMessages = [
        {
          id: 3,
          senderId: 1,
          receiverId: 2,
          content: 'Hello Jane!',
          isRead: false,
          createdAt: '2024-01-15T12:00:00.000Z',
          receiver: {
            id: 2,
            firstName: 'Jane',
            lastName: 'Smith',
            imageFilename: 'jane.jpg'
          }
        }
      ];

      mockMessageFindAll.mockResolvedValue(mockSentMessages);

      const response = await request(app)
        .get('/messages/sent');

      expect(response.status).toBe(200);
      expect(response.body).toEqual(mockSentMessages);
      expect(mockMessageFindAll).toHaveBeenCalledWith({
        where: { senderId: 1 },
        include: [
          {
            model: User,
            as: 'receiver',
            attributes: ['id', 'firstName', 'lastName', 'imageFilename']
          }
        ],
        order: [['createdAt', 'DESC']]
      });
    });

    it('should handle database errors', async () => {
      mockMessageFindAll.mockRejectedValue(new Error('Database error'));

      const response = await request(app)
        .get('/messages/sent');

      expect(response.status).toBe(500);
      expect(response.body).toEqual({ error: 'Database error' });
    });
  });

  describe('GET /:id', () => {
    const mockMessage = {
      id: 1,
      senderId: 2,
      receiverId: 1,
      content: 'Hello there!',
      isRead: false,
      createdAt: '2024-01-15T10:00:00.000Z',
      sender: {
        id: 2,
        firstName: 'Jane',
        lastName: 'Smith',
        imageFilename: 'jane.jpg'
      },
      receiver: {
        id: 1,
        firstName: 'John',
        lastName: 'Doe',
        imageFilename: 'john.jpg'
      },
      update: jest.fn()
    };

    beforeEach(() => {
      mockMessageFindOne.mockResolvedValue(mockMessage);
    });

    it('should get message for receiver and mark as read', async () => {
      mockMessage.update.mockResolvedValue();

      const response = await request(app)
        .get('/messages/1');

      expect(response.status).toBe(200);
      expect(response.body).toEqual({
        id: 1,
        senderId: 2,
        receiverId: 1,
        content: 'Hello there!',
        isRead: false,
        createdAt: '2024-01-15T10:00:00.000Z',
        sender: {
          id: 2,
          firstName: 'Jane',
          lastName: 'Smith',
          imageFilename: 'jane.jpg'
        },
        receiver: {
          id: 1,
          firstName: 'John',
          lastName: 'Doe',
          imageFilename: 'john.jpg'
        }
      });
      expect(mockMessage.update).toHaveBeenCalledWith({ isRead: true });
    });

    it('should get message without marking as read for sender', async () => {
      const senderMessage = { ...mockMessage, senderId: 1, receiverId: 2 };
      mockMessageFindOne.mockResolvedValue(senderMessage);

      const response = await request(app)
        .get('/messages/1');

      expect(response.status).toBe(200);
      expect(response.body).toEqual({
        id: 1,
        senderId: 1,
        receiverId: 2,
        content: 'Hello there!',
        isRead: false,
        createdAt: '2024-01-15T10:00:00.000Z',
        sender: {
          id: 2,
          firstName: 'Jane',
          lastName: 'Smith',
          imageFilename: 'jane.jpg'
        },
        receiver: {
          id: 1,
          firstName: 'John',
          lastName: 'Doe',
          imageFilename: 'john.jpg'
        }
      });
      expect(mockMessage.update).not.toHaveBeenCalled();
    });

    it('should not mark already read message as read', async () => {
      const readMessage = { ...mockMessage, isRead: true };
      mockMessageFindOne.mockResolvedValue(readMessage);

      const response = await request(app)
        .get('/messages/1');

      expect(response.status).toBe(200);
      expect(mockMessage.update).not.toHaveBeenCalled();
    });

    it('should return 404 for non-existent message', async () => {
      mockMessageFindOne.mockResolvedValue(null);

      const response = await request(app)
        .get('/messages/999');

      expect(response.status).toBe(404);
      expect(response.body).toEqual({ error: 'Message not found' });
    });

    it('should handle database errors', async () => {
      mockMessageFindOne.mockRejectedValue(new Error('Database error'));

      const response = await request(app)
        .get('/messages/1');

      expect(response.status).toBe(500);
      expect(response.body).toEqual({ error: 'Database error' });
    });
  });

  describe('POST /', () => {
    const mockReceiver = {
      id: 2,
      firstName: 'Jane',
      lastName: 'Smith',
      email: 'jane@example.com'
    };

    const mockCreatedMessage = {
      id: 5,
      senderId: 1,
      receiverId: 2,
      content: 'Hello Jane!'
    };

    const mockMessageWithSender = {
      ...mockCreatedMessage,
      sender: {
        id: 1,
        firstName: 'John',
        lastName: 'Doe',
        imageFilename: 'john.jpg'
      }
    };

    beforeEach(() => {
      mockUserFindByPk.mockResolvedValue(mockReceiver);
      mockMessageCreate.mockResolvedValue(mockCreatedMessage);
      mockMessageFindByPk.mockResolvedValue(mockMessageWithSender);
    });

    it('should create a new message', async () => {
      const messageData = {
        receiverId: 2,
        content: 'Hello Jane!'
      };

      const response = await request(app)
        .post('/messages')
        .send(messageData);

      expect(response.status).toBe(201);
      expect(response.body).toEqual(mockMessageWithSender);
      expect(mockMessageCreate).toHaveBeenCalledWith({
        senderId: 1,
        receiverId: 2,
        content: 'Hello Jane!',
        imageFilename: null
      });
    });

    it('should return 404 for non-existent receiver', async () => {
      mockUserFindByPk.mockResolvedValue(null);

      const response = await request(app)
        .post('/messages')
        .send({
          receiverId: 999,
          content: 'Test message'
        });

      expect(response.status).toBe(404);
      expect(response.body).toEqual({ error: 'Receiver not found' });
    });

    it('should prevent sending messages to self', async () => {
      const response = await request(app)
        .post('/messages')
        .send({
          receiverId: 1, // Same as sender ID
          content: 'Hello self!'
        });

      expect(response.status).toBe(400);
      expect(response.body).toEqual({ error: 'Cannot send messages to yourself' });
    });

    it('should handle database errors during creation', async () => {
      mockMessageCreate.mockRejectedValue(new Error('Database error'));

      const response = await request(app)
        .post('/messages')
        .send({
          receiverId: 2,
          content: 'Test message'
        });

      expect(response.status).toBe(500);
      expect(response.body).toEqual({ error: 'Database error' });
    });
  });

  describe('PUT /:id/read', () => {
    const mockMessage = {
      id: 1,
      senderId: 2,
      receiverId: 1,
      isRead: false,
      update: jest.fn()
    };

    beforeEach(() => {
      mockMessageFindOne.mockResolvedValue(mockMessage);
    });

    it('should mark message as read', async () => {
      const updatedMessage = { ...mockMessage, isRead: true };
      mockMessage.update.mockResolvedValue(updatedMessage);

      const response = await request(app)
        .put('/messages/1/read');

      expect(response.status).toBe(200);
      expect(response.body).toEqual({
        id: 1,
        senderId: 2,
        receiverId: 1,
        isRead: false
      });
      expect(mockMessage.update).toHaveBeenCalledWith({ isRead: true });
      expect(mockMessageFindOne).toHaveBeenCalledWith({
        where: {
          id: '1',
          receiverId: 1
        }
      });
    });

    it('should return 404 for non-existent message', async () => {
      mockMessageFindOne.mockResolvedValue(null);

      const response = await request(app)
        .put('/messages/999/read');

      expect(response.status).toBe(404);
      expect(response.body).toEqual({ error: 'Message not found' });
    });

    it('should return 404 when user is not the receiver', async () => {
      // Message exists but user is not the receiver (query will return null)
      mockMessageFindOne.mockResolvedValue(null);

      const response = await request(app)
        .put('/messages/1/read');

      expect(response.status).toBe(404);
      expect(response.body).toEqual({ error: 'Message not found' });
    });

    it('should handle database errors', async () => {
      mockMessageFindOne.mockRejectedValue(new Error('Database error'));

      const response = await request(app)
        .put('/messages/1/read');

      expect(response.status).toBe(500);
      expect(response.body).toEqual({ error: 'Database error' });
    });
  });

  describe('GET /unread/count', () => {
    it('should get unread message count for authenticated user', async () => {
      mockMessageCount.mockResolvedValue(5);

      const response = await request(app)
        .get('/messages/unread/count');

      expect(response.status).toBe(200);
      expect(response.body).toEqual({ count: 5 });
      expect(mockMessageCount).toHaveBeenCalledWith({
        where: {
          receiverId: 1,
          isRead: false
        }
      });
    });

    it('should return count of 0 when no unread messages', async () => {
      mockMessageCount.mockResolvedValue(0);

      const response = await request(app)
        .get('/messages/unread/count');

      expect(response.status).toBe(200);
      expect(response.body).toEqual({ count: 0 });
    });

    it('should handle database errors', async () => {
      mockMessageCount.mockRejectedValue(new Error('Database error'));

      const response = await request(app)
        .get('/messages/unread/count');

      expect(response.status).toBe(500);
      expect(response.body).toEqual({ error: 'Database error' });
    });
  });

  describe('Message authorization', () => {
    it('should only find messages where user is sender or receiver', async () => {
      const { Op } = require('sequelize');

      await request(app)
        .get('/messages/1');

      expect(mockMessageFindOne).toHaveBeenCalledWith({
        where: {
          id: '1',
          [Op.or]: [
            { senderId: 1 },
            { receiverId: 1 }
          ]
        },
        include: expect.any(Array)
      });
    });
  });

  describe('Edge cases', () => {
    it('should handle empty inbox', async () => {
      mockMessageFindAll.mockResolvedValue([]);

      const response = await request(app)
        .get('/messages');

      expect(response.status).toBe(200);
      expect(response.body).toEqual([]);
    });

    it('should handle empty sent messages', async () => {
      mockMessageFindAll.mockResolvedValue([]);

      const response = await request(app)
        .get('/messages/sent');

      expect(response.status).toBe(200);
      expect(response.body).toEqual([]);
    });
  });
});