// ── Mocks ──────────────────────────────────────────────────────────────

const mockClearAppConfigCache = jest.fn().mockResolvedValue(undefined);
const mockClearOverrideCache = jest.fn().mockResolvedValue(undefined);

jest.mock('~/cache/getLogStores', () => {
  return jest.fn(() => ({}));
});

jest.mock('~/server/services/start/tools', () => ({
  loadAndFormatTools: jest.fn(() => ({})),
}));

jest.mock('../loadCustomConfig', () => jest.fn().mockResolvedValue({}));

jest.mock('@librechat/data-schemas', () => {
  const actual = jest.requireActual('@librechat/data-schemas');
  return { ...actual, AppService: jest.fn(() => ({ availableTools: {} })) };
});

jest.mock('~/models', () => ({
  getApplicableConfigs: jest.fn().mockResolvedValue([]),
  getUserPrincipals: jest.fn().mockResolvedValue([]),
}));

const mockInvalidateCachedTools = jest.fn().mockResolvedValue(undefined);
jest.mock('../getCachedTools', () => ({
  setCachedTools: jest.fn().mockResolvedValue(undefined),
  invalidateCachedTools: mockInvalidateCachedTools,
}));

const mockClearMcpConfigCache = jest.fn().mockResolvedValue(undefined);
jest.mock('@librechat/api', () => ({
  createAppConfigService: jest.fn(() => ({
    getAppConfig: jest.fn().mockResolvedValue({ availableTools: {} }),
    clearAppConfigCache: mockClearAppConfigCache,
    clearOverrideCache: mockClearOverrideCache,
  })),
  clearMcpConfigCache: mockClearMcpConfigCache,
}));

// ── Tests ──────────────────────────────────────────────────────────────

const { invalidateConfigCaches } = require('../app');

describe('invalidateConfigCaches', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  it('clears all caches', async () => {
    await invalidateConfigCaches();

    expect(mockClearAppConfigCache).toHaveBeenCalledTimes(1);
    expect(mockClearOverrideCache).toHaveBeenCalledTimes(1);
    expect(mockInvalidateCachedTools).toHaveBeenCalledWith({ invalidateGlobal: true });
    expect(mockClearMcpConfigCache).toHaveBeenCalledTimes(1);
  });

  it('passes tenantId through to clearOverrideCache', async () => {
    await invalidateConfigCaches('tenant-a');

    expect(mockClearOverrideCache).toHaveBeenCalledWith('tenant-a');
    expect(mockClearAppConfigCache).toHaveBeenCalledTimes(1);
    expect(mockInvalidateCachedTools).toHaveBeenCalledWith({ invalidateGlobal: true });
  });

  it('all operations run in parallel (not sequentially)', async () => {
    const order = [];

    mockClearAppConfigCache.mockImplementation(
      () =>
        new Promise((r) =>
          setTimeout(() => {
            order.push('base');
            r();
          }, 10),
        ),
    );
    mockClearOverrideCache.mockImplementation(
      () =>
        new Promise((r) =>
          setTimeout(() => {
            order.push('override');
            r();
          }, 10),
        ),
    );
    mockInvalidateCachedTools.mockImplementation(
      () =>
        new Promise((r) =>
          setTimeout(() => {
            order.push('tools');
            r();
          }, 10),
        ),
    );
    mockClearMcpConfigCache.mockImplementation(
      () =>
        new Promise((r) =>
          setTimeout(() => {
            order.push('mcp');
            r();
          }, 10),
        ),
    );

    await invalidateConfigCaches();

    expect(order).toHaveLength(4);
    expect(new Set(order)).toEqual(new Set(['base', 'override', 'tools', 'mcp']));
  });

  it('resolves even when clearAppConfigCache throws (partial failure)', async () => {
    mockClearAppConfigCache.mockRejectedValueOnce(new Error('cache connection lost'));

    await expect(invalidateConfigCaches()).resolves.not.toThrow();

    expect(mockClearOverrideCache).toHaveBeenCalledTimes(1);
    expect(mockInvalidateCachedTools).toHaveBeenCalledWith({ invalidateGlobal: true });
  });
});
