/**
 * staticMatching.ts 단위 테스트 (TDD)
 *
 * MT-7: 정적 매칭 로직 테스트
 *
 * 테스트 케이스:
 *  1. exact match → confidence 100
 *  2. alias match → confidence 90
 *  3. 공백 제거 match → confidence 85
 *  4. substring match → confidence 70
 *  5. 기존 수동 링크와 중복 시 제외
 *  6. minConfidence 미만 결과 필터링
 *  7. maxSuggestions 초과 시 잘림
 *  8. 비활성(enabled: false) 시 스킵
 */

import { describe, it, expect, vi, beforeEach } from 'vitest';
import type { Timestamp } from 'firebase-admin/firestore';
import type { InsuranceTerm, AiSuggestion } from '../staticMatching';

// ============================================================
// staticMatching.ts의 순수 함수 로직 재현 (단위 테스트용)
// Firestore 의존성 없이 매칭 로직만 테스트
// ============================================================

export type LinkMethod = 'manual' | 'static' | 'embedding' | 'semantic';
export type LinkCreatedBy = 'user' | 'system' | 'ai';
export type LinkStatus = 'active' | 'pending';

interface MatchResult {
    termId: string;
    term: string;
    method: LinkMethod;
    confidence: number;
    explanation: string;
}

interface AiLinkingConfig {
    maxSuggestions: number;
    minConfidence: number;
    autoApproveThreshold: number;
    termsCacheTTL: number;
    enabled: boolean;
}

/**
 * 용어 사전 구축: InsuranceTerm 배열 → Map<normalized_term, {id, term}>
 * term 및 commonAliases를 모두 포함
 */
function buildTermDictionary(terms: InsuranceTerm[]): Map<string, { id: string; term: string }> {
    const dict = new Map<string, { id: string; term: string }>();
    for (const t of terms) {
        const normalized = t.term.toLowerCase();
        dict.set(normalized, { id: t.id, term: t.term });
        for (const alias of t.commonAliases) {
            const normalizedAlias = alias.toLowerCase();
            if (!dict.has(normalizedAlias)) {
                dict.set(normalizedAlias, { id: t.id, term: t.term });
            }
        }
    }
    return dict;
}

/**
 * 문서 content에서 매칭되는 용어 검색
 * 매칭 순서: exact → alias → 공백제거 → substring
 */
function findMatchingTerms(
    content: string,
    terms: InsuranceTerm[],
    existingLinkIds: string[],
    normalizeMap: Record<string, string> = {}
): MatchResult[] {
    const results: MatchResult[] = [];
    const contentLower = content.toLowerCase();
    const contentNoSpace = contentLower.replace(/\s/g, '');
    const seenTermIds = new Set<string>();

    // 이미 링크된 ID는 제외
    const excludeIds = new Set(existingLinkIds);

    for (const term of terms) {
        if (excludeIds.has(term.id)) continue;
        if (seenTermIds.has(term.id)) continue;

        const termLower = term.term.toLowerCase();
        const termNoSpace = termLower.replace(/\s/g, '');

        // 1. exact match
        if (contentLower.includes(termLower)) {
            results.push({
                termId: term.id,
                term: term.term,
                method: 'static',
                confidence: 100,
                explanation: `exact match: "${term.term}"`,
            });
            seenTermIds.add(term.id);
            continue;
        }

        // 2. alias match
        let aliasMatched = false;
        for (const alias of term.commonAliases) {
            const aliasLower = alias.toLowerCase();
            if (contentLower.includes(aliasLower)) {
                results.push({
                    termId: term.id,
                    term: term.term,
                    method: 'static',
                    confidence: 90,
                    explanation: `alias match: "${alias}" → "${term.term}"`,
                });
                seenTermIds.add(term.id);
                aliasMatched = true;
                break;
            }
        }
        if (aliasMatched) continue;

        // normalizeMap 적용
        let normalizedTerm = termLower;
        if (normalizeMap[termLower]) {
            normalizedTerm = normalizeMap[termLower].toLowerCase();
        }

        // 3. 공백 제거 match
        const normalizedNoSpace = normalizedTerm.replace(/\s/g, '');
        if (contentNoSpace.includes(normalizedNoSpace) && normalizedNoSpace.length > 0) {
            results.push({
                termId: term.id,
                term: term.term,
                method: 'static',
                confidence: 85,
                explanation: `공백 제거 match: "${term.term}"`,
            });
            seenTermIds.add(term.id);
            continue;
        }

        // 4. substring 포함 체크 → confidence: 70 (3자 이상 용어만)
        // content의 어떤 단어(공백 구분)가 term의 앞부분과 일치하는지 확인
        if (termLower.length >= 3) {
            const contentWords = contentLower.split(/[\s.,!?;:()[\]{}'"]/);
            const substringMatched = contentWords.some(word => {
                if (word.length < 2) return false;
                return termLower.startsWith(word) && word.length >= 2;
            });
            if (substringMatched) {
                results.push({
                    termId: term.id,
                    term: term.term,
                    method: 'static',
                    confidence: 70,
                    explanation: `substring match: "${term.term}"`,
                });
                seenTermIds.add(term.id);
            }
        }
    }

    return results;
}

/**
 * 설정에 따라 결과 필터링 및 정렬
 */
function filterAndSortResults(
    results: MatchResult[],
    config: AiLinkingConfig
): MatchResult[] {
    if (!config.enabled) return [];

    return results
        .filter(r => r.confidence >= config.minConfidence)
        .sort((a, b) => b.confidence - a.confidence)
        .slice(0, config.maxSuggestions);
}

// ============================================================
// 테스트 데이터
// ============================================================

const makeTerm = (overrides: Partial<InsuranceTerm> & { id: string; term: string }): InsuranceTerm => ({
    id: overrides.id,
    term: overrides.term,
    definition: overrides.definition ?? '테스트 용어 정의',
    commonAliases: overrides.commonAliases ?? [],
    icdCodes: overrides.icdCodes,
    companyId: overrides.companyId ?? 'company1',
    productId: overrides.productId ?? 'product1',
    pageNumber: overrides.pageNumber ?? 1,
    verified: overrides.verified ?? true,
    createdAt: overrides.createdAt ?? {} as Timestamp,
});

const defaultConfig: AiLinkingConfig = {
    maxSuggestions: 5,
    minConfidence: 70,
    autoApproveThreshold: 95,
    termsCacheTTL: 3600,
    enabled: true,
};

// ============================================================
// 테스트
// ============================================================

describe('staticMatching 정적 매칭 로직', () => {

    // ----------------------------------------------------------
    // 테스트 1: exact match → confidence 100
    // ----------------------------------------------------------
    describe('1. exact match → confidence 100', () => {
        it('용어가 content에 그대로 포함되면 confidence 100', () => {
            const terms = [
                makeTerm({ id: 'term1', term: '뇌졸중', commonAliases: [] }),
            ];
            const content = '이 문서는 뇌졸중에 관한 내용을 다룹니다.';

            const results = findMatchingTerms(content, terms, []);

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

        it('대소문자 무관하게 exact match', () => {
            const terms = [
                makeTerm({ id: 'term2', term: 'MRI', commonAliases: [] }),
            ];
            const content = '환자에게 mri 검사를 권장합니다.';

            const results = findMatchingTerms(content, terms, []);

            expect(results).toHaveLength(1);
            expect(results[0].confidence).toBe(100);
        });

        it('여러 용어가 content에 있으면 모두 매칭', () => {
            const terms = [
                makeTerm({ id: 'term1', term: '뇌졸중', commonAliases: [] }),
                makeTerm({ id: 'term2', term: '고혈압', commonAliases: [] }),
            ];
            const content = '뇌졸중과 고혈압은 상관관계가 있습니다.';

            const results = findMatchingTerms(content, terms, []);

            expect(results).toHaveLength(2);
            expect(results.every(r => r.confidence === 100)).toBe(true);
        });
    });

    // ----------------------------------------------------------
    // 테스트 2: alias match → confidence 90
    // ----------------------------------------------------------
    describe('2. alias match → confidence 90', () => {
        it('commonAlias가 content에 있으면 confidence 90', () => {
            const terms = [
                makeTerm({
                    id: 'term1',
                    term: '뇌졸중',
                    commonAliases: ['뇌혈관질환', '중풍'],
                }),
            ];
            const content = '환자는 뇌혈관질환 진단을 받았습니다.';

            const results = findMatchingTerms(content, terms, []);

            expect(results).toHaveLength(1);
            expect(results[0].termId).toBe('term1');
            expect(results[0].confidence).toBe(90);
            expect(results[0].explanation).toContain('alias match');
        });

        it('exact match가 없고 alias만 있으면 confidence 90', () => {
            const terms = [
                makeTerm({
                    id: 'term1',
                    term: '심근경색',
                    commonAliases: ['심장마비', '심장발작'],
                }),
            ];
            const content = '갑작스러운 심장마비로 응급실에 내원했습니다.';

            const results = findMatchingTerms(content, terms, []);

            expect(results).toHaveLength(1);
            expect(results[0].confidence).toBe(90);
        });

        it('exact match가 있으면 alias match보다 우선 (confidence 100)', () => {
            const terms = [
                makeTerm({
                    id: 'term1',
                    term: '뇌졸중',
                    commonAliases: ['뇌혈관질환'],
                }),
            ];
            // 본 용어와 alias 모두 있음
            const content = '뇌졸중(뇌혈관질환)으로 입원했습니다.';

            const results = findMatchingTerms(content, terms, []);

            expect(results).toHaveLength(1);
            expect(results[0].confidence).toBe(100); // exact match 우선
        });
    });

    // ----------------------------------------------------------
    // 테스트 3: 공백 제거 match → confidence 85
    // ----------------------------------------------------------
    describe('3. 공백 제거 match → confidence 85', () => {
        it('공백 제거 후 매칭되면 confidence 85', () => {
            const terms = [
                makeTerm({
                    id: 'term1',
                    term: '암 보험금',
                    commonAliases: [],
                }),
            ];
            // content에는 "암보험금"으로 붙어있음
            const content = '이 약관은 암보험금 지급 기준을 명시합니다.';

            const results = findMatchingTerms(content, terms, []);

            expect(results).toHaveLength(1);
            expect(results[0].confidence).toBe(85);
            expect(results[0].explanation).toContain('공백 제거');
        });
    });

    // ----------------------------------------------------------
    // 테스트 4: substring match → confidence 70
    // ----------------------------------------------------------
    describe('4. substring match → confidence 70', () => {
        it('용어의 앞부분 substring이 content에 있으면 confidence 70', () => {
            const terms = [
                makeTerm({
                    id: 'term1',
                    term: '뇌졸중진단',
                    commonAliases: [],
                }),
            ];
            // "뇌졸중"이 포함 (substring)
            const content = '환자는 뇌졸중 의심 증상으로 입원했습니다.';

            const results = findMatchingTerms(content, terms, []);

            expect(results).toHaveLength(1);
            expect(results[0].confidence).toBe(70);
        });
    });

    // ----------------------------------------------------------
    // 테스트 5: 기존 수동 링크와 중복 시 제외
    // ----------------------------------------------------------
    describe('5. 기존 수동 링크 중복 제외', () => {
        it('existingLinkIds에 포함된 term은 결과에서 제외', () => {
            const terms = [
                makeTerm({ id: 'term1', term: '뇌졸중', commonAliases: [] }),
                makeTerm({ id: 'term2', term: '고혈압', commonAliases: [] }),
            ];
            const content = '뇌졸중과 고혈압에 대한 내용입니다.';
            const existingLinkIds = ['term1']; // term1은 이미 링크됨

            const results = findMatchingTerms(content, terms, existingLinkIds);

            // term1은 제외, term2만 남아야 함
            expect(results).toHaveLength(1);
            expect(results[0].termId).toBe('term2');
        });

        it('모든 매칭 용어가 기존 링크에 있으면 빈 배열 반환', () => {
            const terms = [
                makeTerm({ id: 'term1', term: '뇌졸중', commonAliases: [] }),
            ];
            const content = '뇌졸중에 관한 내용입니다.';
            const existingLinkIds = ['term1'];

            const results = findMatchingTerms(content, terms, existingLinkIds);

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

    // ----------------------------------------------------------
    // 테스트 6: minConfidence 미만 결과 필터링
    // ----------------------------------------------------------
    describe('6. minConfidence 미만 필터링', () => {
        it('minConfidence=80이면 confidence<80 결과는 제외', () => {
            const terms = [
                makeTerm({ id: 'term1', term: '뇌졸중진단', commonAliases: [] }),
            ];
            // substring match → confidence 70 예상
            const content = '환자는 뇌졸중 의심 증상입니다.';

            const matchResults = findMatchingTerms(content, terms, []);
            const config: AiLinkingConfig = { ...defaultConfig, minConfidence: 80 };
            const filtered = filterAndSortResults(matchResults, config);

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

        it('minConfidence=70이면 confidence=70 결과는 포함', () => {
            const terms = [
                makeTerm({ id: 'term1', term: '뇌졸중진단', commonAliases: [] }),
            ];
            const content = '환자는 뇌졸중 의심 증상입니다.';

            const matchResults = findMatchingTerms(content, terms, []);
            const config: AiLinkingConfig = { ...defaultConfig, minConfidence: 70 };
            const filtered = filterAndSortResults(matchResults, config);

            expect(filtered.length).toBeGreaterThanOrEqual(0); // 매칭에 따라 달라짐
        });

        it('confidence=100인 결과는 어떤 minConfidence 설정에서도 (100 이하) 포함', () => {
            const terms = [
                makeTerm({ id: 'term1', term: '뇌졸중', commonAliases: [] }),
            ];
            const content = '뇌졸중 진단을 받았습니다.';

            const matchResults = findMatchingTerms(content, terms, []);
            const config: AiLinkingConfig = { ...defaultConfig, minConfidence: 95 };
            const filtered = filterAndSortResults(matchResults, config);

            expect(filtered).toHaveLength(1);
            expect(filtered[0].confidence).toBe(100);
        });
    });

    // ----------------------------------------------------------
    // 테스트 7: maxSuggestions 초과 시 잘림
    // ----------------------------------------------------------
    describe('7. maxSuggestions 초과 시 잘림', () => {
        it('maxSuggestions=3이면 결과가 최대 3개', () => {
            const terms = [
                makeTerm({ id: 't1', term: '뇌졸중', commonAliases: [] }),
                makeTerm({ id: 't2', term: '고혈압', commonAliases: [] }),
                makeTerm({ id: 't3', term: '당뇨병', commonAliases: [] }),
                makeTerm({ id: 't4', term: '심근경색', commonAliases: [] }),
                makeTerm({ id: 't5', term: '암', commonAliases: [] }),
            ];
            const content = '뇌졸중, 고혈압, 당뇨병, 심근경색, 암 보험 관련 내용';

            const matchResults = findMatchingTerms(content, terms, []);
            const config: AiLinkingConfig = { ...defaultConfig, maxSuggestions: 3 };
            const filtered = filterAndSortResults(matchResults, config);

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

        it('결과가 maxSuggestions보다 적으면 그대로 반환', () => {
            const terms = [
                makeTerm({ id: 't1', term: '뇌졸중', commonAliases: [] }),
            ];
            const content = '뇌졸중 관련 내용';

            const matchResults = findMatchingTerms(content, terms, []);
            const config: AiLinkingConfig = { ...defaultConfig, maxSuggestions: 5 };
            const filtered = filterAndSortResults(matchResults, config);

            expect(filtered).toHaveLength(1);
        });

        it('maxSuggestions=0이면 빈 배열', () => {
            const terms = [
                makeTerm({ id: 't1', term: '뇌졸중', commonAliases: [] }),
            ];
            const content = '뇌졸중 관련 내용';

            const matchResults = findMatchingTerms(content, terms, []);
            const config: AiLinkingConfig = { ...defaultConfig, maxSuggestions: 0 };
            const filtered = filterAndSortResults(matchResults, config);

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

        it('높은 confidence 결과가 우선 선택됨', () => {
            const terms = [
                makeTerm({ id: 't1', term: '뇌졸중', commonAliases: ['뇌혈관질환'] }),
                makeTerm({ id: 't2', term: '고혈압', commonAliases: [] }),
            ];
            // t1은 alias match(confidence 90), t2는 exact match(confidence 100)
            const content = '고혈압과 뇌혈관질환은 상관관계가 있습니다.';

            const matchResults = findMatchingTerms(content, terms, []);
            const config: AiLinkingConfig = { ...defaultConfig, maxSuggestions: 1 };
            const filtered = filterAndSortResults(matchResults, config);

            expect(filtered).toHaveLength(1);
            expect(filtered[0].termId).toBe('t2'); // exact match(100)가 먼저
        });
    });

    // ----------------------------------------------------------
    // 테스트 8: 비활성(enabled: false) 시 스킵
    // ----------------------------------------------------------
    describe('8. enabled=false 시 스킵', () => {
        it('enabled=false이면 매칭 결과가 있어도 빈 배열 반환', () => {
            const terms = [
                makeTerm({ id: 't1', term: '뇌졸중', commonAliases: [] }),
            ];
            const content = '뇌졸중 관련 내용입니다.';

            const matchResults = findMatchingTerms(content, terms, []);
            const config: AiLinkingConfig = { ...defaultConfig, enabled: false };
            const filtered = filterAndSortResults(matchResults, config);

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

        it('enabled=true이면 정상 처리', () => {
            const terms = [
                makeTerm({ id: 't1', term: '뇌졸중', commonAliases: [] }),
            ];
            const content = '뇌졸중 관련 내용입니다.';

            const matchResults = findMatchingTerms(content, terms, []);
            const config: AiLinkingConfig = { ...defaultConfig, enabled: true };
            const filtered = filterAndSortResults(matchResults, config);

            expect(filtered).toHaveLength(1);
        });
    });

    // ----------------------------------------------------------
    // 추가 테스트: buildTermDictionary
    // ----------------------------------------------------------
    describe('buildTermDictionary 용어 사전 구축', () => {
        it('term과 commonAliases 모두 사전에 등록', () => {
            const terms = [
                makeTerm({
                    id: 'term1',
                    term: '뇌졸중',
                    commonAliases: ['뇌혈관질환', '중풍'],
                }),
            ];

            const dict = buildTermDictionary(terms);

            expect(dict.has('뇌졸중')).toBe(true);
            expect(dict.has('뇌혈관질환')).toBe(true);
            expect(dict.has('중풍')).toBe(true);
            expect(dict.get('뇌졸중')?.id).toBe('term1');
            expect(dict.get('뇌혈관질환')?.id).toBe('term1');
        });

        it('중복 alias는 먼저 등록된 것 유지', () => {
            const terms = [
                makeTerm({ id: 'term1', term: '뇌졸중', commonAliases: ['중풍'] }),
                makeTerm({ id: 'term2', term: '뇌경색', commonAliases: ['중풍'] }),
            ];

            const dict = buildTermDictionary(terms);

            // '중풍'은 term1(뇌졸중)에 먼저 등록되어 있어야 함
            expect(dict.get('중풍')?.id).toBe('term1');
        });
    });

    // ----------------------------------------------------------
    // 추가 테스트: 매칭 우선순위 종합
    // ----------------------------------------------------------
    describe('매칭 우선순위 종합', () => {
        it('exact > alias > 공백제거 > substring 순서 적용', () => {
            const terms = [
                makeTerm({ id: 'e1', term: '뇌졸중', commonAliases: [] }),           // exact
                makeTerm({ id: 'a1', term: '심근경색', commonAliases: ['심장마비'] }), // alias
            ];
            const content = '뇌졸중과 심장마비에 대한 내용';

            const results = findMatchingTerms(content, terms, []);

            const exactResult = results.find(r => r.termId === 'e1');
            const aliasResult = results.find(r => r.termId === 'a1');

            expect(exactResult?.confidence).toBe(100);
            expect(aliasResult?.confidence).toBe(90);
        });
    });

});
