/**
 * embeddingsConfig — defaults, deep-merge, validation, model catalog,
 * token estimator.
 */

import { describe, test, expect } from '@jest/globals';
import {
  DEFAULT_CONFIG,
  PROVIDERS,
  mergeConfig,
  validateConfig,
  getProviderModelOptions,
  estimateTokens,
} from '../embeddingsConfig.js';

describe('DEFAULT_CONFIG', () => {
  test('starts disabled', () => {
    expect(DEFAULT_CONFIG.provider).toBe('disabled');
  });

  test('default local model is nomic-embed-text-v1.5', () => {
    expect(DEFAULT_CONFIG.local.modelId).toBe('Xenova/nomic-embed-text-v1.5');
  });

  test('default cloud model is text-embedding-3-small (both providers)', () => {
    expect(DEFAULT_CONFIG.azure.modelId).toBe('text-embedding-3-small');
    expect(DEFAULT_CONFIG.openai.modelId).toBe('text-embedding-3-small');
  });

  test('default budget has a sane daily cap', () => {
    expect(DEFAULT_CONFIG.budget.dailyTokenLimit).toBeGreaterThan(0);
  });

  test('PROVIDERS lists all known options', () => {
    expect(PROVIDERS).toEqual(['local', 'azure', 'openai', 'azure-custom', 'disabled']);
  });

  test('azure-custom defaults to empty endpoint + deployment', () => {
    expect(DEFAULT_CONFIG.azureCustom).toEqual(expect.objectContaining({
      endpoint: '', deployment: '', dimensions: 1536, maxBatchSize: 16,
    }));
  });
});

describe('mergeConfig / validateConfig — azure-custom', () => {
  test('mergeConfig honours azureCustom overrides', () => {
    const m = mergeConfig({
      provider: 'azure-custom',
      azureCustom: {
        endpoint: 'https://x.azure.com/openai/v1/',
        deployment: 'embed-v-4-0',
        dimensions: 1024,
      },
    });
    expect(m.azureCustom.endpoint).toBe('https://x.azure.com/openai/v1/');
    expect(m.azureCustom.deployment).toBe('embed-v-4-0');
    expect(m.azureCustom.dimensions).toBe(1024);
  });

  test('validateConfig requires endpoint + deployment when provider=azure-custom', () => {
    const v1 = validateConfig(mergeConfig({ provider: 'azure-custom' }));
    expect(v1.ok).toBe(false);
    expect(v1.errors.join(' ')).toMatch(/endpoint/);
    expect(v1.errors.join(' ')).toMatch(/deployment/);

    const v2 = validateConfig(mergeConfig({
      provider: 'azure-custom',
      azureCustom: { endpoint: 'x', deployment: 'm' },
    }));
    expect(v2.ok).toBe(true);
  });

  test('validateConfig rejects non-positive dimensions for azure-custom', () => {
    const v = validateConfig(mergeConfig({
      provider: 'azure-custom',
      azureCustom: { endpoint: 'x', deployment: 'm', dimensions: -5 },
    }));
    expect(v.ok).toBe(false);
    expect(v.errors.join(' ')).toMatch(/dimensions/);
  });
});

describe('mergeConfig', () => {
  test('null user config → defaults', () => {
    expect(mergeConfig(null).provider).toBe('disabled');
    expect(mergeConfig(undefined).local.modelId).toBe('Xenova/nomic-embed-text-v1.5');
  });

  test('top-level provider override wins', () => {
    expect(mergeConfig({ provider: 'azure' }).provider).toBe('azure');
  });

  test('per-provider overrides merge (do NOT replace) defaults', () => {
    const m = mergeConfig({ local: { modelId: 'Custom/x' } });
    expect(m.local.modelId).toBe('Custom/x');
    expect(m.local.maxBatchSize).toBe(32);   // default preserved
  });

  test('budget merges field-by-field', () => {
    const m = mergeConfig({ budget: { dailyTokenLimit: 100 } });
    expect(m.budget.dailyTokenLimit).toBe(100);
  });

  test('returned object is mutable (defaults stay frozen)', () => {
    const m = mergeConfig({});
    expect(() => { m.provider = 'azure'; }).not.toThrow();
    // The frozen DEFAULT_CONFIG must NOT be mutated through the merge.
    expect(DEFAULT_CONFIG.provider).toBe('disabled');
  });
});

describe('validateConfig', () => {
  test('rejects unknown provider', () => {
    const v = validateConfig({ provider: 'cohere' });
    expect(v.ok).toBe(false);
    expect(v.errors.some(e => /provider/.test(e))).toBe(true);
  });

  test('local requires modelId', () => {
    const v = validateConfig(mergeConfig({ provider: 'local', local: { modelId: '' } }));
    expect(v.ok).toBe(false);
    expect(v.errors.some(e => /local.modelId/.test(e))).toBe(true);
  });

  test('local.maxBatchSize must be positive', () => {
    const v = validateConfig(mergeConfig({ provider: 'local', local: { maxBatchSize: -1 } }));
    expect(v.ok).toBe(false);
  });

  test('valid config passes', () => {
    expect(validateConfig(mergeConfig({ provider: 'azure' })).ok).toBe(true);
    expect(validateConfig(mergeConfig({ provider: 'local' })).ok).toBe(true);
    expect(validateConfig(mergeConfig({ provider: 'disabled' })).ok).toBe(true);
  });

  test('budget.dailyTokenLimit must be 0 or positive int', () => {
    expect(validateConfig(mergeConfig({ provider: 'azure', budget: { dailyTokenLimit: 0 } })).ok).toBe(true);
    expect(validateConfig(mergeConfig({ provider: 'azure', budget: { dailyTokenLimit: 100 } })).ok).toBe(true);
    expect(validateConfig(mergeConfig({ provider: 'azure', budget: { dailyTokenLimit: -1 } })).ok).toBe(false);
    expect(validateConfig(mergeConfig({ provider: 'azure', budget: { dailyTokenLimit: 0.5 } })).ok).toBe(false);
  });
});

describe('getProviderModelOptions', () => {
  test('local options come from the LOCAL_MODELS catalog', () => {
    const opts = getProviderModelOptions('local');
    expect(opts.length).toBeGreaterThanOrEqual(2);
    const ids = opts.map(o => o.id);
    expect(ids).toContain('Xenova/nomic-embed-text-v1.5');
    expect(ids).toContain('Xenova/bge-small-en-v1.5');
  });

  test('azure + openai expose 3-small and 3-large', () => {
    for (const provider of ['azure', 'openai']) {
      const opts = getProviderModelOptions(provider);
      expect(opts.map(o => o.id)).toEqual([
        'text-embedding-3-small',
        'text-embedding-3-large',
      ]);
    }
  });

  test('unknown provider → empty list (no throw)', () => {
    expect(getProviderModelOptions('does-not-exist')).toEqual([]);
  });
});

describe('estimateTokens', () => {
  test('empty / non-string → 0', () => {
    expect(estimateTokens('')).toBe(0);
    expect(estimateTokens(null)).toBe(0);
    expect(estimateTokens(undefined)).toBe(0);
    expect(estimateTokens(42)).toBe(0);
  });

  test('~4 chars/token rule of thumb', () => {
    expect(estimateTokens('a'.repeat(4))).toBe(1);
    expect(estimateTokens('a'.repeat(40))).toBe(10);
    // Ceil for partial tokens (5 chars → 2)
    expect(estimateTokens('a'.repeat(5))).toBe(2);
  });
});
