/**
 * feedback/route.ts POST 엔드포인트 단위 테스트
 *
 * 테스트 대상: POST /api/ai/feedback
 * 프레임워크: vitest
 *
 * 모킹 전략:
 *  - server-only          : 빈 모듈로 대체 (Next.js 서버 전용 가드 우회)
 *  - @/lib/firebase-admin : adminDb (collection, runTransaction) 모킹
 *  - @/lib/auth-middleware : verifyMember 모킹
 *  - firebase-admin/firestore : FieldValue 모킹
 *
 * vi.hoisted를 사용하여 vi.mock 팩토리 함수 내에서 외부 mock 변수를 안전하게
 * 참조할 수 있도록 처리한다.
 */

import { describe, it, expect, vi, beforeEach } from 'vitest';
import { NextRequest, NextResponse } from 'next/server';

// ── vi.hoisted: vi.mock 팩토리보다 먼저 실행되는 변수 선언 ─────────────────

const mocks = vi.hoisted(() => {
    const mockTransactionSet = vi.fn();
    const mockTransactionUpdate = vi.fn();

    const mockRunTransaction = vi.fn(async (callback: (tx: any) => Promise<void>) => {
        await callback({ set: mockTransactionSet, update: mockTransactionUpdate });
    });

    const mockQueryLogsGet = vi.fn();
    const mockQueryLogsDoc = vi.fn(() => ({ get: mockQueryLogsGet }));

    const mockDuplicateGet = vi.fn();
    const mockWhere2 = vi.fn(() => ({ limit: vi.fn(() => ({ get: mockDuplicateGet })) }));
    const mockWhere1 = vi.fn(() => ({ where: mockWhere2 }));

    const mockFeedbackDocRef = { id: 'mock-feedback-id-001' };
    const mockAnswerFeedbackDoc = vi.fn(() => mockFeedbackDocRef);

    const mockAdminReviewQueueAdd = vi.fn();

    const mockCollection = vi.fn((name: string) => {
        if (name === 'query_logs') {
            return { doc: mockQueryLogsDoc };
        }
        if (name === 'answer_feedback') {
            return { doc: mockAnswerFeedbackDoc, where: mockWhere1 };
        }
        if (name === 'admin_review_queue') {
            return { add: mockAdminReviewQueueAdd };
        }
        return {};
    });

    const mockVerifyAuth = vi.fn();

    return {
        mockTransactionSet,
        mockTransactionUpdate,
        mockRunTransaction,
        mockQueryLogsGet,
        mockQueryLogsDoc,
        mockDuplicateGet,
        mockWhere1,
        mockWhere2,
        mockFeedbackDocRef,
        mockAnswerFeedbackDoc,
        mockAdminReviewQueueAdd,
        mockCollection,
        mockVerifyAuth,
    };
});

// ── vi.mock 선언 ──────────────────────────────────────────────────────────

// server-only 가드 우회
vi.mock('server-only', () => ({}));

// firebase-admin/firestore FieldValue 모킹
vi.mock('firebase-admin/firestore', () => ({
    FieldValue: {
        serverTimestamp: () => ({ _serverTimestamp: true }),
    },
}));

// firebase-admin 전체 모킹
vi.mock('firebase-admin', () => ({
    default: {
        apps: [],
        initializeApp: vi.fn(),
        credential: { cert: vi.fn() },
        app: vi.fn(),
    },
    apps: [],
    initializeApp: vi.fn(),
    credential: { cert: vi.fn() },
    app: vi.fn(),
}));

// firebase-admin/auth 모킹
vi.mock('firebase-admin/auth', () => ({
    getAuth: vi.fn(() => ({
        verifyIdToken: vi.fn(),
    })),
}));

// adminDb 모킹
vi.mock('@/lib/firebase-admin', () => ({
    adminDb: {
        collection: mocks.mockCollection,
        runTransaction: mocks.mockRunTransaction,
    },
}));

// verifyMember 모킹
vi.mock('@/lib/auth-middleware', () => ({
    verifyMember: (...args: any[]) => mocks.mockVerifyAuth(...args),
}));

// ── import (모킹 이후) ─────────────────────────────────────────────────────

import { POST } from '../route';

// ── 헬퍼 함수 ─────────────────────────────────────────────────────────────

function makeRequest(body: Record<string, unknown>, headers: Record<string, string> = {}): NextRequest {
    return new NextRequest('http://localhost/api/ai/feedback', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            ...headers,
        },
        body: JSON.stringify(body),
    });
}

function makeAuthHeader(token = 'valid-token'): Record<string, string> {
    return { Authorization: `Bearer ${token}` };
}

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

describe('POST /api/ai/feedback', () => {

    const {
        mockVerifyAuth,
        mockQueryLogsGet,
        mockDuplicateGet,
        mockRunTransaction,
        mockTransactionSet,
        mockTransactionUpdate,
        mockAdminReviewQueueAdd,
    } = mocks;

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

        // 기본값: 인증 성공 (userId = 'test-user-001')
        mockVerifyAuth.mockResolvedValue({ uid: 'test-user-001', email: 'test@example.com' });

        // 기본값: query_logs 문서 존재
        mockQueryLogsGet.mockResolvedValue({
            exists: true,
            data: () => ({ question: '보험 질문입니다.', answer: '답변 내용입니다.' }),
        });

        // 기본값: 중복 피드백 없음
        mockDuplicateGet.mockResolvedValue({ empty: true });

        // 기본값: 트랜잭션 성공
        mockRunTransaction.mockImplementation(async (callback: (tx: any) => Promise<void>) => {
            await callback({ set: mockTransactionSet, update: mockTransactionUpdate });
        });

        // 기본값: admin_review_queue 등록 성공
        mockAdminReviewQueueAdd.mockResolvedValue({ id: 'review-queue-id-001' });
    });

    // ----------------------------------------------------------
    // 인증 실패 → 401
    // ----------------------------------------------------------
    describe('인증 실패', () => {
        it('Authorization 헤더 없이 요청 → verifyMember가 NextResponse(401)를 반환하면 → 401', async () => {
            mockVerifyAuth.mockResolvedValue(
                NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
            );

            const req = makeRequest({ queryId: 'q1', feedbackType: 'correct' });
            const res = await POST(req);

            expect(res.status).toBe(401);
        });

        it('만료된 토큰 → verifyMember가 NextResponse(401)를 반환하면 → 401', async () => {
            mockVerifyAuth.mockResolvedValue(
                NextResponse.json({ error: 'Invalid or expired token' }, { status: 401 })
            );

            const req = makeRequest(
                { queryId: 'q1', feedbackType: 'correct' },
                makeAuthHeader('expired-token')
            );
            const res = await POST(req);

            expect(res.status).toBe(401);
        });

        it('guest 역할 사용자 → verifyMember가 NextResponse(403)를 반환하면 → 403', async () => {
            mockVerifyAuth.mockResolvedValue(
                NextResponse.json({ error: 'Forbidden: Member access required' }, { status: 403 })
            );

            const req = makeRequest(
                { queryId: 'q1', feedbackType: 'correct' },
                makeAuthHeader()
            );
            const res = await POST(req);

            expect(res.status).toBe(403);
        });
    });

    // ----------------------------------------------------------
    // 입력 검증 실패 → 400
    // ----------------------------------------------------------
    describe('입력 검증 실패 → 400', () => {
        it('queryId 누락 → 400', async () => {
            const req = makeRequest(
                { feedbackType: 'correct' },
                makeAuthHeader()
            );
            const res = await POST(req);
            const body = await res.json();

            expect(res.status).toBe(400);
            expect(body.error).toContain('queryId');
        });

        it('feedbackType 누락 → 400', async () => {
            const req = makeRequest(
                { queryId: 'q1' },
                makeAuthHeader()
            );
            const res = await POST(req);
            const body = await res.json();

            expect(res.status).toBe(400);
            expect(body.error).toContain('feedbackType');
        });

        it('잘못된 feedbackType("invalid") → 400', async () => {
            const req = makeRequest(
                { queryId: 'q1', feedbackType: 'invalid' },
                makeAuthHeader()
            );
            const res = await POST(req);
            const body = await res.json();

            expect(res.status).toBe(400);
            expect(body.error).toContain('feedbackType');
        });

        it('잘못된 feedbackType("like") → 400', async () => {
            const req = makeRequest(
                { queryId: 'q1', feedbackType: 'like' },
                makeAuthHeader()
            );
            const res = await POST(req);

            expect(res.status).toBe(400);
        });

        it('comment 1000자 초과 → 400', async () => {
            const longComment = 'A'.repeat(1001);
            const req = makeRequest(
                { queryId: 'q1', feedbackType: 'correct', comment: longComment },
                makeAuthHeader()
            );
            const res = await POST(req);
            const body = await res.json();

            expect(res.status).toBe(400);
            expect(body.error).toContain('1000');
        });

        it('comment 정확히 1000자 → 통과 (400 아님)', async () => {
            const exactComment = 'A'.repeat(1000);
            const req = makeRequest(
                { queryId: 'q1', feedbackType: 'correct', comment: exactComment },
                makeAuthHeader()
            );
            const res = await POST(req);

            // 1000자는 허용이므로 400이 아님 (201)
            expect(res.status).not.toBe(400);
        });
    });

    // ----------------------------------------------------------
    // query_logs 문서 없음 → 404
    // ----------------------------------------------------------
    describe('존재하지 않는 queryId → 404', () => {
        it('query_logs 문서가 없으면 → 404', async () => {
            mockQueryLogsGet.mockResolvedValue({
                exists: false,
                data: () => undefined,
            });

            const req = makeRequest(
                { queryId: 'nonexistent-query', feedbackType: 'correct' },
                makeAuthHeader()
            );
            const res = await POST(req);
            const body = await res.json();

            expect(res.status).toBe(404);
            expect(body.error).toContain('query_logs');
        });
    });

    // ----------------------------------------------------------
    // 중복 피드백 → 409
    // ----------------------------------------------------------
    describe('중복 피드백 → 409', () => {
        it('동일 userId가 이미 피드백을 제출한 경우 → 409', async () => {
            mockDuplicateGet.mockResolvedValue({ empty: false });

            const req = makeRequest(
                { queryId: 'q1', feedbackType: 'correct' },
                makeAuthHeader()
            );
            const res = await POST(req);
            const body = await res.json();

            expect(res.status).toBe(409);
            expect(body.error).toContain('이미');
        });
    });

    // ----------------------------------------------------------
    // 정상 correct 피드백 → 201 + needsReview false
    // ----------------------------------------------------------
    describe('정상 correct 피드백', () => {
        it('correct 피드백 → 201 반환', async () => {
            const req = makeRequest(
                { queryId: 'q1', feedbackType: 'correct' },
                makeAuthHeader()
            );
            const res = await POST(req);

            expect(res.status).toBe(201);
        });

        it('correct 피드백 → success: true, feedbackId 반환', async () => {
            const req = makeRequest(
                { queryId: 'q1', feedbackType: 'correct' },
                makeAuthHeader()
            );
            const res = await POST(req);
            const body = await res.json();

            expect(body.success).toBe(true);
            expect(body.feedbackId).toBeDefined();
            expect(typeof body.feedbackId).toBe('string');
        });

        it('correct 피드백 → needsReview false → admin_review_queue 등록 안 함', async () => {
            const req = makeRequest(
                { queryId: 'q1', feedbackType: 'correct' },
                makeAuthHeader()
            );
            await POST(req);

            // needsReview = false이므로 admin_review_queue.add가 호출되지 않음
            expect(mockAdminReviewQueueAdd).not.toHaveBeenCalled();
        });

        it('correct 피드백 → 트랜잭션 내 answer_feedback.set 호출 확인', async () => {
            const req = makeRequest(
                { queryId: 'q1', feedbackType: 'correct' },
                makeAuthHeader()
            );
            await POST(req);

            expect(mockTransactionSet).toHaveBeenCalledOnce();
            const setArgs = mockTransactionSet.mock.calls[0][1];
            expect(setArgs.feedbackType).toBe('correct');
            expect(setArgs.needsReview).toBe(false);
        });

        it('correct 피드백 → 트랜잭션 내 query_logs.update 호출 확인', async () => {
            const req = makeRequest(
                { queryId: 'q1', feedbackType: 'correct' },
                makeAuthHeader()
            );
            await POST(req);

            expect(mockTransactionUpdate).toHaveBeenCalledOnce();
            const updateArgs = mockTransactionUpdate.mock.calls[0][1];
            expect(updateArgs.feedbackStatus).toBe('correct');
        });
    });

    // ----------------------------------------------------------
    // 정상 incorrect 피드백 → 201 + needsReview true + admin_review_queue 등록
    // ----------------------------------------------------------
    describe('정상 incorrect 피드백', () => {
        it('incorrect 피드백 → 201 반환', async () => {
            const req = makeRequest(
                { queryId: 'q1', feedbackType: 'incorrect' },
                makeAuthHeader()
            );
            const res = await POST(req);

            expect(res.status).toBe(201);
        });

        it('incorrect 피드백 → needsReview true → admin_review_queue 등록', async () => {
            const req = makeRequest(
                { queryId: 'q1', feedbackType: 'incorrect' },
                makeAuthHeader()
            );
            await POST(req);

            expect(mockAdminReviewQueueAdd).toHaveBeenCalledOnce();
        });

        it('incorrect 피드백 → admin_review_queue에 올바른 데이터 전달', async () => {
            const req = makeRequest(
                { queryId: 'q1', feedbackType: 'incorrect', comment: '답변이 틀렸습니다.' },
                makeAuthHeader()
            );
            await POST(req);

            const addArgs = mockAdminReviewQueueAdd.mock.calls[0][0];
            expect(addArgs.queryId).toBe('q1');
            expect(addArgs.feedbackType).toBe('incorrect');
            expect(addArgs.status).toBe('pending');
            expect(addArgs.question).toBe('보험 질문입니다.');
            expect(addArgs.answer).toBe('답변 내용입니다.');
            expect(addArgs.userId).toBe('test-user-001');
        });

        it('incorrect 피드백 → answer_feedback.set에서 needsReview: true', async () => {
            const req = makeRequest(
                { queryId: 'q1', feedbackType: 'incorrect' },
                makeAuthHeader()
            );
            await POST(req);

            const setArgs = mockTransactionSet.mock.calls[0][1];
            expect(setArgs.needsReview).toBe(true);
        });

        it('incomplete 피드백도 needsReview: true → admin_review_queue 등록', async () => {
            const req = makeRequest(
                { queryId: 'q1', feedbackType: 'incomplete' },
                makeAuthHeader()
            );
            await POST(req);

            expect(mockAdminReviewQueueAdd).toHaveBeenCalledOnce();
            const setArgs = mockTransactionSet.mock.calls[0][1];
            expect(setArgs.needsReview).toBe(true);
        });
    });

    // ----------------------------------------------------------
    // comment HTML 태그 제거 (XSS 방지)
    // ----------------------------------------------------------
    describe('comment HTML 태그 제거', () => {
        it('<script> 태그 포함 comment → HTML 태그 제거 후 저장', async () => {
            const req = makeRequest(
                {
                    queryId: 'q1',
                    feedbackType: 'incorrect',
                    comment: '<script>alert("xss")</script>악성 스크립트 포함',
                },
                makeAuthHeader()
            );
            await POST(req);

            const setArgs = mockTransactionSet.mock.calls[0][1];
            // HTML 태그가 제거되어야 함
            expect(setArgs.comment).not.toContain('<script>');
            expect(setArgs.comment).not.toContain('</script>');
            // 태그 외 텍스트는 유지
            expect(setArgs.comment).toContain('악성 스크립트 포함');
        });

        it('<b>강조</b> 태그 → 태그 제거 후 텍스트만 유지', async () => {
            const req = makeRequest(
                {
                    queryId: 'q1',
                    feedbackType: 'correct',
                    comment: '<b>강조</b> 텍스트',
                },
                makeAuthHeader()
            );
            await POST(req);

            const setArgs = mockTransactionSet.mock.calls[0][1];
            expect(setArgs.comment).not.toContain('<b>');
            expect(setArgs.comment).not.toContain('</b>');
            expect(setArgs.comment).toContain('강조');
            expect(setArgs.comment).toContain('텍스트');
        });

        it('여러 HTML 태그 혼합 → 모두 제거', async () => {
            const req = makeRequest(
                {
                    queryId: 'q1',
                    feedbackType: 'incorrect',
                    comment: '<p><strong>답변</strong>이 <em>틀렸습니다</em>.</p>',
                },
                makeAuthHeader()
            );
            await POST(req);

            const setArgs = mockTransactionSet.mock.calls[0][1];
            expect(setArgs.comment).not.toMatch(/<[^>]*>/);
            expect(setArgs.comment).toContain('답변');
            expect(setArgs.comment).toContain('틀렸습니다');
        });

        it('HTML 태그 없는 일반 comment → 변경 없이 저장', async () => {
            const plainComment = '정확한 답변이었습니다.';
            const req = makeRequest(
                {
                    queryId: 'q1',
                    feedbackType: 'correct',
                    comment: plainComment,
                },
                makeAuthHeader()
            );
            await POST(req);

            const setArgs = mockTransactionSet.mock.calls[0][1];
            expect(setArgs.comment).toBe(plainComment);
        });

        it('comment 미입력 → 빈 문자열로 저장', async () => {
            const req = makeRequest(
                { queryId: 'q1', feedbackType: 'correct' },
                makeAuthHeader()
            );
            await POST(req);

            const setArgs = mockTransactionSet.mock.calls[0][1];
            expect(setArgs.comment).toBe('');
        });
    });

    // ----------------------------------------------------------
    // 유효한 feedbackType 전체 (correct / incorrect / incomplete)
    // ----------------------------------------------------------
    describe('유효한 feedbackType 전체', () => {
        it.each(['correct', 'incorrect', 'incomplete'] as const)(
            'feedbackType = "%s" → 201',
            async (feedbackType) => {
                const req = makeRequest(
                    { queryId: 'q1', feedbackType },
                    makeAuthHeader()
                );
                const res = await POST(req);
                expect(res.status).toBe(201);
            }
        );
    });

    // ----------------------------------------------------------
    // answer_feedback에 저장되는 데이터 구조 확인
    // ----------------------------------------------------------
    describe('저장 데이터 구조', () => {
        it('answer_feedback.set에 queryId, feedbackType, userId, needsReview 포함', async () => {
            const req = makeRequest(
                { queryId: 'query-abc', feedbackType: 'incorrect', comment: '오답' },
                makeAuthHeader()
            );
            await POST(req);

            const setArgs = mockTransactionSet.mock.calls[0][1];
            expect(setArgs.queryId).toBe('query-abc');
            expect(setArgs.feedbackType).toBe('incorrect');
            expect(setArgs.userId).toBe('test-user-001');
            expect(setArgs.needsReview).toBe(true);
            expect(setArgs.createdAt).toBeDefined();
        });

        it('admin_review_queue에 feedbackId가 포함됨', async () => {
            const req = makeRequest(
                { queryId: 'q1', feedbackType: 'incorrect' },
                makeAuthHeader()
            );
            await POST(req);

            const addArgs = mockAdminReviewQueueAdd.mock.calls[0][0];
            expect(addArgs.feedbackId).toBe('mock-feedback-id-001');
        });
    });
});
