const mongoose = require('mongoose');
const express = require('express');
const request = require('supertest');
const { v4: uuidv4 } = require('uuid');
const { MongoMemoryServer } = require('mongodb-memory-server');

jest.mock('@librechat/agents', () => ({
  sleep: jest.fn(),
}));

jest.mock('@librechat/api', () => ({
  unescapeLaTeX: jest.fn((x) => x),
  countTokens: jest.fn().mockResolvedValue(10),
}));

jest.mock('@librechat/data-schemas', () => ({
  ...jest.requireActual('@librechat/data-schemas'),
  logger: {
    debug: jest.fn(),
    info: jest.fn(),
    warn: jest.fn(),
    error: jest.fn(),
  },
}));

jest.mock('librechat-data-provider', () => ({
  ...jest.requireActual('librechat-data-provider'),
}));

jest.mock('~/models', () => ({
  saveConvo: jest.fn(),
  getMessage: jest.fn(),
  saveMessage: jest.fn(),
  getMessages: jest.fn(),
  updateMessage: jest.fn(),
  deleteMessages: jest.fn(),
  getConvosQueried: jest.fn(),
  searchMessages: jest.fn(),
  getMessagesByCursor: jest.fn(),
}));

jest.mock('~/server/services/Artifacts/update', () => ({
  findAllArtifacts: jest.fn(),
  replaceArtifactContent: jest.fn(),
}));

jest.mock('~/server/middleware/requireJwtAuth', () => (req, res, next) => next());

jest.mock('~/server/middleware', () => ({
  requireJwtAuth: (req, res, next) => next(),
  validateMessageReq: (req, res, next) => next(),
}));

jest.mock('~/db/models', () => ({
  Message: {
    findOne: jest.fn(),
    find: jest.fn(),
    meiliSearch: jest.fn(),
  },
}));

/* ─── Model-level tests: real MongoDB, proves cross-user deletion is prevented ─── */

const { messageSchema } = require('@librechat/data-schemas');

describe('deleteMessages – model-level IDOR prevention', () => {
  let mongoServer;
  let Message;

  const ownerUserId = 'user-owner-111';
  const attackerUserId = 'user-attacker-222';

  beforeAll(async () => {
    mongoServer = await MongoMemoryServer.create();
    Message = mongoose.models.Message || mongoose.model('Message', messageSchema);
    await mongoose.connect(mongoServer.getUri());
  });

  afterAll(async () => {
    await mongoose.disconnect();
    await mongoServer.stop();
  });

  beforeEach(async () => {
    await Message.deleteMany({});
  });

  it("should NOT delete another user's message when attacker supplies victim messageId", async () => {
    const conversationId = uuidv4();
    const victimMsgId = 'victim-msg-001';

    await Message.create({
      messageId: victimMsgId,
      conversationId,
      user: ownerUserId,
      text: 'Sensitive owner data',
    });

    await Message.deleteMany({ messageId: victimMsgId, user: attackerUserId });

    const victimMsg = await Message.findOne({ messageId: victimMsgId }).lean();
    expect(victimMsg).not.toBeNull();
    expect(victimMsg.user).toBe(ownerUserId);
    expect(victimMsg.text).toBe('Sensitive owner data');
  });

  it("should delete the user's own message", async () => {
    const conversationId = uuidv4();
    const ownMsgId = 'own-msg-001';

    await Message.create({
      messageId: ownMsgId,
      conversationId,
      user: ownerUserId,
      text: 'My message',
    });

    const result = await Message.deleteMany({ messageId: ownMsgId, user: ownerUserId });
    expect(result.deletedCount).toBe(1);

    const deleted = await Message.findOne({ messageId: ownMsgId }).lean();
    expect(deleted).toBeNull();
  });

  it('should scope deletion by conversationId, messageId, and user together', async () => {
    const convoA = uuidv4();
    const convoB = uuidv4();

    await Message.create([
      { messageId: 'msg-a1', conversationId: convoA, user: ownerUserId, text: 'A1' },
      { messageId: 'msg-b1', conversationId: convoB, user: ownerUserId, text: 'B1' },
    ]);

    await Message.deleteMany({ messageId: 'msg-a1', conversationId: convoA, user: attackerUserId });

    const remaining = await Message.find({ user: ownerUserId }).lean();
    expect(remaining).toHaveLength(2);
  });
});

/* ─── Route-level tests: supertest + mocked deleteMessages ─── */

describe('DELETE /:conversationId/:messageId – route handler', () => {
  let app;
  const { deleteMessages } = require('~/models');

  const authenticatedUserId = 'user-owner-123';

  beforeAll(() => {
    const messagesRouter = require('../messages');

    app = express();
    app.use(express.json());
    app.use((req, res, next) => {
      req.user = { id: authenticatedUserId };
      next();
    });
    app.use('/api/messages', messagesRouter);
  });

  beforeEach(() => {
    jest.clearAllMocks();
  });

  it('should pass user and conversationId in the deleteMessages filter', async () => {
    deleteMessages.mockResolvedValue({ deletedCount: 1 });

    await request(app).delete('/api/messages/convo-1/msg-1');

    expect(deleteMessages).toHaveBeenCalledTimes(1);
    expect(deleteMessages).toHaveBeenCalledWith({
      messageId: 'msg-1',
      conversationId: 'convo-1',
      user: authenticatedUserId,
    });
  });

  it('should return 204 on successful deletion', async () => {
    deleteMessages.mockResolvedValue({ deletedCount: 1 });

    const response = await request(app).delete('/api/messages/convo-1/msg-owned');

    expect(response.status).toBe(204);
    expect(deleteMessages).toHaveBeenCalledWith({
      messageId: 'msg-owned',
      conversationId: 'convo-1',
      user: authenticatedUserId,
    });
  });

  it('should return 500 when deleteMessages throws', async () => {
    deleteMessages.mockRejectedValue(new Error('DB failure'));

    const response = await request(app).delete('/api/messages/convo-1/msg-1');

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

describe('message route conversation ownership filters', () => {
  let app;
  const { getMessages, saveConvo, saveMessage } = require('~/models');

  const authenticatedUserId = 'user-owner-123';

  beforeAll(() => {
    const messagesRouter = require('../messages');

    app = express();
    app.use(express.json());
    app.use((req, res, next) => {
      req.user = { id: authenticatedUserId };
      next();
    });
    app.use('/api/messages', messagesRouter);
  });

  beforeEach(() => {
    jest.clearAllMocks();
  });

  it('should save POST messages with the validated URL conversationId', async () => {
    const urlConversationId = '11111111-1111-4111-8111-111111111111';
    const bodyConversationId = '22222222-2222-4222-8222-222222222222';
    const savedMessage = {
      messageId: 'message-1',
      conversationId: urlConversationId,
      text: 'hello',
      user: authenticatedUserId,
    };

    saveMessage.mockResolvedValue(savedMessage);
    saveConvo.mockResolvedValue({ conversationId: urlConversationId });

    const response = await request(app).post(`/api/messages/${urlConversationId}`).send({
      messageId: savedMessage.messageId,
      conversationId: bodyConversationId,
      text: savedMessage.text,
    });

    expect(response.status).toBe(201);
    expect(saveMessage).toHaveBeenCalledWith(
      expect.objectContaining({ userId: authenticatedUserId }),
      expect.objectContaining({
        messageId: savedMessage.messageId,
        conversationId: urlConversationId,
        text: savedMessage.text,
        user: authenticatedUserId,
      }),
      { context: 'POST /api/messages/:conversationId' },
    );
    expect(saveMessage.mock.calls[0][1].conversationId).not.toBe(bodyConversationId);
    expect(saveConvo).toHaveBeenCalledWith(
      expect.objectContaining({ userId: authenticatedUserId }),
      savedMessage,
      { context: 'POST /api/messages/:conversationId' },
    );
  });

  it('should filter conversation message reads by authenticated user', async () => {
    getMessages.mockResolvedValue([{ messageId: 'message-1', conversationId: 'convo-1' }]);

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

    expect(response.status).toBe(200);
    expect(getMessages).toHaveBeenCalledWith(
      { conversationId: 'convo-1', user: authenticatedUserId },
      '-_id -__v -user',
    );
  });

  it('should filter single message reads by authenticated user', async () => {
    getMessages.mockResolvedValue([{ messageId: 'message-1', conversationId: 'convo-1' }]);

    const response = await request(app).get('/api/messages/convo-1/message-1');

    expect(response.status).toBe(200);
    expect(getMessages).toHaveBeenCalledWith(
      { conversationId: 'convo-1', messageId: 'message-1', user: authenticatedUserId },
      '-_id -__v -user',
    );
  });
});
