/**
 * SET 69 — scanner behavior/binding split: an infra noun excludes only the
 * BINDING clause; the behavioral invariant is kept as a testable line tagged
 * with a simulated-dependency clade. Browser-truth exclusions stay whole-line.
 * Plus the rate-window grammar invariant (injected clock, no real timers).
 */
import { describe, expect, it } from 'vitest';
import type { FeatureSpec, LlmClient, RepairInput } from '../src/core/types';
import { scanCapabilities, splitBehaviorBinding } from '../src/tier1/substrate';
import { chooseInvariants, renderSkeleton } from '../src/tier1/grammar';
import { runTier1Job } from '../src/tier1/runner';
import { makeEvalSuiteRunner } from '../src/tier1/testExec';

describe('SET 69 — behavior/binding split', () => {
  const RATE_LINE = '- rate limit uploads: at most 3 per minute per user, sliding window in redis';

  it('sliding-window rate limit splits: behavior kept, binding excised, sim-clock clade', () => {
    const hits = scanCapabilities(`intro prose\n${RATE_LINE}\n- another item`);
    expect(hits).toHaveLength(1);
    const hit = hits[0]!;
    expect(hit.capability).toBe('redis');
    expect(hit.partial).toBe(true);
    expect(hit.behavioralSplit).toBeDefined();
    expect(hit.behavioralSplit!.simClade).toBe('sim-clock');
    expect(hit.behavioralSplit!.behavior).toContain('at most 3 per minute');
    expect(hit.behavioralSplit!.behavior).not.toMatch(/redis/i);
    expect(hit.behavioralSplit!.behavior).toContain('injected clock');
  });

  it('store semantics without time → sim-store clade', () => {
    const split = splitBehaviorBinding(
      '- dedupe submitted emails, persisted in postgres', 'database');
    expect(split).not.toBeNull();
    expect(split!.simClade).toBe('sim-store');
    expect(split!.behavior).toContain('in-memory store');
    expect(split!.behavior).not.toMatch(/postgres/i);
  });

  it('pure-binding items (no behavioral core) stay honestly whole-line excluded', () => {
    const hits = scanCapabilities('- migrate to postgres with prisma\n- x');
    expect(hits[0]!.partial).toBeFalsy();
    expect(hits[0]!.behavioralSplit).toBeUndefined();
  });

  it('browser-truth exclusions never split', () => {
    const hits = scanCapabilities('- verify focus ring via getComputedStyle at 320px\n- x');
    expect(hits[0]!.capability).toBe('computed-style');
    expect(hits[0]!.behavioralSplit).toBeUndefined();
  });

  it('rate-window invariant gates first and scaffolds an injected clock', () => {
    const inv = chooseInvariants({ name: 'rate limit uploads', kind: 'edge', license: 'at most 3 per minute' });
    expect(inv[0]).toBe('rate-window');
    const sk = renderSkeleton('rate-window', 'makeLimiter');
    expect(sk.body).toContain('const clock = () => now;');
    expect(sk.body).toContain('makeLimiter(⟨ARGS⟩, clock)');
    expect(sk.body).not.toMatch(/setTimeout|Date\.now/);
    expect(sk.holes).toEqual(['ARGS', 'KEY', 'EXPECT', 'ADVANCE', 'KEY', 'EXPECT2']);
  });

  it('e2e: rate-limit edge case is KEPT under the split, not excluded', async () => {
    const spec = {
      title: 'Limiter', prompt: 'limiter',
      features: [{
        id: 'F1', name: 'limit', description: 'limits calls', parentId: null,
        deps: [], files: ['src/limit.ts'], testFile: 'tests/f1-limit.test.ts',
        edgeCases: ['- rate limit: at most 2 calls per second, sliding window in redis'],
        envVars: [],
        exports: [{ file: 'src/limit.ts', symbol: 'limit' }],
        stubs: [{ path: 'src/limit.ts', content: `export function limit(n: number): boolean { throw new Error('stub'); }` }],
      }],
    } as unknown as FeatureSpec;
    const llm = {
      async encodeFeatures() { return spec; },
      async generateTests() { return { content: '', parts: { imports: [], cases: [] } }; },
      async fixTestCases(_s: unknown, _f: unknown, req: { name: string }[]) {
        return { cases: [{ name: req[0]!.name, kind: 'behavior' as const, body: 'expect(limit(1)).toBe(true);' }] };
      },
      async generateImplementation() {
        return [{ path: 'src/limit.ts', content: 'export function limit(n: number): boolean { return true; }' }];
      },
      async repair(_i: RepairInput) {
        return [{ path: 'src/limit.ts', content: 'export function limit(n: number): boolean { return true; }' }];
      },
    } as unknown as LlmClient;
    const res = await runTier1Job({ prompt: 'limiter', llm, runSuite: makeEvalSuiteRunner() });
    const msgs = res.log.map(e => e.message);
    // The split kept the case — no whole-line exclusion, no abandonment.
    expect(msgs.some(m => /^behavior\/binding split: case ".*" kept — binding/.test(m))).toBe(true);
    expect(msgs.some(m => /excluded: ".*" — requires redis/.test(m))).toBe(false);
    expect(res.problems.some(p => /excluded — requires redis/.test(p))).toBe(false);
    // Disclosure is preserved: the hit is still on the deferred ledger.
    expect(res.deferred?.some(d => d.behavioralSplit?.simClade === 'sim-clock')).toBe(true);
  });
});
