/**
 * AzureCustomEmbeddingProvider tests.
 *
 * Pins:
 *   - URL normalization: baseURL with/without trailing slash, already
 *     -suffixed URLs, raw input — all converge on `<base>/embeddings`.
 *   - Required constructor args (endpoint, deployment, getApiKey).
 *   - getInfo() reports azure-custom + correct dims + user's deployment.
 *   - Happy path POSTs `{input, model, encoding_format:'float'}` with
 *     bearer auth.
 *   - Missing key → AUTH_ERROR, no HTTP call.
 *   - Fingerprint encodes model + dims so swapping deployment triggers
 *     re-embed via the existing VectorStore drift logic.
 *   - Custom dimensions honoured.
 *   - Default dimensions kick in when none supplied.
 *   - Vendor key rotation is read via the callback on each call.
 */

import { describe, test, expect, jest, beforeEach } from '@jest/globals';
import { EmbeddingError, EMBEDDING_ERROR_CODES } from '../providerInterface.js';
import {
  AzureCustomEmbeddingProvider,
  normalizeEmbeddingsUrl,
} from '../providers/azureCustomProvider.js';

function makeMockFetch({ status = 200, json = null, text = '', throwError = null } = {}) {
  return jest.fn(async () => {
    if (throwError) throw throwError;
    return {
      ok: status >= 200 && status < 300,
      status,
      async json() {
        if (json === null) throw new Error('no json');
        return json;
      },
      async text() { return text; },
    };
  });
}

describe('normalizeEmbeddingsUrl', () => {
  test('appends /embeddings to a baseURL with trailing slash', () => {
    expect(normalizeEmbeddingsUrl('https://x.azure.com/openai/v1/'))
      .toBe('https://x.azure.com/openai/v1/embeddings');
  });

  test('appends /embeddings to a baseURL without trailing slash', () => {
    expect(normalizeEmbeddingsUrl('https://x.azure.com/openai/v1'))
      .toBe('https://x.azure.com/openai/v1/embeddings');
  });

  test('leaves URL untouched when it already ends with /embeddings', () => {
    expect(normalizeEmbeddingsUrl('https://x.azure.com/openai/v1/embeddings'))
      .toBe('https://x.azure.com/openai/v1/embeddings');
  });

  test('strips trailing slashes even on already-suffixed URLs', () => {
    expect(normalizeEmbeddingsUrl('https://x.azure.com/openai/v1/embeddings/'))
      .toBe('https://x.azure.com/openai/v1/embeddings');
  });

  test('non-string / empty input returned as-is (defensive)', () => {
    expect(normalizeEmbeddingsUrl(null)).toBeNull();
    expect(normalizeEmbeddingsUrl('')).toBe('');
    expect(normalizeEmbeddingsUrl('  ')).toBe('  ');
  });
});

describe('AzureCustomEmbeddingProvider — constructor', () => {
  test('rejects missing endpoint / deployment / getApiKey', () => {
    expect(() => new AzureCustomEmbeddingProvider({})).toThrow(/endpoint/);
    expect(() => new AzureCustomEmbeddingProvider({ endpoint: 'https://x' })).toThrow(/deployment/);
    expect(() => new AzureCustomEmbeddingProvider({
      endpoint: 'https://x', deployment: 'm',
    })).toThrow(/getApiKey/);
  });

  test('default dimensions = 1536 when none supplied', () => {
    const p = new AzureCustomEmbeddingProvider({
      endpoint: 'https://x/v1/',
      deployment: 'embed-v-4-0',
      getApiKey: () => 'k',
    });
    expect(p.getInfo().dimensions).toBe(1536);
  });

  test('custom dimensions honoured', () => {
    const p = new AzureCustomEmbeddingProvider({
      endpoint: 'https://x/v1/',
      deployment: 'embed-v-4-0',
      dimensions: 1024,
      getApiKey: () => 'k',
    });
    expect(p.getInfo().dimensions).toBe(1024);
  });

  test('invalid dimensions fall back to default', () => {
    const p = new AzureCustomEmbeddingProvider({
      endpoint: 'https://x/v1/',
      deployment: 'embed-v-4-0',
      dimensions: -5,
      getApiKey: () => 'k',
    });
    expect(p.getInfo().dimensions).toBe(1536);
  });
});

describe('AzureCustomEmbeddingProvider — getInfo + health', () => {
  test('getInfo reports azure-custom + the user\'s deployment as modelId', () => {
    const p = new AzureCustomEmbeddingProvider({
      endpoint: 'https://x.azure.com/openai/v1/',
      deployment: 'embed-v-4-0',
      getApiKey: () => 'k',
    });
    expect(p.getInfo()).toMatchObject({
      name: 'azure-custom',
      modelId: 'embed-v-4-0',
      dimensions: 1536,
      isLocal: false,
      distinguishesQueryAndDocument: false,
    });
  });

  test('health() reflects missing key with a remediation message', async () => {
    const getApiKey = jest.fn(() => null);
    const p = new AzureCustomEmbeddingProvider({
      endpoint: 'https://x/v1/', deployment: 'm', getApiKey,
    });
    const h = await p.health();
    expect(h.ok).toBe(false);
    expect(h.reason).toMatch(/API key/);
  });
});

describe('AzureCustomEmbeddingProvider — HTTP call shape', () => {
  let fetchImpl, getApiKey, provider;

  beforeEach(() => {
    fetchImpl = makeMockFetch({
      status: 200,
      json: { data: [{ embedding: Array(1536).fill(0.01) }] },
    });
    getApiKey = jest.fn(() => 'sk-azure-test');
    provider = new AzureCustomEmbeddingProvider({
      endpoint: 'https://loxia-ai-resource.services.ai.azure.com/openai/v1/',
      deployment: 'embed-v-4-0',
      getApiKey,
      fetchImpl,
    });
  });

  test('POSTs to the normalized URL with bearer auth', async () => {
    await provider.embedDocuments(['hello']);
    const [url, init] = fetchImpl.mock.calls[0];
    expect(url).toBe('https://loxia-ai-resource.services.ai.azure.com/openai/v1/embeddings');
    expect(init.headers.Authorization).toBe('Bearer sk-azure-test');
  });

  test('sends {input, model, encoding_format:"float"} body', async () => {
    await provider.embedDocuments(['hello']);
    const [, init] = fetchImpl.mock.calls[0];
    expect(JSON.parse(init.body)).toEqual({
      input: ['hello'],
      model: 'embed-v-4-0',
      encoding_format: 'float',
    });
  });

  test('missing API key → AUTH_ERROR, no HTTP call', async () => {
    getApiKey.mockReturnValue(null);
    await expect(provider.embedDocuments(['x'])).rejects.toMatchObject({
      code: EMBEDDING_ERROR_CODES.AUTH_ERROR,
      transient: false,
      provider: 'azure-custom',
    });
    expect(fetchImpl).not.toHaveBeenCalled();
  });

  test('API key is read fresh per call (rotation works)', async () => {
    await provider.embedDocuments(['a']);
    getApiKey.mockReturnValue('sk-azure-rotated');
    await provider.embedDocuments(['b']);
    expect(fetchImpl.mock.calls[1][1].headers.Authorization).toBe('Bearer sk-azure-rotated');
  });

  test('batches respect maxBatchSize', async () => {
    // The mock returns one embedding per input it sees, so the per-batch
    // count always matches what the base class expects.
    const dynamicFetch = jest.fn(async (_url, init) => {
      const body = JSON.parse(init.body);
      const count = Array.isArray(body.input) ? body.input.length : 1;
      return {
        ok: true,
        status: 200,
        async json() {
          return { data: Array(count).fill({ embedding: Array(1536).fill(0.01) }) };
        },
        async text() { return ''; },
      };
    });
    provider = new AzureCustomEmbeddingProvider({
      endpoint: 'https://x/v1/',
      deployment: 'embed-v-4-0',
      getApiKey,
      fetchImpl: dynamicFetch,
      maxBatchSize: 2,
    });
    await provider.embedDocuments(['a', 'b', 'c']);  // 2-chunk batch then single
    expect(dynamicFetch).toHaveBeenCalledTimes(2);
    const batches = dynamicFetch.mock.calls.map(c => JSON.parse(c[1].body).input.length);
    expect(batches).toEqual([2, 1]);
  });
});

describe('AzureCustomEmbeddingProvider — fingerprint', () => {
  test('fingerprint encodes model + dimensions so swaps trigger re-embed', () => {
    const a = new AzureCustomEmbeddingProvider({
      endpoint: 'https://x/v1/', deployment: 'embed-v-4-0', dimensions: 1536, getApiKey: () => 'k',
    });
    const b = new AzureCustomEmbeddingProvider({
      endpoint: 'https://x/v1/', deployment: 'embed-v-4-0', dimensions: 1024, getApiKey: () => 'k',
    });
    const c = new AzureCustomEmbeddingProvider({
      endpoint: 'https://x/v1/', deployment: 'text-embedding-3-small', dimensions: 1536, getApiKey: () => 'k',
    });
    expect(a.getModelFingerprint()).toBe('azure-custom:embed-v-4-0@1536d');
    expect(b.getModelFingerprint()).toBe('azure-custom:embed-v-4-0@1024d');
    expect(c.getModelFingerprint()).toBe('azure-custom:text-embedding-3-small@1536d');
    expect(a.getModelFingerprint()).not.toBe(b.getModelFingerprint());
    expect(a.getModelFingerprint()).not.toBe(c.getModelFingerprint());
  });
});
