import { describe, expect, it, vi, afterEach } from 'vitest';
import { samplerOptsFromAgent } from '../inference/stream-phase.js';
import { createContentFlusher } from '../inference/content-flusher.js';
import type { Sql } from '../../db.js';
import type { Agent } from '../../types/api.js';

const BASE_AGENT: Agent = {
  id: 'test-agent',
  name: 'Test',
  description: 'test',
  system_prompt: '',
  temperature: 0.7,
  top_p: null,
  top_k: null,
  min_p: null,
  presence_penalty: null,
  top_n_sigma: null,
  dry_multiplier: null,
  dry_base: null,
  dry_allowed_length: null,
  dry_penalty_last_n: null,
  tools: ['view_file'],
  model: null,
  source: 'global',
  max_tool_calls: null,
  steps: null,
};

describe('samplerOptsFromAgent', () => {
  it('maps every nullable sampler field to undefined when agent is null', () => {
    expect(samplerOptsFromAgent(null)).toEqual({
      temperature: undefined,
      top_p: undefined,
      top_k: undefined,
      min_p: undefined,
      presence_penalty: undefined,
      top_n_sigma: undefined,
      dry_multiplier: undefined,
      dry_base: undefined,
      dry_allowed_length: undefined,
      dry_penalty_last_n: undefined,
    });
  });

  it('strips null sampler fields to undefined but keeps numeric values', () => {
    const agent: Agent = {
      ...BASE_AGENT,
      temperature: 0.5,
      top_p: 0.9,
      top_k: null,
      min_p: 0.05,
      presence_penalty: null,
      top_n_sigma: 1,
      dry_multiplier: null,
      dry_base: 1.75,
      dry_allowed_length: null,
      dry_penalty_last_n: 256,
    };
    expect(samplerOptsFromAgent(agent)).toEqual({
      temperature: 0.5,
      top_p: 0.9,
      top_k: undefined,
      min_p: 0.05,
      presence_penalty: undefined,
      top_n_sigma: 1,
      dry_multiplier: undefined,
      dry_base: 1.75,
      dry_allowed_length: undefined,
      dry_penalty_last_n: 256,
    });
  });

  it('never includes a tools field (callers add it)', () => {
    expect('tools' in samplerOptsFromAgent(BASE_AGENT)).toBe(false);
  });
});

describe('createContentFlusher', () => {
  afterEach(() => {
    vi.useRealTimers();
  });

  // A tagged-template stub matching postgres' sql`...` shape. Records the
  // interpolated content snapshot (values[0]) of each UPDATE.
  function makeSqlSpy() {
    const writes: string[] = [];
    const sql = ((_strings: TemplateStringsArray, ...values: unknown[]) => {
      writes.push(values[0] as string);
      return Promise.resolve([]);
    }) as unknown as Sql;
    return { sql, writes };
  }

  it('debounces: many scheduleFlush calls in one window produce one write', async () => {
    vi.useFakeTimers();
    const { sql, writes } = makeSqlSpy();
    let content = '';
    const flusher = createContentFlusher(sql, 'msg-1', () => content, 500);

    content = 'a';
    flusher.scheduleFlush();
    content = 'ab';
    flusher.scheduleFlush();
    content = 'abc';
    flusher.scheduleFlush();

    expect(writes).toHaveLength(0); // nothing before the interval elapses
    vi.advanceTimersByTime(500);
    await flusher.drain();

    expect(writes).toHaveLength(1);
    // snapshot is read at fire time → latest content, not the value at schedule time
    expect(writes[0]).toBe('abc');
  });

  it('arms a fresh timer after a flush fires', async () => {
    vi.useFakeTimers();
    const { sql, writes } = makeSqlSpy();
    let content = 'one';
    const flusher = createContentFlusher(sql, 'msg-1', () => content, 500);

    flusher.scheduleFlush();
    vi.advanceTimersByTime(500);
    await Promise.resolve();

    content = 'two';
    flusher.scheduleFlush();
    vi.advanceTimersByTime(500);
    await flusher.drain();

    expect(writes).toEqual(['one', 'two']);
  });

  it('drain cancels a pending timer without performing a final flush', async () => {
    vi.useFakeTimers();
    const { sql, writes } = makeSqlSpy();
    let content = 'pending';
    const flusher = createContentFlusher(sql, 'msg-1', () => content, 500);

    flusher.scheduleFlush();
    // Drain before the timer fires — the pending flush is cancelled, not forced.
    await flusher.drain();
    vi.advanceTimersByTime(500);
    await Promise.resolve();

    expect(writes).toHaveLength(0);
  });
});
