/**
 * Deterministic mock LLM (MOCK_LLM=1). Simulates the full pipeline against a
 * fixed product prompt — a URL-shortener library — and deliberately injects
 * three bugs at build time, one per major bug-class family:
 *
 *   F1  codec    -> idiosyncratic behavior bug (off-by-one in decode; caught
 *                   only by the frozen IR — 'test-failure')
 *   F3  service  -> mechanical wiring bug (imports a symbol that does not
 *                   exist — 'undefined-symbol' via tsc)
 *   F4  stats    -> committed stub ('todo-stub' + test failures)
 *
 * Repair responses return the corrected files, so a mock run demonstrates the
 * whole doctrine end-to-end: frozen IR -> build -> real-time authentication
 * failures -> RHA localization -> bounded repair -> green.
 */
import type {
  DebugSink,
  DrawOpts,
  FeatureNode,
  FeatureSpec,
  GeneratedFile,
  LlmCallRecord,
  LlmClient,
  RepairInput,
  UsageSink,
} from '../core/types';

const STUBS: Record<string, GeneratedFile[]> = {
  F1: [{
    path: 'src/codec.ts',
    content: `export function encodeId(_n: number): string { throw new Error('stub: not implemented'); }
export function decodeId(_s: string): number { throw new Error('stub: not implemented'); }
`,
  }],
  F2: [{
    path: 'src/store.ts',
    content: `export type Clock = () => number;

export class MemoryStore<V> {
  constructor(_clock?: Clock) { throw new Error('stub: not implemented'); }
  set(_key: string, _value: V, _ttlMs?: number): void { throw new Error('stub: not implemented'); }
  get(_key: string): V | undefined { throw new Error('stub: not implemented'); }
  delete(_key: string): boolean { throw new Error('stub: not implemented'); }
}
`,
  }],
  F3: [{
    path: 'src/service.ts',
    content: `import type { Clock } from './store';

export interface ShortenOptions {
  alias?: string;
  ttlMs?: number;
}

export class Shortener {
  constructor(_clock?: Clock) { throw new Error('stub: not implemented'); }
  shorten(_url: string, _options?: ShortenOptions): string { throw new Error('stub: not implemented'); }
  resolve(_code: string): string | undefined { throw new Error('stub: not implemented'); }
}
`,
  }],
  F4: [{
    path: 'src/stats.ts',
    content: `export interface TopEntry {
  code: string;
  hits: number;
}

export class StatsTracker {
  record(_code: string): void { throw new Error('stub: not implemented'); }
  count(_code: string): number { throw new Error('stub: not implemented'); }
  top(_n: number): TopEntry[] { throw new Error('stub: not implemented'); }
}
`,
  }],
};

const FEATURES: Array<Omit<FeatureNode, 'stubs'>> = [
  {
    id: 'F1',
    name: 'Base62 codec',
    description: 'Encode non-negative integer ids to base62 strings and decode them back.',
    parentId: null,
    deps: [],
    files: ['src/codec.ts'],
    testFile: 'tests/f1-codec.test.ts',
    edgeCases: ['zero encodes to "0"', 'round-trip identity', 'negative input rejected', 'non-integer rejected', 'invalid characters rejected on decode', 'empty string rejected on decode'],
    exports: [
      { file: 'src/codec.ts', symbol: 'encodeId' },
      { file: 'src/codec.ts', symbol: 'decodeId' },
    ],
    envVars: [],
  },
  {
    id: 'F2',
    name: 'Memory store with TTL',
    description: 'In-memory key-value store with optional per-entry TTL and an injectable clock for deterministic expiry.',
    parentId: null,
    deps: [],
    files: ['src/store.ts'],
    testFile: 'tests/f2-store.test.ts',
    edgeCases: ['missing key returns undefined', 'entry expires exactly at ttl boundary', 'overwrite replaces value and ttl', 'delete removes entry'],
    exports: [{ file: 'src/store.ts', symbol: 'MemoryStore' }],
    envVars: [],
  },
  {
    id: 'F3',
    name: 'Shortener service',
    description: 'Shorten http(s) URLs to sequential base62 codes with optional custom alias and TTL; resolve codes back to URLs.',
    parentId: null,
    deps: ['F1', 'F2'],
    files: ['src/service.ts'],
    testFile: 'tests/f3-service.test.ts',
    edgeCases: ['non-http(s) url rejected', 'duplicate alias rejected', 'expiry via injected clock', 'sequential codes unique'],
    exports: [{ file: 'src/service.ts', symbol: 'Shortener' }],
    envVars: [],
  },
  {
    id: 'F4',
    name: 'Hit statistics',
    description: 'Track resolve hits per code and report the top-N most-hit codes with deterministic tie-breaking.',
    parentId: null,
    deps: ['F3'],
    files: ['src/stats.ts'],
    testFile: 'tests/f4-stats.test.ts',
    edgeCases: ['unknown code counts as zero', 'top(0) is empty', 'negative n rejected', 'ties broken lexicographically'],
    exports: [{ file: 'src/stats.ts', symbol: 'StatsTracker' }],
    envVars: [],
  },
];

// ---------------------------------------------------------------------------
// Frozen IR (test suites)
// ---------------------------------------------------------------------------

const TESTS: Record<string, string> = {
  F1: `import { describe, it, expect } from 'vitest';
import { encodeId, decodeId } from '../src/codec';

describe('F1 base62 codec', () => {
  it('encodes small integers', () => {
    expect(encodeId(0)).toBe('0');
    expect(encodeId(61)).toBe('Z');
    expect(encodeId(62)).toBe('10');
  });

  it('round-trips values', () => {
    for (const n of [0, 1, 61, 62, 12345, 987654321]) {
      expect(decodeId(encodeId(n))).toBe(n);
    }
  });

  it('edge: rejects negative input', () => {
    expect(() => encodeId(-1)).toThrow(RangeError);
  });

  it('edge: rejects non-integer input', () => {
    expect(() => encodeId(1.5)).toThrow(RangeError);
  });

  it('edge: decode rejects invalid characters', () => {
    expect(() => decodeId('ab!')).toThrow(RangeError);
  });

  it('edge: decode rejects empty string', () => {
    expect(() => decodeId('')).toThrow(RangeError);
  });
});
`,
  F2: `import { describe, it, expect } from 'vitest';
import { MemoryStore } from '../src/store';

describe('F2 memory store', () => {
  it('sets and gets values', () => {
    const store = new MemoryStore<string>();
    store.set('a', 'x');
    expect(store.get('a')).toBe('x');
  });

  it('edge: missing key returns undefined', () => {
    expect(new MemoryStore<string>().get('nope')).toBeUndefined();
  });

  it('edge: entries expire at the ttl boundary with an injected clock', () => {
    let now = 1000;
    const store = new MemoryStore<string>(() => now);
    store.set('k', 'v', 500);
    expect(store.get('k')).toBe('v');
    now = 1500;
    expect(store.get('k')).toBeUndefined();
  });

  it('edge: overwrite replaces value and ttl', () => {
    let now = 0;
    const store = new MemoryStore<string>(() => now);
    store.set('k', 'v1', 100);
    store.set('k', 'v2');
    now = 5000;
    expect(store.get('k')).toBe('v2');
  });

  it('edge: delete removes entries', () => {
    const store = new MemoryStore<string>();
    store.set('k', 'v');
    expect(store.delete('k')).toBe(true);
    expect(store.get('k')).toBeUndefined();
  });
});
`,
  F3: `import { describe, it, expect } from 'vitest';
import { Shortener } from '../src/service';

describe('F3 shortener service', () => {
  it('shortens and resolves urls', () => {
    const s = new Shortener();
    const code = s.shorten('https://example.com');
    expect(s.resolve(code)).toBe('https://example.com');
  });

  it('integration: sequential codes are base62 and unique', () => {
    const s = new Shortener();
    const a = s.shorten('https://a.example');
    const b = s.shorten('https://b.example');
    expect(a).toBe('0');
    expect(b).toBe('1');
  });

  it('supports custom aliases', () => {
    const s = new Shortener();
    const code = s.shorten('https://example.com', { alias: 'promo' });
    expect(code).toBe('promo');
    expect(s.resolve('promo')).toBe('https://example.com');
  });

  it('edge: rejects non-http(s) urls', () => {
    expect(() => new Shortener().shorten('ftp://files.example')).toThrow(TypeError);
  });

  it('edge: rejects duplicate alias', () => {
    const s = new Shortener();
    s.shorten('https://one.example', { alias: 'dup' });
    expect(() => s.shorten('https://two.example', { alias: 'dup' })).toThrow();
  });

  it('integration: entries expire with an injected clock', () => {
    let now = 0;
    const s = new Shortener(() => now);
    const code = s.shorten('https://example.com', { ttlMs: 100 });
    now = 200;
    expect(s.resolve(code)).toBeUndefined();
  });
});
`,
  F4: `import { describe, it, expect } from 'vitest';
import { StatsTracker } from '../src/stats';
import { Shortener } from '../src/service';

describe('F4 hit statistics', () => {
  it('edge: unknown code counts as zero', () => {
    expect(new StatsTracker().count('missing')).toBe(0);
  });

  it('records hits per code', () => {
    const t = new StatsTracker();
    t.record('a');
    t.record('a');
    t.record('b');
    expect(t.count('a')).toBe(2);
    expect(t.count('b')).toBe(1);
  });

  it('reports top-N by hits', () => {
    const t = new StatsTracker();
    for (let i = 0; i < 3; i++) t.record('x');
    for (let i = 0; i < 5; i++) t.record('y');
    t.record('z');
    expect(t.top(2)).toEqual([
      { code: 'y', hits: 5 },
      { code: 'x', hits: 3 },
    ]);
  });

  it('edge: ties broken lexicographically', () => {
    const t = new StatsTracker();
    t.record('b');
    t.record('a');
    expect(t.top(2)).toEqual([
      { code: 'a', hits: 1 },
      { code: 'b', hits: 1 },
    ]);
  });

  it('edge: top(0) is empty and negative n rejected', () => {
    const t = new StatsTracker();
    t.record('a');
    expect(t.top(0)).toEqual([]);
    expect(() => t.top(-1)).toThrow(RangeError);
  });

  it('integration: counts resolves issued by the shortener', () => {
    const t = new StatsTracker();
    const s = new Shortener();
    const code = s.shorten('https://example.com');
    if (s.resolve(code) !== undefined) t.record(code);
    expect(t.count(code)).toBe(1);
  });
});
`,
};

// ---------------------------------------------------------------------------
// Implementations — correct versions
// ---------------------------------------------------------------------------

const CODEC_OK = `const ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';

export function encodeId(n: number): string {
  if (!Number.isInteger(n) || n < 0) throw new RangeError('encodeId requires a non-negative integer');
  if (n === 0) return '0';
  let out = '';
  let v = n;
  while (v > 0) {
    out = ALPHABET[v % 62] + out;
    v = Math.floor(v / 62);
  }
  return out;
}

export function decodeId(s: string): number {
  if (s.length === 0) throw new RangeError('decodeId requires a non-empty string');
  let v = 0;
  for (const ch of s) {
    const idx = ALPHABET.indexOf(ch);
    if (idx === -1) throw new RangeError('invalid base62 character: ' + ch);
    v = v * 62 + idx;
  }
  return v;
}
`;

// Injected bug: off-by-one in decode. Compiles cleanly; only the frozen IR
// (round-trip + small-integer cases) catches it. Bug class: test-failure.
const CODEC_BUGGY = CODEC_OK.replace('v = v * 62 + idx;', 'v = v * 62 + idx + 1;');

const STORE_OK = `export type Clock = () => number;

interface Entry<V> {
  value: V;
  expiresAt: number | null;
}

export class MemoryStore<V> {
  private entries = new Map<string, Entry<V>>();

  constructor(private clock: Clock = () => Date.now()) {}

  set(key: string, value: V, ttlMs?: number): void {
    const expiresAt = ttlMs === undefined ? null : this.clock() + ttlMs;
    this.entries.set(key, { value, expiresAt });
  }

  get(key: string): V | undefined {
    const e = this.entries.get(key);
    if (!e) return undefined;
    if (e.expiresAt !== null && this.clock() >= e.expiresAt) {
      this.entries.delete(key);
      return undefined;
    }
    return e.value;
  }

  delete(key: string): boolean {
    return this.entries.delete(key);
  }
}
`;

const SERVICE_OK = `import { encodeId } from './codec';
import { MemoryStore, type Clock } from './store';

export interface ShortenOptions {
  alias?: string;
  ttlMs?: number;
}

export class Shortener {
  private store: MemoryStore<string>;
  private nextId = 0;

  constructor(clock?: Clock) {
    this.store = new MemoryStore<string>(clock);
  }

  shorten(url: string, options: ShortenOptions = {}): string {
    if (!/^https?:\\/\\//.test(url)) throw new TypeError('only http(s) URLs can be shortened');
    if (options.alias !== undefined && this.store.get(options.alias) !== undefined) {
      throw new Error('alias already in use: ' + options.alias);
    }
    const code = options.alias ?? this.allocateCode();
    this.store.set(code, url, options.ttlMs);
    return code;
  }

  resolve(code: string): string | undefined {
    return this.store.get(code);
  }

  private allocateCode(): string {
    let code = encodeId(this.nextId++);
    while (this.store.get(code) !== undefined) code = encodeId(this.nextId++);
    return code;
  }
}
`;

// Injected bug: imports a symbol that does not exist in the codec module.
// Fails tsc (TS2305) and crashes the F3/F4 test files at load. Bug class:
// undefined-symbol — the canonical mechanical wiring failure.
const SERVICE_BUGGY = SERVICE_OK
  .replace("import { encodeId } from './codec';", "import { encode } from './codec';")
  .replace('let code = encodeId(this.nextId++);', 'let code = encode(this.nextId++);')
  .replace('while (this.store.get(code) !== undefined) code = encodeId(this.nextId++);', 'while (this.store.get(code) !== undefined) code = encode(this.nextId++);');

const STATS_OK = `export interface TopEntry {
  code: string;
  hits: number;
}

export class StatsTracker {
  private counts = new Map<string, number>();

  record(code: string): void {
    this.counts.set(code, (this.counts.get(code) ?? 0) + 1);
  }

  count(code: string): number {
    return this.counts.get(code) ?? 0;
  }

  top(n: number): TopEntry[] {
    if (!Number.isInteger(n) || n < 0) throw new RangeError('top(n) requires a non-negative integer');
    return [...this.counts.entries()]
      .map(([code, hits]) => ({ code, hits }))
      .sort((a, b) => b.hits - a.hits || a.code.localeCompare(b.code))
      .slice(0, n);
  }
}
`;

// Injected bug: committed stub. Bug class: todo-stub (plus test failures).
const STATS_BUGGY = STATS_OK.replace(
  /top\(n: number\): TopEntry\[\] \{[\s\S]*?\n  \}/,
  `top(n: number): TopEntry[] {
    // TODO: implement ranking
    throw new Error('not implemented');
  }`,
);

const BUILD_OUTPUT: Record<string, GeneratedFile[]> = {
  F1: [{ path: 'src/codec.ts', content: CODEC_BUGGY }],
  F2: [{ path: 'src/store.ts', content: STORE_OK }],
  F3: [{ path: 'src/service.ts', content: SERVICE_BUGGY }],
  F4: [{ path: 'src/stats.ts', content: STATS_BUGGY }],
};

const REPAIR_OUTPUT: Record<string, GeneratedFile[]> = {
  F1: [{ path: 'src/codec.ts', content: CODEC_OK }],
  F3: [{ path: 'src/service.ts', content: SERVICE_OK }],
  F4: [{ path: 'src/stats.ts', content: STATS_OK }],
};

const MOCK_TOKENS: Record<LlmCallRecord['op'], [number, number]> = {
  encode: [800, 1500],
  ir: [1200, 900],
  build: [2500, 1400],
  repair: [3000, 800],
  fixStub: [600, 400],
  vanilla: [4000, 2000],
  referee: [1500, 800],
  judge: [900, 60],
  watch: [1200, 220],
  concept: [900, 600],
};

export class MockLlm implements LlmClient {
  // onDebug accepted for makeLlm uniformity; the mock has no real LLM I/O to trace.
  constructor(private onUsage?: UsageSink, _onDebug?: DebugSink) {}

  /** Features that already received an initial (buggy) build — next draw heals. */
  private builtOnce = new Set<string>();

  private meter(op: LlmCallRecord['op'], featureId: string | null) {
    const [inp, out] = MOCK_TOKENS[op];
    this.onUsage?.({
      ts: new Date().toISOString(),
      op,
      featureId,
      model: 'mock-fixture',
      ms: 1,
      inputTokens: inp,
      outputTokens: out,
      cacheReadTokens: 0,
      cacheWriteTokens: 0,
    });
  }

  async enumerateFeatures(_prompt: string, _opts?: DrawOpts): Promise<import('../core/types').FeatureCandidate[]> {
    this.meter('encode', null);
    return [
      { name: 'URL shortening', scope: 'shorten a long URL to a short code and resolve it back' },
      { name: 'TTL expiry', scope: 'codes expire after a configurable time-to-live' },
      { name: 'hit statistics', scope: 'count resolutions per code' },
    ];
  }

  async probeFeature(_candidate: import('../core/types').FeatureCandidate, _opts?: DrawOpts): Promise<import('../core/types').FeatureProbe> {
    this.meter('encode', null);
    // The fixture is internally sufficient — every feature is clear.
    return { clear: true, assertion: 'shorten(url) returns a code; expand(code) returns url', question: '' };
  }

  async fixStub(
    _path: string,
    _ctx: { diagnostics: string; callingLines?: string; contractText?: string; checkedIdioms?: string },
    _opts?: DrawOpts,
  ): Promise<string> {
    // Mock has no broken stubs to regenerate — return a minimal throwing stub.
    return `export function __mockStub(): never { throw new Error('stub: not implemented'); }\n`;
  }

  async encodeFeatures(prompt: string, _stubFeedback?: string, _opts?: DrawOpts): Promise<FeatureSpec> {
    this.meter('encode', null);
    return {
      title: 'URL Shortener Library',
      prompt,
      features: FEATURES.map(f => ({
        ...f,
        deps: [...f.deps],
        files: [...f.files],
        edgeCases: [...f.edgeCases],
        exports: f.exports.map(e => ({ ...e })),
        envVars: [...f.envVars],
        stubs: (STUBS[f.id] ?? []).map(s => ({ ...s })),
      })),
    };
  }

  async repairFeatures(
    spec: FeatureSpec,
    offendingIds: string[],
    _problems: string[],
    _opts?: DrawOpts,
  ): Promise<FeatureNode[]> {
    this.meter('encode', null);
    // Echo the requested features unchanged (fixtures are already valid).
    const want = new Set(offendingIds);
    return spec.features.filter(f => want.has(f.id)).map(f => ({
      ...f,
      deps: [...f.deps],
      files: [...f.files],
      edgeCases: [...f.edgeCases],
      exports: f.exports.map(e => ({ ...e })),
      envVars: [...f.envVars],
      stubs: f.stubs.map(s => ({ ...s })),
    }));
  }

  async generateTests(_spec: FeatureSpec, feature: FeatureNode, _feedback?: string, _opts?: DrawOpts): Promise<{ content: string; parts?: undefined }> {
    this.meter('ir', feature.id);
    const t = TESTS[feature.id];
    if (!t) throw new Error(`mock has no IR fixture for ${feature.id}`);
    // Fixtures are internally consistent, so the mock never reaches the
    // cross-suite gate's per-case repair; it therefore emits no structured parts.
    return { content: t };
  }

  async fixTestCases(
    _spec: FeatureSpec,
    _feature: FeatureNode,
    _brokenCases: { name: string; kind: 'behavior' | 'edge' | 'integration'; errors: string[] }[],
    _importErrors: string[],
    _currentImports: { from: string; symbols: string[] }[],
    _opts?: DrawOpts,
  ): Promise<{ imports?: { from: string; symbols: string[] }[]; cases: { name: string; kind: 'behavior' | 'edge' | 'integration'; body: string }[] }> {
    // Planned case names ≠ fixture it() titles. Return nothing so the runner
    // falls through to generateTests and freezes the whole mock fixture.
    return { cases: [] };
  }

  async generateImplementation(
    _spec: FeatureSpec,
    feature: FeatureNode,
    _frozenTest: string,
    _dependencyContext: GeneratedFile[],
    _opts?: DrawOpts,
  ): Promise<GeneratedFile[]> {
    this.meter('build', feature.id);
    // Fresh-from-contract heal: after the first (intentionally buggy) build,
    // subsequent draws return the repaired fixture — same doctrine as the old
    // repair() path, without ever embedding the broken impl in a prompt.
    if (this.builtOnce.has(feature.id) && REPAIR_OUTPUT[feature.id]) {
      return REPAIR_OUTPUT[feature.id].map(f => ({ ...f }));
    }
    this.builtOnce.add(feature.id);
    const files = BUILD_OUTPUT[feature.id];
    if (!files) throw new Error(`mock has no build fixture for ${feature.id}`);
    return files.map(f => ({ ...f }));
  }

  async repair(input: RepairInput, _opts?: DrawOpts): Promise<GeneratedFile[]> {
    this.meter('repair', input.feature.id);
    const files = REPAIR_OUTPUT[input.feature.id];
    if (!files) throw new Error(`mock has no repair fixture for ${input.feature.id}`);
    return files.map(f => ({ ...f }));
  }

  /** SET 13 — exactly one metered call; returns a tiny self-tested artifact. */
  async vanillaDraw(_prompt: string): Promise<{
    title: string;
    files: GeneratedFile[];
    tests: GeneratedFile[];
  }> {
    this.meter('vanilla', null);
    return {
      title: 'Vanilla Greeter',
      files: [{
        path: 'src/greet.ts',
        content: `export function greet(name: string): string {\n  return 'hello ' + name;\n}\n`,
      }],
      tests: [{
        path: 'tests/greet.test.ts',
        content: `import { describe, it, expect } from 'vitest';
import { greet } from '../src/greet';

describe('greet', () => {
  it('greets', () => {
    expect(greet('a')).toBe('hello a');
  });
});
`,
      }],
    };
  }
}
