/**
 * Tests for the code-map stub renderer — an OPTIONAL renderer transform
 * that replaces prior-turn code-map tool-result content with a compact
 * stub. Designed to make the agent's working context smaller without
 * touching the persistence layer.
 *
 * Contract:
 *   - INERT until called. Functions are pure; idempotent on repeat.
 *   - Only rewrites messages identified as code-map tool results.
 *   - NEVER stubs the current turn (the one the agent is acting on now).
 *   - Persists the original content under `_originalContent` so audit /
 *     replay / debug remain lossless.
 *   - Detection uses substring scans only — no regex.
 */
import { describe, test, expect } from '@jest/globals';
import {
  detectCodeMapResult,
  buildStub,
  applyCodeMapStubbing,
} from '../codeMapStubRenderer.js';

describe('detectCodeMapResult', () => {
  test('detects a skeleton result with filePath', () => {
    const content =
      '[Tool Results — 1 result from 1 batch: code-map]\n' +
      'code-map skeleton:\n' +
      '  filePath: src/foo.ts\n' +
      '  class Foo { method(); }';
    const d = detectCodeMapResult(content);
    expect(d.kind).toBe('skeleton');
    expect(d.filePath).toBe('src/foo.ts');
  });

  test('detects a read-range result', () => {
    const content =
      'code-map read-range result\n' +
      '"filePath": "src/bar.ts"\n' +
      'line content here';
    const d = detectCodeMapResult(content);
    expect(d.kind).toBe('read-range');
    expect(d.filePath).toBe('src/bar.ts');
  });

  test('returns kind: null on non-code-map content', () => {
    expect(detectCodeMapResult('memory list returned 3 items').kind).toBeNull();
    expect(detectCodeMapResult('filesystem read complete').kind).toBeNull();
    expect(detectCodeMapResult('').kind).toBeNull();
    expect(detectCodeMapResult(null).kind).toBeNull();
    expect(detectCodeMapResult(undefined).kind).toBeNull();
  });

  test('returns kind: null when "code-map" appears without "skeleton" or "read-range"', () => {
    const d = detectCodeMapResult('code-map invoked but no action');
    expect(d.kind).toBeNull();
  });

  test('handles filePath without explicit marker — falls back to null', () => {
    const d = detectCodeMapResult('code-map skeleton: some content here');
    expect(d.kind).toBe('skeleton');
    expect(d.filePath).toBeNull();
  });

  test('handles quoted JSON-style filePath', () => {
    const content = '{"toolId":"code-map","action":"skeleton","filePath":"apps/web/src/Button.tsx"}';
    const d = detectCodeMapResult(content);
    expect(d.kind).toBe('skeleton');
    expect(d.filePath).toBe('apps/web/src/Button.tsx');
  });

  test('handles bare-key filePath followed by space', () => {
    const content = 'code-map skeleton result file: src/index.ts more text';
    const d = detectCodeMapResult(content);
    expect(d.kind).toBe('skeleton');
    expect(d.filePath).toBe('src/index.ts');
  });
});

describe('buildStub', () => {
  test('compact stub with filePath', () => {
    const s = buildStub({ kind: 'skeleton', filePath: 'src/foo.ts' });
    expect(s).toBe('[code-map skeleton src/foo.ts — cached; see `## CODEBASE KNOWLEDGE`]');
  });

  test('compact stub without filePath', () => {
    const s = buildStub({ kind: 'read-range', filePath: null });
    expect(s).toBe('[code-map read-range — cached; see `## CODEBASE KNOWLEDGE`]');
  });

  test('REGRESSION: stub is short — no prescriptive prose', () => {
    const s = buildStub({ kind: 'skeleton', filePath: 'a/b/c.ts' });
    expect(s.length).toBeLessThan(120);
    expect(s.toLowerCase()).not.toContain('please');
    expect(s.toLowerCase()).not.toContain('re-issue');
    expect(s.toLowerCase()).not.toContain('do not');
  });
});

describe('applyCodeMapStubbing', () => {
  const makeSkeletonResult = (filePath, idx = 0) => ({
    id: `tool-result-${idx}`,
    role: 'user',
    type: 'tool-result',
    content: `code-map skeleton:\n  filePath: ${filePath}\n  class Foo { ${'longContent '.repeat(100)} }`,
    responseTurnId: `turn-${idx}`,
  });

  test('empty message list passes through', () => {
    expect(applyCodeMapStubbing([])).toEqual([]);
  });

  test('null / undefined / non-array passes through unchanged', () => {
    expect(applyCodeMapStubbing(null)).toBeNull();
    expect(applyCodeMapStubbing(undefined)).toBeUndefined();
  });

  test('non-tool-result messages are untouched', () => {
    const msgs = [
      { role: 'user', content: 'hi' },
      { role: 'assistant', content: 'hello' },
      { role: 'user', type: 'consolidated-input', content: 'thanks' },
    ];
    expect(applyCodeMapStubbing(msgs)).toEqual(msgs);
  });

  test('stubs prior-turn code-map skeleton; preserves current turn', () => {
    const t1 = makeSkeletonResult('src/foo.ts', 1);
    const t2 = makeSkeletonResult('src/bar.ts', 2);
    const result = applyCodeMapStubbing([t1, t2]);
    // t1 (prior) stubbed
    expect(result[0]._stubbed).toBe(true);
    expect(result[0].content).toContain('[code-map skeleton src/foo.ts');
    expect(result[0]._originalContent).toBe(t1.content);
    // t2 (most recent tool-result) preserved
    expect(result[1]._stubbed).toBeUndefined();
    expect(result[1].content).toBe(t2.content);
  });

  test('respects currentTurnId when provided', () => {
    const t1 = makeSkeletonResult('src/a.ts', 1);
    const t2 = makeSkeletonResult('src/b.ts', 2);
    const t3 = makeSkeletonResult('src/c.ts', 3);
    // Mark turn-2 as the current one explicitly.
    const result = applyCodeMapStubbing([t1, t2, t3], { currentTurnId: 'turn-2' });
    expect(result[0]._stubbed).toBe(true);   // turn-1 stubbed
    expect(result[1]._stubbed).toBeUndefined(); // turn-2 preserved (explicit current)
    expect(result[2]._stubbed).toBe(true);   // turn-3 stubbed (not current)
  });

  test('non-code-map tool-results are NOT stubbed (memory, filesystem, etc.)', () => {
    const memResult = {
      role: 'user', type: 'tool-result',
      content: '[Tool Results] memory list: 3 entries returned',
      responseTurnId: 'turn-1',
    };
    const fsResult = {
      role: 'user', type: 'tool-result',
      content: '[Tool Results] filesystem read complete: 200 bytes',
      responseTurnId: 'turn-2',
    };
    const codemap = makeSkeletonResult('src/foo.ts', 3);
    const result = applyCodeMapStubbing([memResult, fsResult, codemap]);
    expect(result[0].content).toBe(memResult.content);    // memory: untouched
    expect(result[1].content).toBe(fsResult.content);     // filesystem: untouched
    expect(result[2].content).toBe(codemap.content);      // code-map most-recent: preserved
  });

  test('IDEMPOTENT: running twice produces the same result', () => {
    const msgs = [
      makeSkeletonResult('src/a.ts', 1),
      makeSkeletonResult('src/b.ts', 2),
      makeSkeletonResult('src/c.ts', 3),
    ];
    const once = applyCodeMapStubbing(msgs);
    const twice = applyCodeMapStubbing(once);
    expect(twice).toEqual(once);
  });

  test('TOKEN-SAVINGS REGRESSION: stub is at least 10× shorter than original', () => {
    const heavyResult = {
      role: 'user', type: 'tool-result',
      content: 'code-map skeleton:\n  filePath: src/big.ts\n' + 'lineContent '.repeat(500),
      responseTurnId: 'turn-1',
    };
    const recentResult = makeSkeletonResult('src/recent.ts', 2);
    const result = applyCodeMapStubbing([heavyResult, recentResult]);
    expect(result[0].content.length).toBeLessThan(heavyResult.content.length / 10);
  });

  test('PRESERVES original content for audit/replay', () => {
    const t1 = makeSkeletonResult('src/foo.ts', 1);
    const t2 = makeSkeletonResult('src/bar.ts', 2);
    const result = applyCodeMapStubbing([t1, t2]);
    expect(result[0]._originalContent).toBe(t1.content);
    // No lossy mutation: we can fully recover the original.
    expect(result[0]._originalContent.length).toBe(t1.content.length);
  });

  test('NEVER MUTATES the input array', () => {
    const t1 = makeSkeletonResult('src/foo.ts', 1);
    const t2 = makeSkeletonResult('src/bar.ts', 2);
    const t1Snap = JSON.stringify(t1);
    const t2Snap = JSON.stringify(t2);
    applyCodeMapStubbing([t1, t2]);
    expect(JSON.stringify(t1)).toBe(t1Snap);
    expect(JSON.stringify(t2)).toBe(t2Snap);
  });
});
