/**
 * skillsTool tests for search + backfill-search-index.
 *
 * Pins the wiring contracts only — SkillsIndexer's internals are
 * covered by its own suite. We test:
 *   - search dispatches to indexer when enabled
 *   - search falls back to substring over service.listSkills() when disabled
 *   - missing query → clean error
 *   - backfill walks listSkills + readSkill for each + calls indexer.backfill
 *   - backfill is resilient when readSkill fails for one (uses metadata only)
 *
 * Mocks: SKILLS_ACTIONS enum (already extended), SkillsIndexer, and the
 * underlying skills service the tool consumes.
 */

import { jest, describe, test, expect, beforeEach } from '@jest/globals';

const mockIndexerInstance = {
  isEnabled: true,
  indexOne:  jest.fn().mockResolvedValue({ indexed: true }),
  deleteOne: jest.fn().mockResolvedValue({ deleted: true }),
  backfill:  jest.fn().mockResolvedValue({ ok: true, indexed: 2, skipped: 0, failed: 0 }),
  search:    jest.fn().mockResolvedValue({ enabled: true, hits: [] }),
  stats:     jest.fn().mockResolvedValue({ count: 2, enabled: true }),
};
const MockSkillsIndexer = jest.fn(() => mockIndexerInstance);
jest.unstable_mockModule('../../services/embeddings/indexers/skillsIndexer.js', () => ({
  SkillsIndexer: MockSkillsIndexer,
  skillToText: () => '',
}));

// Mock the skills service. Needs `.initialize()` because the tool calls
// it via `_ensureSkillsService()` before dispatching.
const mockSkillsService = {
  initialize:   jest.fn().mockResolvedValue(undefined),
  listSkills:   jest.fn(),
  readSkill:    jest.fn(),
  describeSkill: jest.fn(),
};
jest.unstable_mockModule('../../services/skillsService.js', () => ({
  getSkillsService: () => mockSkillsService,
}));

const { default: SkillsTool } = await import('../skillsTool.js');

function makeTool({ embeddingsEnabled = true } = {}) {
  const tool = new SkillsTool();
  mockIndexerInstance.isEnabled = embeddingsEnabled;
  mockIndexerInstance.search.mockClear();
  mockIndexerInstance.backfill.mockClear();
  MockSkillsIndexer.mockClear();
  mockSkillsService.listSkills.mockClear();
  mockSkillsService.readSkill.mockClear();
  tool.aiService = { getEmbeddingService: () => ({ isEnabled: embeddingsEnabled }) };
  tool._skillsIndexer = null;
  return tool;
}

const skillList = [
  { name: 'git-rebase-recovery',    description: 'Recover from a broken rebase' },
  { name: 'docker-build-debug',     description: 'Debug docker build failures' },
  { name: 'react-hook-deps',        description: 'Fix exhaustive-deps warnings' },
];

describe('skillsTool — search wiring', () => {
  test('missing query → clean error', async () => {
    const tool = makeTool();
    const r = await tool.execute({ action: 'search' });
    expect(r.success).toBe(false);
    expect(r.error).toMatch(/query/i);
  });

  test('disabled embeddings → substring fallback over service.listSkills', async () => {
    const tool = makeTool({ embeddingsEnabled: false });
    mockSkillsService.listSkills.mockResolvedValue(skillList);
    const r = await tool.execute({ action: 'search', query: 'rebase' });
    expect(mockIndexerInstance.search).not.toHaveBeenCalled();
    expect(r.result.mode).toBe('substring-fallback');
    expect(r.result.hits.some(s => s.name === 'git-rebase-recovery')).toBe(true);
  });

  test('enabled embeddings → forwards query + topK + hybrid to indexer', async () => {
    const tool = makeTool();
    mockIndexerInstance.search.mockResolvedValue({ enabled: true, hits: [] });
    await tool.execute({ action: 'search', query: 'rebase', topK: 3, hybrid: false });
    expect(mockIndexerInstance.search).toHaveBeenCalledWith('rebase', { topK: 3, hybrid: false });
  });

  test('enabled embeddings → returns hits in result', async () => {
    const tool = makeTool();
    mockIndexerInstance.search.mockResolvedValue({
      enabled: true,
      hits: [
        { id: 'git-rebase-recovery', score: 0.9, metadata: { description: 'Recover...' } },
      ],
    });
    const r = await tool.execute({ action: 'search', query: 'rebase' });
    expect(r.success).toBe(true);
    expect(r.result.mode).toBe('hybrid');
    expect(r.result.hits[0].id).toBe('git-rebase-recovery');
  });

  test('indexer error surfaces in result', async () => {
    const tool = makeTool();
    mockIndexerInstance.search.mockResolvedValue({ enabled: true, hits: [], error: 'rate-limited' });
    const r = await tool.execute({ action: 'search', query: 'q' });
    expect(r.success).toBe(false);
    expect(r.result.error).toBe('rate-limited');
  });
});

describe('skillsTool — backfill-search-index wiring', () => {
  test('disabled → clean error', async () => {
    const tool = makeTool({ embeddingsEnabled: false });
    const r = await tool.execute({ action: 'backfill-search-index' });
    expect(r.success).toBe(false);
    expect(r.message).toMatch(/not enabled/i);
    expect(mockIndexerInstance.backfill).not.toHaveBeenCalled();
  });

  test('walks listSkills + readSkill for each + calls indexer.backfill', async () => {
    const tool = makeTool();
    mockSkillsService.listSkills.mockResolvedValue(skillList);
    mockSkillsService.readSkill
      .mockResolvedValueOnce({ content: 'rebase step 1...' })
      .mockResolvedValueOnce({ content: 'docker tip 1...' })
      .mockResolvedValueOnce({ content: 'react fix 1...' });

    const r = await tool.execute({ action: 'backfill-search-index' });

    expect(mockSkillsService.listSkills).toHaveBeenCalled();
    expect(mockSkillsService.readSkill).toHaveBeenCalledTimes(3);
    const passed = mockIndexerInstance.backfill.mock.calls[0][0];
    expect(passed).toHaveLength(3);
    expect(passed[0]).toMatchObject({ name: 'git-rebase-recovery', content: 'rebase step 1...' });
    expect(r.success).toBe(true);
    expect(r.result.indexed).toBe(2);   // from the stub return
  });

  test('readSkill failure on one skill falls back to metadata only', async () => {
    const tool = makeTool();
    mockSkillsService.listSkills.mockResolvedValue([skillList[0], skillList[1]]);
    mockSkillsService.readSkill
      .mockRejectedValueOnce(new Error('file gone'))
      .mockResolvedValueOnce({ content: 'docker tip' });

    await tool.execute({ action: 'backfill-search-index' });
    const passed = mockIndexerInstance.backfill.mock.calls[0][0];
    expect(passed).toHaveLength(2);
    // First skill made it in without content (failed-read fallback).
    expect(passed[0]).toMatchObject({ name: 'git-rebase-recovery' });
    expect(passed[0].content).toBeUndefined();
    // Second one has its content.
    expect(passed[1].content).toBe('docker tip');
  });

  test('listSkills throw → clean error', async () => {
    const tool = makeTool();
    mockSkillsService.listSkills.mockRejectedValue(new Error('disk full'));
    const r = await tool.execute({ action: 'backfill-search-index' });
    expect(r.success).toBe(false);
    expect(r.message).toMatch(/listSkills failed/);
  });
});
