import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';

vi.mock('../pb', () => ({
  pb: { collection: () => ({ create: () => ({ catch: () => {} }) }) },
}));

import { chunkText, buildKnownIdsHint, MAX_CHUNK_CHARS, OVERLAP_CHARS } from '../extractionPipeline';

describe('chunkText', () => {
  let warnSpy;
  beforeEach(() => { warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); });
  afterEach(() => { warnSpy.mockRestore(); });

  it('returns the original text as a single chunk when below maxChars', () => {
    const result = chunkText('A short paragraph.');
    expect(result).toEqual(['A short paragraph.']);
  });

  it('returns empty array for empty/whitespace input', () => {
    expect(chunkText('')).toEqual([]);
    expect(chunkText('   \n  ')).toEqual([]);
  });

  it('splits along sentence boundaries with overlap between adjacent chunks', () => {
    const sentence = 'This is a sentence with exactly a known length. ';
    const text = sentence.repeat(100); // ~5000 chars
    const chunks = chunkText(text, { maxChars: 600, overlapChars: 150 });

    expect(chunks.length).toBeGreaterThan(1);
    for (const c of chunks) {
      expect(c.length).toBeLessThanOrEqual(600);
    }
    // Adjacent chunks share trailing text — the overlap should be non-empty.
    for (let i = 1; i < chunks.length; i++) {
      const tail = chunks[i - 1].slice(-150);
      // The new chunk must begin with content that appears at the tail of the prior chunk.
      const firstHundred = chunks[i].slice(0, 100);
      // At least one word from the tail should appear in the head of the next chunk.
      const words = tail.split(/\s+/).filter((w) => w.length > 3);
      const shared = words.some((w) => firstHundred.includes(w));
      expect(shared).toBe(true);
    }
  });

  it('hard-splits a single sentence that exceeds maxChars and logs a warning', () => {
    const huge = 'word '.repeat(400).trim() + '.'; // ~2000 chars, no sentence break
    const chunks = chunkText(huge, { maxChars: 500, overlapChars: 50 });
    expect(chunks.length).toBeGreaterThan(1);
    expect(warnSpy).toHaveBeenCalled();
    for (const c of chunks) expect(c.length).toBeLessThanOrEqual(500);
  });

  it('handles paragraph-only splits when no sentence punctuation is present', () => {
    const paragraphs = Array.from({ length: 10 }, (_, i) => `paragraph ${i} content here`).join('\n\n');
    const chunks = chunkText(paragraphs, { maxChars: 80, overlapChars: 20 });
    expect(chunks.length).toBeGreaterThan(1);
  });

  it('uses the documented defaults', () => {
    expect(MAX_CHUNK_CHARS).toBe(8000);
    expect(OVERLAP_CHARS).toBe(800);
  });
});

describe('buildKnownIdsHint', () => {
  it('returns empty string when no IDs are known', () => {
    expect(buildKnownIdsHint([])).toBe('');
    expect(buildKnownIdsHint(undefined)).toBe('');
    expect(buildKnownIdsHint(null)).toBe('');
  });

  it('formats topics as id: "label" pairs with a leading instruction', () => {
    const hint = buildKnownIdsHint([
      { id: 'software-engineer', label: 'Software Engineer' },
      { id: 'onboarding-buddy', label: 'Onboarding Buddy' },
    ]);
    expect(hint).toContain('Already-extracted topics');
    expect(hint).toContain('- software-engineer: "Software Engineer"');
    expect(hint).toContain('- onboarding-buddy: "Onboarding Buddy"');
    expect(hint.endsWith('\n')).toBe(true);
  });

  it('caps the hint at the 200 most recent topics', () => {
    const topics = Array.from({ length: 250 }, (_, i) => ({ id: `topic-${i}`, label: `Topic ${i}` }));
    const hint = buildKnownIdsHint(topics);
    // The newest topics must appear; the oldest must not.
    expect(hint).toContain('topic-249');
    expect(hint).toContain('topic-50');
    expect(hint).not.toContain('topic-49');
    expect(hint).not.toContain('topic-0\n');
  });
});
