/**
 * Gap Detector / Need Engine / Knowledge Inventory / Learning Queue
 * (Self-Evolving OS Phase 3 — 성장 루프 코어) 테스트.
 */
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { detectGaps } from '../src/intelligence/gapDetector';
import { computeNeeds, knowledgeInventory, formatNeedsMarkdown, NEED_WEIGHTS } from '../src/intelligence/needEngine';
import {
    loadQueue,
    saveQueue,
    mergeNeedsIntoQueue,
    formatQueueMarkdown,
    QueueItem,
} from '../src/intelligence/learningQueue';
import type { ReflectionRecord } from '../src/intelligence/reflectionStore';

function mkReflection(partial: Partial<ReflectionRecord>): ReflectionRecord {
    return {
        ts: '2026-06-11T10:00:00.000Z',
        taskId: 'meeting-minutes',
        taskLabel: '회의록',
        confidenceScore: 70,
        confidenceBand: 'medium',
        missing: [],
        escalated: false,
        criticIssues: null,
        promptPreview: 'p',
        retrieval: { chunkCount: 3, topScore: 0.6 },
        weakGrounding: false,
        ...partial,
    };
}

describe('detectGaps', () => {
    const okSignals = { chunkCount: 4, topScore: 0.7, conflictCount: 0, ambiguityDetected: false };
    const noGrounding = { chunkCount: 0, topScore: 0, conflictCount: 0, ambiguityDetected: false };

    it('누락 3개 이상 → high', () => {
        const g = detectGaps({
            coverage: { ran: true, taskId: 'meeting-minutes', taskLabel: '회의록', covered: [], missing: ['참석자', '담당자', '기한'] },
            signals: okSignals, taskId: 'meeting-minutes',
        });
        expect(g.severity).toBe('high');
        expect(g.summary).toContain('3개 누락');
    });

    it('근거 0건 단독 → low, 고영향 업무 + 누락이면 한 단계 상향', () => {
        const clean = detectGaps({
            coverage: { ran: false, covered: [], missing: [] },
            signals: noGrounding, taskId: null,
        });
        expect(clean.severity).toBe('low');
        expect(clean.weakGrounding).toBe(true);

        const worse = detectGaps({
            coverage: { ran: true, taskId: 'meeting-minutes', taskLabel: '회의록', covered: [], missing: ['기한'] },
            signals: noGrounding, taskId: 'meeting-minutes',
        });
        expect(worse.severity).toBe('high'); // medium(누락1) + 고영향·근거없음 bump
    });

    it('갭 없으면 none', () => {
        const g = detectGaps({
            coverage: { ran: true, taskId: 'meeting-minutes', taskLabel: '회의록', covered: ['참석자'], missing: [] },
            signals: okSignals, taskId: 'meeting-minutes',
        });
        expect(g.severity).toBe('none');
        expect(g.summary).toBe('갭 없음');
    });
});

describe('computeNeeds', () => {
    it('약한 그라운딩·누락 많은 업무가 높은 점수를 받는다', () => {
        const records: ReflectionRecord[] = [
            // 회의록: 깨끗한 수행 3회
            mkReflection({}), mkReflection({}), mkReflection({}),
            // 시장조사: 근거 없음 + 누락 + 저확신 2회
            mkReflection({ taskId: 'market-research', taskLabel: '시장조사', weakGrounding: true, missing: ['출처', '시장 규모'], confidenceScore: 40, retrieval: { chunkCount: 0, topScore: 0 } }),
            mkReflection({ taskId: 'market-research', taskLabel: '시장조사', weakGrounding: true, missing: ['출처'], confidenceScore: 45, retrieval: { chunkCount: 0, topScore: 0 } }),
        ];
        const needs = computeNeeds(records);
        expect(needs[0].taskId).toBe('market-research');
        expect(needs[0].score).toBeGreaterThan(needs[1].score);
        expect(needs[0].topMisses).toContain('출처');
        expect(needs[0].reason).toContain('누락');
    });

    it('가중치 합이 1', () => {
        const sum = Object.values(NEED_WEIGHTS).reduce((s, w) => s + w, 0);
        expect(sum).toBeCloseTo(1.0);
    });

    it('기록 없으면 빈 배열 + md 안내', () => {
        expect(computeNeeds([])).toEqual([]);
        expect(formatNeedsMarkdown([], [])).toContain('기록 없음');
    });
});

describe('knowledgeInventory', () => {
    it('그라운딩 평균으로 보유/부족/없음 판정', () => {
        const records: ReflectionRecord[] = [
            mkReflection({ retrieval: { chunkCount: 5, topScore: 0.8 } }),
            mkReflection({ taskId: 'market-research', taskLabel: '시장조사', retrieval: { chunkCount: 0, topScore: 0 } }),
            mkReflection({ taskId: 'work-research', taskLabel: '업무조사', retrieval: { chunkCount: 1, topScore: 0.3 } }),
        ];
        const inv = knowledgeInventory(records);
        const byId = new Map(inv.map((i) => [i.taskId, i.status]));
        expect(byId.get('meeting-minutes')).toBe('sufficient');
        expect(byId.get('market-research')).toBe('missing');
        expect(byId.get('work-research')).toBe('partial');
    });
});

describe('learningQueue', () => {
    const needs = computeNeeds([
        mkReflection({ taskId: 'market-research', taskLabel: '시장조사', weakGrounding: true, missing: ['출처'], confidenceScore: 40 }),
    ]);

    it('save → load 라운드트립 + 우선순위 정렬 저장', () => {
        const brain = fs.mkdtempSync(path.join(os.tmpdir(), 'astra-test-queue-'));
        const queue = mergeNeedsIntoQueue([], needs, '2026-06-11T00:00:00.000Z');
        expect(saveQueue(brain, queue)).toBe(true);
        const loaded = loadQueue(brain);
        expect(loaded.length).toBe(1);
        expect(loaded[0].status).toBe('proposed');
        expect(loaded[0].topic).toContain('시장조사');
    });

    it('proposed 는 갱신되지만 approved 는 불변 (Permission Based Learning)', () => {
        const approved: QueueItem = {
            id: 'need-market-research', topic: '시장조사 역량 보강', priority: 10, reason: '이전',
            status: 'approved', createdAt: 'a', updatedAt: 'a',
        };
        const merged = mergeNeedsIntoQueue([approved], needs, '2026-06-11T00:00:00.000Z');
        expect(merged.length).toBe(1);
        expect(merged[0].status).toBe('approved');
        expect(merged[0].priority).toBe(10); // Need 점수로 덮어쓰지 않음
        expect(merged[0].reason).toBe('이전');
    });

    it('새 주제는 proposed 로 추가된다', () => {
        const other: QueueItem = {
            id: 'need-schedule', topic: '일정', priority: 5, reason: 'r',
            status: 'done', createdAt: 'a', updatedAt: 'a',
        };
        const merged = mergeNeedsIntoQueue([other], needs, 'now');
        expect(merged.length).toBe(2);
        expect(merged.find((q) => q.id === 'need-market-research')?.status).toBe('proposed');
        expect(merged.find((q) => q.id === 'need-schedule')?.status).toBe('done'); // 불변
    });

    it('formatQueueMarkdown — 승인 안내 포함', () => {
        const md = formatQueueMarkdown(mergeNeedsIntoQueue([], needs, 'now'));
        expect(md).toContain('approved');
        expect(md).toContain('시장조사');
    });
});
