import { describe, it, expect } from 'vitest';
import { estimateTokens, estimateAudioTokens, estimateMessageTokens, getContextWindow, shouldCompact, CONTEXT_WINDOWS } from './tokens.js';

describe('estimateTokens', () => {
  it('returns 0 for empty string', () => {
    // estimateTokens('') should be 0 — Math.ceil(0/4) = 0
    expect(estimateTokens('')).toBe(0);
  });

  it('estimates ~1 token per 4 characters', () => {
    // 'abcd' = 4 chars → ceil(4/4) = 1
    expect(estimateTokens('abcd')).toBe(1);
    // 'abcde' = 5 chars → ceil(5/4) = 2
    expect(estimateTokens('abcde')).toBe(2);
  });

  it('handles longer text', () => {
    const text = 'a'.repeat(100);
    expect(estimateTokens(text)).toBe(25);  // 100/4 = 25
  });
});

describe('estimateAudioTokens', () => {
  it('returns positive number for valid audio data', () => {
    // 10000 base64 chars → ~7500 bytes → ~3.75s → ceil(3.75 * 32) = 120
    const source = { media_type: 'audio/ogg', data: 'A'.repeat(10000) };
    const tokens = estimateAudioTokens(source);
    expect(tokens).toBeGreaterThan(0);
    expect(tokens).toBe(120);
  });

  it('returns at least 1 for very short audio', () => {
    // 1 byte of base64 data → very tiny duration, but minimum is 1
    const source = { media_type: 'audio/ogg', data: 'A' };
    expect(estimateAudioTokens(source)).toBe(1);
  });

  it('returns 0 for empty audio data', () => {
    const source = { media_type: 'audio/ogg', data: '' };
    expect(estimateAudioTokens(source)).toBe(0);
  });

  it('longer audio data produces more tokens', () => {
    const short = { media_type: 'audio/ogg', data: 'A'.repeat(1000) };
    const long = { media_type: 'audio/ogg', data: 'A'.repeat(100000) };
    expect(estimateAudioTokens(long)).toBeGreaterThan(estimateAudioTokens(short));
  });
});

describe('estimateMessageTokens', () => {
  it('returns 0 for empty array', () => {
    expect(estimateMessageTokens([])).toBe(0);
  });

  it('includes overhead per message', () => {
    // 'abcd' = 1 token content + 4 overhead = 5 per message
    const messages = [{ role: 'user' as const, content: 'abcd' }];
    expect(estimateMessageTokens(messages)).toBe(5);
  });

  it('sums multiple messages', () => {
    const messages = [
      { role: 'user' as const, content: 'abcd' },     // 1 + 4 = 5
      { role: 'assistant' as const, content: 'abcd' }, // 1 + 4 = 5
    ];
    expect(estimateMessageTokens(messages)).toBe(10);
  });

  it('includes audio token estimate for multimodal messages', () => {
    // Text part: 'hello' = 5 chars → ceil(5/4) = 2 text tokens
    // Audio part: 10000 base64 chars → 120 audio tokens (see estimateAudioTokens test)
    // Overhead: 4
    // Total: 2 + 120 + 4 = 126
    const messages = [
      {
        role: 'user' as const,
        content: [
          { type: 'text' as const, text: 'hello' },
          { type: 'audio' as const, source: { media_type: 'audio/ogg', data: 'A'.repeat(10000) } },
        ],
      },
    ];
    expect(estimateMessageTokens(messages)).toBe(126);
  });
});

describe('getContextWindow', () => {
  it('returns known window for Claude Sonnet', () => {
    expect(getContextWindow('claude-sonnet-4-20250514')).toBe(200_000);
  });

  it('returns known window for GPT-4o', () => {
    expect(getContextWindow('gpt-4o')).toBe(128_000);
  });

  it('returns default 128000 for unknown model', () => {
    expect(getContextWindow('unknown-model')).toBe(128_000);
  });

  it('returns override when provided', () => {
    expect(getContextWindow('claude-sonnet-4-20250514', 50_000)).toBe(50_000);
  });

  it('returns override even for unknown model', () => {
    expect(getContextWindow('unknown-model', 32_000)).toBe(32_000);
  });
});

describe('shouldCompact', () => {
  it('returns false when messages are well below threshold', () => {
    const messages = [{ role: 'user' as const, content: 'hello' }];
    expect(shouldCompact({
      messages,
      model: 'gpt-4o',  // 128k context window
    })).toBe(false);
  });

  it('returns true when messages exceed threshold', () => {
    // Create messages that exceed 80% of a small context window
    // context window = 100, threshold = 80% = 80 tokens
    // each message: ceil(400/4) + 4 = 104 tokens → well over 80
    const messages = [{ role: 'user' as const, content: 'a'.repeat(400) }];
    expect(shouldCompact({
      messages,
      model: 'unknown',
      contextWindow: 100,
      thresholdPct: 80,
    })).toBe(true);
  });

  it('respects custom thresholdPct', () => {
    // 1 message: ceil(20/4) + 4 = 9 tokens
    // contextWindow = 100, thresholdPct = 5 → threshold = 5 tokens
    const messages = [{ role: 'user' as const, content: 'a'.repeat(20) }];
    expect(shouldCompact({
      messages,
      model: 'unknown',
      contextWindow: 100,
      thresholdPct: 5,
    })).toBe(true);
  });

  it('uses model lookup when no contextWindow override', () => {
    // gpt-3.5-turbo = 16385 tokens, default threshold 80% = 13108
    // Large message to exceed: ceil(60000/4) + 4 = 15004 tokens → over 13108
    const messages = [{ role: 'user' as const, content: 'a'.repeat(60000) }];
    expect(shouldCompact({
      messages,
      model: 'gpt-3.5-turbo',
    })).toBe(true);
  });
});
