/**
 * Tests for buildPlanBlock — the formatter that turns a list of
 * plan/* memories into the AGENT WORKING PLAN block injected into
 * the agent's system prompt every turn.
 *
 * What we pin:
 *   - Empty input → empty block, no truncation
 *   - Multiple plans render under a single header, newest-first
 *     (the caller sorts; we just preserve order)
 *   - Per-plan soft cap truncates with a clear marker + records the
 *     plan in `truncated`
 *   - Cross-plan hard cap drops later plans (records them too)
 *   - Header / footer markers are stable (so downstream consumers
 *     can rely on them for detection)
 */
import { describe, test, expect } from '@jest/globals';
import { buildPlanBlock, _PLAN_INJECTION_INTERNALS } from '../planInjection.js';

const { PER_PLAN_TOKEN_CAP, TOTAL_PLAN_TOKEN_CAP } = _PLAN_INJECTION_INTERNALS;

function makePlan(title, content, updatedAt = '2026-05-16T12:00:00.000Z') {
  return { title, content, updatedAt, createdAt: updatedAt };
}

describe('buildPlanBlock', () => {
  test('empty input → empty block', () => {
    const out = buildPlanBlock([]);
    expect(out.block).toBe('');
    expect(out.truncated).toEqual([]);
  });

  test('null/undefined input → empty block (no crash)', () => {
    expect(buildPlanBlock(null).block).toBe('');
    expect(buildPlanBlock(undefined).block).toBe('');
  });

  test('single plan: wraps in header + footer, includes title + content', () => {
    const out = buildPlanBlock([makePlan('plan/research', 'Find ten papers.')]);
    expect(out.block).toMatch(/AGENT WORKING PLAN/);
    expect(out.block).toMatch(/END WORKING PLAN/);
    expect(out.block).toMatch(/plan\/research/);
    expect(out.block).toMatch(/Find ten papers\./);
    expect(out.truncated).toEqual([]);
  });

  test('multiple plans render under a SINGLE header (not one each)', () => {
    const out = buildPlanBlock([
      makePlan('plan/a', 'one'),
      makePlan('plan/b', 'two'),
      makePlan('plan/c', 'three'),
    ]);
    // Exactly ONE header / footer
    expect((out.block.match(/AGENT WORKING PLAN/g) || []).length).toBe(1);
    expect((out.block.match(/END WORKING PLAN/g) || []).length).toBe(1);
    // All three plans present
    expect(out.block).toMatch(/plan\/a/);
    expect(out.block).toMatch(/plan\/b/);
    expect(out.block).toMatch(/plan\/c/);
  });

  test('REGRESSION: per-plan soft cap truncates over-budget content with a clear marker', () => {
    const bigContent = 'x'.repeat(PER_PLAN_TOKEN_CAP * 4 * 2);  // 2× the cap in chars
    const out = buildPlanBlock([makePlan('plan/big', bigContent)]);
    expect(out.block).toMatch(/content truncated/);
    expect(out.truncated.length).toBe(1);
    expect(out.truncated[0].title).toBe('plan/big');
    expect(out.truncated[0].reason).toMatch(/per-plan cap/);
  });

  test('REGRESSION: cross-plan hard cap drops later plans entirely', () => {
    // Each plan is small (under per-plan cap) but in aggregate exceed total.
    // Use 8 plans of ~1500 tokens each → ~12000 total, exceeds 8000 cap.
    const planSize = 1500 * 4;
    const many = [];
    for (let i = 0; i < 8; i += 1) {
      many.push(makePlan(`plan/p${i}`, 'y'.repeat(planSize)));
    }
    const out = buildPlanBlock(many);
    // Some plans got DROPPED entirely (not just truncated).
    const droppedReasons = out.truncated.filter(t => /cross-plan cap/.test(t.reason));
    expect(droppedReasons.length).toBeGreaterThan(0);
    // The dropped plans should be absent from the block.
    for (const t of droppedReasons) {
      expect(out.block).not.toMatch(new RegExp(t.title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
    }
  });

  test('missing content (null/undefined) is rendered as empty string, no crash', () => {
    const out = buildPlanBlock([
      { title: 'plan/empty', content: null, updatedAt: '2026-01-01T00:00:00.000Z' },
      { title: 'plan/undef', content: undefined, updatedAt: '2026-01-01T00:00:00.000Z' },
    ]);
    expect(out.block).toMatch(/plan\/empty/);
    expect(out.block).toMatch(/plan\/undef/);
    expect(out.truncated).toEqual([]);
  });

  test('untitled plans get a fallback label, no crash', () => {
    const out = buildPlanBlock([{ title: undefined, content: 'oops', updatedAt: '2026-01-01T00:00:00Z' }]);
    expect(out.block).toMatch(/plan\/\(untitled\)/);
  });

  test('updatedAt is formatted as readable "YYYY-MM-DD HH:MM:SS"', () => {
    const out = buildPlanBlock([makePlan('plan/r', 'x', '2026-05-16T14:32:00.000Z')]);
    expect(out.block).toMatch(/2026-05-16 14:32:00/);
  });

  test('plans are emitted in input order (caller sorts; we preserve)', () => {
    const out = buildPlanBlock([
      makePlan('plan/first',  'A', '2026-05-16T12:00:00Z'),
      makePlan('plan/second', 'B', '2026-05-15T12:00:00Z'),
    ]);
    expect(out.block.indexOf('plan/first')).toBeLessThan(out.block.indexOf('plan/second'));
  });

  test('all plans truncated below threshold → "Summarize one" hint', () => {
    // Edge case: every plan exceeds the total cap on its own (impossible
    // mathematically given our caps, but defensive — same handler covers
    // "all dropped" path)
    const huge = 'z'.repeat(TOTAL_PLAN_TOKEN_CAP * 4 * 10);
    const out = buildPlanBlock([makePlan('plan/giant1', huge), makePlan('plan/giant2', huge)]);
    // Header still present
    expect(out.block).toMatch(/AGENT WORKING PLAN/);
  });
});
