/**
 * Tests for memoryService extensions:
 *   - appendToMemory: additive update with timestamped blocks + soft-cap
 *     behavior (older append blocks drop, original creation content
 *     preserved)
 *   - getCatalog: compact, recency-sorted, archived-excluded list for
 *     system-prompt injection
 *
 * Together these enable the "task-attached memory grows as agent works"
 * pattern and the per-turn AGENT MEMORY catalog injection.
 */
import { describe, test, expect, beforeEach } from '@jest/globals';
import fs from 'fs/promises';
import path from 'path';
import os from 'os';
import MemoryService from '../memoryService.js';

let tmpDir;
let service;
beforeEach(async () => {
  tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'memsvc-test-'));
  service = new MemoryService({
    info(){}, warn(){}, error(){}, debug(){},
  });
  // Skip initialize() (which sets up production paths) and override
  // agentsDir to a temp dir for isolation. Must set `initialized=true`
  // so loadMemories() doesn't re-run initialize() and clobber agentsDir.
  service.agentsDir = tmpDir;
  service.initialized = true;
});

describe('appendToMemory', () => {
  test('appends a timestamped block to existing content', async () => {
    const created = await service.addMemory('agent-1', {
      title: 'task-context/foo', content: 'Initial context.',
    });
    const updated = await service.appendToMemory('agent-1', created.id, 'Finding: API uses bearer tokens.');
    expect(updated).not.toBeNull();
    expect(updated.content).toContain('Initial context.');
    expect(updated.content).toMatch(/## \d{4}-\d{2}-\d{2}T[\d:.]+Z — agent/);
    expect(updated.content).toContain('Finding: API uses bearer tokens.');
  });

  test('returns null for non-existent memory id', async () => {
    const res = await service.appendToMemory('agent-1', 'mem-nope', 'whatever');
    expect(res).toBeNull();
  });

  test('rejects empty body (no-op)', async () => {
    const created = await service.addMemory('agent-1', { title: 't', content: 'c' });
    expect(await service.appendToMemory('agent-1', created.id, '')).toBeNull();
    expect(await service.appendToMemory('agent-1', created.id, null)).toBeNull();
  });

  test('updates the updatedAt timestamp', async () => {
    const created = await service.addMemory('agent-1', { title: 't', content: 'c' });
    const before = created.updatedAt;
    await new Promise(r => setTimeout(r, 10));
    const after = await service.appendToMemory('agent-1', created.id, 'more');
    expect(after.updatedAt).not.toBe(before);
  });

  test('SOFT CAP: when content exceeds 16K, drops the OLDEST append block (creation content preserved)', async () => {
    const ORIGINAL = 'ORIGINAL_CONTENT_MARKER ' + 'a'.repeat(1000);
    const mem = await service.addMemory('agent-1', { title: 't', content: ORIGINAL });
    // Append several large blocks until we go past the cap
    for (let i = 0; i < 20; i++) {
      await service.appendToMemory('agent-1', mem.id, `Append block ${i}: ` + 'X'.repeat(900));
    }
    const all = await service.loadMemories('agent-1');
    const final = all.find(m => m.id === mem.id);
    expect(final.content.length).toBeLessThanOrEqual(16_500); // a little slack for the cap math
    // Original creation content MUST survive
    expect(final.content).toContain('ORIGINAL_CONTENT_MARKER');
    // Most recent append must survive
    expect(final.content).toContain('Append block 19');
    // Earliest append should have been dropped
    expect(final.content).not.toContain('Append block 0:');
  });
});

describe('getCatalog', () => {
  test('returns empty catalog when no memories exist', async () => {
    const cat = await service.getCatalog('agent-1');
    expect(cat.entries).toEqual([]);
    expect(cat.total).toBe(0);
  });

  test('lists memories sorted newest-first by updatedAt', async () => {
    const m1 = await service.addMemory('agent-1', { title: 'first', content: 'a' });
    await new Promise(r => setTimeout(r, 10));
    const m2 = await service.addMemory('agent-1', { title: 'second', content: 'b' });
    const cat = await service.getCatalog('agent-1');
    expect(cat.entries).toHaveLength(2);
    expect(cat.entries[0].id).toBe(m2.id);  // newest first
    expect(cat.entries[1].id).toBe(m1.id);
  });

  test('caps at the requested limit and reports total', async () => {
    for (let i = 0; i < 25; i++) {
      await service.addMemory('agent-1', { title: `m-${i}`, content: 'x' });
    }
    const cat = await service.getCatalog('agent-1', 10);
    expect(cat.entries).toHaveLength(10);
    expect(cat.total).toBe(25);
  });

  test('excludes memories with archived=true', async () => {
    const m1 = await service.addMemory('agent-1', { title: 'visible', content: 'a' });
    const m2 = await service.addMemory('agent-1', { title: 'hidden',  content: 'b' });
    // Manually archive m2 by direct file mutation (no archive API yet)
    const all = await service.loadMemories('agent-1');
    const idx = all.findIndex(m => m.id === m2.id);
    all[idx].archived = true;
    await service.saveMemories('agent-1', all);

    const cat = await service.getCatalog('agent-1');
    expect(cat.entries.map(e => e.id)).toContain(m1.id);
    expect(cat.entries.map(e => e.id)).not.toContain(m2.id);
    expect(cat.total).toBe(1);
  });

  test('catalog entries are compact: id, title, short description, updatedAt', async () => {
    const m = await service.addMemory('agent-1', {
      title: 'plan/foo',
      description: 'A short description',
      content: 'lots of content here that should NOT appear in the catalog',
    });
    const cat = await service.getCatalog('agent-1');
    expect(cat.entries[0]).toEqual({
      id: m.id,
      title: 'plan/foo',
      description: 'A short description',
      updatedAt: m.updatedAt || m.createdAt,
    });
    // CRITICAL: content must NOT be in the catalog (prompt-token control)
    expect(cat.entries[0]).not.toHaveProperty('content');
  });
});
