// v1.13.5: truncate.ts unit coverage. Each test isolates TRUNCATION_DIR
// under os.tmpdir() so concurrent vitest runs don't collide and the suite
// stays self-cleaning. cleanupTruncations is covered by file-system half
// only; the orphan-reap branch needs a real Postgres and is tested via the
// smoke flow rather than vitest.
import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
import { promises as fs } from 'fs';
import path from 'path';
import os from 'os';

// Set the env var BEFORE importing the module so its module-load constant
// reads the test directory rather than /tmp/boocode-truncations.
const testDir = path.join(os.tmpdir(), `boocode-truncate-test-${process.pid}-${Date.now()}`);
process.env.BOOCODE_TRUNCATION_DIR = testDir;

const mod = await import('../truncate.js');
const { storeTruncation, readTruncation, truncateIfNeeded, MAX_TRUNCATION_BYTES } = mod;

beforeAll(async () => {
  await fs.mkdir(testDir, { recursive: true });
});

afterEach(async () => {
  // Drop every file between tests so id-collision asserts and orphan-style
  // counts start from zero.
  const entries = await fs.readdir(testDir).catch(() => [] as string[]);
  await Promise.all(entries.map((n) => fs.unlink(path.join(testDir, n)).catch(() => {})));
});

describe('storeTruncation / readTruncation roundtrip', () => {
  it('writes and reads identical content', async () => {
    const original = 'hello\nworld\n' + 'x'.repeat(500);
    const id = await storeTruncation(original);
    expect(id).toMatch(/^tr_[0-9a-v]{12}$/);
    const got = await readTruncation(id);
    expect(got).toBe(original);
  });

  it('readTruncation returns null for unknown ids', async () => {
    const got = await readTruncation('tr_000000000000');
    expect(got).toBeNull();
  });

  it('readTruncation rejects malformed ids (returns null, never escapes dir)', async () => {
    // Path traversal attempt; readTruncation should not even try to open.
    const got = await readTruncation('../../etc/passwd');
    expect(got).toBeNull();
  });
});

describe('truncateIfNeeded', () => {
  it('returns sliced content with no outputPath when wasTruncated=false', async () => {
    const out = await truncateIfNeeded({
      fullContent: 'irrelevant',
      slicedContent: 'visible',
      wasTruncated: false,
    });
    expect(out).toEqual({ content: 'visible', truncated: false });
    expect('outputPath' in out).toBe(false);
  });

  it('stashes full content and returns outputPath when wasTruncated=true', async () => {
    const full = 'line1\nline2\nline3\nline4\n';
    const sliced = 'line1\nline2\n[truncated]';
    const out = await truncateIfNeeded({
      fullContent: full,
      slicedContent: sliced,
      wasTruncated: true,
    });
    expect(out.content).toBe(sliced);
    expect(out.truncated).toBe(true);
    expect(out.outputPath).toMatch(/^tr_[0-9a-v]{12}$/);
    const stashed = await readTruncation(out.outputPath!);
    expect(stashed).toBe(full);
  });

  it('skips storage but still reports truncated when fullContent exceeds the cap', async () => {
    // Build content larger than MAX_TRUNCATION_BYTES. Use a Buffer to size
    // it without holding a literal that triggers the gigantic-string lint.
    const oversized = Buffer.alloc(MAX_TRUNCATION_BYTES + 1, 'x').toString('utf8');
    const sliced = 'preview...';
    const out = await truncateIfNeeded({
      fullContent: oversized,
      slicedContent: sliced,
      wasTruncated: true,
    });
    expect(out).toEqual({ content: sliced, truncated: true });
    expect('outputPath' in out).toBe(false);
  });

  it('storage failure surfaces as truncated without outputPath', async () => {
    // Force writeFile to throw. Spy at the fs module level since truncate.ts
    // imports { promises as fs } and storeTruncation calls fs.writeFile.
    const spy = vi.spyOn(fs, 'writeFile').mockRejectedValueOnce(new Error('disk full'));
    const out = await truncateIfNeeded({
      fullContent: 'short',
      slicedContent: 'sliced',
      wasTruncated: true,
    });
    expect(out).toEqual({ content: 'sliced', truncated: true });
    expect('outputPath' in out).toBe(false);
    spy.mockRestore();
  });
});
