/**
 * SET 47 — GATE_UNAVAILABLE fail-open, suite scaffold wire, ownership-first
 * import resolver, export-conformance dedupe, project live epochs, build tones.
 */
import { afterEach, describe, expect, it } from 'vitest';
import type { FeatureSpec, GeneratedFile } from '../src/core/types';
import {
  GATE_UNAVAILABLE,
  probeErrOrNull,
  setModuleEvalProbe,
  vetArtifactWrite,
  vetArtifactWriteDetailed,
  vetArtifactWrites,
  vetArtifactWritesAsync,
} from '../src/tier1/testExec';
import {
  attemptMechanicalFix,
  classifyFailure,
  refusesImplRedrawForOwnedSymbol,
} from '../src/tier1/substrate';
import {
  isScaffoldOwnedSymbol,
  scaffoldOwnershipMap,
  wireSuiteScaffoldImports,
} from '../src/tier1/suiteScaffoldWire';
import { conformExportsToStubs } from '../src/tier1/exportConformance';
import { aggregateProjectStats } from '../src/tier1/statsAggregate';
import { eventTone } from '../src/tier1/eventTone';

function specWithSlugify(): FeatureSpec {
  return {
    title: 't',
    prompt: 'p',
    features: [
      {
        id: 'F1',
        name: 'util',
        description: 'slugify',
        parentId: null,
        deps: [],
        files: ['src/util.ts'],
        testFile: 'tests/util.test.ts',
        edgeCases: [],
        exports: [{ file: 'src/util.ts', symbol: 'slugify' }],
        envVars: [],
        stubs: [{
          path: 'src/util.ts',
          content: "export function slugify(s: string): string { throw new Error('stub'); }\n",
        }],
      },
      {
        id: 'F2',
        name: 'article',
        description: 'articles',
        parentId: null,
        deps: ['F1'],
        files: ['src/article.ts'],
        testFile: 'tests/article.test.ts',
        edgeCases: [],
        exports: [{ file: 'src/article.ts', symbol: 'createArticle' }],
        envVars: [],
        stubs: [{
          path: 'src/article.ts',
          content: "export function createArticle(t: string): { slug: string } { throw new Error('stub'); }\n",
        }],
      },
    ],
  };
}

afterEach(() => {
  setModuleEvalProbe(null);
});

describe('SET 47a GATE_UNAVAILABLE fail-open on module probe vet', () => {
  it('probe throwing unavailable → draw accepted via remaining checks', () => {
    const Orig = globalThis.Function;
    // @ts-expect-error — CSP simulation
    globalThis.Function = function () {
      throw new EvalError('Refused because unsafe-eval');
    };
    try {
      const cand: GeneratedFile = {
        path: 'src/model.ts',
        content: 'export function model(): number { return 1; }\n',
      };
      const detail = vetArtifactWriteDetailed(cand, [cand], 'full');
      expect(detail.reason).toBeNull();
      expect(detail.gateUnavailable).toBe(true);
      expect(vetArtifactWrite(cand, [cand], 'full')).toBeNull();
      const batch = vetArtifactWrites([cand], [], 'full');
      expect(batch.ok).toHaveLength(1);
      expect(batch.rejected).toHaveLength(0);
      expect(batch.gateUnavailable).toBe(true);
    } finally {
      globalThis.Function = Orig;
    }
  });

  it('async sandbox probe unavailable → fail open; real crash still rejects', async () => {
    setModuleEvalProbe(async () => GATE_UNAVAILABLE);
    const cand: GeneratedFile = {
      path: 'src/ok.ts',
      content: 'export const ok = 1;\n',
    };
    const open = await vetArtifactWritesAsync([cand], [], 'full');
    expect(open.ok).toHaveLength(1);
    expect(open.gateUnavailable).toBe(true);

    setModuleEvalProbe(async () => 'boom in module');
    const bad: GeneratedFile = {
      path: 'src/bad.ts',
      content: 'export const bad = 1;\n',
    };
    const closed = await vetArtifactWritesAsync([bad], [], 'full');
    expect(closed.rejected.some(r => r.reason.includes('module probe: boom'))).toBe(true);
  });

  it('probeErrOrNull treats GATE_UNAVAILABLE as skip', () => {
    expect(probeErrOrNull(GATE_UNAVAILABLE)).toBeNull();
    expect(probeErrOrNull('real crash')).toBe('real crash');
    expect(probeErrOrNull(null)).toBeNull();
  });
});

describe('SET 47b suite scaffold wire at freeze', () => {
  it('suite referencing owned symbol without import → freeze injects it', () => {
    const spec = specWithSlugify();
    const suite = [
      "import { describe, it, expect } from 'vitest';",
      "describe('a', () => {",
      "  it('x', () => { expect(slugify('Hi')).toBe('hi'); });",
      '});',
    ].join('\n');
    const wired = wireSuiteScaffoldImports(suite, 'tests/article.test.ts', spec);
    expect(wired.injected.map(i => i.symbol)).toContain('slugify');
    expect(wired.content).toMatch(/import\s*\{\s*slugify\s*\}\s*from\s*'[^']+'/);
    expect(isScaffoldOwnedSymbol(spec, 'slugify')).toBe(true);
    expect(scaffoldOwnershipMap(spec).get('slugify')).toBe('src/util.ts');
  });

  it('undefined-symbol|<owned> refuses impl redraws', () => {
    const spec = specWithSlugify();
    expect(refusesImplRedrawForOwnedSymbol('undefined-symbol|slugify', spec)).toBe(true);
    expect(refusesImplRedrawForOwnedSymbol('undefined-symbol|nope', spec)).toBe(false);
    expect(refusesImplRedrawForOwnedSymbol('assertion|eq', spec)).toBe(false);
  });
});

describe('SET 47c ownership-first import resolver', () => {
  it('suite free ident + unique stub owner → mechanical import (not "no unique import site")', () => {
    const spec = specWithSlugify();
    const artifact: GeneratedFile[] = [
      {
        path: 'src/util.ts',
        content: 'export function slugify(s: string): string { return s.toLowerCase(); }\n',
      },
      {
        path: 'src/article.ts',
        content: 'export function createArticle(t: string) { return { slug: t }; }\n',
      },
    ];
    // Mentions of slugify in multiple files used to poison uniqueness — ownership wins.
    const suite: GeneratedFile = {
      path: 'tests/article.test.ts',
      content: [
        "import { describe, it, expect } from 'vitest';",
        "import { createArticle } from '../src/article';",
        "// docs mention slugify for humans",
        "describe('a', () => {",
        "  it('x', () => { expect(slugify('A')).toBe('a'); });",
        '});',
      ].join('\n'),
    };
    const failure = classifyFailure('x', 'slugify is not defined');
    expect(failure.kind).toBe('undefined-symbol');
    const attempt = attemptMechanicalFix(failure, artifact, suite, spec);
    expect(attempt.status).toBe('fixed');
    expect(attempt.fix?.path).toBe('tests/article.test.ts');
    expect(attempt.fix?.content).toMatch(/import\s*\{\s*slugify\s*\}/);
    expect(attempt.reason ?? '').not.toMatch(/no unique import site/);
  });
});

describe('SET 47e export-conformance event dedupe', () => {
  it('conform reports one note per symbol even across multi-pass', () => {
    const stubs: GeneratedFile[] = [{
      path: 'src/log.ts',
      content: [
        'export function logRequest(...a: unknown[]): void { throw new Error("stub"); }',
        'export function recordMetric(...a: unknown[]): void { throw new Error("stub"); }',
      ].join('\n'),
    }];
    const impl: GeneratedFile[] = [{
      path: 'src/log.ts',
      content: 'export {};\n',
    }];
    const { fixes } = conformExportsToStubs(impl, stubs);
    const notes = fixes.map(f => f.note);
    expect(notes.filter(n => n === 'added missing export logRequest')).toHaveLength(1);
    expect(notes.filter(n => n === 'added missing export recordMetric')).toHaveLength(1);
  });
});

describe('SET 47g project live epoch counting', () => {
  it('live in-flight epoch counts (never silent zeros)', () => {
    const summary = aggregateProjectStats([
      {
        id: 'abc123',
        ts: '2026-07-26T12:00:00.000Z',
        projectId: 'abc123',
        live: true,
        metrics: { tokensIn: 10, tokensOut: 5 },
      },
    ], 'abc123');
    expect(summary.totalsRuns).toBeGreaterThanOrEqual(1);
    expect(summary.liveEpochInProgress).toBe(true);
  });

  it('completed root run shows 1 epoch', () => {
    const summary = aggregateProjectStats([
      {
        id: 'root1',
        ts: '2026-07-26T12:00:00.000Z',
        projectId: 'root1',
        problems: 0,
        result: {},
        metrics: { tokensIn: 100, tokensOut: 40 },
      },
    ], 'root1');
    expect(summary.totalsRuns).toBe(1);
    expect(summary.liveEpochInProgress).toBeFalsy();
  });
});

describe('SET 42h build-phase tone pins', () => {
  it('llm call build/concept → yellow; superbuffer build → yellow', () => {
    expect(eventTone({ message: 'llm call — build: src/a.ts (10→ / 5← tok)', phase: 'build' })).toBe('yellow');
    expect(eventTone({ message: 'llm call — concept: F1 ideation', phase: 'build' })).toBe('yellow');
    expect(eventTone({
      message: 'superbuffer: assembled (1 stubs, 0 frozen, 0 templates, 0 lessons, 0 siblings; pages [spec:10]) stage=build for "x"',
      phase: 'build',
    })).toBe('yellow');
  });

  it('built N/N green; built 0/N and partial → friction', () => {
    expect(eventTone({ message: 'built 3/3 owned files', phase: 'build' })).toBe('green');
    expect(eventTone({ message: 'built 0/3 owned files', phase: 'build' })).toBe('friction');
    expect(eventTone({ message: 'built 1/3 owned files', phase: 'build' })).toBe('friction');
  });

  it('build-phase notify → dim', () => {
    expect(eventTone({ message: 'notify: Building implementations', phase: 'build' })).toBe('dim');
  });
});
