/**
 * RelatedDocsSidebar 컴포넌트 단위 테스트
 * 이리스(개발1팀 FE) 작성 — Task 820.1 Phase 1-A
 *
 * 테스트 케이스:
 *  1. 추천이 없으면 null 렌더링
 *  2. 추천 목록이 confidence 내림차순 정렬되어 표시
 *  3. 승인 버튼 클릭 시 links 컬렉션에 문서 생성
 *  4. 거부 버튼 클릭 시 dismissed: true 마킹
 *  5. 로딩 상태 표시
 *
 * 환경: jsdom
 * 프레임워크: vitest + @testing-library/react
 */

// @vitest-environment jsdom

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { render, screen, fireEvent, waitFor, act } from '@testing-library/react';
import { Timestamp } from 'firebase/firestore';

// ── Firebase/Firestore 모킹 ─────────────────────────────────────────────────

const mockGetDocs = vi.fn();
const mockGetDoc = vi.fn();
const mockAddDoc = vi.fn();
const mockUpdateDoc = vi.fn();
const mockCollection = vi.fn();
const mockDoc = vi.fn();
const mockQuery = vi.fn();
const mockWhere = vi.fn();
const mockOrderBy = vi.fn();
const mockLimit = vi.fn();
const mockServerTimestamp = vi.fn(() => ({ _serverTimestamp: true }));

vi.mock('firebase/firestore', () => ({
    collection: (...args: unknown[]) => mockCollection(...args),
    doc: (...args: unknown[]) => mockDoc(...args),
    query: (...args: unknown[]) => mockQuery(...args),
    where: (...args: unknown[]) => mockWhere(...args),
    orderBy: (...args: unknown[]) => mockOrderBy(...args),
    limit: (...args: unknown[]) => mockLimit(...args),
    getDocs: (...args: unknown[]) => mockGetDocs(...args),
    getDoc: (...args: unknown[]) => mockGetDoc(...args),
    addDoc: (...args: unknown[]) => mockAddDoc(...args),
    updateDoc: (...args: unknown[]) => mockUpdateDoc(...args),
    serverTimestamp: () => mockServerTimestamp(),
    arrayUnion: (...args: unknown[]) => ({ _arrayUnion: args }),
    Timestamp: {
        now: () => ({ seconds: 1700000000, nanoseconds: 0 }),
        fromDate: (d: Date) => ({ seconds: Math.floor(d.getTime() / 1000), nanoseconds: 0 }),
    },
}));

vi.mock('@/lib/firebase', () => ({
    db: {},
}));

// ── sonner toast 모킹 ───────────────────────────────────────────────────────

const mockToastSuccess = vi.fn();
const mockToastError = vi.fn();

vi.mock('sonner', () => ({
    toast: {
        success: (...args: unknown[]) => mockToastSuccess(...args),
        error: (...args: unknown[]) => mockToastError(...args),
    },
}));

// ── next/navigation 모킹 ────────────────────────────────────────────────────

vi.mock('next/navigation', () => ({
    useRouter: () => ({ push: vi.fn() }),
}));

// ── next/link 모킹 ──────────────────────────────────────────────────────────

vi.mock('next/link', () => ({
    default: ({ href, children, className, onClick }: {
        href: string;
        children: React.ReactNode;
        className?: string;
        onClick?: (e: React.MouseEvent) => void;
    }) => (
        <a href={href} className={className} onClick={onClick}>
            {children}
        </a>
    ),
}));

// ── AuthContext 모킹 ─────────────────────────────────────────────────────────

vi.mock('@/contexts/AuthContext', () => ({
    useAuth: () => ({
        user: { uid: 'test-user-123', email: 'test@example.com' },
    }),
}));

// ── 테스트 헬퍼 ─────────────────────────────────────────────────────────────

import React from 'react';

/** AiSuggestion 픽스처 생성 */
function makeSuggestion(overrides: Partial<{
    id: string;
    targetDocId: string;
    targetTitle: string;
    method: string;
    confidence: number;
    createdBy: string;
    dismissed: boolean;
}> = {}) {
    return {
        id: overrides.id ?? 'sug-1',
        targetDocId: overrides.targetDocId ?? 'doc-target-1',
        targetTitle: overrides.targetTitle ?? '관련 문서 제목',
        method: overrides.method ?? 'embedding',
        confidence: overrides.confidence ?? 80,
        createdBy: overrides.createdBy ?? 'system',
        dismissed: overrides.dismissed ?? false,
        createdAt: { seconds: 1700000000, nanoseconds: 0 },
    };
}

/** getDocs 반환값 픽스처 */
function makeSnapshot(suggestions: ReturnType<typeof makeSuggestion>[]) {
    return {
        docs: suggestions.map(sug => ({
            id: sug.id,
            data: () => sug,
        })),
        empty: suggestions.length === 0,
        size: suggestions.length,
    };
}

// ── 테스트 ──────────────────────────────────────────────────────────────────

describe('RelatedDocsSidebar', () => {
    let RelatedDocsSidebar: typeof import('../RelatedDocsSidebar').default;

    beforeEach(async () => {
        vi.clearAllMocks();

        // 기본값: 빈 snapshot 반환
        mockGetDocs.mockResolvedValue(makeSnapshot([]));
        mockAddDoc.mockResolvedValue({ id: 'new-link-id' });
        mockUpdateDoc.mockResolvedValue(undefined);
        // mockDoc은 호출 인자를 path로 기록한 객체를 반환
        mockDoc.mockImplementation((...args: unknown[]) => {
            const pathStr = args.filter(a => typeof a === 'string').join('/');
            return { _path: pathStr };
        });
        // getDoc 기본값: config 문서 없음, targetDoc 존재함
        // config/aiLinking은 exists:false, documents/{id}는 exists:true
        mockGetDoc.mockImplementation((docRef: unknown) => {
            const ref = docRef as { _path?: string };
            if (ref?._path?.startsWith('config/')) {
                return Promise.resolve({ exists: () => false });
            }
            return Promise.resolve({ exists: () => true });
        });

        // 동적 import로 모킹 이후 컴포넌트 로드
        const mod = await import('../RelatedDocsSidebar');
        RelatedDocsSidebar = mod.default;
    });

    afterEach(() => {
        vi.restoreAllMocks();
    });

    // ─────────────────────────────────────────────────────────────
    // TC-1: 추천이 없으면 null 렌더링
    // ─────────────────────────────────────────────────────────────
    describe('TC-1: 추천이 없으면 null 렌더링', () => {
        it('ai_suggestions가 빈 경우 컴포넌트가 null을 반환한다', async () => {
            mockGetDocs.mockResolvedValue(makeSnapshot([]));

            const { container } = render(
                <RelatedDocsSidebar documentId="doc-1" documentTitle="테스트 문서" />
            );

            // 로딩 완료 대기
            await waitFor(() => {
                expect(container.firstChild).toBeNull();
            });
        });

        it('모든 항목이 dismissed=true이면 null을 반환한다', async () => {
            mockGetDocs.mockResolvedValue(makeSnapshot([
                makeSuggestion({ id: 'sug-1', dismissed: true }),
                makeSuggestion({ id: 'sug-2', dismissed: true }),
            ]));

            const { container } = render(
                <RelatedDocsSidebar documentId="doc-1" documentTitle="테스트 문서" />
            );

            await waitFor(() => {
                expect(container.firstChild).toBeNull();
            });
        });

        it('static method만 있으면 null을 반환한다', async () => {
            mockGetDocs.mockResolvedValue(makeSnapshot([
                makeSuggestion({ id: 'sug-1', method: 'static', targetTitle: '용어 문서' }),
                makeSuggestion({ id: 'sug-2', method: 'static', targetTitle: '다른 용어' }),
            ]));

            const { container } = render(
                <RelatedDocsSidebar documentId="doc-1" documentTitle="테스트 문서" />
            );

            await waitFor(() => {
                expect(container.firstChild).toBeNull();
            });
        });
    });

    // ─────────────────────────────────────────────────────────────
    // TC-2: confidence 내림차순 정렬 표시
    // ─────────────────────────────────────────────────────────────
    describe('TC-2: 추천 목록이 confidence 내림차순 정렬되어 표시', () => {
        it('confidence 95, 72, 85 순서로 입력 시 95 → 85 → 72 순서로 렌더링된다', async () => {
            mockGetDocs.mockResolvedValue(makeSnapshot([
                makeSuggestion({ id: 'sug-1', targetTitle: '문서 A', confidence: 95 }),
                makeSuggestion({ id: 'sug-2', targetTitle: '문서 B', confidence: 72 }),
                makeSuggestion({ id: 'sug-3', targetTitle: '문서 C', confidence: 85 }),
            ]));

            render(
                <RelatedDocsSidebar documentId="doc-1" documentTitle="테스트 문서" />
            );

            await waitFor(() => {
                expect(screen.getByText('문서 A')).toBeDefined();
            });

            const titles = screen.getAllByRole('link').map((el: HTMLElement) => el.textContent?.trim());
            const docATitleIdx = titles.findIndex((t: string | undefined) => t?.includes('문서 A'));
            const docBTitleIdx = titles.findIndex((t: string | undefined) => t?.includes('문서 B'));
            const docCTitleIdx = titles.findIndex((t: string | undefined) => t?.includes('문서 C'));

            // confidence 높은 순: 문서 A(95) > 문서 C(85) > 문서 B(72)
            expect(docATitleIdx).toBeLessThan(docCTitleIdx);
            expect(docCTitleIdx).toBeLessThan(docBTitleIdx);
        });

        it('최대 5개까지만 표시된다', async () => {
            const suggestions = Array.from({ length: 7 }, (_, i) =>
                makeSuggestion({
                    id: `sug-${i}`,
                    targetTitle: `문서 ${i + 1}`,
                    confidence: 90 - i,
                })
            );
            mockGetDocs.mockResolvedValue(makeSnapshot(suggestions));

            render(
                <RelatedDocsSidebar documentId="doc-1" documentTitle="테스트 문서" />
            );

            await waitFor(() => {
                expect(screen.getByText('문서 1')).toBeDefined();
            });

            // 6번째, 7번째 문서는 표시되지 않아야 함
            expect(screen.queryByText('문서 6')).toBeNull();
            expect(screen.queryByText('문서 7')).toBeNull();
        });
    });

    // ─────────────────────────────────────────────────────────────
    // TC-3: 승인 버튼 클릭 시 links 컬렉션에 문서 생성
    // ─────────────────────────────────────────────────────────────
    describe('TC-3: 승인 버튼 클릭 시 links 컬렉션에 문서 생성', () => {
        it('승인 버튼 클릭 시 소스 문서의 outgoingLinks/outgoingLinkIds가 업데이트된다', async () => {
            mockGetDocs.mockResolvedValue(makeSnapshot([
                makeSuggestion({
                    id: 'sug-1',
                    targetDocId: 'target-doc-1',
                    targetTitle: '대상 문서',
                    method: 'embedding',
                    confidence: 88,
                }),
            ]));

            render(
                <RelatedDocsSidebar documentId="doc-source-1" documentTitle="소스 문서" />
            );

            await waitFor(() => {
                expect(screen.getByText('대상 문서')).toBeDefined();
            });

            // 승인 버튼 (체크 아이콘 버튼) 클릭
            const approveButtons = screen.getAllByTitle('관련 문서로 연결');
            await act(async () => {
                fireEvent.click(approveButtons[0]);
            });

            // updateDoc이 2번 호출됨 (1: outgoingLinks, 2: dismissed)
            await waitFor(() => {
                expect(mockUpdateDoc).toHaveBeenCalledTimes(2);
            });

            // 첫 번째 호출: 소스 문서의 outgoingLinks/outgoingLinkIds 업데이트
            const firstCall = mockUpdateDoc.mock.calls[0];
            expect(firstCall[0]._path).toContain('doc-source-1');
            const updateData = firstCall[1];
            expect(updateData.outgoingLinks).toEqual({ _arrayUnion: ['대상 문서'] });
            expect(updateData.outgoingLinkIds).toEqual({ _arrayUnion: ['target-doc-1'] });
            expect(updateData.updatedAt).toBeDefined();
        });

        it('승인 후 ai_suggestions에 dismissed: true 마킹이 호출된다', async () => {
            mockGetDocs.mockResolvedValue(makeSnapshot([
                makeSuggestion({ id: 'sug-1', targetTitle: '대상 문서' }),
            ]));

            render(
                <RelatedDocsSidebar documentId="doc-source-1" documentTitle="소스 문서" />
            );

            await waitFor(() => {
                expect(screen.getByText('대상 문서')).toBeDefined();
            });

            const approveButtons = screen.getAllByTitle('관련 문서로 연결');
            await act(async () => {
                fireEvent.click(approveButtons[0]);
            });

            await waitFor(() => {
                expect(mockUpdateDoc).toHaveBeenCalledTimes(2);
            });

            // 두 번째 호출이 dismissed 마킹
            const dismissCall = mockUpdateDoc.mock.calls[1];
            expect(dismissCall[1]).toMatchObject({ dismissed: true });
        });

        it('승인 후 해당 항목이 UI에서 제거된다 (optimistic update)', async () => {
            mockGetDocs.mockResolvedValue(makeSnapshot([
                makeSuggestion({ id: 'sug-1', targetTitle: '제거될 문서' }),
                makeSuggestion({ id: 'sug-2', targetTitle: '남아있는 문서', confidence: 70 }),
            ]));

            render(
                <RelatedDocsSidebar documentId="doc-source-1" documentTitle="소스 문서" />
            );

            await waitFor(() => {
                expect(screen.getByText('제거될 문서')).toBeDefined();
            });

            const approveButtons = screen.getAllByTitle('관련 문서로 연결');
            await act(async () => {
                fireEvent.click(approveButtons[0]);
            });

            await waitFor(() => {
                expect(screen.queryByText('제거될 문서')).toBeNull();
            });
            expect(screen.getByText('남아있는 문서')).toBeDefined();
        });

        it('승인 성공 시 토스트 메시지가 표시된다', async () => {
            mockGetDocs.mockResolvedValue(makeSnapshot([
                makeSuggestion({ id: 'sug-1', targetTitle: '대상 문서' }),
            ]));

            render(
                <RelatedDocsSidebar documentId="doc-source-1" documentTitle="소스 문서" />
            );

            await waitFor(() => {
                expect(screen.getByText('대상 문서')).toBeDefined();
            });

            const approveButtons = screen.getAllByTitle('관련 문서로 연결');
            await act(async () => {
                fireEvent.click(approveButtons[0]);
            });

            await waitFor(() => {
                expect(mockToastSuccess).toHaveBeenCalledWith('관련 문서가 연결되었습니다');
            });
        });
    });

    // ─────────────────────────────────────────────────────────────
    // TC-4: 거부 버튼 클릭 시 dismissed: true 마킹
    // ─────────────────────────────────────────────────────────────
    describe('TC-4: 거부 버튼 클릭 시 dismissed: true 마킹', () => {
        it('거부 버튼 클릭 시 updateDoc에 dismissed: true가 전달된다', async () => {
            mockGetDocs.mockResolvedValue(makeSnapshot([
                makeSuggestion({ id: 'sug-1', targetTitle: '거부할 문서' }),
            ]));

            render(
                <RelatedDocsSidebar documentId="doc-source-1" documentTitle="소스 문서" />
            );

            await waitFor(() => {
                expect(screen.getByText('거부할 문서')).toBeDefined();
            });

            const dismissButtons = screen.getAllByTitle('추천 거부');
            await act(async () => {
                fireEvent.click(dismissButtons[0]);
            });

            await waitFor(() => {
                expect(mockUpdateDoc).toHaveBeenCalledTimes(1);
            });

            const updateCall = mockUpdateDoc.mock.calls[0];
            expect(updateCall[1]).toMatchObject({ dismissed: true });
        });

        it('거부 후 links 컬렉션에 문서가 생성되지 않는다', async () => {
            mockGetDocs.mockResolvedValue(makeSnapshot([
                makeSuggestion({ id: 'sug-1', targetTitle: '거부할 문서' }),
            ]));

            render(
                <RelatedDocsSidebar documentId="doc-source-1" documentTitle="소스 문서" />
            );

            await waitFor(() => {
                expect(screen.getByText('거부할 문서')).toBeDefined();
            });

            const dismissButtons = screen.getAllByTitle('추천 거부');
            await act(async () => {
                fireEvent.click(dismissButtons[0]);
            });

            await waitFor(() => {
                expect(mockUpdateDoc).toHaveBeenCalledTimes(1);
            });

            expect(mockAddDoc).not.toHaveBeenCalled();
        });

        it('거부 후 해당 항목이 UI에서 제거된다', async () => {
            mockGetDocs.mockResolvedValue(makeSnapshot([
                makeSuggestion({ id: 'sug-1', targetTitle: '거부할 문서' }),
                makeSuggestion({ id: 'sug-2', targetTitle: '남아있는 문서', confidence: 70 }),
            ]));

            render(
                <RelatedDocsSidebar documentId="doc-source-1" documentTitle="소스 문서" />
            );

            await waitFor(() => {
                expect(screen.getByText('거부할 문서')).toBeDefined();
            });

            const dismissButtons = screen.getAllByTitle('추천 거부');
            await act(async () => {
                fireEvent.click(dismissButtons[0]);
            });

            await waitFor(() => {
                expect(screen.queryByText('거부할 문서')).toBeNull();
            });
            expect(screen.getByText('남아있는 문서')).toBeDefined();
        });

        it('거부 성공 시 토스트 메시지가 표시된다', async () => {
            mockGetDocs.mockResolvedValue(makeSnapshot([
                makeSuggestion({ id: 'sug-1', targetTitle: '거부할 문서' }),
            ]));

            render(
                <RelatedDocsSidebar documentId="doc-source-1" documentTitle="소스 문서" />
            );

            await waitFor(() => {
                expect(screen.getByText('거부할 문서')).toBeDefined();
            });

            const dismissButtons = screen.getAllByTitle('추천 거부');
            await act(async () => {
                fireEvent.click(dismissButtons[0]);
            });

            await waitFor(() => {
                expect(mockToastSuccess).toHaveBeenCalledWith('추천이 거부되었습니다');
            });
        });
    });

    // ─────────────────────────────────────────────────────────────
    // TC-5: 로딩 상태 표시
    // ─────────────────────────────────────────────────────────────
    describe('TC-5: 로딩 상태 표시', () => {
        it('데이터 로딩 중에 스피너가 렌더링된다', async () => {
            // getDocs를 pending 상태로 유지
            let resolveGetDocs!: (value: unknown) => void;
            mockGetDocs.mockReturnValue(
                new Promise((res) => {
                    resolveGetDocs = res;
                })
            );

            render(
                <RelatedDocsSidebar documentId="doc-1" documentTitle="테스트 문서" />
            );

            // 로딩 스피너가 있어야 함
            const spinner = document.querySelector('.animate-spin');
            expect(spinner).not.toBeNull();

            // 정리: pending promise 해소
            resolveGetDocs(makeSnapshot([]));
        });

        it('로딩 완료 후 스피너가 사라진다', async () => {
            mockGetDocs.mockResolvedValue(makeSnapshot([
                makeSuggestion({ id: 'sug-1', targetTitle: '완료 문서' }),
            ]));

            render(
                <RelatedDocsSidebar documentId="doc-1" documentTitle="테스트 문서" />
            );

            await waitFor(() => {
                expect(screen.getByText('완료 문서')).toBeDefined();
            });

            const spinner = document.querySelector('.animate-spin');
            expect(spinner).toBeNull();
        });
    });

    // ─────────────────────────────────────────────────────────────
    // TC-6: UI 요소 렌더링 확인
    // ─────────────────────────────────────────────────────────────
    describe('TC-6: UI 요소 렌더링', () => {
        it('섹션 제목이 렌더링된다', async () => {
            mockGetDocs.mockResolvedValue(makeSnapshot([
                makeSuggestion({ id: 'sug-1', targetTitle: '테스트 문서' }),
            ]));

            render(
                <RelatedDocsSidebar documentId="doc-1" documentTitle="소스 문서" />
            );

            await waitFor(() => {
                expect(screen.getByText('테스트 문서')).toBeDefined();
            });

            // 섹션 헤딩 "AI 추천" 또는 관련 텍스트 확인
            expect(screen.getByText(/AI 추천/)).toBeDefined();
        });

        it('embedding method 추천에 "유사 문서" 뱃지가 표시된다', async () => {
            mockGetDocs.mockResolvedValue(makeSnapshot([
                makeSuggestion({ id: 'sug-1', method: 'embedding', targetTitle: '임베딩 문서' }),
            ]));

            render(
                <RelatedDocsSidebar documentId="doc-1" documentTitle="소스 문서" />
            );

            await waitFor(() => {
                expect(screen.getByText('유사 문서')).toBeDefined();
            });
        });

        it('semantic method 추천에 "AI 분석" 뱃지가 표시된다', async () => {
            mockGetDocs.mockResolvedValue(makeSnapshot([
                makeSuggestion({ id: 'sug-1', method: 'semantic', targetTitle: 'AI 문서' }),
            ]));

            render(
                <RelatedDocsSidebar documentId="doc-1" documentTitle="소스 문서" />
            );

            await waitFor(() => {
                expect(screen.getByText('AI 분석')).toBeDefined();
            });
        });

        it('confidence 90 이상이면 green 색상 바가 렌더링된다', async () => {
            mockGetDocs.mockResolvedValue(makeSnapshot([
                makeSuggestion({ id: 'sug-1', confidence: 95, targetTitle: '높은 신뢰도 문서' }),
            ]));

            render(
                <RelatedDocsSidebar documentId="doc-1" documentTitle="소스 문서" />
            );

            await waitFor(() => {
                expect(screen.getByText('높은 신뢰도 문서')).toBeDefined();
            });

            const greenBar = document.querySelector('.bg-green-500');
            expect(greenBar).not.toBeNull();
        });

        it('confidence 70-89이면 blue 색상 바가 렌더링된다', async () => {
            mockGetDocs.mockResolvedValue(makeSnapshot([
                makeSuggestion({ id: 'sug-1', confidence: 80, targetTitle: '중간 신뢰도 문서' }),
            ]));

            render(
                <RelatedDocsSidebar documentId="doc-1" documentTitle="소스 문서" />
            );

            await waitFor(() => {
                expect(screen.getByText('중간 신뢰도 문서')).toBeDefined();
            });

            const blueBar = document.querySelector('.bg-blue-500');
            expect(blueBar).not.toBeNull();
        });

        it('confidence 70 미만이면 gray 색상 바가 렌더링된다', async () => {
            mockGetDocs.mockResolvedValue(makeSnapshot([
                makeSuggestion({ id: 'sug-1', confidence: 60, targetTitle: '낮은 신뢰도 문서' }),
            ]));

            render(
                <RelatedDocsSidebar documentId="doc-1" documentTitle="소스 문서" />
            );

            await waitFor(() => {
                expect(screen.getByText('낮은 신뢰도 문서')).toBeDefined();
            });

            const grayBar = document.querySelector('.bg-gray-400');
            expect(grayBar).not.toBeNull();
        });
    });

    // ─────────────────────────────────────────────────────────────
    // TC-7: method별 아이콘 렌더링
    // ─────────────────────────────────────────────────────────────
    describe('TC-7: method별 아이콘 렌더링', () => {
        it('embedding method 뱃지에 네트워크 아이콘(data-icon="embedding")이 렌더링된다', async () => {
            mockGetDocs.mockResolvedValue(makeSnapshot([
                makeSuggestion({ id: 'sug-1', method: 'embedding', targetTitle: '임베딩 문서' }),
            ]));

            render(
                <RelatedDocsSidebar documentId="doc-1" documentTitle="소스 문서" />
            );

            await waitFor(() => {
                expect(screen.getByText('임베딩 문서')).toBeDefined();
            });

            const icon = document.querySelector('[data-icon="embedding"]');
            expect(icon).not.toBeNull();
        });

        it('semantic method 뱃지에 별 아이콘(data-icon="semantic")이 렌더링된다', async () => {
            mockGetDocs.mockResolvedValue(makeSnapshot([
                makeSuggestion({ id: 'sug-1', method: 'semantic', targetTitle: 'AI 문서' }),
            ]));

            render(
                <RelatedDocsSidebar documentId="doc-1" documentTitle="소스 문서" />
            );

            await waitFor(() => {
                expect(screen.getByText('AI 문서')).toBeDefined();
            });

            const icon = document.querySelector('[data-icon="semantic"]');
            expect(icon).not.toBeNull();
        });

        it('manual method 뱃지에 사람 아이콘(data-icon="manual")이 렌더링된다', async () => {
            mockGetDocs.mockResolvedValue(makeSnapshot([
                makeSuggestion({ id: 'sug-1', method: 'manual', targetTitle: '수동 문서' }),
            ]));

            render(
                <RelatedDocsSidebar documentId="doc-1" documentTitle="소스 문서" />
            );

            await waitFor(() => {
                expect(screen.getByText('수동 문서')).toBeDefined();
            });

            const icon = document.querySelector('[data-icon="manual"]');
            expect(icon).not.toBeNull();
        });
    });

    // ─────────────────────────────────────────────────────────────
    // TC-8: config 외부화 (Firestore config/aiLinking)
    // ─────────────────────────────────────────────────────────────
    describe('TC-8: config 외부화', () => {
        it('Firestore config/aiLinking에서 maxSuggestions를 읽어 적용한다', async () => {
            // config 문서: maxSuggestions = 3
            mockGetDoc.mockResolvedValue({
                exists: () => true,
                data: () => ({ maxSuggestions: 3 }),
            });

            // 제안 7개 준비
            const suggestions = Array.from({ length: 7 }, (_, i) =>
                makeSuggestion({
                    id: `sug-${i}`,
                    targetTitle: `문서 ${i + 1}`,
                    confidence: 90 - i,
                })
            );
            mockGetDocs.mockResolvedValue(makeSnapshot(suggestions));

            render(
                <RelatedDocsSidebar documentId="doc-1" documentTitle="소스 문서" />
            );

            // config 로드 및 suggestions 로드 모두 완료 대기
            await waitFor(() => {
                expect(screen.getByText('문서 1')).toBeDefined();
            });

            // maxSuggestions=3이므로 4번째 이후 문서는 표시되지 않아야 함
            expect(screen.queryByText('문서 4')).toBeNull();
            expect(screen.queryByText('문서 5')).toBeNull();
        });

        it('Firestore config 로드 실패 시 기본값 5를 사용한다', async () => {
            // config 로드 실패 (첫 번째 호출), 이후 target doc 존재 확인은 성공
            mockGetDoc.mockImplementationOnce(() => Promise.reject(new Error('Firestore 접근 실패')));
            mockGetDoc.mockResolvedValue({ exists: () => true });

            const suggestions = Array.from({ length: 7 }, (_, i) =>
                makeSuggestion({
                    id: `sug-${i}`,
                    targetTitle: `문서 ${i + 1}`,
                    confidence: 90 - i,
                })
            );
            mockGetDocs.mockResolvedValue(makeSnapshot(suggestions));

            render(
                <RelatedDocsSidebar documentId="doc-1" documentTitle="소스 문서" />
            );

            await waitFor(() => {
                expect(screen.getByText('문서 1')).toBeDefined();
            });

            // 기본값 5 사용 → 5번째까지 표시, 6번째부터 없음
            expect(screen.queryByText('문서 6')).toBeNull();
            expect(screen.queryByText('문서 7')).toBeNull();
        });

        it('Firestore config 문서가 없을 때 기본값 5를 사용한다', async () => {
            // config 문서 없음 (첫 번째 호출), 이후 target doc 존재 확인은 true
            mockGetDoc.mockImplementationOnce(() => Promise.resolve({ exists: () => false }));
            mockGetDoc.mockResolvedValue({ exists: () => true });

            const suggestions = Array.from({ length: 7 }, (_, i) =>
                makeSuggestion({
                    id: `sug-${i}`,
                    targetTitle: `문서 ${i + 1}`,
                    confidence: 90 - i,
                })
            );
            mockGetDocs.mockResolvedValue(makeSnapshot(suggestions));

            render(
                <RelatedDocsSidebar documentId="doc-1" documentTitle="소스 문서" />
            );

            await waitFor(() => {
                expect(screen.getByText('문서 1')).toBeDefined();
            });

            expect(screen.queryByText('문서 6')).toBeNull();
            expect(screen.queryByText('문서 7')).toBeNull();
        });
    });
});
