/**
 * Phase 2 — large-input map-reduce core.
 *
 * Pure orchestration with an injected `callLLM`, so no network / SDK is touched.
 */

import {
    runMapReduce,
    shouldMapReduce,
    chunkCharBudget,
    inputBudgetTokens,
    type MapReduceConfig,
    type MapReduceDeps,
} from '../src/agent/handlePrompt/largeInputMapReduce';
import type { ChatMessage } from '../src/agent';

const estimateTokens = (s: string) => Math.ceil((s || '').length / 4);

const cfg: MapReduceConfig = {
    enabled: true,
    triggerRatio: 0.6,
    concurrency: 2,
    maxDepth: 3,
    showProvenance: false,
};

function isExtract(messages: ChatMessage[]): boolean {
    return /추출기/.test(messages[0]?.content ?? '');
}
function chunkLabel(messages: ChatMessage[]): string {
    const m = (messages[1]?.content ?? '').match(/자료 조각 (\d+)\/(\d+)/);
    return m ? m[1] : '?';
}

// ~12 short markdown sections → forces multiple chunks under a small window.
const bigContent = Array.from({ length: 12 }, (_, i) =>
    `## 섹션 ${i + 1}\n안건 ${i + 1}: 결정사항과 수치 ${i * 10}. ` + '내용 '.repeat(40)
).join('\n\n');

describe('shouldMapReduce', () => {
    test('triggers only above window * triggerRatio and when enabled', () => {
        expect(shouldMapReduce(6200, 10000, cfg)).toBe(true);  // > 6000
        expect(shouldMapReduce(5000, 10000, cfg)).toBe(false); // < 6000
        expect(shouldMapReduce(99999, 10000, { ...cfg, enabled: false })).toBe(false);
        expect(shouldMapReduce(100, 0, cfg)).toBe(false);      // unknown window
    });
});

describe('budget helpers', () => {
    test('inputBudgetTokens reserves output + safety', () => {
        // 10000 - sys(500) - max(2048, 1000)=2048 - safety(512) = 6940
        expect(inputBudgetTokens(10000, 500, 512)).toBe(6940);
    });
    test('chunkCharBudget is positive and scales with the window', () => {
        const small = chunkCharBudget(4000, 200, 512);
        const big = chunkCharBudget(16000, 200, 512);
        expect(small).toBeGreaterThan(0);
        expect(big).toBeGreaterThan(small);
    });
});

describe('runMapReduce', () => {
    function deps(callLLM: MapReduceDeps['callLLM']): MapReduceDeps {
        return { callLLM, estimateTokens };
    }
    const params = {
        intent: '회의록을 안건별로 정리해줘',
        largeContent: bigContent,
        windowTokens: 4000,
        systemTokens: 200,
        safetyMargin: 512,
        cfg,
    };

    test('extracts relevant facts per chunk and condenses them', async () => {
        const seen: string[] = [];
        const r = await runMapReduce(
            deps(async (messages) => {
                expect(isExtract(messages)).toBe(true);
                const k = chunkLabel(messages);
                seen.push(k);
                return `추출-${k}`;
            }),
            params,
        );
        expect(r.allIrrelevant).toBe(false);
        expect(r.chunkCount).toBeGreaterThan(1);
        expect(r.relevantCount).toBe(r.chunkCount);
        expect(r.condensedContext).toContain('추출-1');
        // every chunk was visited
        expect(seen.length).toBe(r.chunkCount);
    });

    test('all-irrelevant chunks → allIrrelevant with empty context', async () => {
        const r = await runMapReduce(
            deps(async () => '(관련 없음)'),
            params,
        );
        expect(r.allIrrelevant).toBe(true);
        expect(r.relevantCount).toBe(0);
        expect(r.condensedContext).toBe('');
    });

    test('respects concurrency limit', async () => {
        let active = 0;
        let peak = 0;
        await runMapReduce(
            deps(async (messages) => {
                active++;
                peak = Math.max(peak, active);
                await new Promise((res) => setTimeout(res, 5));
                active--;
                return `x-${chunkLabel(messages)}`;
            }),
            params,
        );
        expect(peak).toBeLessThanOrEqual(cfg.concurrency);
    });

    test('a failing chunk extraction falls back to truncated raw (not a crash)', async () => {
        let call = 0;
        const r = await runMapReduce(
            deps(async (messages) => {
                if (isExtract(messages) && ++call === 1) throw new Error('boom');
                return `ok-${chunkLabel(messages)}`;
            }),
            params,
        );
        expect(r.allIrrelevant).toBe(false);
        // The failed chunk still contributed (raw fallback), so relevantCount === chunkCount.
        expect(r.relevantCount).toBe(r.chunkCount);
    });

    test('tags provenance when showProvenance is on', async () => {
        const r = await runMapReduce(
            deps(async (messages) => `발췌-${chunkLabel(messages)}`),
            { ...params, cfg: { ...cfg, showProvenance: true } },
        );
        expect(r.condensedContext).toMatch(/\[조각 \d+\]/);
    });

    test('hierarchical reduce kicks in when extractions overflow the context ceiling', async () => {
        // Tiny window so even a few extractions exceed the ceiling → reduce rounds run.
        let reduceCalls = 0;
        const r = await runMapReduce(
            deps(async (messages) => {
                if (isExtract(messages)) {
                    return '관련 사실 '.repeat(60); // big extraction per chunk
                }
                reduceCalls++;
                return '통합본'; // reduce collapses to something small
            }),
            { ...params, windowTokens: 2200 },
        );
        expect(reduceCalls).toBeGreaterThan(0);
        expect(r.reduceDepth).toBeGreaterThan(0);
        expect(r.allIrrelevant).toBe(false);
    });
});
