import { SendMessageCommand } from '../commands/send-message/send-message.command';
import { SendMessageHandler } from '../commands/send-message/send-message.handler';

describe('SendMessageHandler', () => {
  let handler: SendMessageHandler;
  let mockConversationRepo: {
    findById: ReturnType<typeof vi.fn>;
    updateLastMessage: ReturnType<typeof vi.fn>;
    incrementUnreadCount: ReturnType<typeof vi.fn>;
  };
  let mockMessageRepo: {
    create: ReturnType<typeof vi.fn>;
  };
  let mockEventBus: { publish: ReturnType<typeof vi.fn> };
  let mockLogger: { log: ReturnType<typeof vi.fn>; warn: ReturnType<typeof vi.fn>; error: ReturnType<typeof vi.fn> };

  const conversation = {
    id: 'conv-1',
    status: 'ACTIVE' as const,
    participants: [
      { id: 'p-1', conversationId: 'conv-1', userId: 'user-1', unreadCount: 0, lastReadAt: null, joinedAt: new Date() },
      { id: 'p-2', conversationId: 'conv-1', userId: 'user-2', unreadCount: 0, lastReadAt: null, joinedAt: new Date() },
    ],
  };

  const messageEntity = {
    id: 'msg-1',
    conversationId: 'conv-1',
    senderId: 'user-1',
    type: 'TEXT' as const,
    content: 'Hello!',
    metadata: null,
    editedAt: null,
    deletedAt: null,
    createdAt: new Date(),
  };

  beforeEach(() => {
    mockConversationRepo = {
      findById: vi.fn().mockResolvedValue(conversation),
      updateLastMessage: vi.fn().mockResolvedValue(undefined),
      incrementUnreadCount: vi.fn().mockResolvedValue(undefined),
    };
    mockMessageRepo = {
      create: vi.fn().mockResolvedValue(messageEntity),
    };
    mockEventBus = { publish: vi.fn() };
    mockLogger = { log: vi.fn(), warn: vi.fn(), error: vi.fn() };

    handler = new SendMessageHandler(
      mockConversationRepo as any,
      mockMessageRepo as any,
      mockEventBus as any,
      mockLogger as any,
    );
  });

  it('sends a message successfully', async () => {
    const command = new SendMessageCommand('conv-1', 'user-1', 'Hello!');

    const result = await handler.execute(command);

    expect(result.id).toBe('msg-1');
    expect(mockMessageRepo.create).toHaveBeenCalledWith({
      conversationId: 'conv-1',
      senderId: 'user-1',
      type: 'TEXT',
      content: 'Hello!',
      metadata: undefined,
    });
    expect(mockConversationRepo.updateLastMessage).toHaveBeenCalled();
    expect(mockConversationRepo.incrementUnreadCount).toHaveBeenCalledWith('conv-1', 'user-1');
    expect(mockEventBus.publish).toHaveBeenCalled();
  });

  it('throws NotFoundException when conversation does not exist', async () => {
    mockConversationRepo.findById.mockResolvedValue(null);

    const command = new SendMessageCommand('conv-999', 'user-1', 'Hello!');

    await expect(handler.execute(command)).rejects.toThrow();
  });

  it('throws ForbiddenException when user is not a participant', async () => {
    const command = new SendMessageCommand('conv-1', 'user-3', 'Hello!');

    await expect(handler.execute(command)).rejects.toThrow('Bạn không phải là thành viên của hội thoại này');
  });

  it('throws ValidationException when conversation is closed', async () => {
    mockConversationRepo.findById.mockResolvedValue({ ...conversation, status: 'CLOSED' });

    const command = new SendMessageCommand('conv-1', 'user-1', 'Hello!');

    await expect(handler.execute(command)).rejects.toThrow('Hội thoại đã đóng');
  });

  it('publishes MessageSentEvent after successful send', async () => {
    const command = new SendMessageCommand('conv-1', 'user-1', 'Hello!');

    await handler.execute(command);

    expect(mockEventBus.publish).toHaveBeenCalledWith(
      expect.objectContaining({
        eventName: 'message.sent',
        aggregateId: 'msg-1',
        conversationId: 'conv-1',
        senderId: 'user-1',
        content: 'Hello!',
      }),
    );
  });
});
