/**
 * SET 15 / 44 — capability scanner calibration + plan-gate partial exclusion.
 */
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { describe, expect, it } from 'vitest';
import type { FeatureSpec, LlmClient } from '../src/core/types';
import { formatPermanentDeferral } from '../src/tier1/reportEvidence';
import { runTier1Job } from '../src/tier1/runner';
import {
  findPromptLineNumber,
  scanCapabilities,
} from '../src/tier1/substrate';
import { makeEvalSuiteRunner } from '../src/tier1/testExec';

/**
 * WikiClone-style fixture. Numbered items only are scanned; prose is noise.
 * #7 / #11 are mixed (assertable HTML + capability clause) → partial.
 * Previously-correct full deferrals keep firing on their item numbers.
 */
const WIKI_CLONE = [
  'Build a minimal wiki clone as a pure TypeScript library.',
  'This preamble mentions Redis SMTP CREATE TABLE axe-core viewport — prose must not defer.',
  '2. Viewport must be 1024px wide with a focus ring.',
  '5. Use axe-core for a11y audits on every page render.',
  '7. Sidebar renders `<nav class="toc">` links and collapses when viewport ≤ 768px.',
  '11. Head includes `<meta name="viewport" content="width=device-width">` and asserts computed width ≥ 1024px.',
  '12. Head includes `<meta name="viewport" content="width=device-width">` only.',
  '27. CREATE TABLE pages (id BIGSERIAL PRIMARY KEY, body tsvector);',
  '35. Redis sliding window rate limit on edits.',
  '50. SMTP email on publish via nodemailer.',
  '84. Persist sessions with CREATE TABLE sessions (id BIGSERIAL).',
  '85. Cache page HTML in redis.',
  '86. Serve favicon with Content-Type: image/png.',
  '87. Open a WebSocket for live presence.',
  '88. Listen with http.createServer on TCP.',
  '89. Fetch remote MediaWiki over axios.',
  '90. Write dumps with fs.promises.writeFile.',
  '91. ReadFileSync the on-disk corpus at process.cwd.',
  '99. Sliding window throttle backed by ioredis.',
  '116. ALTER TABLE pages ADD COLUMN search tsvector.',
  '119. Playwright screenshot of the rendered wiki.',
  '125. querySelector live DOM focus ring checks.',
  '126. getComputedStyle on the title element.',
  '139. document.cookie session bridge in the browser.',
  'Closing paragraph: also mentions postgres redis SMTP viewport — still prose.',
].join('\n');

function pureLibSpec(edgeCases: string[] = ['zero']): FeatureSpec {
  return {
    title: 'Wiki',
    prompt: 'wiki',
    features: [{
      id: 'F1', name: 'wiki', description: 'wiki clone', parentId: null,
      deps: [], files: ['src/wiki.ts'], testFile: 'tests/f1.test.ts',
      edgeCases, envVars: [],
      exports: [{ file: 'src/wiki.ts', symbol: 'render' }],
      stubs: [{
        path: 'src/wiki.ts',
        content: 'export function render(s: string): string { throw new Error("stub"); }\n',
      }],
    }],
  } as unknown as FeatureSpec;
}

function mockClient(edgeCases?: string[]): LlmClient {
  return {
    async enumerateFeatures() { return []; },
    async probeFeature() { return { clear: true, assertion: 'works', question: '' }; },
    async encodeFeatures() { return pureLibSpec(edgeCases); },
    async repairFeatures() { return []; },
    async fixStub() { return ''; },
    async generateTests() { return { content: '', parts: { imports: [], cases: [] } }; },
    async fixTestCases(_s, _f, broken) {
      return {
        cases: broken.map(b => ({
          name: b.name,
          kind: b.kind as 'behavior' | 'edge' | 'integration',
          body: 'expect(render("x")).toBe("x");',
        })),
      };
    },
    async generateImplementation() {
      return [{ path: 'src/wiki.ts', content: 'export function render(s: string): string { return s; }\n' }];
    },
    async repair() { return []; },
  };
}

describe('SET 15 capability scanner', () => {
  it('skips prose; #7 and #11 are partial; bare meta viewport is no hit', () => {
    const hits = scanCapabilities(WIKI_CLONE);
    const byLine = new Map(hits.map(h => [h.line, h]));

    expect(hits.some(h => /preamble|Closing paragraph/i.test(h.item))).toBe(false);

    expect(byLine.get(5)?.partial).toBe(true); // fixture line of item #7
    expect(byLine.get(5)?.capability).toBe('computed-style');
    expect(byLine.get(5)?.clause).toMatch(/viewport/i);

    expect(byLine.get(6)?.partial).toBe(true); // item #11
    expect(byLine.get(6)?.clause).toMatch(/computed|1024px|≥/i);

    // Bare meta viewport in backticks — no capability hit.
    expect(hits.some(h => h.item.includes('only') && h.item.includes('meta'))).toBe(false);

    // SET 69 — infra-noun items with a surviving behavioral core now split
    // (#35 rate limit, #85 cache, #99 throttle); SQL-bound and browser-truth
    // items remain honest whole-line exclusions.
    for (const n of [2, 5, 27, 50, 84, 86, 87, 88, 89, 90, 91, 116, 119, 125, 126, 139]) {
      const hit = hits.find(h => new RegExp(`^\\s*${n}[.)]\\s`).test(h.item));
      expect(hit, `expected full deferral for #${n}`).toBeTruthy();
      expect(hit!.partial, `#${n} should be full deferral`).toBeFalsy();
    }

    // #35 "Redis sliding window rate limit" — the canonical split: a
    // deterministic counter + clock. Binding excised, injected-clock clade.
    const rl = hits.find(h => /^\s*35[.)]\s/.test(h.item))!;
    expect(rl.partial).toBe(true);
    expect(rl.behavioralSplit?.simClade).toBe('sim-clock');
    expect(rl.behavioralSplit?.behavior).toMatch(/rate limit/i);
    expect(rl.behavioralSplit?.behavior).not.toMatch(/redis/i);
    // #85 caching is protocol-independent → in-memory store clade.
    const cache = hits.find(h => /^\s*85[.)]\s/.test(h.item))!;
    expect(cache.behavioralSplit?.simClade).toBe('sim-store');
    // #99 sliding-window throttle — same class as #35.
    const throttle = hits.find(h => /^\s*99[.)]\s/.test(h.item))!;
    expect(throttle.behavioralSplit?.simClade).toBe('sim-clock');
    expect(throttle.behavioralSplit?.behavior).not.toMatch(/ioredis/i);
  });

  it('plan-gate keeps partial items and emits excluded honestly; full still drops', async () => {
    const edges = [
      'Sidebar renders `<nav class="toc">` links and collapses when viewport ≤ 768px.',
      'CREATE TABLE pages (id BIGSERIAL PRIMARY KEY, body tsvector);',
      'zero',
    ];
    const events: string[] = [];
    const res = await runTier1Job({
      prompt: WIKI_CLONE,
      rawPrompt: WIKI_CLONE,
      llm: mockClient(edges),
      runSuite: makeEvalSuiteRunner(),
      onEvent: e => events.push(e.message),
    });
    expect(events.some(m => m.includes('partially excluded:'))).toBe(false);
    expect(events.some(m => m.includes('excluded:') && /viewport/i.test(m))).toBe(true);
    expect(events.some(m => m.includes('excluded:') && m.includes('database'))).toBe(true);
    // Partial case kept in provenance; CREATE TABLE dropped.
    expect(res.provenance.some(p => /nav class="toc"/i.test(p.license))).toBe(true);
    expect(res.provenance.some(p => /CREATE TABLE/i.test(p.license))).toBe(false);
    // Original prompt line carried (not lying with line 1).
    const partialEvt = events.find(m => m.includes('excluded:') && /viewport/i.test(m));
    expect(partialEvt).toMatch(/\(line \d+\)/);
    expect(partialEvt).not.toMatch(/\(line 1\)/);
  });

  it('SET 44 — user-facing capability copy has no deferred/tier-scope framing', async () => {
    const edges = [
      'Sidebar renders `<nav class="toc">` links and collapses when viewport ≤ 768px.',
      'CREATE TABLE pages (id BIGSERIAL PRIMARY KEY, body tsvector);',
      'zero',
    ];
    const events: string[] = [];
    const res = await runTier1Job({
      prompt: WIKI_CLONE,
      rawPrompt: WIKI_CLONE,
      llm: mockClient(edges),
      runSuite: makeEvalSuiteRunner(),
      onEvent: e => events.push(e.message),
    });
    const surface = [...events, ...res.problems].join('\n');
    expect(surface).not.toMatch(/out of Tier 1 scope/);
    expect(surface).not.toMatch(/deferred \(permanent until/);
    expect(surface).not.toMatch(/partially deferred:/);
    expect(formatPermanentDeferral('database')).toBe(
      'excluded — requires database; not testable in this runtime',
    );
    expect(formatPermanentDeferral('database')).not.toMatch(/deferred|tier exists/i);

    // Grep-level: scanner emit / report helpers must not retain the old phrases.
    const owned = [
      'src/tier1/reportEvidence.ts',
      'src/tier1/runner.ts',
      'src/tier1/exportRepo.ts',
    ].map(p => readFileSync(resolve(process.cwd(), p), 'utf8')).join('\n');
    expect(owned).not.toMatch(/out of Tier 1 scope/);
    expect(owned).not.toMatch(/deferred \(permanent until/);
    expect(owned).not.toMatch(/partially deferred:/);
  });

  it('findPromptLineNumber omits when license is absent (never invents 1)', () => {
    expect(findPromptLineNumber(WIKI_CLONE, 'CREATE TABLE pages (id BIGSERIAL PRIMARY KEY, body tsvector);'))
      .toBeGreaterThan(1);
    expect(findPromptLineNumber(WIKI_CLONE, 'totally absent license text xyzzy')).toBeUndefined();
  });
});
