/**
 * Tests for the aiService ↔ EmbeddingService wiring.
 *
 * Pins:
 *   - `embeddingService` starts as null on the instance.
 *   - `setEmbeddingService` attaches; `getEmbeddingService` reads back.
 *   - `aiService.embed("a string")`   → embeddingService.embedQuery
 *   - `aiService.embed(["a","b"])`    → embeddingService.embedDocuments
 *   - `embed()` defaults kind='document' for arrays; passes `kind` through.
 *   - Calling `embed()` without an attached service throws clearly.
 *
 * We do NOT exercise the full aiService class — just the embedding
 * surface area. The constructor is tolerant enough to accept a minimal
 * config; the heavy network code paths are never reached.
 */

import { describe, test, expect, jest, beforeEach } from '@jest/globals';
import { default as AIService } from '../aiService.js';

function makeService() {
  // Bare-minimum config; aiService falls back to defaults for anything missing.
  return new AIService({ backend: { baseUrl: 'https://stub.example' } }, null, null, null);
}

function makeStubEmbeddingService() {
  return {
    isEnabled: true,
    embedQuery: jest.fn(async () => new Float32Array([1, 0, 0, 0])),
    embedDocuments: jest.fn(async (texts) =>
      texts.map(() => new Float32Array([0.5, 0.5, 0, 0]))),
  };
}

describe('aiService — embeddings wiring', () => {
  let aiService, stub;
  beforeEach(() => {
    aiService = makeService();
    stub = makeStubEmbeddingService();
  });

  test('embeddingService starts as null', () => {
    expect(aiService.embeddingService).toBeNull();
    expect(aiService.getEmbeddingService()).toBeNull();
  });

  test('setEmbeddingService attaches; getEmbeddingService reads back', () => {
    aiService.setEmbeddingService(stub);
    expect(aiService.embeddingService).toBe(stub);
    expect(aiService.getEmbeddingService()).toBe(stub);
  });

  test('setEmbeddingService(null) detaches cleanly', () => {
    aiService.setEmbeddingService(stub);
    aiService.setEmbeddingService(null);
    expect(aiService.getEmbeddingService()).toBeNull();
  });

  test('embed("string") → embedQuery', async () => {
    aiService.setEmbeddingService(stub);
    const out = await aiService.embed('hello');
    expect(stub.embedQuery).toHaveBeenCalledWith('hello');
    expect(stub.embedDocuments).not.toHaveBeenCalled();
    expect(out).toBeInstanceOf(Float32Array);
  });

  test('embed([a, b]) → embedDocuments with default kind=document', async () => {
    aiService.setEmbeddingService(stub);
    const out = await aiService.embed(['a', 'b']);
    expect(stub.embedDocuments).toHaveBeenCalledWith(['a', 'b'], 'document');
    expect(stub.embedQuery).not.toHaveBeenCalled();
    expect(out).toHaveLength(2);
  });

  test('embed([a, b], {kind:"query"}) forwards kind', async () => {
    aiService.setEmbeddingService(stub);
    await aiService.embed(['q1', 'q2'], { kind: 'query' });
    expect(stub.embedDocuments).toHaveBeenCalledWith(['q1', 'q2'], 'query');
  });

  test('embed() without an attached service throws clearly', async () => {
    await expect(aiService.embed('hi')).rejects.toThrow(/EmbeddingService/i);
  });
});

describe('aiService — augmentSystemPrompt (auto-recall integration)', () => {
  let aiService, stub;
  beforeEach(() => {
    aiService = makeService();
    stub = makeStubEmbeddingService();
    // The embedding service needs to look minimally real for the
    // augmentation path: `isEnabled` + `config.autoRecall.enabled`.
    stub.config = { autoRecall: { enabled: true, threshold: 0.5, topK: 3, maxTokens: 400, surfaces: ['memory', 'reminisce'] } };
  });

  test('returns the prompt unchanged when no service attached', async () => {
    const out = await aiService.augmentSystemPrompt('SYSTEM', 'agent-1', 'hello world');
    expect(out).toBe('SYSTEM');
  });

  test('returns the prompt unchanged when service is disabled', async () => {
    stub.isEnabled = false;
    aiService.setEmbeddingService(stub);
    const out = await aiService.augmentSystemPrompt('SYSTEM', 'agent-1', 'hello world');
    expect(out).toBe('SYSTEM');
  });

  test('returns the prompt unchanged when recentText is empty', async () => {
    aiService.setEmbeddingService(stub);
    expect(await aiService.augmentSystemPrompt('SYSTEM', 'agent-1', '')).toBe('SYSTEM');
    expect(await aiService.augmentSystemPrompt('SYSTEM', 'agent-1', '   ')).toBe('SYSTEM');
    expect(await aiService.augmentSystemPrompt('SYSTEM', 'agent-1', null)).toBe('SYSTEM');
  });

  test('non-string systemPrompt is treated as empty (defensive)', async () => {
    aiService.setEmbeddingService(stub);
    const out = await aiService.augmentSystemPrompt(null, 'agent-1', 'text');
    // No items below indexer stubs → empty section → empty string.
    expect(out).toBe('');
  });

  test('never throws even on internal failures', async () => {
    aiService.setEmbeddingService(stub);
    // No indexers wired through embeddings service factory — should still resolve.
    await expect(aiService.augmentSystemPrompt('SYSTEM', 'agent-1', 'hello')).resolves.toBeDefined();
  });

  test('appends a labelled section when items cross the threshold', async () => {
    // Inject a stub that pretends to be a fully-working indexer.
    aiService.setEmbeddingService(stub);
    aiService._memoryIndexerForRecall = {
      isEnabled: true,
      search: jest.fn(async () => ({
        enabled: true,
        hits: [{ id: 'm1', score: 0.9, metadata: { text: 'a remembered thing', title: 'Note', createdAt: '2026-05-25T00:00:00Z' } }],
      })),
    };
    aiService._reminisceIndexerForRecall = {
      isEnabled: true,
      search: jest.fn(async () => ({ enabled: true, hits: [] })),
    };
    const out = await aiService.augmentSystemPrompt('SYSTEM', 'agent-1', 'tell me about that');
    expect(out).toContain('SYSTEM');
    expect(out).toContain('## RELEVANT CONTEXT (auto-recalled)');
    expect(out).toContain('a remembered thing');
  });

  test('appends nothing when indexers return no items above threshold', async () => {
    aiService.setEmbeddingService(stub);
    aiService._memoryIndexerForRecall = {
      isEnabled: true,
      search: jest.fn(async () => ({ enabled: true, hits: [{ id: 'm1', score: 0.1, metadata: { text: 'irrelevant' } }] })),
    };
    aiService._reminisceIndexerForRecall = { isEnabled: true, search: jest.fn(async () => ({ enabled: true, hits: [] })) };
    const out = await aiService.augmentSystemPrompt('SYSTEM', 'agent-1', 'query');
    expect(out).toBe('SYSTEM');
  });

  test('per-turn opts override config (e.g. higher threshold)', async () => {
    aiService.setEmbeddingService(stub);
    aiService._memoryIndexerForRecall = {
      isEnabled: true,
      search: jest.fn(async () => ({
        enabled: true,
        hits: [{ id: 'm1', score: 0.7, metadata: { text: 'borderline', title: 'note' } }],
      })),
    };
    aiService._reminisceIndexerForRecall = { isEnabled: true, search: jest.fn(async () => ({ enabled: true, hits: [] })) };
    // Per-call threshold 0.8 should drop the 0.7 hit.
    const out = await aiService.augmentSystemPrompt('SYSTEM', 'agent-1', 'query', { threshold: 0.8 });
    expect(out).toBe('SYSTEM');
  });
});
