/**
 * TaskIndexer tests.
 *
 * Pins:
 *   - taskToText pack order: title → description → [tags]
 *   - Per-agent scope (surface='tasks')
 *   - indexOne / deleteOne / backfill round-trip
 *   - statusFilter narrows search results
 *   - Disabled path returns clean structured results
 *   - Skips invalid / no-text tasks
 *   - Embed failure surfaces failed counts
 */

import { describe, test, expect, jest, beforeEach, afterEach } from '@jest/globals';
import os from 'os';
import fsp from 'fs/promises';
import path from 'path';
import { TaskIndexer, taskToText } from '../taskIndexer.js';
import { EmbeddingService } from '../../embeddingService.js';
import { EmbeddingProvider } from '../../providerInterface.js';

class StubProvider extends EmbeddingProvider {
  constructor(dim = 8) {
    super(); this._dimensions = dim; this.calls = [];
  }
  getInfo() {
    return {
      name: 'azure', modelId: 'stub-tasks', dimensions: this._dimensions,
      maxBatchSize: 32, maxTokensPerText: 1000,
      distinguishesQueryAndDocument: false, isLocal: false,
    };
  }
  getModelFingerprint() { return 'stub-tasks:v1'; }
  async _embed(texts) {
    this.calls.push({ texts: [...texts] });
    return texts.map(t => {
      const v = Array(this._dimensions).fill(0);
      v[(t.charCodeAt(0) || 0) % this._dimensions] = 1;
      return v;
    });
  }
}

function makeIndexer({ tmpDir, enabled = true } = {}) {
  const apiKeyManager = { getEmbeddingApiKey: () => 'k' };
  if (!enabled) {
    const svc = new EmbeddingService({ apiKeyManager, baseUrl: 'https://x', stateDir: tmpDir });
    return { indexer: new TaskIndexer({ embeddingService: svc }), svc };
  }
  const svc = new EmbeddingService({
    config: { provider: 'azure' },
    apiKeyManager, baseUrl: 'https://x', stateDir: tmpDir,
    providerOverrides: { azure: () => new StubProvider() },
  });
  return { indexer: new TaskIndexer({ embeddingService: svc }), svc };
}

describe('taskToText', () => {
  test('pack order: title → description → [tags]', () => {
    const t = taskToText({
      id: 't1', title: 'Refactor auth', description: 'Migrate to OAuth 2.1',
      status: 'in_progress', priority: 'high', tags: ['security'],
    });
    expect(t.indexOf('Refactor auth')).toBeLessThan(t.indexOf('Migrate to OAuth'));
    expect(t.indexOf('Migrate to OAuth')).toBeLessThan(t.indexOf('[status:in_progress'));
    expect(t).toContain('priority:high');
    expect(t).toContain('security');
  });

  test('null / empty task returns empty string', () => {
    expect(taskToText(null)).toBe('');
    expect(taskToText({ id: 't1' })).toBe('');
  });
});

describe('TaskIndexer — disabled path', () => {
  let tmpDir;
  beforeEach(async () => { tmpDir = await fsp.mkdtemp(path.join(os.tmpdir(), 'loxia-tidx-')); });
  afterEach(async () => { await fsp.rm(tmpDir, { recursive: true, force: true }); });

  test('returns disabled-shaped results', async () => {
    const { indexer } = makeIndexer({ tmpDir, enabled: false });
    expect((await indexer.indexOne('a', { id: 't', title: 'x' })).reason).toBe('disabled');
    expect((await indexer.search('a', 'x')).enabled).toBe(false);
    expect((await indexer.backfill('a', [])).reason).toBe('disabled');
  });
});

describe('TaskIndexer — round-trip', () => {
  let tmpDir, indexer;
  beforeEach(async () => {
    tmpDir = await fsp.mkdtemp(path.join(os.tmpdir(), 'loxia-tidx-'));
    ({ indexer } = makeIndexer({ tmpDir }));
  });
  afterEach(async () => { await fsp.rm(tmpDir, { recursive: true, force: true }); });

  test('indexOne + search finds the task', async () => {
    await indexer.indexOne('a1', { id: 't1', title: 'Refactor auth flow', status: 'open' });
    const r = await indexer.search('a1', 'auth refactor');
    expect(r.hits.some(h => h.id === 't1')).toBe(true);
  });

  test('statusFilter narrows results', async () => {
    await indexer.backfill('a1', [
      { id: 't1', title: 'open auth task', status: 'open' },
      { id: 't2', title: 'done auth task', status: 'completed' },
    ]);
    const r = await indexer.search('a1', 'auth', { statusFilter: 'open' });
    expect(r.hits.every(h => h.metadata?.status === 'open')).toBe(true);
    expect(r.hits.find(h => h.id === 't1')).toBeDefined();
    expect(r.hits.find(h => h.id === 't2')).toBeUndefined();
  });

  test('deleteOne removes the task', async () => {
    await indexer.indexOne('a1', { id: 't1', title: 'x' });
    await indexer.indexOne('a1', { id: 't2', title: 'y' });
    await indexer.deleteOne('a1', 't1');
    const r = await indexer.search('a1', 'x');
    expect(r.hits.find(h => h.id === 't1')).toBeUndefined();
  });

  test('backfill drops + rebuilds', async () => {
    await indexer.indexOne('a1', { id: 'stale', title: 'old' });
    await indexer.backfill('a1', [{ id: 'fresh', title: 'new' }]);
    const r = await indexer.search('a1', 'new');
    expect(r.hits.find(h => h.id === 'stale')).toBeUndefined();
    expect(r.hits.find(h => h.id === 'fresh')).toBeDefined();
  });

  test('per-agent isolation: a1 cannot see a2 tasks', async () => {
    await indexer.indexOne('a1', { id: 't1', title: 'private one' });
    await indexer.indexOne('a2', { id: 't1', title: 'private two' });
    const r = await indexer.search('a1', 'private');
    expect(r.hits.every(h => h.metadata?.title?.includes('one'))).toBe(true);
  });

  test('skips invalid / no-text tasks in backfill', async () => {
    const r = await indexer.backfill('a1', [
      { id: 'good', title: 'ok' },
      { /* no id */ title: 'orphan' },
      { id: 'empty' },
    ]);
    expect(r.indexed).toBe(1);
    expect(r.skipped).toBe(2);
  });
});
