const mongoose = require('mongoose');

const mockLogger = { error: jest.fn(), warn: jest.fn(), info: jest.fn(), debug: jest.fn() };
const mockGetTenantId = jest.fn();

jest.mock('@librechat/data-schemas', () => ({
  logger: mockLogger,
  getTenantId: mockGetTenantId,
  SYSTEM_TENANT_ID: '__SYSTEM__',
}));

const { AccessRoleIds, ResourceType, PrincipalType } =
  jest.requireActual('librechat-data-provider');

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

jest.mock('@librechat/api', () => ({
  enrichRemoteAgentPrincipals: jest.fn(),
  backfillRemoteAgentPermissions: jest.fn(),
}));

const mockBulkUpdateResourcePermissions = jest.fn();

jest.mock('~/server/services/PermissionService', () => ({
  bulkUpdateResourcePermissions: (...args) => mockBulkUpdateResourcePermissions(...args),
  ensureGroupPrincipalExists: jest.fn(),
  getEffectivePermissions: jest.fn(),
  ensurePrincipalExists: jest.fn(),
  getAvailableRoles: jest.fn(),
  findAccessibleResources: jest.fn(),
  getResourcePermissionsMap: jest.fn(),
}));

const mockRemoveAgentFromUserFavorites = jest.fn();

jest.mock('~/models', () => ({
  aggregateAclEntries: jest.fn(),
  searchPrincipals: jest.fn(),
  sortPrincipalsByRelevance: jest.fn(),
  calculateRelevanceScore: jest.fn(),
  removeAgentFromUserFavorites: (...args) => mockRemoveAgentFromUserFavorites(...args),
}));

jest.mock('~/server/services/GraphApiService', () => ({
  entraIdPrincipalFeatureEnabled: jest.fn(() => false),
  searchEntraIdPrincipals: jest.fn(),
}));

const db = require('~/models');
const {
  updateResourcePermissions,
  searchPrincipals,
  getResourcePermissions,
} = require('../PermissionsController');

const createMockReq = (overrides = {}) => ({
  params: { resourceType: ResourceType.AGENT, resourceId: '507f1f77bcf86cd799439011' },
  body: { updated: [], removed: [], public: false },
  user: { id: 'user-1', role: 'USER' },
  headers: { authorization: '' },
  ...overrides,
});

const createMockRes = () => {
  const res = {};
  res.status = jest.fn().mockReturnValue(res);
  res.json = jest.fn().mockReturnValue(res);
  return res;
};

const flushPromises = () => new Promise((resolve) => setImmediate(resolve));

describe('PermissionsController', () => {
  beforeEach(() => {
    jest.clearAllMocks();
    mockGetTenantId.mockReturnValue(undefined);
  });

  describe('searchPrincipals', () => {
    beforeEach(() => {
      db.searchPrincipals.mockResolvedValue([]);
      db.calculateRelevanceScore.mockReturnValue(50);
      db.sortPrincipalsByRelevance.mockImplementation((results) => results);
    });

    it('rejects non-string query parameters', async () => {
      const req = createMockReq({
        query: { q: ['alice'] },
      });
      const res = createMockRes();

      await searchPrincipals(req, res);

      expect(res.status).toHaveBeenCalledWith(400);
      expect(res.json).toHaveBeenCalledWith({
        error: 'Query parameter "q" is required and must not be empty',
      });
      expect(db.searchPrincipals).not.toHaveBeenCalled();
    });

    it('searches with the trimmed literal query', async () => {
      db.searchPrincipals.mockResolvedValue([
        {
          id: 'user-1',
          type: PrincipalType.USER,
          name: 'Regex [invalid User',
          source: 'local',
        },
      ]);

      const req = createMockReq({
        query: { q: '  [invalid  ', limit: '5', types: PrincipalType.USER },
      });
      const res = createMockRes();

      await searchPrincipals(req, res);

      expect(db.searchPrincipals).toHaveBeenCalledWith('[invalid', 5, [PrincipalType.USER]);
      expect(db.calculateRelevanceScore).toHaveBeenCalledWith(
        expect.objectContaining({ name: 'Regex [invalid User' }),
        '[invalid',
      );
      expect(res.status).toHaveBeenCalledWith(200);
      expect(res.json).toHaveBeenCalledWith(
        expect.objectContaining({
          query: '[invalid',
          limit: 5,
          count: 1,
        }),
      );
    });

    it('does not expose internal error details on search failures', async () => {
      db.searchPrincipals.mockRejectedValue(new Error('database failure with internal detail'));

      const req = createMockReq({
        query: { q: 'alice' },
      });
      const res = createMockRes();

      await searchPrincipals(req, res);

      expect(res.status).toHaveBeenCalledWith(500);
      expect(res.json).toHaveBeenCalledWith({
        error: 'Failed to search principals',
      });
    });
  });

  describe('getResourcePermissions — principal details', () => {
    const currentTenantId = 'tenant-a';
    const otherTenantId = 'tenant-b';
    const userId = new mongoose.Types.ObjectId();
    const groupId = new mongoose.Types.ObjectId();

    it('omits joined user and group details outside the current request context', async () => {
      mockGetTenantId.mockReturnValue(currentTenantId);
      db.aggregateAclEntries.mockResolvedValue([
        {
          principalType: PrincipalType.USER,
          accessRoleId: AccessRoleIds.AGENT_VIEWER,
          userInfo: {
            _id: userId,
            tenantId: otherTenantId,
            name: 'Outside User',
            email: 'outside-user@example.com',
            avatar: 'outside-user.png',
          },
        },
        {
          principalType: PrincipalType.GROUP,
          accessRoleId: AccessRoleIds.AGENT_VIEWER,
          groupInfo: {
            _id: groupId,
            tenantId: otherTenantId,
            name: 'Outside Group',
            email: 'outside-group@example.com',
            avatar: 'outside-group.png',
          },
        },
        {
          principalType: PrincipalType.PUBLIC,
          accessRoleId: AccessRoleIds.AGENT_VIEWER,
        },
      ]);

      const req = createMockReq();
      const res = createMockRes();

      await getResourcePermissions(req, res);

      expect(res.status).toHaveBeenCalledWith(200);
      expect(res.json).toHaveBeenCalledWith({
        resourceType: ResourceType.AGENT,
        resourceId: req.params.resourceId,
        principals: [],
        public: true,
        publicAccessRoleId: AccessRoleIds.AGENT_VIEWER,
      });
      expect(JSON.stringify(res.json.mock.calls[0][0])).not.toContain('outside-user@example.com');
      expect(JSON.stringify(res.json.mock.calls[0][0])).not.toContain('outside-group@example.com');
    });

    it('includes joined user and group details in the current request context', async () => {
      mockGetTenantId.mockReturnValue(currentTenantId);
      db.aggregateAclEntries.mockResolvedValue([
        {
          principalType: PrincipalType.USER,
          accessRoleId: AccessRoleIds.AGENT_VIEWER,
          userInfo: {
            _id: userId,
            tenantId: currentTenantId,
            name: 'Current User',
            email: 'current-user@example.com',
            avatar: 'current-user.png',
          },
        },
        {
          principalType: PrincipalType.GROUP,
          accessRoleId: AccessRoleIds.AGENT_VIEWER,
          groupInfo: {
            _id: groupId,
            tenantId: currentTenantId,
            name: 'Current Group',
            email: 'current-group@example.com',
            avatar: 'current-group.png',
          },
        },
      ]);

      const req = createMockReq();
      const res = createMockRes();

      await getResourcePermissions(req, res);

      expect(res.status).toHaveBeenCalledWith(200);
      expect(res.json.mock.calls[0][0].principals).toEqual([
        expect.objectContaining({
          type: PrincipalType.USER,
          id: userId.toString(),
          email: 'current-user@example.com',
        }),
        expect.objectContaining({
          type: PrincipalType.GROUP,
          id: groupId.toString(),
          email: 'current-group@example.com',
        }),
      ]);
    });
  });

  describe('updateResourcePermissions — favorites cleanup', () => {
    const agentObjectId = new mongoose.Types.ObjectId().toString();
    const revokedUserId = new mongoose.Types.ObjectId().toString();

    beforeEach(() => {
      mockBulkUpdateResourcePermissions.mockResolvedValue({
        granted: [],
        updated: [],
        revoked: [{ type: PrincipalType.USER, id: revokedUserId, name: 'Revoked User' }],
        errors: [],
      });

      mockRemoveAgentFromUserFavorites.mockResolvedValue(undefined);
    });

    it('removes agent from revoked users favorites on AGENT resource type', async () => {
      const req = createMockReq({
        params: { resourceType: ResourceType.AGENT, resourceId: agentObjectId },
        body: {
          updated: [],
          removed: [{ type: PrincipalType.USER, id: revokedUserId }],
          public: false,
        },
      });
      const res = createMockRes();

      await updateResourcePermissions(req, res);
      await flushPromises();

      expect(res.status).toHaveBeenCalledWith(200);
      expect(mockRemoveAgentFromUserFavorites).toHaveBeenCalledWith(agentObjectId, [revokedUserId]);
    });

    it('removes agent from revoked users favorites on REMOTE_AGENT resource type', async () => {
      const req = createMockReq({
        params: { resourceType: ResourceType.REMOTE_AGENT, resourceId: agentObjectId },
        body: {
          updated: [],
          removed: [{ type: PrincipalType.USER, id: revokedUserId }],
          public: false,
        },
      });
      const res = createMockRes();

      await updateResourcePermissions(req, res);
      await flushPromises();

      expect(mockRemoveAgentFromUserFavorites).toHaveBeenCalledWith(agentObjectId, [revokedUserId]);
    });

    it('uses results.revoked (validated) not raw request payload', async () => {
      const validId = new mongoose.Types.ObjectId().toString();
      const invalidId = 'not-a-valid-id';

      mockBulkUpdateResourcePermissions.mockResolvedValue({
        granted: [],
        updated: [],
        revoked: [{ type: PrincipalType.USER, id: validId }],
        errors: [{ principal: { type: PrincipalType.USER, id: invalidId }, error: 'Invalid ID' }],
      });

      const req = createMockReq({
        params: { resourceType: ResourceType.AGENT, resourceId: agentObjectId },
        body: {
          updated: [],
          removed: [
            { type: PrincipalType.USER, id: validId },
            { type: PrincipalType.USER, id: invalidId },
          ],
          public: false,
        },
      });
      const res = createMockRes();

      await updateResourcePermissions(req, res);
      await flushPromises();

      expect(mockRemoveAgentFromUserFavorites).toHaveBeenCalledWith(agentObjectId, [validId]);
    });

    it('skips cleanup when no USER principals are revoked', async () => {
      mockBulkUpdateResourcePermissions.mockResolvedValue({
        granted: [],
        updated: [],
        revoked: [{ type: PrincipalType.GROUP, id: 'group-1' }],
        errors: [],
      });

      const req = createMockReq({
        params: { resourceType: ResourceType.AGENT, resourceId: agentObjectId },
        body: {
          updated: [],
          removed: [{ type: PrincipalType.GROUP, id: 'group-1' }],
          public: false,
        },
      });
      const res = createMockRes();

      await updateResourcePermissions(req, res);
      await flushPromises();

      expect(mockRemoveAgentFromUserFavorites).not.toHaveBeenCalled();
    });

    it('skips cleanup for non-agent resource types', async () => {
      mockBulkUpdateResourcePermissions.mockResolvedValue({
        granted: [],
        updated: [],
        revoked: [{ type: PrincipalType.USER, id: revokedUserId }],
        errors: [],
      });

      const req = createMockReq({
        params: { resourceType: ResourceType.PROMPTGROUP, resourceId: agentObjectId },
        body: {
          updated: [],
          removed: [{ type: PrincipalType.USER, id: revokedUserId }],
          public: false,
        },
      });
      const res = createMockRes();

      await updateResourcePermissions(req, res);
      await flushPromises();

      expect(res.status).toHaveBeenCalledWith(200);
      expect(mockRemoveAgentFromUserFavorites).not.toHaveBeenCalled();
    });

    it('handles agent not found gracefully', async () => {
      mockRemoveAgentFromUserFavorites.mockResolvedValue(undefined);

      const req = createMockReq({
        params: { resourceType: ResourceType.AGENT, resourceId: agentObjectId },
        body: {
          updated: [],
          removed: [{ type: PrincipalType.USER, id: revokedUserId }],
          public: false,
        },
      });
      const res = createMockRes();

      await updateResourcePermissions(req, res);
      await flushPromises();

      expect(mockRemoveAgentFromUserFavorites).toHaveBeenCalled();
      expect(res.status).toHaveBeenCalledWith(200);
    });

    it('logs error when removeAgentFromUserFavorites fails without blocking response', async () => {
      mockRemoveAgentFromUserFavorites.mockRejectedValue(new Error('DB connection lost'));

      const req = createMockReq({
        params: { resourceType: ResourceType.AGENT, resourceId: agentObjectId },
        body: {
          updated: [],
          removed: [{ type: PrincipalType.USER, id: revokedUserId }],
          public: false,
        },
      });
      const res = createMockRes();

      await updateResourcePermissions(req, res);
      await flushPromises();

      expect(res.status).toHaveBeenCalledWith(200);
      expect(mockLogger.error).toHaveBeenCalledWith(
        '[removeRevokedAgentFromFavorites] Error cleaning up favorites',
        expect.any(Error),
      );
    });
  });
});
