import {
    extractVisibleFinal,
    shouldFinalOnlyRetry,
    shouldAutoContinue,
    looksCutOff,
    mergeContinuationParts,
    buildContinuationUserPrompt,
} from '../src/core/responseRecovery';

describe('responseRecovery.extractVisibleFinal — thought quarantine', () => {
    it('leaves a plain answer untouched', () => {
        const out = extractVisibleFinal('안녕하세요! 무엇을 도와드릴까요?');
        expect(out.visible).toBe('안녕하세요! 무엇을 도와드릴까요?');
        expect(out.hadHiddenReasoning).toBe(false);
        expect(out.wasThoughtOnly).toBe(false);
    });

    it('keeps only the Harmony `final` channel and discards analysis', () => {
        const raw = '<|channel|>analysis<|message|>Let me think about this carefully...<|end|><|start|>assistant<|channel|>final<|message|>최종 답변입니다.';
        const out = extractVisibleFinal(raw);
        expect(out.visible).toBe('최종 답변입니다.');
        expect(out.hadFinalChannel).toBe(true);
        expect(out.hadHiddenReasoning).toBe(true);
        expect(out.hiddenReasoning).toContain('think about this');
        expect(out.wasThoughtOnly).toBe(false);
    });

    it('strips an UNCLOSED thought channel (model ran out of tokens mid-thought) → thought-only', () => {
        const raw = '<|channel>thought\nThinking Process:\nLet me figure out how to approach this and';
        const out = extractVisibleFinal(raw);
        expect(out.visible).toBe('');
        expect(out.hadHiddenReasoning).toBe(true);
        expect(out.wasThoughtOnly).toBe(true);
        expect(shouldFinalOnlyRetry(out)).toBe(true);
    });

    it('strips a closed <think>…</think> block', () => {
        const out = extractVisibleFinal('<think>reasoning here, multi\nline</think>\n\n실제 답변입니다.');
        expect(out.visible).toBe('실제 답변입니다.');
        expect(out.hadHiddenReasoning).toBe(true);
    });

    it('strips an unclosed <think> running to EOS → thought-only', () => {
        const out = extractVisibleFinal("<think>I'm thinking and then I run out of");
        expect(out.visible).toBe('');
        expect(out.wasThoughtOnly).toBe(true);
    });

    it('strips a leading "Thinking Process:" block up to the answer boundary', () => {
        const out = extractVisibleFinal('Thinking Process:\nStep 1: consider X\nStep 2: consider Y\n## 요약\n실제 답변 본문입니다.');
        expect(out.visible).toContain('## 요약');
        expect(out.visible).toContain('실제 답변 본문');
        expect(out.visible).not.toContain('Step 1');
        expect(out.hadHiddenReasoning).toBe(true);
    });

    it('treats a leading "Thinking Process:" with no answer boundary as thought-only', () => {
        const out = extractVisibleFinal('Thinking Process:\nStep 1...\nStep 2... and I ran out of tokens here');
        expect(out.visible).toBe('');
        expect(out.wasThoughtOnly).toBe(true);
    });

    it('does NOT strip a legitimate "## Thinking Process" markdown heading (no colon)', () => {
        const out = extractVisibleFinal('## Thinking Process\n여기서는 사고 과정 자체를 설명하는 답변입니다.');
        expect(out.visible).toContain('## Thinking Process');
        expect(out.visible).toContain('사고 과정 자체를 설명');
        expect(out.hadHiddenReasoning).toBe(false);
    });

    it('handles empty / whitespace input', () => {
        expect(extractVisibleFinal('').visible).toBe('');
        expect(extractVisibleFinal('   \n  ').visible).toBe('');
        expect(extractVisibleFinal(null as any).visible).toBe('');
        expect(extractVisibleFinal('').wasThoughtOnly).toBe(false);
    });
});

describe('responseRecovery.looksCutOff', () => {
    it('flags answers that plainly end mid-sentence / mid-structure', () => {
        expect(looksCutOff('당신은 복잡한 아이디어나 목표를 구체적인 실행 계획과 체계적인 문서화로')).toBe(true); // ends with the particle "로"
        expect(looksCutOff('우리는 이 문제를 해결하기 위해 다음과 같은 단계를')).toBe(true);              // ends with object marker "를"
        expect(looksCutOff('the implementation is not yet complete and we need to')).toBe(true);   // mid-English
        expect(looksCutOff('the items are: foo, bar,')).toBe(true);                                  // trailing comma
        expect(looksCutOff('here is the code:\n```python\nprint("hi")')).toBe(true);                 // unclosed fence
        expect(looksCutOff('정리하면 다음 항목들이 중요합니다:\n- 첫 번째 항목\n- 두 번째 항목\n- ')).toBe(true);  // dangling bullet
    });
    it('does NOT flag complete-looking answers', () => {
        expect(looksCutOff('이것은 완전히 끝난 답변이고 마침표도 붙어 있습니다.')).toBe(false);
        expect(looksCutOff('이것은 마침표 없이 끝나는 한국어 문장입니다')).toBe(false);          // ends with "다" — valid
        expect(looksCutOff('네, 그렇게 하면 됩니다')).toBe(false);
        expect(looksCutOff('done.')).toBe(false);
        expect(looksCutOff('짧음')).toBe(false);                                                  // too short to judge
    });
});

describe('responseRecovery.shouldAutoContinue', () => {
    it('continues when the engine reports the output cap was hit', () => {
        expect(shouldAutoContinue('output-limit', 'x'.repeat(200), 3500, 4096)).toBe(true);
        expect(shouldAutoContinue('output-limit', 'x'.repeat(200), 50, 4096)).toBe(true); // engine said so → trust it
    });
    it('continues when generation reached ~the cap even if the engine said "complete"', () => {
        expect(shouldAutoContinue('complete', 'x'.repeat(200), 4000, 4096)).toBe(true);
    });
    it('continues when the answer plainly ends mid-sentence (engine reason unclear)', () => {
        expect(shouldAutoContinue('unknown', '당신은 복잡한 아이디어나 목표를 구체적인 실행 계획과 체계적인 문서화로', 60, 4096)).toBe(true);
        expect(shouldAutoContinue('complete', 'the implementation continues here and we still need to', 100, 4096)).toBe(true);
    });
    it('does NOT continue from a tiny fragment or a complete-looking answer', () => {
        expect(shouldAutoContinue('output-limit', 'short', 4000, 4096)).toBe(false);            // < 24 chars
        expect(shouldAutoContinue('complete', '이것은 완전히 끝난 답변이고 마침표도 붙어 있습니다.', 100, 4096)).toBe(false);
        expect(shouldAutoContinue('unknown', '이것은 마침표 없이 끝나는 한국어 문장이고 충분히 길다고 본다', 100, 4096)).toBe(false);
    });
    it('does NOT continue on stop reasons that more text cannot fix', () => {
        expect(shouldAutoContinue('context-overflow', 'x'.repeat(200), 4000, 4096)).toBe(false);
        expect(shouldAutoContinue('error', 'x'.repeat(200), 4000, 4096)).toBe(false);
        expect(shouldAutoContinue('user-stopped', 'x'.repeat(200), 4000, 4096)).toBe(false);
        expect(shouldAutoContinue('tool-calls', 'x'.repeat(200), 4000, 4096)).toBe(false);
    });
});

describe('responseRecovery.mergeContinuationParts', () => {
    it('handles empty inputs', () => {
        expect(mergeContinuationParts('', 'hello')).toBe('hello');
        expect(mergeContinuationParts('hello', '')).toBe('hello');
        expect(mergeContinuationParts('', '')).toBe('');
    });
    it('joins with a paragraph break when the previous part ended cleanly', () => {
        expect(mergeContinuationParts('첫 번째 부분.', '두 번째 부분.')).toBe('첫 번째 부분.\n\n두 번째 부분.');
    });
    it('removes a verbatim overlap the continuation re-stated, splicing mid-sentence', () => {
        const a = 'the answer continues here and here';
        const b = 'continues here and here, then more';
        expect(mergeContinuationParts(a, b)).toBe('the answer continues here and here, then more');
    });
});

describe('responseRecovery.buildContinuationUserPrompt', () => {
    it('includes the original question and the tail of the answer so far', () => {
        const p = buildContinuationUserPrompt('원래 질문은 무엇인가?', 'a'.repeat(50) + 'TAIL_MARKER');
        expect(p).toContain('원래 질문은 무엇인가?');
        expect(p).toContain('TAIL_MARKER');
        expect(p).toMatch(/continue/i);
    });
    it('truncates a long answer-so-far to its tail', () => {
        const long = 'HEAD_MARKER' + 'b'.repeat(3000) + 'TAIL_MARKER';
        const p = buildContinuationUserPrompt('q', long, 1400);
        expect(p).toContain('TAIL_MARKER');
        expect(p).not.toContain('HEAD_MARKER');
        expect(p).toContain('…');
    });
});
