/**
 * indexLogger.ts 단위 테스트
 *
 * 테스트 대상:
 *  - 평균 청크 크기 올바르게 계산되는지
 *  - 단서조항 보존율 올바르게 계산되는지
 *  - 조항 수(제N조 패턴) 올바르게 카운트되는지
 *  - 재인덱싱 시 addedArticles/removedArticles 비례 추정
 *  - blueGreenMode=true 시 switchedAt이 serverTimestamp인지
 *  - Firestore에 올바른 데이터가 저장되는지
 */

import { describe, it, expect, vi, beforeEach } from 'vitest';

// ─────────────────────────────────────────────
// vi.hoisted()로 mock 변수 선언
// ─────────────────────────────────────────────

const { mockAdd, mockCollection, mockFirestore } = vi.hoisted(() => {
    const mockAdd = vi.fn().mockResolvedValue({ id: 'log-doc-id' });
    const mockCollection = vi.fn().mockReturnValue({ add: mockAdd });
    const mockFirestore = vi.fn().mockReturnValue({ collection: mockCollection });
    return { mockAdd, mockCollection, mockFirestore };
});

vi.mock('@/lib/firebase-admin', () => ({
    getFirebaseAdmin: vi.fn().mockReturnValue({
        firestore: mockFirestore,
    }),
}));

vi.mock('firebase-admin/firestore', () => ({
    FieldValue: {
        increment: vi.fn((n: number) => ({ _increment: n })),
        serverTimestamp: vi.fn(() => ({ _serverTimestamp: true })),
    },
}));

import { logIndexingResult } from '../indexLogger';

// ─────────────────────────────────────────────
// beforeEach: mock 초기화
// ─────────────────────────────────────────────

beforeEach(() => {
    vi.clearAllMocks();
    mockAdd.mockResolvedValue({ id: 'log-doc-id' });
    mockCollection.mockReturnValue({ add: mockAdd });
    mockFirestore.mockReturnValue({ collection: mockCollection });
});

// ─────────────────────────────────────────────
// 기본 호출 파라미터 헬퍼
// ─────────────────────────────────────────────

function baseParams(overrides: Partial<Parameters<typeof logIndexingResult>[0]> = {}) {
    return {
        productId: 'prod-001',
        companyId: 'comp-001',
        jobId: 'job-001',
        isReindex: false,
        blueGreenMode: false,
        previousChunksCount: 0,
        newChunksCount: 3,
        chunks: ['청크1 내용', '청크2 내용', '청크3 내용'],
        ...overrides,
    };
}

// ─────────────────────────────────────────────
// 평균 청크 크기 계산
// ─────────────────────────────────────────────

describe('평균 청크 크기 (avgChunkSize)', () => {
    it('모든 청크 문자 수의 평균이 올바르게 계산되어야 한다', async () => {
        // 청크: "aa"(2자), "bbbb"(4자), "cccccc"(6자) → 평균 = (2+4+6)/3 = 4
        const chunks = ['aa', 'bbbb', 'cccccc'];
        await logIndexingResult(baseParams({ chunks, newChunksCount: chunks.length }));

        const [savedData] = mockAdd.mock.calls[0];
        expect(savedData.avgChunkSize).toBe(4);
    });

    it('청크가 비어있으면 avgChunkSize=0이어야 한다', async () => {
        await logIndexingResult(baseParams({ chunks: [], newChunksCount: 0 }));

        const [savedData] = mockAdd.mock.calls[0];
        expect(savedData.avgChunkSize).toBe(0);
    });

    it('소수점은 반올림되어야 한다 (1자+2자 → 평균 1.5 → 2)', async () => {
        const chunks = ['a', 'bb'];
        await logIndexingResult(baseParams({ chunks, newChunksCount: chunks.length }));

        const [savedData] = mockAdd.mock.calls[0];
        expect(savedData.avgChunkSize).toBe(2);
    });
});

// ─────────────────────────────────────────────
// 단서조항 보존율 계산
// ─────────────────────────────────────────────

describe('단서조항 보존율 (provisoPreservationRate)', () => {
    it('단서 키워드가 있는 청크 비율을 올바르게 계산해야 한다', async () => {
        // 3개 중 1개("다만" 포함) → 보존율 = 1/3 ≈ 0.333
        const chunks = [
            '일반 약관 조항 내용입니다.',
            '다만 이 경우에는 예외가 적용됩니다.',
            '보험금 지급 기준에 대한 내용입니다.',
        ];
        await logIndexingResult(baseParams({ chunks, newChunksCount: chunks.length }));

        const [savedData] = mockAdd.mock.calls[0];
        expect(savedData.provisoPreservationRate).toBeCloseTo(0.333, 2);
    });

    it('단서 키워드 "그러나"가 있는 청크도 카운트되어야 한다', async () => {
        const chunks = [
            '그러나 다음의 경우에는 보장하지 않습니다.',
            '일반 약관 조항 내용입니다.',
        ];
        await logIndexingResult(baseParams({ chunks, newChunksCount: chunks.length }));

        const [savedData] = mockAdd.mock.calls[0];
        // 1/2 = 0.5
        expect(savedData.provisoPreservationRate).toBe(0.5);
    });

    it('단서 키워드가 없는 청크만 있으면 보존율=0이어야 한다', async () => {
        const chunks = [
            '제1조 보험계약의 성립',
            '제2조 보험금 지급 사유',
        ];
        await logIndexingResult(baseParams({ chunks, newChunksCount: chunks.length }));

        const [savedData] = mockAdd.mock.calls[0];
        expect(savedData.provisoPreservationRate).toBe(0);
    });

    it('청크가 비어있으면 보존율=0이어야 한다', async () => {
        await logIndexingResult(baseParams({ chunks: [], newChunksCount: 0 }));

        const [savedData] = mockAdd.mock.calls[0];
        expect(savedData.provisoPreservationRate).toBe(0);
    });
});

// ─────────────────────────────────────────────
// 조항 수 카운트 (totalArticlesCount)
// ─────────────────────────────────────────────

describe('조항 수 카운트 (totalArticlesCount)', () => {
    it('"제N조" 패턴이 포함된 청크에서 조항 수를 올바르게 카운트해야 한다', async () => {
        const chunks = [
            '제1조 보험계약의 성립\n이 계약은 보험청약서에 의하여 성립합니다.',
            '제2조 보험금 지급 사유\n피보험자가 질병으로 인하여 입원한 경우.',
            '제3조 면책 사항\n다만 이 경우에는 보장하지 않습니다.',
        ];
        await logIndexingResult(baseParams({ chunks, newChunksCount: chunks.length }));

        const [savedData] = mockAdd.mock.calls[0];
        expect(savedData.totalArticlesCount).toBe(3);
    });

    it('동일한 조항이 여러 청크에 걸쳐 있어도 중복 카운트하지 않아야 한다', async () => {
        const chunks = [
            '제1조 보험계약 성립 (1항)',
            '제1조 보험계약 성립 (2항)', // 동일 조항 → Set으로 중복 제거
            '제2조 보험금 지급 사유',
        ];
        await logIndexingResult(baseParams({ chunks, newChunksCount: chunks.length }));

        const [savedData] = mockAdd.mock.calls[0];
        // 공백 제거 후 "제1조"와 "제2조" → 2개
        expect(savedData.totalArticlesCount).toBe(2);
    });

    it('조항 패턴이 없는 청크에서는 조항 수=0이어야 한다', async () => {
        const chunks = [
            '일반 설명 텍스트입니다.',
            '부록: 보험료 산출 기준',
        ];
        await logIndexingResult(baseParams({ chunks, newChunksCount: chunks.length }));

        const [savedData] = mockAdd.mock.calls[0];
        expect(savedData.totalArticlesCount).toBe(0);
    });
});

// ─────────────────────────────────────────────
// addedArticles / removedArticles 비례 추정
// ─────────────────────────────────────────────

describe('addedArticles / removedArticles 비례 추정 (재인덱싱)', () => {
    it('재인덱싱 시 청크 수 증가 → addedArticles > 0, removedArticles=0이어야 한다', async () => {
        // newChunksCount=20, previousChunksCount=10, chunksDelta=10
        // 조항 수: 제1조~제4조 = 4개
        // addedArticles = Math.round(4 * (10/20)) = Math.round(2) = 2
        const chunks = [
            '제1조 보험계약의 성립',
            '제2조 보험금 지급 사유',
            '제3조 면책 사항',
            '제4조 계약의 해지',
        ];
        await logIndexingResult(baseParams({
            chunks,
            isReindex: true,
            previousChunksCount: 10,
            newChunksCount: 20,
        }));

        const [savedData] = mockAdd.mock.calls[0];
        expect(savedData.addedArticles).toBeGreaterThan(0);
        expect(savedData.removedArticles).toBe(0);
    });

    it('재인덱싱 시 청크 수 감소 → removedArticles > 0, addedArticles=0이어야 한다', async () => {
        // newChunksCount=10, previousChunksCount=20, chunksDelta=-10
        // 조항 수: 제1조~제4조 = 4개
        // removedArticles = Math.round(4 * (10/20)) = Math.round(2) = 2
        const chunks = [
            '제1조 보험계약의 성립',
            '제2조 보험금 지급 사유',
            '제3조 면책 사항',
            '제4조 계약의 해지',
        ];
        await logIndexingResult(baseParams({
            chunks,
            isReindex: true,
            previousChunksCount: 20,
            newChunksCount: 10,
        }));

        const [savedData] = mockAdd.mock.calls[0];
        expect(savedData.removedArticles).toBeGreaterThan(0);
        expect(savedData.addedArticles).toBe(0);
    });

    it('재인덱싱이 아닌 경우(isReindex=false) addedArticles=0, removedArticles=0이어야 한다', async () => {
        const chunks = ['제1조 보험계약의 성립', '제2조 보험금'];
        await logIndexingResult(baseParams({
            chunks,
            isReindex: false,
            previousChunksCount: 0,
            newChunksCount: chunks.length,
        }));

        const [savedData] = mockAdd.mock.calls[0];
        expect(savedData.addedArticles).toBe(0);
        expect(savedData.removedArticles).toBe(0);
    });

    it('재인덱싱이어도 previousChunksCount=0이면 addedArticles=0, removedArticles=0이어야 한다', async () => {
        const chunks = ['제1조 보험계약의 성립', '제2조 보험금'];
        await logIndexingResult(baseParams({
            chunks,
            isReindex: true,
            previousChunksCount: 0,
            newChunksCount: chunks.length,
        }));

        const [savedData] = mockAdd.mock.calls[0];
        expect(savedData.addedArticles).toBe(0);
        expect(savedData.removedArticles).toBe(0);
    });
});

// ─────────────────────────────────────────────
// blueGreenMode & switchedAt
// ─────────────────────────────────────────────

describe('blueGreenMode 및 switchedAt', () => {
    it('blueGreenMode=true 시 switchedAt이 serverTimestamp이어야 한다', async () => {
        await logIndexingResult(baseParams({ blueGreenMode: true }));

        const [savedData] = mockAdd.mock.calls[0];
        expect(savedData.blueGreenMode).toBe(true);
        expect((savedData.switchedAt as any)._serverTimestamp).toBe(true);
    });

    it('blueGreenMode=false 시 switchedAt이 null이어야 한다', async () => {
        await logIndexingResult(baseParams({ blueGreenMode: false }));

        const [savedData] = mockAdd.mock.calls[0];
        expect(savedData.blueGreenMode).toBe(false);
        expect(savedData.switchedAt).toBeNull();
    });
});

// ─────────────────────────────────────────────
// Firestore 저장 데이터 검증
// ─────────────────────────────────────────────

describe('Firestore 저장 데이터 검증', () => {
    it('index_logs 컬렉션에 올바른 필드가 저장되어야 한다', async () => {
        const chunks = ['제1조 일반 약관 내용', '다만 예외 조항이 있습니다.'];
        await logIndexingResult({
            productId: 'prod-test',
            companyId: 'comp-test',
            jobId: 'job-test',
            isReindex: true,
            blueGreenMode: true,
            previousChunksCount: 5,
            newChunksCount: 7,
            chunks,
        });

        // collection 이름 확인
        expect(mockCollection).toHaveBeenCalledWith('index_logs');

        // 저장된 데이터 확인
        const [savedData] = mockAdd.mock.calls[0];
        expect(savedData.productId).toBe('prod-test');
        expect(savedData.companyId).toBe('comp-test');
        expect(savedData.jobId).toBe('job-test');
        expect(savedData.isReindex).toBe(true);
        expect(savedData.blueGreenMode).toBe(true);
        expect(savedData.previousChunksCount).toBe(5);
        expect(savedData.newChunksCount).toBe(7);
        // chunksDelta = 7 - 5 = 2
        expect(savedData.chunksDelta).toBe(2);
        // createdAt은 serverTimestamp
        expect((savedData.createdAt as any)._serverTimestamp).toBe(true);
    });

    it('add()가 정확히 1회 호출되어야 한다', async () => {
        await logIndexingResult(baseParams());
        expect(mockAdd).toHaveBeenCalledOnce();
    });
});
