/**
 * embeddingMatching.ts 단위 테스트 (TDD)
 *
 * Phase 1-B: 임베딩 유사도 추천 테스트
 *
 * 테스트 케이스:
 *  1. content_hash 계산 함수 — SHA-256 해시 정확성
 *  2. content_hash 변경 시에만 embedding 재생성 트리거
 *  3. content_hash 미변경 시 스킵
 *  4. mergeResults — 정적 매칭 우선, 중복 제거
 *  5. mergeResults — maxLinksPerDoc 상한 적용
 *  6. mergeResults — 빈 입력 처리
 *  7. embeddingMatching.enabled=false 시 스킵
 */

import { describe, it, expect } from 'vitest';
import { createHash } from 'crypto';
import type { MatchResult } from '../staticMatching';
import {
    computeContentHash,
    mergeResults,
    shouldRegenerateEmbedding,
    type EmbeddingMatchResult,
    type MergedResult,
} from '../embeddingMatching';

// ============================================================
// 테스트 헬퍼
// ============================================================

function makeStaticResult(
    overrides: Partial<MatchResult> & { termId: string; term: string }
): MatchResult {
    return {
        termId: overrides.termId,
        term: overrides.term,
        method: overrides.method ?? 'static',
        confidence: overrides.confidence ?? 100,
        explanation: overrides.explanation ?? `exact match: "${overrides.term}"`,
    };
}

function makeEmbeddingResult(
    overrides: Partial<EmbeddingMatchResult> & { targetDocId: string; targetTitle: string }
): EmbeddingMatchResult {
    return {
        targetDocId: overrides.targetDocId,
        targetTitle: overrides.targetTitle,
        method: 'embedding',
        confidence: overrides.confidence ?? 85,
        similarity: overrides.similarity ?? 0.85,
        explanation: overrides.explanation ?? `embedding similarity: ${overrides.similarity ?? 0.85}`,
    };
}

// ============================================================
// 테스트 1: content_hash 계산 함수 — SHA-256 해시 정확성
// ============================================================

describe('1. computeContentHash — SHA-256 해시 정확성', () => {
    it('동일한 content는 동일한 해시 반환', () => {
        const content = '보험 약관 내용입니다.';
        const hash1 = computeContentHash(content);
        const hash2 = computeContentHash(content);

        expect(hash1).toBe(hash2);
    });

    it('다른 content는 다른 해시 반환', () => {
        const hash1 = computeContentHash('보험 약관 내용 A');
        const hash2 = computeContentHash('보험 약관 내용 B');

        expect(hash1).not.toBe(hash2);
    });

    it('반환값이 올바른 SHA-256 hex 형식 (64자)', () => {
        const hash = computeContentHash('테스트 내용');

        expect(hash).toHaveLength(64);
        expect(hash).toMatch(/^[0-9a-f]{64}$/);
    });

    it('Node.js crypto 모듈과 동일한 결과', () => {
        const content = '임베딩 매칭 테스트';
        const expected = createHash('sha256').update(content, 'utf8').digest('hex');
        const actual = computeContentHash(content);

        expect(actual).toBe(expected);
    });

    it('빈 문자열도 해시 계산 가능', () => {
        const hash = computeContentHash('');

        expect(hash).toHaveLength(64);
        expect(hash).toMatch(/^[0-9a-f]{64}$/);
    });
});

// ============================================================
// 테스트 2: content_hash 변경 시에만 embedding 재생성 트리거
// ============================================================

describe('2. shouldRegenerateEmbedding — content_hash 변경 감지', () => {
    it('content_hash와 embedding_hash가 다르면 재생성 필요 (true)', () => {
        const contentHash = computeContentHash('새로운 내용');
        const embeddingHash = computeContentHash('이전 내용');

        const result = shouldRegenerateEmbedding(contentHash, embeddingHash);

        expect(result).toBe(true);
    });

    it('content_hash와 embedding_hash가 같으면 재생성 불필요 (false)', () => {
        const content = '동일한 내용';
        const contentHash = computeContentHash(content);
        const embeddingHash = computeContentHash(content);

        const result = shouldRegenerateEmbedding(contentHash, embeddingHash);

        expect(result).toBe(false);
    });
});

// ============================================================
// 테스트 3: content_hash 미변경 시 스킵
// ============================================================

describe('3. content_hash 미변경 시 embedding 재생성 스킵', () => {
    it('embedding_hash가 undefined이면 재생성 필요 (true)', () => {
        const contentHash = computeContentHash('어떤 내용');

        const result = shouldRegenerateEmbedding(contentHash, undefined);

        expect(result).toBe(true);
    });

    it('embedding_hash가 null이면 재생성 필요 (true)', () => {
        const contentHash = computeContentHash('어떤 내용');

        const result = shouldRegenerateEmbedding(contentHash, null);

        expect(result).toBe(true);
    });

    it('content_hash가 변경되지 않으면 재생성 불필요 (false)', () => {
        const sameContent = '변경 없는 내용';
        const hash = computeContentHash(sameContent);

        // content_hash == embedding_hash이면 스킵
        const result = shouldRegenerateEmbedding(hash, hash);

        expect(result).toBe(false);
    });
});

// ============================================================
// 테스트 4: mergeResults — 정적 매칭 우선, 중복 제거
// ============================================================

describe('4. mergeResults — 정적 매칭 우선, 중복 제거', () => {
    it('정적 매칭 결과가 임베딩 결과보다 우선 순위', () => {
        const staticResults: MatchResult[] = [
            makeStaticResult({ termId: 'doc1', term: '뇌졸중', confidence: 100 }),
        ];
        const embeddingResults: EmbeddingMatchResult[] = [
            makeEmbeddingResult({ targetDocId: 'doc2', targetTitle: '고혈압', confidence: 85 }),
        ];

        const merged = mergeResults(staticResults, embeddingResults, 10);

        // 정적 매칭이 먼저 나와야 함
        expect(merged[0].method).toBe('static');
        expect(merged[0].targetDocId).toBe('doc1');
        expect(merged[1].method).toBe('embedding');
        expect(merged[1].targetDocId).toBe('doc2');
    });

    it('동일 targetDocId가 양쪽에 있으면 높은 confidence 유지', () => {
        const staticResults: MatchResult[] = [
            makeStaticResult({ termId: 'doc1', term: '뇌졸중', confidence: 90 }),
        ];
        const embeddingResults: EmbeddingMatchResult[] = [
            // 같은 targetDocId이지만 높은 confidence
            makeEmbeddingResult({ targetDocId: 'doc1', targetTitle: '뇌졸중', confidence: 95 }),
        ];

        const merged = mergeResults(staticResults, embeddingResults, 10);

        // 중복 제거 후 1개만
        expect(merged).toHaveLength(1);
        // 높은 confidence(95) 유지
        expect(merged[0].confidence).toBe(95);
    });

    it('동일 targetDocId가 양쪽에 있으면 정적 매칭이 더 높은 경우 정적 유지', () => {
        const staticResults: MatchResult[] = [
            makeStaticResult({ termId: 'doc1', term: '뇌졸중', confidence: 100 }),
        ];
        const embeddingResults: EmbeddingMatchResult[] = [
            makeEmbeddingResult({ targetDocId: 'doc1', targetTitle: '뇌졸중', confidence: 80 }),
        ];

        const merged = mergeResults(staticResults, embeddingResults, 10);

        expect(merged).toHaveLength(1);
        expect(merged[0].confidence).toBe(100);
        expect(merged[0].method).toBe('static');
    });
});

// ============================================================
// 테스트 5: mergeResults — maxLinksPerDoc 상한 적용
// ============================================================

describe('5. mergeResults — maxLinksPerDoc 상한 적용', () => {
    it('maxLinksPerDoc=3이면 결과가 최대 3개', () => {
        const staticResults: MatchResult[] = [
            makeStaticResult({ termId: 'doc1', term: '뇌졸중', confidence: 100 }),
            makeStaticResult({ termId: 'doc2', term: '고혈압', confidence: 95 }),
        ];
        const embeddingResults: EmbeddingMatchResult[] = [
            makeEmbeddingResult({ targetDocId: 'doc3', targetTitle: '당뇨', confidence: 85 }),
            makeEmbeddingResult({ targetDocId: 'doc4', targetTitle: '심근경색', confidence: 80 }),
            makeEmbeddingResult({ targetDocId: 'doc5', targetTitle: '암', confidence: 75 }),
        ];

        const merged = mergeResults(staticResults, embeddingResults, 3);

        expect(merged).toHaveLength(3);
    });

    it('maxLinksPerDoc=0이면 빈 배열', () => {
        const staticResults: MatchResult[] = [
            makeStaticResult({ termId: 'doc1', term: '뇌졸중', confidence: 100 }),
        ];
        const embeddingResults: EmbeddingMatchResult[] = [
            makeEmbeddingResult({ targetDocId: 'doc2', targetTitle: '고혈압', confidence: 85 }),
        ];

        const merged = mergeResults(staticResults, embeddingResults, 0);

        expect(merged).toHaveLength(0);
    });

    it('전체 결과가 maxLinksPerDoc보다 적으면 그대로 반환', () => {
        const staticResults: MatchResult[] = [
            makeStaticResult({ termId: 'doc1', term: '뇌졸중', confidence: 100 }),
        ];
        const embeddingResults: EmbeddingMatchResult[] = [
            makeEmbeddingResult({ targetDocId: 'doc2', targetTitle: '고혈압', confidence: 85 }),
        ];

        const merged = mergeResults(staticResults, embeddingResults, 10);

        expect(merged).toHaveLength(2);
    });
});

// ============================================================
// 테스트 6: mergeResults — 빈 입력 처리
// ============================================================

describe('6. mergeResults — 빈 입력 처리', () => {
    it('두 입력 모두 빈 배열이면 빈 배열 반환', () => {
        const merged = mergeResults([], [], 10);

        expect(merged).toHaveLength(0);
    });

    it('정적 결과만 있을 때 정상 처리', () => {
        const staticResults: MatchResult[] = [
            makeStaticResult({ termId: 'doc1', term: '뇌졸중', confidence: 100 }),
        ];

        const merged = mergeResults(staticResults, [], 10);

        expect(merged).toHaveLength(1);
        expect(merged[0].method).toBe('static');
    });

    it('임베딩 결과만 있을 때 정상 처리', () => {
        const embeddingResults: EmbeddingMatchResult[] = [
            makeEmbeddingResult({ targetDocId: 'doc1', targetTitle: '뇌졸중', confidence: 85 }),
        ];

        const merged = mergeResults([], embeddingResults, 10);

        expect(merged).toHaveLength(1);
        expect(merged[0].method).toBe('embedding');
    });
});

// ============================================================
// 테스트 7: embeddingMatching.enabled=false 시 스킵
// ============================================================

describe('7. embeddingMatching.enabled=false 시 스킵', () => {
    it('enabled=false이면 임베딩 결과가 없어도 정적 결과만 반환', () => {
        const staticResults: MatchResult[] = [
            makeStaticResult({ termId: 'doc1', term: '뇌졸중', confidence: 100 }),
        ];
        // enabled=false 시뮬레이션: 임베딩 결과를 빈 배열로 전달
        const embeddingResults: EmbeddingMatchResult[] = [];

        const merged = mergeResults(staticResults, embeddingResults, 10);

        expect(merged).toHaveLength(1);
        expect(merged[0].method).toBe('static');
    });

    it('enabled=false이면 임베딩 결과 0개 병합 확인', () => {
        // embeddingMatching이 disabled일 때 빈 embeddingResults 전달
        const merged = mergeResults([], [], 10);

        expect(merged).toHaveLength(0);
    });

    it('정적 결과만 병합해도 MergedResult 타입 준수', () => {
        const staticResults: MatchResult[] = [
            makeStaticResult({ termId: 'doc1', term: '뇌졸중', confidence: 100 }),
        ];

        const merged = mergeResults(staticResults, [], 10);

        expect(merged[0]).toHaveProperty('targetDocId');
        expect(merged[0]).toHaveProperty('targetTitle');
        expect(merged[0]).toHaveProperty('method');
        expect(merged[0]).toHaveProperty('confidence');
        expect(merged[0]).toHaveProperty('explanation');
    });
});

// ============================================================
// 추가 테스트: mergeResults confidence 정렬
// ============================================================

describe('mergeResults — 정렬 확인', () => {
    it('정적 매칭 내에서 confidence 내림차순 정렬', () => {
        const staticResults: MatchResult[] = [
            makeStaticResult({ termId: 'doc1', term: '뇌졸중', confidence: 70 }),
            makeStaticResult({ termId: 'doc2', term: '고혈압', confidence: 100 }),
            makeStaticResult({ termId: 'doc3', term: '당뇨', confidence: 85 }),
        ];

        const merged = mergeResults(staticResults, [], 10);

        // 정적 매칭들은 confidence 내림차순이어야 함
        expect(merged[0].confidence).toBeGreaterThanOrEqual(merged[1].confidence);
        expect(merged[1].confidence).toBeGreaterThanOrEqual(merged[2].confidence);
    });

    it('정적+임베딩 병합 시 confidence 내림차순 정렬', () => {
        const staticResults: MatchResult[] = [
            makeStaticResult({ termId: 'doc1', term: '뇌졸중', confidence: 70 }),
        ];
        const embeddingResults: EmbeddingMatchResult[] = [
            makeEmbeddingResult({ targetDocId: 'doc2', targetTitle: '고혈압', confidence: 90 }),
        ];

        const merged = mergeResults(staticResults, embeddingResults, 10);

        // confidence 높은 것이 먼저 (임베딩 90 > 정적 70)
        expect(merged[0].confidence).toBeGreaterThanOrEqual(merged[1].confidence);
    });
});
