/**
 * memoryTool tests for reminisce.semantic-search + reminisce-backfill-embeddings.
 *
 * Pins:
 *   - mode='semantic-search' routes through ReminisceIndexer
 *   - Falls back to substring `search` when embeddings are disabled
 *   - Hydrates messageIds back to live messages from the archive
 *   - reminisce-backfill-embeddings walks conversations.full + calls
 *     indexer.backfill, surfaces counts in the response
 *   - validation: query required for semantic-search
 *   - mode enum includes semantic-search
 *
 * Mocks: memoryService (existing pattern), ReminisceIndexer (stubbed
 * exactly like MemoryIndexer in the Phase 1 test), and a fake agentPool
 * that returns an archive of messages.
 */

import { jest, describe, test, expect, beforeEach } from '@jest/globals';
import { createMockLogger } from '../../__test-utils__/mockFactories.js';

const mockMemoryService = {
  initialize: jest.fn().mockResolvedValue(undefined),
  addMemory:    jest.fn(),
  updateMemory: jest.fn(),
  appendToMemory: jest.fn(),
  deleteMemory: jest.fn(),
  listMemories: jest.fn(),
  readMemory:   jest.fn(),
  searchMemories: jest.fn(),
  getMemoryStats: jest.fn(),
};
jest.unstable_mockModule('../../services/memoryService.js', () => ({
  getMemoryService: jest.fn(() => mockMemoryService),
  PLAN_TITLE_PREFIX: 'plan/',
}));

const mockMemoryIndexerInstance = {
  isEnabled: false,
  indexOne:  jest.fn().mockResolvedValue({ indexed: true }),
  deleteOne: jest.fn().mockResolvedValue({ deleted: true }),
  indexMany: jest.fn().mockResolvedValue({ indexed: 0 }),
  backfill:  jest.fn().mockResolvedValue({ ok: true, indexed: 0 }),
  search:    jest.fn().mockResolvedValue({ enabled: true, hits: [] }),
};
jest.unstable_mockModule('../../services/embeddings/indexers/memoryIndexer.js', () => ({
  MemoryIndexer: jest.fn(() => mockMemoryIndexerInstance),
  memoryToText: () => '',
}));

const mockReminisceIndexerInstance = {
  isEnabled: true,
  indexOne:  jest.fn().mockResolvedValue({ indexed: true, chunks: 1 }),
  deleteOne: jest.fn().mockResolvedValue({ deleted: true, removed: 1 }),
  indexMany: jest.fn().mockResolvedValue({ indexedMessages: 0, indexedChunks: 0, skipped: 0, failed: 0 }),
  backfill:  jest.fn().mockResolvedValue({ ok: true, indexedMessages: 5, indexedChunks: 9, skipped: 1, failed: 0 }),
  search:    jest.fn().mockResolvedValue({ enabled: true, hits: [] }),
};
const MockReminisceIndexer = jest.fn(() => mockReminisceIndexerInstance);
jest.unstable_mockModule('../../services/embeddings/indexers/reminisceIndexer.js', () => ({
  ReminisceIndexer: MockReminisceIndexer,
  messageToText: () => '',
  estimateMessageTokens: () => 0,
}));

const { default: MemoryTool } = await import('../memoryTool.js');

function makeTool({ embeddingsEnabled = true, archive = null } = {}) {
  const logger = createMockLogger();
  const tool = new MemoryTool({}, logger);

  mockReminisceIndexerInstance.isEnabled = embeddingsEnabled;
  mockReminisceIndexerInstance.search.mockClear();
  mockReminisceIndexerInstance.backfill.mockClear();
  MockReminisceIndexer.mockClear();
  for (const fn of Object.values(mockMemoryService)) {
    if (typeof fn?.mockClear === 'function') fn.mockClear();
  }

  tool.aiService = { getEmbeddingService: () => ({ isEnabled: embeddingsEnabled }) };
  tool._reminisceIndexer = null;
  tool._memoryIndexer    = null;

  const agentPool = {
    getAgent: jest.fn(async (id) => ({
      id,
      conversations: archive ? { full: { messages: archive } } : { full: { messages: [] } },
    })),
  };
  return { tool, agentPool };
}

const archive = [
  { id: 'm1', role: 'user',      content: 'how do we dedup telegram errors?', createdAt: '2026-05-25T00:00:00Z' },
  { id: 'm2', role: 'assistant', content: 'we collapse identical errors within a 5min window', createdAt: '2026-05-25T00:01:00Z' },
  { id: 'm3', role: 'user',      content: 'what about retries?', createdAt: '2026-05-25T00:02:00Z' },
];

describe('reminisce mode enum', () => {
  test('semantic-search is now in the valid mode set', async () => {
    const { tool, agentPool } = makeTool({ archive });
    // Bad mode rejected
    const bad = await tool.execute(
      { actions: [{ type: 'reminisce', mode: 'not-a-mode', query: 'x' }] },
      { agentId: 'a1', agentPool },
    );
    expect(bad.success).toBe(false);
    expect(bad.error).toMatch(/semantic-search/);
    // semantic-search accepted
    mockReminisceIndexerInstance.search.mockResolvedValue({ enabled: true, hits: [] });
    const ok = await tool.execute(
      { actions: [{ type: 'reminisce', mode: 'semantic-search', query: 'x' }] },
      { agentId: 'a1', agentPool },
    );
    expect(ok.success).toBe(true);
    expect(ok.mode).toBe('semantic-search');
  });
});

describe('reminisce.semantic-search — enabled path', () => {
  test('routes through ReminisceIndexer with topK + hybrid defaults', async () => {
    const { tool, agentPool } = makeTool({ archive });
    mockReminisceIndexerInstance.search.mockResolvedValue({ enabled: true, hits: [] });

    await tool.execute(
      { actions: [{ type: 'reminisce', mode: 'semantic-search', query: 'telegram dedup' }] },
      { agentId: 'a1', agentPool },
    );

    expect(mockReminisceIndexerInstance.search).toHaveBeenCalledWith(
      'a1', 'telegram dedup', { topK: 10, hybrid: true },
    );
  });

  test('topK + hybrid forwarded', async () => {
    const { tool, agentPool } = makeTool({ archive });
    mockReminisceIndexerInstance.search.mockResolvedValue({ enabled: true, hits: [] });

    await tool.execute(
      { actions: [{ type: 'reminisce', mode: 'semantic-search', query: 'q', topK: 3, hybrid: false }] },
      { agentId: 'a1', agentPool },
    );

    expect(mockReminisceIndexerInstance.search).toHaveBeenCalledWith(
      'a1', 'q', { topK: 3, hybrid: false },
    );
  });

  test('hits hydrate back to live messages from the archive', async () => {
    const { tool, agentPool } = makeTool({ archive });
    mockReminisceIndexerInstance.search.mockResolvedValue({
      enabled: true,
      hits: [
        { messageId: 'm2', score: 0.9, chunkIndex: 0, chunkText: 'snippet', metadata: {} },
        { messageId: 'm1', score: 0.6, chunkIndex: 0, chunkText: 'snippet', metadata: {} },
      ],
    });

    const r = await tool.execute(
      { actions: [{ type: 'reminisce', mode: 'semantic-search', query: 'telegram' }] },
      { agentId: 'a1', agentPool },
    );

    expect(r.success).toBe(true);
    expect(r.mode).toBe('semantic-search');
    expect(r.modeUsed).toBe('hybrid');
    expect(r.result.hits).toHaveLength(2);
    expect(r.result.hits[0]).toMatchObject({
      messageId: 'm2',
      role: 'assistant',
      score: 0.9,
      content: expect.stringContaining('5min window'),
    });
    expect(r.result.hits[1]).toMatchObject({
      messageId: 'm1',
      role: 'user',
      content: expect.stringContaining('telegram errors'),
    });
  });

  test('missing message marked as missing (does not silently drop)', async () => {
    const { tool, agentPool } = makeTool({ archive });
    mockReminisceIndexerInstance.search.mockResolvedValue({
      enabled: true,
      hits: [{ messageId: 'gone', score: 0.7, chunkIndex: 0, chunkText: 'archived snippet', metadata: {} }],
    });

    const r = await tool.execute(
      { actions: [{ type: 'reminisce', mode: 'semantic-search', query: 'q' }] },
      { agentId: 'a1', agentPool },
    );

    expect(r.result.hits[0]).toMatchObject({ messageId: 'gone', missing: true });
    expect(r.result.hits[0].snippet).toBe('archived snippet');
  });

  test('indexer error surfaces in result without throwing', async () => {
    const { tool, agentPool } = makeTool({ archive });
    mockReminisceIndexerInstance.search.mockResolvedValue({
      enabled: true,
      hits: [],
      error: 'rate-limited',
    });

    const r = await tool.execute(
      { actions: [{ type: 'reminisce', mode: 'semantic-search', query: 'q' }] },
      { agentId: 'a1', agentPool },
    );

    expect(r.success).toBe(false);
    expect(r.result.error).toBe('rate-limited');
  });
});

describe('reminisce.semantic-search — disabled fallback', () => {
  test('falls back to substring `search` when embeddings disabled', async () => {
    const { tool, agentPool } = makeTool({ embeddingsEnabled: false, archive });

    const r = await tool.execute(
      { actions: [{ type: 'reminisce', mode: 'semantic-search', query: 'dedup' }] },
      { agentId: 'a1', agentPool },
    );

    expect(mockReminisceIndexerInstance.search).not.toHaveBeenCalled();
    expect(r.mode).toBe('semantic-search');
    expect(r.modeUsed).toBe('substring-fallback');
  });

  test('missing query → clean error', async () => {
    const { tool, agentPool } = makeTool({ archive });
    const r = await tool.execute(
      { actions: [{ type: 'reminisce', mode: 'semantic-search' }] },
      { agentId: 'a1', agentPool },
    );
    expect(r.success).toBe(false);
    expect(r.error).toMatch(/query/i);
  });
});

describe('reminisce-backfill-embeddings action', () => {
  test('disabled → clean error, no indexer call', async () => {
    const { tool, agentPool } = makeTool({ embeddingsEnabled: false, archive });
    const r = await tool.execute(
      { actions: [{ type: 'reminisce-backfill-embeddings' }] },
      { agentId: 'a1', agentPool },
    );
    expect(r.success).toBe(false);
    expect(r.message).toMatch(/not enabled/i);
    expect(mockReminisceIndexerInstance.backfill).not.toHaveBeenCalled();
  });

  test('enabled → walks conversations.full + calls indexer.backfill', async () => {
    const { tool, agentPool } = makeTool({ archive });
    mockReminisceIndexerInstance.backfill.mockResolvedValue({
      ok: true, indexedMessages: 3, indexedChunks: 5, skipped: 0, failed: 0,
    });

    const r = await tool.execute(
      { actions: [{ type: 'reminisce-backfill-embeddings' }] },
      { agentId: 'a1', agentPool },
    );

    expect(agentPool.getAgent).toHaveBeenCalledWith('a1');
    expect(mockReminisceIndexerInstance.backfill).toHaveBeenCalledWith(
      'a1',
      expect.arrayContaining([
        expect.objectContaining({ id: 'm1' }),
        expect.objectContaining({ id: 'm2' }),
        expect.objectContaining({ id: 'm3' }),
      ]),
    );
    expect(r.success).toBe(true);
    expect(r.indexedMessages).toBe(3);
    expect(r.indexedChunks).toBe(5);
  });

  test('reports failure counts cleanly', async () => {
    const { tool, agentPool } = makeTool({ archive });
    mockReminisceIndexerInstance.backfill.mockResolvedValue({
      ok: true, indexedMessages: 0, indexedChunks: 0, skipped: 0, failed: 3, error: 'rate-limited',
    });

    const r = await tool.execute(
      { actions: [{ type: 'reminisce-backfill-embeddings' }] },
      { agentId: 'a1', agentPool },
    );

    expect(r.success).toBe(false);
    expect(r.failed).toBe(3);
    expect(r.error).toBe('rate-limited');
  });

  test('missing agentPool → clean error', async () => {
    const { tool } = makeTool({ archive });
    const r = await tool.execute(
      { actions: [{ type: 'reminisce-backfill-embeddings' }] },
      { agentId: 'a1' },
    );
    expect(r.success).toBe(false);
    expect(r.error).toMatch(/agentPool/i);
  });
});
