import { Test } from '@nestjs/testing';
import { NotFoundException, ForbiddenException } from '@nestjs/common';
import { MessagesService } from './messages.service';
import { PrismaService } from '@chatapp/database';
import { SearchService } from '../search/search.service';

describe('MessagesService', () => {
  let service: MessagesService;
  let prisma: any;
  let searchService: any;

  beforeEach(async () => {
    prisma = {
      message: {
        create: jest.fn(),
        findFirst: jest.fn(),
        findMany: jest.fn(),
        update: jest.fn(),
      },
      messageReaction: {
        create: jest.fn(),
        deleteMany: jest.fn(),
      },
      savedMessage: {
        create: jest.fn(),
        deleteMany: jest.fn(),
      },
      channel: {
        update: jest.fn(),
        findFirst: jest.fn().mockResolvedValue({ id: 'ch-1', workspaceId: 'ws-1', name: 'general' }),
      },
      channelMember: {
        findFirst: jest.fn().mockResolvedValue({ id: 'cm-1', userId: 'user-1', channelId: 'ch-1', workspaceId: 'ws-1' }),
      },
    };

    searchService = {
      indexMessage: jest.fn().mockResolvedValue({}),
    };

    const moduleRef = await Test.createTestingModule({
      providers: [
        MessagesService,
        { provide: PrismaService, useValue: prisma },
        { provide: SearchService, useValue: searchService },
      ],
    }).compile();

    service = moduleRef.get<MessagesService>(MessagesService);
  });

  describe('send()', () => {
    it('crea mensaje y actualiza lastMessageAt del canal', async () => {
      const mockMessage = {
        id: 'msg-1',
        text: 'Hello',
        channelId: 'ch-1',
        userId: 'user-1',
        workspaceId: 'ws-1',
        createdAt: new Date(),
        user: { id: 'user-1', displayName: 'Test' },
        channel: { id: 'ch-1', name: 'general' },
      };
      prisma.message.create.mockResolvedValue(mockMessage);
      prisma.channel.update.mockResolvedValue({});

      const result = await service.send('ch-1', 'user-1', 'ws-1', { text: 'Hello' });

      expect(prisma.message.create).toHaveBeenCalledWith({
        data: expect.objectContaining({
          channelId: 'ch-1',
          userId: 'user-1',
          workspaceId: 'ws-1',
          text: 'Hello',
        }),
        include: {
          user: { select: { id: true, displayName: true } },
          channel: { select: { id: true, name: true } },
        },
      });
      expect(prisma.channel.update).toHaveBeenCalledWith({
        where: { id: 'ch-1' },
        data: { lastMessageAt: expect.any(Date) },
      });
      expect(result).toBe(mockMessage);
      expect(searchService.indexMessage).toHaveBeenCalled();
    });

    it('si es thread reply, actualiza replyCount del padre', async () => {
      const mockMessage = {
        id: 'msg-2',
        text: 'Reply',
        channelId: 'ch-1',
        userId: 'user-1',
        workspaceId: 'ws-1',
        createdAt: new Date(),
        user: { id: 'user-1', displayName: 'Test' },
        channel: { id: 'ch-1', name: 'general' },
      };
      prisma.message.create.mockResolvedValue(mockMessage);
      prisma.channel.update.mockResolvedValue({});
      prisma.message.update.mockResolvedValue({});

      await service.send('ch-1', 'user-1', 'ws-1', {
        text: 'Reply',
        thread_root_id: 'parent-1',
      });

      expect(prisma.message.update).toHaveBeenCalledWith({
        where: { id: 'parent-1' },
        data: {
          replyCount: { increment: 1 },
          latestReplyTs: expect.any(Date),
        },
      });
    });
  });

  describe('edit()', () => {
    it('permite editar mensaje propio', async () => {
      prisma.message.findFirst.mockResolvedValue({
        id: 'msg-1',
        userId: 'user-1',
        text: 'Old text',
      });
      prisma.message.update.mockResolvedValue({
        id: 'msg-1',
        text: 'New text',
        isEdited: true,
      });

      const result = await service.edit('msg-1', 'user-1', 'ws-1', { text: 'New text' });

      expect(prisma.message.update).toHaveBeenCalledWith({
        where: { id: 'msg-1' },
        data: expect.objectContaining({
          text: 'New text',
          isEdited: true,
          editedBy: 'user-1',
        }),
      });
      expect(result.text).toBe('New text');
    });

    it('lanza ForbiddenException al editar mensaje de otro', async () => {
      prisma.message.findFirst.mockResolvedValue({
        id: 'msg-1',
        userId: 'user-2',
        text: 'Not yours',
      });

      await expect(
        service.edit('msg-1', 'user-1', 'ws-1', { text: 'hacked' }),
      ).rejects.toThrow(ForbiddenException);
    });

    it('lanza NotFoundException si mensaje no existe', async () => {
      prisma.message.findFirst.mockResolvedValue(null);

      await expect(
        service.edit('msg-x', 'user-1', 'ws-1', { text: 'test' }),
      ).rejects.toThrow(NotFoundException);
    });
  });

  describe('delete()', () => {
    it('permite borrar mensaje propio', async () => {
      prisma.message.findFirst.mockResolvedValue({
        id: 'msg-1',
        userId: 'user-1',
      });
      prisma.message.update.mockResolvedValue({});

      const result = await service.delete('msg-1', 'user-1', 'ws-1');

      expect(prisma.message.update).toHaveBeenCalledWith({
        where: { id: 'msg-1' },
        data: {
          isDeleted: true,
          deletedAt: expect.any(Date),
          deletedBy: 'user-1',
        },
      });
      expect(result.deleted).toBe(true);
    });

    it('lanza ForbiddenException al borrar mensaje de otro', async () => {
      prisma.message.findFirst.mockResolvedValue({
        id: 'msg-1',
        userId: 'user-2',
      });

      await expect(service.delete('msg-1', 'user-1', 'ws-1')).rejects.toThrow(ForbiddenException);
    });
  });

  describe('addReaction()', () => {
    it('crea reaccion', async () => {
      prisma.message.findFirst.mockResolvedValue({ id: 'msg-1', userId: 'user-1' });
      prisma.messageReaction.create.mockResolvedValue({
        id: 'reaction-1',
        messageId: 'msg-1',
        userId: 'user-1',
        emoji: '👍',
      });

      const result = await service.addReaction('msg-1', 'user-1', '👍', 'ws-1');

      expect(prisma.messageReaction.create).toHaveBeenCalledWith({
        data: { messageId: 'msg-1', userId: 'user-1', emoji: '👍' },
      });
      expect(result.emoji).toBe('👍');
    });

    it('retorna objeto si la reaccion ya existe (idempotente)', async () => {
      prisma.message.findFirst.mockResolvedValue({ id: 'msg-1', userId: 'user-1' });
      prisma.messageReaction.create.mockRejectedValue(new Error('Already exists'));

      const result = await service.addReaction('msg-1', 'user-1', '👍', 'ws-1');

      expect(result).toEqual({ message_id: 'msg-1', user_id: 'user-1', emoji: '👍' });
    });
  });

  describe('getThread()', () => {
    it('devuelve respuestas del thread', async () => {
      const mockReplies = [
        {
          id: 'msg-2',
          text: 'Reply 1',
          threadRootId: 'msg-1',
          userId: 'user-2',
          user: { id: 'user-2', displayName: 'User 2', avatarUrl: null },
          reactions: [],
        },
        {
          id: 'msg-3',
          text: 'Reply 2',
          threadRootId: 'msg-1',
          userId: 'user-3',
          user: { id: 'user-3', displayName: 'User 3', avatarUrl: null },
          reactions: [],
        },
      ];
      prisma.message.findMany.mockResolvedValue(mockReplies);

      const result = await service.getThread('msg-1', 'ws-1');

      expect(prisma.message.findMany).toHaveBeenCalledWith({
        where: { threadRootId: 'msg-1', workspaceId: 'ws-1', isDeleted: false },
        orderBy: { ts: 'asc' },
        include: {
          user: { select: { id: true, displayName: true, avatarUrl: true } },
          reactions: true,
        },
      });
      expect(result.messages).toHaveLength(2);
    });
  });
});