import { describe, expect, it } from 'vitest';
import { converge } from '../src/core/bounded';
import type { DrawOpts, LlmClient } from '../src/core/types';
import { ClaudeClient } from '../src/core/llm';
import { LocalLlmClient } from '../src/core/localLlm';
import { MockLlm } from '../src/mock/mockLlm';

describe('converge meaningful-progress stall', () => {
  it('stalls when metric decreases by exactly 1/pass from 170', async () => {
    let n = 170;
    const outcome = await converge({
      measure: () => Array.from({ length: n }, (_, i) => `e${i}`),
      signature: e => e.join('|'),
      stallLimit: 100,
      progressStall: 8,
      maxPasses: 40,
      repair: async () => { n = Math.max(0, n - 1); },
    });
    // Meaningful delta at 170 is max(3, ceil(8.5))=9; shaving 1/pass never
    // resets the window — must stall well before clearing all 170.
    expect(outcome.stalled).toBe(true);
    expect(outcome.passes).toBeLessThanOrEqual(20);
    expect(n).toBeGreaterThan(140);
  });
});

describe('converge entropy-reset hook', () => {
  it('retries via onStall (fresh-draw) before reporting stalled', async () => {
    let value = 3;
    let stallCalls = 0;
    const tempsSeen: (number | undefined)[] = [];
    const outcome = await converge({
      measure: () => (value > 0 ? ['err'] : []),
      signature: e => e.join('|'),
      stallLimit: 2,
      progressStall: 2,
      maxPasses: 20,
      repair: async () => {
        /* anchored repair makes no progress */
      },
      onStall: async () => {
        stallCalls++;
        // Decorrelated fresh draw that clears the basin.
        tempsSeen.push(0.9);
        value = 0;
        return true;
      },
    });
    expect(stallCalls).toBe(1);
    expect(tempsSeen).toEqual([0.9]);
    expect(outcome.errors).toEqual([]);
    expect(outcome.stalled).toBe(false);
  });

  it('reports stalled when onStall cannot recover', async () => {
    let stallCalls = 0;
    const outcome = await converge({
      measure: () => ['err'],
      signature: e => e.join('|'),
      stallLimit: 2,
      progressStall: 100,
      maxPasses: 10,
      repair: async () => {},
      onStall: async () => {
        stallCalls++;
        return false;
      },
    });
    expect(stallCalls).toBe(1);
    expect(outcome.stalled).toBe(true);
    expect(outcome.errors).toEqual(['err']);
  });
});

describe('LlmClient DrawOpts plumbing', () => {
  it('ClaudeClient, LocalLlmClient, and MockLlm implement LlmClient (opts on every method)', () => {
    // Assignability to LlmClient is the contract: every method accepts DrawOpts.
    const _claude: LlmClient = null as unknown as ClaudeClient;
    const _local: LlmClient = null as unknown as LocalLlmClient;
    const _mock: LlmClient = null as unknown as MockLlm;
    void _claude;
    void _local;
    void _mock;
    expect(true).toBe(true);
  });

  it('MockLlm runtime accepts opts on every method', async () => {
    const m = new MockLlm();
    const opts: DrawOpts = { temperature: 0.85, avoid: 'tabu' };
    const prompt = 'url shortener';
    await m.enumerateFeatures(prompt, opts);
    await m.probeFeature({ name: 'x', scope: 'y' }, opts);
    const spec = await m.encodeFeatures(prompt, undefined, opts);
    await m.repairFeatures(spec, [spec.features[0].id], ['thin'], opts);
    await m.fixStub('src/x.ts', { diagnostics: 'err' }, opts);
    await m.generateTests(spec, spec.features[0], undefined, opts);
    await m.fixTestCases(spec, spec.features[0], [], [], [], opts);
    await m.generateImplementation(spec, spec.features[0], '// test', [], opts);
    // repair needs a feature with a fixture — F1 has one
    const f1 = spec.features.find(f => f.id === 'F1') ?? spec.features[0];
    try {
      await m.repair({
        spec,
        feature: f1,
        findings: [],
        parallelInstances: [],
        frozenTest: '',
        ownedFiles: [],
        dependencyContext: [],
        failureOutput: '',
        attempt: 1,
        lineage: [],
      }, opts);
    } catch {
      // Fixture may lack repair for some ids — signature acceptance is enough.
    }
    expect(true).toBe(true);
  });
});
