/**
 * MockLLMClient — IAIService 의 Mock 구현체.
 *
 * 의도: 회사 모드 dispatcher / ChunkedWriter / ceoPlanner 등 LLM 을 호출하는 코드
 * 경로를 *CI 환경에서도 테스트* 가능하게. 실제 Ollama / LM Studio 없이도 응답을
 * 미리 정의하거나 동적으로 생성 가능.
 *
 * 사용 예:
 *   const ai = new MockLLMClient();
 *   ai.setNextResponse('plan generated');
 *   const result = await ai.chat({ user: 'do this' });
 *
 *   // 또는 동적 응답:
 *   ai.setResponder((req) => req.user.includes('analyze') ? 'analysis...' : 'ok');
 *
 *   // 호출 이력 검증:
 *   expect(ai.calls).toHaveLength(2);
 *   expect(ai.calls[0].user).toBe('do this');
 */
import type {
    IAIService,
    AIChatRequest,
    AIChatResult,
} from '../../src/core/services';

export interface RecordedCall {
    user: string;
    system?: string;
    model?: string;
    timeoutMs?: number;
    signalAborted?: boolean;
}

type Responder = (req: AIChatRequest) => string | { content: string; empty?: boolean };

export class MockLLMClient implements IAIService {
    /** 모든 chat / call 호출의 입력 인자가 시간 순서로 누적. */
    public readonly calls: RecordedCall[] = [];

    /** FIFO 큐 — setNextResponse 로 push, chat 호출마다 shift. */
    private readonly queued: string[] = [];

    /** 큐가 비었을 때 사용할 fallback. setResponder 로 정의 가능. */
    private responder: Responder | null = null;

    /** queued / responder 모두 없으면 이 값을 그대로. */
    private defaultResponse = 'mock response — set via setNextResponse / setResponder';

    /**
     * 다음 호출(들) 에 사용할 응답을 FIFO 큐에 push.
     * 여러 번 push 하면 순차적으로 소비.
     */
    setNextResponse(text: string): void {
        this.queued.push(text);
    }

    /**
     * 모든 호출에 대해 동적으로 응답 생성. setNextResponse 큐가 우선.
     */
    setResponder(fn: Responder): void {
        this.responder = fn;
    }

    /** 모든 큐 / responder / 이력 초기화. test setup 사이에 reset 용. */
    reset(): void {
        this.calls.length = 0;
        this.queued.length = 0;
        this.responder = null;
    }

    async call(prompt: string): Promise<string> {
        const result = await this.chat({ user: prompt });
        return result.content;
    }

    async chat(req: AIChatRequest): Promise<AIChatResult> {
        this.calls.push({
            user: req.user,
            system: req.system,
            model: req.model,
            timeoutMs: req.timeoutMs,
            signalAborted: req.signal?.aborted,
        });
        // signal 이 이미 aborted 면 AbortError 던짐 — 실제 fetch 동작 모방.
        if (req.signal?.aborted) {
            const err = new Error('AbortError');
            err.name = 'AbortError';
            throw err;
        }
        let content: string;
        let empty = false;
        if (this.queued.length > 0) {
            content = this.queued.shift()!;
        } else if (this.responder) {
            const out = this.responder(req);
            if (typeof out === 'string') {
                content = out;
            } else {
                content = out.content;
                empty = !!out.empty;
            }
        } else {
            content = this.defaultResponse;
        }
        return {
            content,
            engine: 'lmstudio',
            model: req.model || 'mock-model',
            empty: empty || !content,
        };
    }
}
