/**
 * Tests for the plural-canonical envelope on the memory tool.
 *
 * Memory and code-map were previously singular-only ({action: "X", ...}).
 * All other tools (filesystem, terminal, taskmanager, file-content-replace)
 * used plural ({actions: [{type: "X", ...}]}). Production agents
 * (Kimi-K2.6, gpt-5.4) drifted between shapes mid-session and emitted
 * the wrong shape for whichever tool they reached for. Unifying on
 * plural-canonical removes the inconsistency.
 *
 * Contract:
 *   - {actions: [...]} is the canonical input
 *   - {action, ...} singular is accepted (backward-compat)
 *   - Both `type` and `action` inside an action item are accepted
 *   - Batched (multi-action) calls execute every action and return
 *     a results[] array
 *   - Single-action calls (canonical OR singular) return the unwrapped
 *     legacy shape — no regression for existing callers
 */
import { jest, describe, test, expect, beforeEach } from '@jest/globals';

const mockMemoryService = {
  initialize:        jest.fn().mockResolvedValue(undefined),
  loadMemories:      jest.fn().mockResolvedValue([]),
  saveMemories:      jest.fn().mockResolvedValue(undefined),
  readMemory:        jest.fn(),
  listMemories:      jest.fn().mockResolvedValue({ count: 0, memories: [], grouped: {} }),
  addMemory:         jest.fn(),
  updateMemory:      jest.fn(),
  appendToMemory:    jest.fn(),
  deleteMemory:      jest.fn(),
  searchMemories:    jest.fn(),
  getStats:          jest.fn(),
  getCatalog:        jest.fn(),
};
jest.unstable_mockModule('../../services/memoryService.js', () => ({
  getMemoryService: jest.fn(() => mockMemoryService),
  PLAN_TITLE_PREFIX: 'plan/',
}));

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

const SILENT_LOGGER = { info() {}, warn() {}, error() {}, debug() {} };

function makeTool() {
  const tool = new MemoryTool({}, SILENT_LOGGER);
  return tool;
}
const ctx = { agentId: 'agent-1', agent: { id: 'agent-1' } };

beforeEach(() => {
  for (const fn of Object.values(mockMemoryService)) {
    if (typeof fn.mockReset === 'function') fn.mockReset();
  }
  mockMemoryService.initialize.mockResolvedValue(undefined);
  mockMemoryService.loadMemories.mockResolvedValue([]);
  mockMemoryService.saveMemories.mockResolvedValue(undefined);
});

describe('_normalizeToActions — canonical-plural promotion', () => {
  const tool = makeTool();
  test('promotes singular {action: "list"} to [{type: "list"}]', () => {
    const r = tool._normalizeToActions({ action: 'list' });
    expect(r).toEqual([{ type: 'list' }]);
  });

  test('flattens singular {action: "X", parameters: {id}} to [{type, id}]', () => {
    const r = tool._normalizeToActions({ action: 'read', parameters: { id: 'mem-1' } });
    expect(r).toEqual([{ type: 'read', id: 'mem-1' }]);
  });

  test('accepts canonical {actions: [{type, ...}]} unchanged', () => {
    const r = tool._normalizeToActions({ actions: [{ type: 'read', id: 'mem-1' }] });
    expect(r).toEqual([{ type: 'read', id: 'mem-1' }]);
  });

  test('renames {actions: [{action: "X", ...}]} to [{type: "X", ...}]', () => {
    const r = tool._normalizeToActions({ actions: [{ action: 'read', id: 'mem-1' }] });
    expect(r).toEqual([{ type: 'read', id: 'mem-1' }]);
  });

  test('returns empty array on missing input', () => {
    expect(tool._normalizeToActions({})).toEqual([]);
    expect(tool._normalizeToActions(null)).toEqual([]);
  });
});

describe('execute — single-action backward compatibility', () => {
  test('singular {action: "list"} returns the legacy unwrapped shape', async () => {
    mockMemoryService.listMemories.mockResolvedValueOnce({ count: 1, memories: [], grouped: {} });
    const r = await makeTool().execute({ action: 'list', level: 'titles' }, ctx);
    // Old contract: a single-action call returns the inner result
    // directly, NOT wrapped in {results: [...]}.
    expect(r.success).toBe(true);
    expect(r.action).toBe('list');
    expect(r).not.toHaveProperty('batched');
  });

  test('canonical plural with 1 action returns the same unwrapped shape', async () => {
    mockMemoryService.listMemories.mockResolvedValueOnce({ count: 0, memories: [], grouped: {} });
    const r = await makeTool().execute({ actions: [{ type: 'list', level: 'titles' }] }, ctx);
    expect(r.success).toBe(true);
    expect(r.action).toBe('list');
    expect(r).not.toHaveProperty('batched');
  });
});

describe('execute — multi-action batching (new capability)', () => {
  test('two reads in one call returns results[] with both', async () => {
    const m1 = { id: 'mem-1', title: 'A', description: 'a', content: 'CONTENT-A', createdAt: new Date().toISOString() };
    const m2 = { id: 'mem-2', title: 'B', description: 'b', content: 'CONTENT-B', createdAt: new Date().toISOString() };
    mockMemoryService.readMemory.mockImplementation(async (_a, id) => {
      if (id === 'mem-1') return m1;
      if (id === 'mem-2') return m2;
      return null;
    });
    const r = await makeTool().execute({
      actions: [
        { type: 'read', id: 'mem-1' },
        { type: 'read', id: 'mem-2' },
      ],
    }, ctx);
    expect(r.batched).toBe(true);
    expect(r.actionCount).toBe(2);
    expect(r.results).toHaveLength(2);
    expect(r.success).toBe(true);
    expect(r.results[0].memory?.id).toBe('mem-1');
    expect(r.results[1].memory?.id).toBe('mem-2');
  });

  test('one action failure does not abort the batch (each result carries success)', async () => {
    const m1 = { id: 'mem-1', title: 'A', description: 'a', content: 'CA', createdAt: new Date().toISOString() };
    mockMemoryService.readMemory.mockImplementation(async (_a, id) => (id === 'mem-1' ? m1 : null));
    const r = await makeTool().execute({
      actions: [
        { type: 'read', id: 'mem-1' },        // succeeds
        { type: 'read', id: 'mem-not-here' }, // not-found
      ],
    }, ctx);
    expect(r.batched).toBe(true);
    expect(r.results).toHaveLength(2);
    expect(r.results[0].success).toBe(true);
    // Not-found is communicated via success:false in the per-action result.
    expect(r.results[1].success).toBe(false);
    expect(r.results[1].message || r.results[1].error).toMatch(/not found|Memory not found/i);
  });
});

describe('customValidateParameters — accepts both shapes', () => {
  test('singular {action} validates', () => {
    const v = makeTool().customValidateParameters({ action: 'read', id: 'mem-1' });
    expect(v.valid).toBe(true);
  });

  test('canonical plural validates per-action', () => {
    const v = makeTool().customValidateParameters({
      actions: [
        { type: 'read', id: 'mem-1' },
        { type: 'add',  title: 'T', content: 'C' },
      ],
    });
    expect(v.valid).toBe(true);
  });

  test('plural with one bad action reports that action by index', () => {
    const v = makeTool().customValidateParameters({
      actions: [
        { type: 'read', id: 'mem-1' },
        { type: 'add' /* missing title+content */ },
      ],
    });
    expect(v.valid).toBe(false);
    expect(v.errors.join(' ')).toMatch(/actions\[1\]/);
  });

  test('plural rejects unknown type', () => {
    const v = makeTool().customValidateParameters({
      actions: [{ type: 'frobnicate' }],
    });
    expect(v.valid).toBe(false);
    expect(v.errors.join(' ')).toMatch(/type must be one of/);
  });

  test('empty params rejected with helpful message', () => {
    const v = makeTool().customValidateParameters({});
    expect(v.valid).toBe(false);
    expect(v.errors.join(' ')).toMatch(/action is required|actions/);
  });
});
