import { BadRequestException, NotFoundException } from '@nestjs/common';
import { Types } from 'mongoose';
import { SupportService } from './support.service';

const userId = '507f1f77bcf86cd799439011';
const otherUserId = '507f191e810c19729de860ea';
const ticketId = '64f1f77bcf86cd799439011a';
const adminId = '507f191e810c19729de860eb';

const asDoc = <T extends Record<string, any>>(payload: T) => ({
  id: payload._id?.toString?.() ?? payload._id,
  ...payload,
  toObject: () => payload,
});

describe('SupportService', () => {
  let service: SupportService;
  let repository: Record<string, jest.Mock>;
  let storage: Record<string, jest.Mock>;
  let notifications: Record<string, jest.Mock>;

  beforeEach(() => {
    repository = {
      createTicket: jest.fn(),
      createMessage: jest.fn(),
      findUserTickets: jest.fn(),
      countTickets: jest.fn(),
      findUserTicket: jest.fn(),
      findMessages: jest.fn(),
      closeTicket: jest.fn(),
      findAdminTickets: jest.fn(),
      findAdminTicket: jest.fn(),
      findTicketById: jest.fn(),
      updateTicketAfterMessage: jest.fn(),
      updateStatus: jest.fn(),
      assign: jest.fn(),
    };
    storage = {
      saveFile: jest.fn(),
    };
    notifications = {
      create: jest.fn(),
    };
    service = new SupportService(repository as any, storage as any, notifications as any);
  });

  it('creates a support ticket without image and first user message', async () => {
    const ticket = asDoc({
      _id: new Types.ObjectId(ticketId),
      userId: new Types.ObjectId(userId),
      subject: 'Upload problem',
      category: 'technical',
      priority: 'normal',
      status: 'open',
      lastMessageAt: new Date(),
      resolvedAt: null,
      closedAt: null,
    });
    repository.createTicket.mockResolvedValue(ticket);
    repository.createMessage.mockResolvedValue(asDoc({ _id: new Types.ObjectId(), ticketId }));

    const result = await service.createTicket(userId, {
      subject: 'Upload problem',
      message: 'File upload failed',
      category: 'technical',
      priority: 'normal',
    });

    expect(result.message).toBe('Support ticket created successfully');
    expect(result.item.status).toBe('open');
    expect(repository.createMessage).toHaveBeenCalledWith(
      expect.objectContaining({
        ticketId,
        senderId: userId,
        senderRole: 'user',
        message: 'File upload failed',
        attachmentUrl: '',
        attachmentType: null,
      }),
    );
  });

  it('stores optional image attachment for the first message', async () => {
    storage.saveFile.mockResolvedValue('/uploads/support/images/support-test.jpg');
    repository.createTicket.mockResolvedValue(
      asDoc({
        _id: new Types.ObjectId(ticketId),
        userId: new Types.ObjectId(userId),
        subject: 'Image problem',
        category: 'technical',
        priority: 'normal',
        status: 'open',
      }),
    );
    repository.createMessage.mockResolvedValue(asDoc({ _id: new Types.ObjectId(), ticketId }));

    await service.createTicket(
      userId,
      { subject: 'Image problem', message: 'See image' },
      { mimetype: 'image/jpeg', size: 10, buffer: Buffer.from('img'), originalname: 'a.jpg' },
    );

    expect(storage.saveFile).toHaveBeenCalledWith(
      expect.objectContaining({
        folderSegments: ['support', 'images'],
        extension: '.jpg',
        contentType: 'image/jpeg',
      }),
    );
    expect(repository.createMessage).toHaveBeenCalledWith(
      expect.objectContaining({
        attachmentUrl: '/uploads/support/images/support-test.jpg',
        attachmentType: 'image',
      }),
    );
  });

  it('rejects unsupported image mimetypes for support attachments', async () => {
    await expect(
      service.createTicket(
        userId,
        { subject: 'Vector problem', message: 'See image' },
        {
          mimetype: 'image/svg+xml',
          size: 10,
          buffer: Buffer.from('svg'),
          originalname: 'vector.svg',
        },
      ),
    ).rejects.toBeInstanceOf(BadRequestException);
    expect(storage.saveFile).not.toHaveBeenCalled();
    expect(repository.createTicket).not.toHaveBeenCalled();
  });

  it('lists only the current user tickets with pagination', async () => {
    const ticket = asDoc({
      _id: new Types.ObjectId(ticketId),
      userId: new Types.ObjectId(userId),
      subject: 'My ticket',
      category: 'technical',
      priority: 'normal',
      status: 'open',
    });
    repository.findUserTickets.mockResolvedValue([ticket]);
    repository.countTickets.mockResolvedValue(1);

    const result = await service.listMyTickets(userId, { page: 1, limit: 20 });

    expect(repository.findUserTickets).toHaveBeenCalledWith(
      expect.objectContaining({ userId: expect.any(Types.ObjectId) }),
      0,
      20,
      { lastMessageAt: -1 },
    );
    expect(result.items).toHaveLength(1);
    expect(result.pagination.total).toBe(1);
  });

  it('blocks access to another user ticket', async () => {
    repository.findUserTicket.mockResolvedValue(null);

    await expect(service.getMyTicket(userId, ticketId)).rejects.toBeInstanceOf(NotFoundException);
  });

  it('adds a user message to an owned open ticket', async () => {
    repository.findUserTicket.mockResolvedValue(
      asDoc({
        _id: new Types.ObjectId(ticketId),
        userId: new Types.ObjectId(userId),
        status: 'open',
      }),
    );
    repository.createMessage.mockResolvedValue(
      asDoc({
        _id: new Types.ObjectId(),
        ticketId: new Types.ObjectId(ticketId),
        senderId: new Types.ObjectId(userId),
        senderRole: 'user',
        message: 'More details',
        attachmentUrl: '',
      }),
    );

    const result = await service.addUserMessage(userId, ticketId, { message: 'More details' });

    expect(result.item.message).toBe('More details');
    expect(repository.updateTicketAfterMessage).toHaveBeenCalledWith(ticketId, {});
  });

  it('rejects empty user messages without image', async () => {
    repository.findUserTicket.mockResolvedValue(
      asDoc({
        _id: new Types.ObjectId(ticketId),
        userId: new Types.ObjectId(userId),
        status: 'open',
      }),
    );

    await expect(service.addUserMessage(userId, ticketId, { message: '' })).rejects.toBeInstanceOf(
      BadRequestException,
    );
  });

  it('admin list returns populated user details', async () => {
    repository.findAdminTickets.mockResolvedValue([
      asDoc({
        _id: new Types.ObjectId(ticketId),
        userId: {
          _id: new Types.ObjectId(userId),
          name: 'User One',
          username: 'userone',
          email: 'user@example.com',
          avatar: '',
        },
        subject: 'Need help',
        category: 'technical',
        priority: 'high',
        status: 'open',
      }),
    ]);
    repository.countTickets.mockResolvedValue(1);

    const result = await service.listAdminTickets({ page: 1, limit: 20, status: 'open' });

    expect(result.items[0].user?.email).toBe('user@example.com');
    expect(result.items[0].user?.username).toBe('userone');
  });

  it('admin reply updates open ticket to in_progress and notifies when actor is a user id', async () => {
    repository.findTicketById.mockResolvedValue(
      asDoc({
        _id: new Types.ObjectId(ticketId),
        userId: new Types.ObjectId(otherUserId),
        status: 'open',
      }),
    );
    repository.createMessage.mockResolvedValue(
      asDoc({
        _id: new Types.ObjectId(),
        ticketId: new Types.ObjectId(ticketId),
        senderId: new Types.ObjectId(adminId),
        senderRole: 'admin',
        message: 'We are checking',
        attachmentUrl: '',
      }),
    );

    await service.addAdminMessage(adminId, ticketId, { message: 'We are checking' });

    expect(repository.updateTicketAfterMessage).toHaveBeenCalledWith(ticketId, { status: 'in_progress' });
    expect(notifications.create).toHaveBeenCalledWith(
      expect.objectContaining({
        actorId: adminId,
        recipientId: otherUserId,
        type: 'support_reply',
        resourceType: 'support_ticket',
      }),
    );
  });

  it('updates status and assignment', async () => {
    repository.updateStatus.mockResolvedValue(
      asDoc({
        _id: new Types.ObjectId(ticketId),
        userId: new Types.ObjectId(userId),
        status: 'resolved',
      }),
    );
    repository.assign.mockResolvedValue(
      asDoc({
        _id: new Types.ObjectId(ticketId),
        userId: new Types.ObjectId(userId),
        assignedAdminId: new Types.ObjectId(adminId),
      }),
    );

    const status = await service.updateAdminStatus(ticketId, { status: 'resolved' });
    const assigned = await service.assignAdmin(ticketId, { adminId });

    expect(status.item.status).toBe('resolved');
    expect(assigned.item.assignedAdminId).toBe(adminId);
  });
});
