/**
 * Integration test for the delete-time path of issue #12776.
 *
 * Covers the full flow through `processDeleteRequest`:
 *   1. Real Agent + File docs in an in-memory Mongo.
 *   2. Invoke the delete service.
 *   3. Assert both the File record is gone and every agent's
 *      tool_resources.*.file_ids no longer references the deleted id.
 *
 * Uses FileSources.text so the strategy layer (disk / S3 / OpenAI) is a
 * no-op — we don't need real filesystem access to exercise the agent
 * reference cleanup, which is what issue #12776 is about.
 */

const mongoose = require('mongoose');
const { MongoMemoryServer } = require('mongodb-memory-server');
const { agentSchema, fileSchema, createMethods } = require('@librechat/data-schemas');
const { FileSources } = require('librechat-data-provider');

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

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

jest.mock('@librechat/api', () => ({
  sanitizeFilename: jest.fn((n) => n),
  parseText: jest.fn().mockResolvedValue({ text: '', bytes: 0 }),
  processAudioFile: jest.fn(),
}));

jest.mock('~/server/controllers/assistants/v2', () => ({
  addResourceFileId: jest.fn(),
  deleteResourceFileId: jest.fn(),
}));

jest.mock('~/server/controllers/assistants/helpers', () => ({
  getOpenAIClient: jest.fn(),
}));

jest.mock('~/server/services/Tools/credentials', () => ({
  loadAuthValues: jest.fn(),
}));

jest.mock('~/server/services/Files/strategies', () => ({
  getStrategyFunctions: jest.fn(() => ({ deleteFile: jest.fn().mockResolvedValue(undefined) })),
}));

jest.mock('~/server/services/Files/Audio/STTService', () => ({
  STTService: { getInstance: jest.fn() },
}));

jest.mock('~/server/services/Config', () => ({
  checkCapability: jest.fn().mockResolvedValue(true),
}));

jest.mock('~/cache', () => ({
  getLogStores: jest.fn(() => ({ get: jest.fn(), set: jest.fn(), delete: jest.fn() })),
}));

// Replace the mocked `~/models` from the sibling process.spec.js with real,
// mongoose-backed methods. All our in-memory models share this module.
jest.mock('~/models', () => {
  const mongoose = require('mongoose');
  const { createMethods } = require('@librechat/data-schemas');
  return createMethods(mongoose, {
    removeAllPermissions: jest.fn().mockResolvedValue(undefined),
  });
});

require('module-alias/register');
const { processDeleteRequest } = require('./process');

describe('processDeleteRequest — agent reference cleanup (issue #12776)', () => {
  let mongoServer;
  let Agent;
  let File;

  beforeAll(async () => {
    mongoServer = await MongoMemoryServer.create();
    await mongoose.connect(mongoServer.getUri());

    // createMethods (via ~/models) registers the File model as a side-effect,
    // but we also need the Agent model registered before any queries run.
    Agent = mongoose.models.Agent || mongoose.model('Agent', agentSchema);
    File = mongoose.models.File || mongoose.model('File', fileSchema);
    // Touch createMethods once so the migration/setup side-effects run.
    createMethods(mongoose, { removeAllPermissions: jest.fn() });
  }, 30000);

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

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

  const seedFile = async (file_id, userId) =>
    File.create({
      file_id,
      user: userId,
      filename: `${file_id}.txt`,
      filepath: `/tmp/${file_id}`,
      object: 'file',
      type: 'text/plain',
      bytes: 1,
      source: FileSources.text,
    });

  const seedAgent = async (authorId, tool_resources) =>
    Agent.create({
      id: `agent_${Math.random().toString(36).slice(2, 10)}`,
      name: 'Integration Test Agent',
      provider: 'test',
      model: 'test-model',
      author: authorId,
      tool_resources,
    });

  const buildReq = (fileDocs, extraBody = {}) => ({
    user: { id: fileDocs[0].user.toString() },
    body: { files: fileDocs, ...extraBody },
    config: { fileStrategy: 'local', fileConfig: {}, endpoints: {} },
  });

  test('strips deleted file_ids from every agent that referenced them', async () => {
    const userId = new mongoose.Types.ObjectId();
    const keeperId = `file_keeper_${Math.random().toString(36).slice(2, 10)}`;
    const deletedId = `file_deleted_${Math.random().toString(36).slice(2, 10)}`;

    const deletedFile = await seedFile(deletedId, userId);
    await seedFile(keeperId, userId);

    // Two agents both reference the file that's about to be deleted, plus the
    // keeper. A third, unrelated agent has a different file_id and must not be
    // touched by the cleanup.
    const agentA = await seedAgent(userId, {
      file_search: { file_ids: [deletedId, keeperId] },
    });
    const agentB = await seedAgent(userId, {
      execute_code: { file_ids: [deletedId] },
    });
    const untouchedAgent = await seedAgent(userId, {
      context: { file_ids: [keeperId] },
    });

    await processDeleteRequest({ req: buildReq([deletedFile.toObject()]), files: [deletedFile] });

    expect(await File.findOne({ file_id: deletedId })).toBeNull();
    expect(await File.findOne({ file_id: keeperId })).not.toBeNull();

    const updatedA = await Agent.findOne({ id: agentA.id }).lean();
    const updatedB = await Agent.findOne({ id: agentB.id }).lean();
    const updatedUntouched = await Agent.findOne({ id: untouchedAgent.id }).lean();

    expect(updatedA.tool_resources.file_search.file_ids).toEqual([keeperId]);
    expect(updatedB.tool_resources.execute_code.file_ids).toEqual([]);
    expect(updatedUntouched.tool_resources.context.file_ids).toEqual([keeperId]);
  });

  test('is a no-op when no agent references the deleted file', async () => {
    const userId = new mongoose.Types.ObjectId();
    const loneId = `file_lone_${Math.random().toString(36).slice(2, 10)}`;
    const loneFile = await seedFile(loneId, userId);
    const unrelatedAgent = await seedAgent(userId, {
      file_search: { file_ids: ['other_id'] },
    });

    await processDeleteRequest({ req: buildReq([loneFile.toObject()]), files: [loneFile] });

    expect(await File.findOne({ file_id: loneId })).toBeNull();
    const after = await Agent.findOne({ id: unrelatedAgent.id }).lean();
    expect(after.tool_resources.file_search.file_ids).toEqual(['other_id']);
  });

  test('still deletes the file when the agent cleanup step throws', async () => {
    const userId = new mongoose.Types.ObjectId();
    const targetId = `file_target_${Math.random().toString(36).slice(2, 10)}`;
    const targetFile = await seedFile(targetId, userId);

    const db = require('~/models');
    const original = db.removeAgentResourceFilesFromAllAgents;
    db.removeAgentResourceFilesFromAllAgents = jest
      .fn()
      .mockRejectedValue(new Error('simulated cleanup failure'));

    try {
      await processDeleteRequest({ req: buildReq([targetFile.toObject()]), files: [targetFile] });
      expect(await File.findOne({ file_id: targetId })).toBeNull();
    } finally {
      db.removeAgentResourceFilesFromAllAgents = original;
    }
  });
});
