import { EventEmitter } from 'node:events';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
// ─── Mock SDKs ──────────────────────────────────────
const { mockGenerateContent, mockCreate, mockChatCreate } = vi.hoisted(() => ({
    mockGenerateContent: vi.fn(),
    mockCreate: vi.fn(),
    mockChatCreate: vi.fn(),
}));
vi.mock('@google/genai', () => ({
    GoogleGenAI: class {
        models = { generateContent: mockGenerateContent };
    },
}));
vi.mock('@anthropic-ai/sdk', () => ({
    default: class {
        messages = { create: mockCreate };
    },
}));
vi.mock('openai', () => ({
    default: class {
        chat = { completions: { create: mockChatCreate } };
    },
}));
vi.mock('../ui.js', () => ({
    log: { info: vi.fn(), success: vi.fn(), warn: vi.fn(), error: vi.fn(), dim: vi.fn() },
}));
function createMockChild() {
    const child = new EventEmitter();
    child.stdout = new EventEmitter();
    child.stderr = new EventEmitter();
    child.kill = vi.fn();
    return child;
}
let mockChild;
vi.mock('node:child_process', () => ({
    spawn: vi.fn(() => mockChild),
    execFile: vi.fn(),
}));
// ─── Import orchestrators ───────────────────────────
import { cleanTmpDir } from '../test-utils.js';
import { invokeAnthropicOrchestrator } from './anthropic-orchestrator.js';
import { invokeGeminiOrchestrator } from './gemini-orchestrator.js';
import { invokeOpenAIOrchestrator } from './openai-orchestrator.js';
import { invokeShellOrchestrator } from './shell-orchestrator.js';
// ─── Shared test opts ───────────────────────────────
const baseOpts = {
    prompt: 'conformance test prompt',
    model: 'test-model',
    cwd: '.',
    tag: 'Conformance',
    totemDir: '.totem',
};
/**
 * The six admission-contract transport fields (mmnto-ai/totem#2102). Providers
 * are pure transport: every vendor payload is built explicitly field-by-field,
 * so supplying these must leave the vendor call byte-identical.
 */
const ADMISSION_TRANSPORT = {
    task: 'conformance-task',
    groundingBundle: { items: [] },
    backendAdmissionClass: 'self_grounding_agent',
    contextPolicy: { budget: 8000 },
    outputContract: { citationsRequired: true, verifyFallback: true },
    runMetadata: { caller: 'conformance' },
};
// ─── Conformance contract assertions ────────────────
function assertOrchestratorResult(result) {
    expect(typeof result.content).toBe('string');
    expect(typeof result.durationMs).toBe('number');
    expect(result.durationMs).toBeGreaterThanOrEqual(0);
    // Token counts are number | null
    if (result.inputTokens !== null) {
        expect(typeof result.inputTokens).toBe('number');
    }
    if (result.outputTokens !== null) {
        expect(typeof result.outputTokens).toBe('number');
    }
}
const sdkFixtures = [
    {
        name: 'gemini',
        envKey: 'GEMINI_API_KEY',
        setupHappy: () => {
            mockGenerateContent.mockResolvedValueOnce({
                text: 'conformance-ok',
                usageMetadata: { promptTokenCount: 10, candidatesTokenCount: 5 },
                candidates: [{ finishReason: 'STOP' }],
            });
        },
        setupQuota: () => {
            mockGenerateContent.mockRejectedValueOnce(Object.assign(new Error('quota exceeded'), { status: 429 }));
        },
        setupGenericError: () => {
            mockGenerateContent.mockRejectedValueOnce(new Error('Model not found'));
        },
        invoke: () => invokeGeminiOrchestrator(baseOpts),
        vendorMock: mockGenerateContent,
        invokeWith: (extra) => invokeGeminiOrchestrator({ ...baseOpts, ...extra }),
    },
    {
        name: 'anthropic',
        envKey: 'ANTHROPIC_API_KEY',
        setupHappy: () => {
            mockCreate.mockResolvedValueOnce({
                content: [{ type: 'text', text: 'conformance-ok' }],
                usage: { input_tokens: 10, output_tokens: 5 },
                stop_reason: 'end_turn',
            });
        },
        setupQuota: () => {
            mockCreate.mockRejectedValueOnce(Object.assign(new Error('rate limit'), { status: 429 }));
        },
        setupGenericError: () => {
            mockCreate.mockRejectedValueOnce(new Error('invalid_api_key'));
        },
        invoke: () => invokeAnthropicOrchestrator(baseOpts),
        vendorMock: mockCreate,
        invokeWith: (extra) => invokeAnthropicOrchestrator({ ...baseOpts, ...extra }),
    },
    {
        name: 'openai',
        envKey: 'OPENAI_API_KEY',
        setupHappy: () => {
            mockChatCreate.mockResolvedValueOnce({
                choices: [{ message: { content: 'conformance-ok' }, finish_reason: 'stop' }],
                usage: { prompt_tokens: 10, completion_tokens: 5 },
            });
        },
        setupQuota: () => {
            mockChatCreate.mockRejectedValueOnce(Object.assign(new Error('rate limit'), { status: 429 }));
        },
        setupGenericError: () => {
            mockChatCreate.mockRejectedValueOnce(new Error('invalid_api_key'));
        },
        invoke: () => invokeOpenAIOrchestrator(baseOpts),
        vendorMock: mockChatCreate,
        invokeWith: (extra) => invokeOpenAIOrchestrator({ ...baseOpts, ...extra }),
    },
];
describe.each(sdkFixtures)('$name provider conformance', (fixture) => {
    beforeEach(() => {
        vi.clearAllMocks();
        process.env[fixture.envKey] = 'test-key';
    });
    afterEach(() => {
        delete process.env[fixture.envKey];
    });
    it('returns a valid OrchestratorResult on happy path', async () => {
        fixture.setupHappy();
        const result = await fixture.invoke();
        assertOrchestratorResult(result);
        expect(result.content).toBe('conformance-ok');
        expect(result.inputTokens).toBe(10);
        expect(result.outputTokens).toBe(5);
    });
    it('throws descriptive error when API key is missing', async () => {
        delete process.env[fixture.envKey];
        await expect(fixture.invoke()).rejects.toThrow(/\[Totem Error\].*No .+ API key found/);
    });
    it('converts quota/429 errors to QuotaError', async () => {
        fixture.setupQuota();
        await expect(fixture.invoke()).rejects.toHaveProperty('name', 'QuotaError');
    });
    it('wraps generic API errors with [Totem Error] prefix', async () => {
        fixture.setupGenericError();
        await expect(fixture.invoke()).rejects.toThrow('[Totem Error]');
    });
    it('includes durationMs >= 0 in result', async () => {
        fixture.setupHappy();
        const result = await fixture.invoke();
        expect(result.durationMs).toBeGreaterThanOrEqual(0);
    });
    // ─── Admission-contract transport (mmnto-ai/totem#2102) ──
    it('vendor payload is byte-identical with and without the admission transport fields', async () => {
        fixture.setupHappy();
        await fixture.invokeWith({});
        fixture.setupHappy();
        await fixture.invokeWith(ADMISSION_TRANSPORT);
        const [bare] = fixture.vendorMock.mock.calls[0];
        const [withTransport] = fixture.vendorMock.mock.calls[1];
        expect(withTransport).toEqual(bare);
        // None of the Totem-specific transport keys leak into the vendor payload.
        for (const key of Object.keys(ADMISSION_TRANSPORT)) {
            expect(withTransport).not.toHaveProperty(key);
        }
    });
});
// ─── Shell Provider Conformance ─────────────────────
describe('shell provider conformance', () => {
    let tmpDir;
    beforeEach(() => {
        tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'totem-conf-'));
        vi.clearAllMocks();
        vi.spyOn(console, 'error').mockImplementation(() => { });
        mockChild = createMockChild();
    });
    afterEach(() => {
        cleanTmpDir(tmpDir);
        vi.restoreAllMocks();
    });
    const shellOpts = () => ({
        prompt: 'conformance test prompt',
        command: 'echo {file}',
        model: 'test-model',
        cwd: tmpDir,
        tag: 'Conformance',
        totemDir: '.totem',
    });
    function emitSuccess(data) {
        process.nextTick(() => {
            mockChild.stdout.emit('data', Buffer.from(data));
            mockChild.emit('close', 0);
        });
    }
    function emitFailure(code, stderr = '') {
        process.nextTick(() => {
            if (stderr)
                mockChild.stderr.emit('data', Buffer.from(stderr));
            mockChild.emit('close', code);
        });
    }
    it('returns a valid OrchestratorResult on happy path', async () => {
        emitSuccess('conformance-ok');
        const result = await invokeShellOrchestrator(shellOpts());
        assertOrchestratorResult(result);
        expect(result.content).toBe('conformance-ok');
        // Shell orchestrator returns null tokens for non-Gemini output
        expect(result.inputTokens).toBeNull();
        expect(result.outputTokens).toBeNull();
    });
    it('returns structured tokens when output is Gemini JSON', async () => {
        const geminiOutput = JSON.stringify({
            response: 'conformance-ok',
            stats: {
                models: {
                    'test-model': {
                        tokens: { input: 10, candidates: 5 },
                        api: { totalLatencyMs: 1000 },
                    },
                },
            },
        });
        emitSuccess(geminiOutput);
        const result = await invokeShellOrchestrator(shellOpts());
        assertOrchestratorResult(result);
        expect(result.content).toBe('conformance-ok');
        expect(result.inputTokens).toBe(10);
        expect(result.outputTokens).toBe(5);
    });
    it('converts quota errors to QuotaError', async () => {
        emitFailure(1, '429 Too Many Requests quota exceeded');
        await expect(invokeShellOrchestrator(shellOpts())).rejects.toHaveProperty('name', 'QuotaError');
    });
    it('wraps generic errors with [Totem Error] prefix', async () => {
        emitFailure(1, 'something went wrong');
        await expect(invokeShellOrchestrator(shellOpts())).rejects.toThrow('[Totem Error]');
    });
    it('includes durationMs >= 0 in result', async () => {
        emitSuccess('ok');
        const result = await invokeShellOrchestrator(shellOpts());
        expect(result.durationMs).toBeGreaterThanOrEqual(0);
    });
    // ─── Admission-contract transport (mmnto-ai/totem#2102) ──
    it('ignores the admission transport fields — no leak anywhere in the spawn payload', async () => {
        const { spawn } = await import('node:child_process');
        emitSuccess('conformance-ok');
        const result = await invokeShellOrchestrator({ ...shellOpts(), ...ADMISSION_TRANSPORT });
        expect(result.content).toBe('conformance-ok');
        // #2148 round-1: assert over the WHOLE spawn payload, not just the
        // command string — normalized over node's two spawn overloads,
        // (cmd, options) and (cmd, argv, options), so a transport value smuggled
        // via argv, env, cwd, or any other option surfaces here too.
        const spawnCall = vi.mocked(spawn).mock.calls[0];
        const command = spawnCall[0];
        const argv = Array.isArray(spawnCall[1]) ? spawnCall[1] : [];
        const options = (Array.isArray(spawnCall[1]) ? spawnCall[2] : spawnCall[1]) ?? {};
        const wholePayload = [command, JSON.stringify(argv), JSON.stringify(options)].join('\n');
        // The legit command content still crosses the wire…
        expect(command).toContain('echo');
        expect(wholePayload).toContain('echo');
        // …but none of the admission transport values appear ANYWHERE in it.
        for (const value of ['self_grounding_agent', 'conformance-task', '8000', 'citationsRequired']) {
            expect(wholePayload).not.toContain(value);
        }
    });
});
//# sourceMappingURL=conformance.test.js.map