import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import Database from 'better-sqlite3';
import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import { runMigrations } from '../../db/migrations.js';
import { JobRunner } from '../job-runner.js';
function createMockBridge() {
    let sendHandler = null;
    const activeKeys = new Set();
    let failSummaries = false;
    return {
        send: vi.fn((opts) => {
            const key = opts.squadName ?? opts.cwd;
            if (activeKeys.has(key)) {
                opts.onError({ code: null, message: 'Pipeline is running' });
                return;
            }
            activeKeys.add(key);
            // Auto-handle L1 summary calls
            if (key.startsWith('summary:')) {
                if (failSummaries) {
                    opts.onError({ code: 1, message: 'Summary generation failed' });
                }
                else {
                    opts.onDone('• Resultado principal da execução\n• Métricas e dados gerados\n• Próximos passos identificados');
                }
                activeKeys.delete(key);
                return;
            }
            if (sendHandler)
                sendHandler(opts);
        }),
        isActive: vi.fn((key) => activeKeys.has(key)),
        kill: vi.fn((key) => { activeKeys.delete(key); }),
        _setSendHandler: (handler) => { sendHandler = handler; },
        _setFailSummaries: (fail) => { failSummaries = fail; },
        _activeKeys: activeKeys,
    };
}
describe('JobRunner', () => {
    let db;
    let mockBridge;
    let runner;
    let broadcast;
    beforeEach(() => {
        db = new Database(':memory:');
        runMigrations(db);
        mockBridge = createMockBridge();
        broadcast = vi.fn();
        runner = new JobRunner({ db, bridge: mockBridge, broadcast, squadsDir: '/tmp/squads', broadcastToAll: vi.fn() });
    });
    afterEach(() => {
        db.close();
        vi.restoreAllMocks();
    });
    it('should execute a job and log activity', async () => {
        mockBridge._setSendHandler((opts) => {
            opts.onChunk('Report generated');
            opts.onDone('Report generated');
            mockBridge._activeKeys.delete(opts.squadName ?? opts.cwd);
        });
        const result = await runner.run({
            squadName: 'test-squad',
            prompt: 'Generate report',
            timeoutMinutes: 5,
            retryOnFailure: 0,
            attempt: 1,
        });
        expect(result).toBe('completed');
        const activity = db.prepare("SELECT * FROM squad_activities WHERE squad_code = 'test-squad' AND triggered_by = 'schedule'").get();
        expect(activity).toBeDefined();
        expect(activity.status).toBe('completed');
        expect(activity.attempt).toBe(1);
    });
    it('should broadcast schedule events', async () => {
        mockBridge._setSendHandler((opts) => {
            opts.onChunk('chunk');
            opts.onDone('chunk');
            mockBridge._activeKeys.delete(opts.squadName ?? opts.cwd);
        });
        await runner.run({
            squadName: 'test-squad',
            prompt: 'Generate report',
            timeoutMinutes: 5,
            retryOnFailure: 0,
            attempt: 1,
        });
        expect(broadcast).toHaveBeenCalledWith('test-squad', expect.objectContaining({ type: 'schedule:started' }));
        expect(broadcast).toHaveBeenCalledWith('test-squad', expect.objectContaining({ type: 'schedule:chunk' }));
        expect(broadcast).toHaveBeenCalledWith('test-squad', expect.objectContaining({ type: 'schedule:done' }));
    });
    it('should log failed activity on error', async () => {
        mockBridge._setSendHandler((opts) => {
            opts.onError({ code: 1, message: 'Error occurred' });
            mockBridge._activeKeys.delete(opts.squadName ?? opts.cwd);
        });
        const result = await runner.run({
            squadName: 'test-squad',
            prompt: 'Fail me',
            timeoutMinutes: 5,
            retryOnFailure: 0,
            attempt: 1,
        });
        expect(result).toBe('failed');
        const activity = db.prepare("SELECT * FROM squad_activities WHERE squad_code = 'test-squad'").get();
        expect(activity.status).toBe('failed');
    });
    it('should queue overlapping runs for same squad', async () => {
        mockBridge._setSendHandler(() => { });
        runner.run({
            squadName: 'test-squad',
            prompt: 'First run',
            timeoutMinutes: 5,
            retryOnFailure: 0,
            attempt: 1,
        });
        const result = await runner.run({
            squadName: 'test-squad',
            prompt: 'Second run',
            timeoutMinutes: 5,
            retryOnFailure: 0,
            attempt: 1,
        });
        expect(result).toBe('queued');
    });
    it('should create pipeline tasks on run and mark done on completion', async () => {
        const fs = await import('node:fs');
        const path = await import('node:path');
        const os = await import('node:os');
        const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pipeline-test-'));
        const squadDir = path.join(tmpDir, 'pipeline-squad');
        fs.mkdirSync(squadDir, { recursive: true });
        fs.writeFileSync(path.join(squadDir, 'squad.yaml'), `squad:
  code: pipeline-squad
  name: Pipeline Squad
  description: Test
  icon: star
  agents:
    - id: agent1
      name: Agent One
      icon: robot
      prompt: agents/agent1.md
    - id: agent2
      name: Agent Two
      icon: robot
      prompt: agents/agent2.md
  pipeline:
    steps:
      - id: step-01
        agent: agent1
        label: Research
      - id: step-02
        agent: agent2
        label: Write report`);
        const pipelineRunner = new JobRunner({
            db, bridge: mockBridge, broadcast, squadsDir: tmpDir,
            broadcastToAll: vi.fn(),
        });
        mockBridge._setSendHandler((opts) => {
            opts.onDone('All done');
            mockBridge._activeKeys.delete(opts.squadName ?? opts.cwd);
        });
        await pipelineRunner.run({
            squadName: 'pipeline-squad',
            prompt: 'Run pipeline',
            timeoutMinutes: 5,
            retryOnFailure: 0,
            attempt: 1,
        });
        const tasks = db.prepare("SELECT * FROM kanban_tasks WHERE squad_code = 'pipeline-squad' ORDER BY position").all();
        expect(tasks).toHaveLength(2);
        expect(tasks[0].title).toBe('Research');
        expect(tasks[0].agent_name).toBe('Agent One');
        expect(tasks[0].step_id).toBe('step-01');
        expect(tasks[0].status).toBe('done');
        expect(tasks[0].triggered_by).toBe('schedule');
        expect(tasks[1].title).toBe('Write report');
        expect(tasks[1].status).toBe('done');
        fs.rmSync(tmpDir, { recursive: true, force: true });
    });
    it('should mark pipeline tasks as failed on error', async () => {
        const fs = await import('node:fs');
        const path = await import('node:path');
        const os = await import('node:os');
        const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pipeline-fail-'));
        const squadDir = path.join(tmpDir, 'fail-squad');
        fs.mkdirSync(squadDir, { recursive: true });
        fs.writeFileSync(path.join(squadDir, 'squad.yaml'), `squad:
  code: fail-squad
  name: Fail Squad
  description: Test
  icon: star
  agents:
    - id: agent1
      name: Agent One
      icon: robot
      prompt: agents/agent1.md
  pipeline:
    steps:
      - id: step-01
        agent: agent1
        label: Will fail`);
        const pipelineRunner = new JobRunner({
            db, bridge: mockBridge, broadcast, squadsDir: tmpDir,
            broadcastToAll: vi.fn(),
        });
        mockBridge._setSendHandler((opts) => {
            opts.onError({ code: 1, message: 'Pipeline crashed' });
            mockBridge._activeKeys.delete(opts.squadName ?? opts.cwd);
        });
        await pipelineRunner.run({
            squadName: 'fail-squad',
            prompt: 'Run',
            timeoutMinutes: 5,
            retryOnFailure: 0,
            attempt: 1,
        });
        const tasks = db.prepare("SELECT * FROM kanban_tasks WHERE squad_code = 'fail-squad'").all();
        expect(tasks).toHaveLength(1);
        expect(tasks[0].status).toBe('failed');
        expect(tasks[0].result).toBe('Pipeline crashed');
        fs.rmSync(tmpDir, { recursive: true, force: true });
    });
    describe('chain auto-trigger', () => {
        let tmpSquadsDir;
        beforeEach(() => {
            tmpSquadsDir = fs.mkdtempSync(path.join(os.tmpdir(), 'chain-test-'));
        });
        afterEach(() => {
            fs.rmSync(tmpSquadsDir, { recursive: true, force: true });
        });
        function createSquadYaml(name, chain) {
            const squadDir = path.join(tmpSquadsDir, name);
            fs.mkdirSync(squadDir, { recursive: true });
            let yaml = `squad:
  code: ${name}
  name: ${name}
  description: Test squad
  icon: star
  agents:
    - id: agent1
      name: Agent One
      icon: robot
      prompt: agents/agent1.md
  pipeline:
    steps:
      - id: step-01
        agent: agent1
        label: Do work`;
            if (chain) {
                if (typeof chain === 'string') {
                    yaml += `\n  chain: ${chain}`;
                }
                else if (Array.isArray(chain)) {
                    yaml += `\n  chain:`;
                    for (const c of chain) {
                        if (typeof c === 'string') {
                            yaml += `\n    - ${c}`;
                        }
                        else {
                            yaml += `\n    - target_squad: ${c.target_squad}`;
                            if (c.trigger)
                                yaml += `\n      trigger: ${c.trigger}`;
                            if (c.description)
                                yaml += `\n      description: "${c.description}"`;
                        }
                    }
                }
            }
            fs.writeFileSync(path.join(squadDir, 'squad.yaml'), yaml);
        }
        it('should auto-trigger chained squad and save handoff file', async () => {
            createSquadYaml('source-squad', 'target-squad');
            createSquadYaml('target-squad');
            const chainRunner = new JobRunner({
                db, bridge: mockBridge, broadcast, squadsDir: tmpSquadsDir, broadcastToAll: vi.fn(),
            });
            const calls = [];
            mockBridge._setSendHandler((opts) => {
                const name = opts.squadName?.replace('schedule:', '') ?? '';
                calls.push(name);
                opts.onDone('Full output from ' + name);
                mockBridge._activeKeys.delete(opts.squadName ?? opts.cwd);
            });
            await chainRunner.run({
                squadName: 'source-squad',
                prompt: 'Run source',
                timeoutMinutes: 5,
                retryOnFailure: 0,
                attempt: 1,
            });
            // Source completes, then chain triggers target
            expect(calls).toContain('source-squad');
            expect(calls).toContain('target-squad');
            // Handoff file was created with full output
            const handoffFile = path.join(tmpSquadsDir, 'source-squad', '_output', 'handoff-to-target-squad.md');
            expect(fs.existsSync(handoffFile)).toBe(true);
            const content = fs.readFileSync(handoffFile, 'utf-8');
            expect(content).toContain('Full output from source-squad');
            expect(content).toContain('source-squad');
            expect(content).toContain('target-squad');
            // Broadcast includes handoff file path
            expect(broadcast).toHaveBeenCalledWith('source-squad', expect.objectContaining({
                type: 'chain:triggered',
                sourceSquad: 'source-squad',
                targetSquad: 'target-squad',
                handoffFile,
            }));
            // Target squad prompt references the handoff file
            const targetCall = mockBridge.send.mock.calls.find((c) => c[0].squadName === 'schedule:target-squad');
            expect(targetCall).toBeDefined();
            expect(targetCall[0].prompt).toContain(handoffFile);
        });
        it('should not trigger chain when no chain config exists', async () => {
            createSquadYaml('solo-squad');
            const chainRunner = new JobRunner({
                db, bridge: mockBridge, broadcast, squadsDir: tmpSquadsDir, broadcastToAll: vi.fn(),
            });
            const calls = [];
            mockBridge._setSendHandler((opts) => {
                calls.push(opts.squadName?.replace('schedule:', '') ?? '');
                opts.onDone('Done');
                mockBridge._activeKeys.delete(opts.squadName ?? opts.cwd);
            });
            await chainRunner.run({
                squadName: 'solo-squad',
                prompt: 'Run solo',
                timeoutMinutes: 5,
                retryOnFailure: 0,
                attempt: 1,
            });
            expect(calls).toEqual(['solo-squad']);
            expect(broadcast).not.toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ type: 'chain:triggered' }));
        });
        it('should skip chain target if squad directory does not exist', async () => {
            createSquadYaml('orphan-squad', 'nonexistent-squad');
            const chainRunner = new JobRunner({
                db, bridge: mockBridge, broadcast, squadsDir: tmpSquadsDir, broadcastToAll: vi.fn(),
            });
            mockBridge._setSendHandler((opts) => {
                opts.onDone('Done');
                mockBridge._activeKeys.delete(opts.squadName ?? opts.cwd);
            });
            const result = await chainRunner.run({
                squadName: 'orphan-squad',
                prompt: 'Run orphan',
                timeoutMinutes: 5,
                retryOnFailure: 0,
                attempt: 1,
            });
            expect(result).toBe('completed');
            expect(broadcast).not.toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ type: 'chain:triggered' }));
        });
        it('should trigger multiple chained squads', async () => {
            createSquadYaml('multi-source', ['target-a', 'target-b']);
            createSquadYaml('target-a');
            createSquadYaml('target-b');
            const chainRunner = new JobRunner({
                db, bridge: mockBridge, broadcast, squadsDir: tmpSquadsDir, broadcastToAll: vi.fn(),
            });
            const calls = [];
            mockBridge._setSendHandler((opts) => {
                calls.push(opts.squadName?.replace('schedule:', '') ?? '');
                opts.onDone('Output');
                mockBridge._activeKeys.delete(opts.squadName ?? opts.cwd);
            });
            await chainRunner.run({
                squadName: 'multi-source',
                prompt: 'Run multi',
                timeoutMinutes: 5,
                retryOnFailure: 0,
                attempt: 1,
            });
            expect(calls).toContain('multi-source');
            expect(calls).toContain('target-a');
            expect(calls).toContain('target-b');
        });
        it('should handle object-format chain with target_squad field', async () => {
            createSquadYaml('obj-source', [
                { target_squad: 'obj-target', trigger: 'on_complete' },
            ]);
            createSquadYaml('obj-target');
            const chainRunner = new JobRunner({
                db, bridge: mockBridge, broadcast, squadsDir: tmpSquadsDir, broadcastToAll: vi.fn(),
            });
            const calls = [];
            mockBridge._setSendHandler((opts) => {
                calls.push(opts.squadName?.replace('schedule:', '') ?? '');
                opts.onDone('Output');
                mockBridge._activeKeys.delete(opts.squadName ?? opts.cwd);
            });
            await chainRunner.run({
                squadName: 'obj-source',
                prompt: 'Run obj',
                timeoutMinutes: 5,
                retryOnFailure: 0,
                attempt: 1,
            });
            expect(calls).toContain('obj-source');
            expect(calls).toContain('obj-target');
        });
        it('should use L1 intelligent summary in chain prompt', async () => {
            createSquadYaml('big-source', [
                { target_squad: 'desc-target', trigger: 'on_complete', description: 'Publica artigo no blog' },
            ]);
            createSquadYaml('desc-target');
            const chainRunner = new JobRunner({
                db, bridge: mockBridge, broadcast, squadsDir: tmpSquadsDir, broadcastToAll: vi.fn(),
            });
            const bigOutput = 'X'.repeat(5000);
            mockBridge._setSendHandler((opts) => {
                const name = opts.squadName?.replace('schedule:', '') ?? '';
                if (name === 'big-source') {
                    opts.onDone(bigOutput);
                }
                else {
                    opts.onDone('target done');
                }
                mockBridge._activeKeys.delete(opts.squadName ?? opts.cwd);
            });
            await chainRunner.run({
                squadName: 'big-source',
                prompt: 'Run big',
                timeoutMinutes: 5,
                retryOnFailure: 0,
                attempt: 1,
            });
            // Handoff file should be truncated (last 2000 chars)
            const handoffFile = path.join(tmpSquadsDir, 'big-source', '_output', 'handoff-to-desc-target.md');
            const content = fs.readFileSync(handoffFile, 'utf-8');
            const bodyAfterSeparator = content.split('---\n\n')[1] ?? '';
            expect(bodyAfterSeparator.length).toBeLessThanOrEqual(2000);
            // Chain prompt should use L1 intelligent summary, not raw truncation
            const targetCall = mockBridge.send.mock.calls.find((c) => c[0].squadName === 'schedule:desc-target');
            expect(targetCall).toBeDefined();
            const prompt = targetCall[0].prompt;
            expect(prompt).toContain('Publica artigo no blog');
            expect(prompt).toContain(handoffFile);
            expect(prompt).toContain('Resumo da squad anterior');
            // L1 summary should be used instead of raw truncated text
            expect(prompt).toContain('Resultado principal');
            expect(prompt).not.toContain('XXXXX');
            // Prompt is much shorter with L1 summary
            expect(prompt.length).toBeLessThan(500);
            // maxTurns should be set
            expect(targetCall[0].maxTurns).toBe(25);
            // Summary bridge call was made with maxTurns: 1
            const summaryCall = mockBridge.send.mock.calls.find((c) => c[0].squadName?.startsWith('summary:'));
            expect(summaryCall).toBeDefined();
            expect(summaryCall[0].maxTurns).toBe(1);
        });
        it('should fallback to truncation when L1 summary fails', async () => {
            createSquadYaml('fail-source', 'fail-target');
            createSquadYaml('fail-target');
            mockBridge._setFailSummaries(true);
            const chainRunner = new JobRunner({
                db, bridge: mockBridge, broadcast, squadsDir: tmpSquadsDir, broadcastToAll: vi.fn(),
            });
            mockBridge._setSendHandler((opts) => {
                const name = opts.squadName?.replace('schedule:', '') ?? '';
                if (name === 'fail-source') {
                    opts.onDone('A'.repeat(1000));
                }
                else {
                    opts.onDone('done');
                }
                mockBridge._activeKeys.delete(opts.squadName ?? opts.cwd);
            });
            await chainRunner.run({
                squadName: 'fail-source',
                prompt: 'Run',
                timeoutMinutes: 5,
                retryOnFailure: 0,
                attempt: 1,
            });
            const targetCall = mockBridge.send.mock.calls.find((c) => c[0].squadName === 'schedule:fail-target');
            expect(targetCall).toBeDefined();
            const prompt = targetCall[0].prompt;
            // Should NOT contain L1 summary bullets
            expect(prompt).not.toContain('Resultado principal');
            // Should contain truncated original (last 500 chars of A's)
            expect(prompt).toContain('A'.repeat(500));
            mockBridge._setFailSummaries(false);
        });
    });
});
//# sourceMappingURL=job-runner.test.js.map