/**
 * 통합 테스트: 전체 검토 라이프사이클
 *
 * Firebase Emulator 없이 비즈니스 로직의 통합 동작을 검증합니다.
 * 각 섹션:
 *   A. 역할별 접근 매트릭스 (48건)
 *   B. 상태 전이 통합 테스트 (12건)
 *   C. 자기 검토 금지 (5건)
 *   D. 감사 로그 무결성 (5건)
 *   E. 경량 수정 7조건 통합 테스트 (14건)
 *   F. Custom Claims 통합 테스트 (4건)
 */

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

// ── A. 역할별 접근 매트릭스에 사용할 타입/함수 ──────────────────────────────

import type { UserRole } from '../../../../nextapp/src/shared/types/roles';
import {
  hasMinRole,
  ROLE_HIERARCHY,
  ROLE_PERMISSIONS,
} from '../../../../nextapp/src/shared/types/roles';

// ── B/C: 상태 머신 & 리뷰어 배정 ────────────────────────────────────────────

import {
  transitionStatus,
  assessRiskLevel,
  canApprove,
  isValidTransition,
} from '../../reviewStateMachine';

import {
  filterSelfReviewers,
} from '../../reviewerAssignment';
import type { ReviewerCandidate } from '../../reviewerAssignment';

// ── D. 감사 로그 & F. Custom Claims ─────────────────────────────────────────

// vi.hoisted: mock 팩토리보다 먼저 초기화 (hoisting 이슈 방지)
const {
  mockAuditSet,
  mockAuditLogId,
  mockAuditLoggerInfo,
  mockAuditTimestampNow,
  mockSetCustomUserClaims,
  mockCallerDocGet,
  mockUsersCollection,
} = vi.hoisted(() => {
  const mockAuditSet = vi.fn().mockResolvedValue(undefined);
  const mockAuditLogId = 'integration-audit-log-id';
  const mockAuditLoggerInfo = vi.fn();
  const mockAuditTimestampNow = vi.fn(() => ({ seconds: 1700000000, nanoseconds: 0 }));
  const mockSetCustomUserClaims = vi.fn().mockResolvedValue(undefined);
  const mockCallerDocGet = vi.fn().mockResolvedValue({
    data: () => ({ role: 'admin' }),
  });
  const mockUsersCollection = {
    docs: [
      { id: 'user1', data: () => ({ role: 'admin' }) },
      { id: 'user2', data: () => ({ role: 'member' }) },
      { id: 'user3', data: () => ({ role: 'guest' }) },
    ],
  };
  return {
    mockAuditSet,
    mockAuditLogId,
    mockAuditLoggerInfo,
    mockAuditTimestampNow,
    mockSetCustomUserClaims,
    mockCallerDocGet,
    mockUsersCollection,
  };
});

vi.mock('firebase-admin', () => {
  const mockLogRef = {
    id: mockAuditLogId,
    set: mockAuditSet,
  };
  const mockAuditLogsCol = { doc: vi.fn(() => mockLogRef) };
  const mockDocRef = { collection: vi.fn(() => mockAuditLogsCol) };
  const mockDocsCol = {
    doc: vi.fn(() => ({
      ...mockDocRef,
      get: mockCallerDocGet,
    })),
    get: vi.fn().mockResolvedValue(mockUsersCollection),
  };
  const mockFirestoreInst = {
    collection: vi.fn((_name: string) => mockDocsCol),
  };

  const firestoreFn = vi.fn(() => mockFirestoreInst);
  (firestoreFn as any).Timestamp = { now: mockAuditTimestampNow };
  (firestoreFn as any).FieldValue = { serverTimestamp: () => 'SERVER_TIMESTAMP' };

  return {
    firestore: firestoreFn,
    apps: [{}],
    initializeApp: vi.fn(),
    auth: vi.fn(() => ({
      getUser: vi.fn().mockResolvedValue({ uid: 'mock-uid' }),
      setCustomUserClaims: mockSetCustomUserClaims,
    })),
  };
});

vi.mock('firebase-functions', () => ({
  logger: {
    info: mockAuditLoggerInfo,
    warn: vi.fn(),
    error: vi.fn(),
  },
  firestore: {
    document: vi.fn(() => ({
      onWrite: vi.fn((handler: any) => handler),
      onCreate: vi.fn((handler: any) => handler),
    })),
  },
  https: {
    onCall: vi.fn((handler: any) => handler),
    HttpsError: class MockHttpsError extends Error {
      code: string;
      constructor(code: string, message: string) {
        super(message);
        this.code = code;
      }
    },
  },
}));

import { writeAuditLog } from '../../auditLog';
import * as admin from 'firebase-admin';

// ── E. 경량 수정 ─────────────────────────────────────────────────────────────

import {
  isMinorCharChange,
  hasNoNumericChange,
  hasNoNegationChange,
  hasNoUrlChange,
  hasNoLegalRefChange,
  isNotHighRiskCategory,
  isLightweightEditExempt,
} from '../../lightweightEditExemption';

// ── F. Custom Claims ─────────────────────────────────────────────────────────

import { syncCustomClaims } from '../../setCustomClaims';
import { backfillCustomClaims } from '../../backfillClaims';

// ── 헬퍼: DB mock factory ────────────────────────────────────────────────────

function makeExemptionDb(recentCount: number) {
  const mockQuery = {
    where: vi.fn().mockReturnThis(),
    get: vi.fn().mockResolvedValue({ size: recentCount }),
  };
  return {
    collection: vi.fn(() => ({
      doc: vi.fn(() => ({
        collection: vi.fn(() => mockQuery),
      })),
    })),
  } as any;
}

function makeReviewer(
  uid: string,
  recentAssignmentCount: number,
  role: string = 'reviewer'
): ReviewerCandidate {
  return { uid, name: `User ${uid}`, role, recentAssignmentCount };
}

// ════════════════════════════════════════════════════════════════════════════
// A. 역할별 접근 매트릭스
// ════════════════════════════════════════════════════════════════════════════

describe('A. 역할별 접근 매트릭스', () => {
  // 권한 체크 헬퍼
  const canReadDoc = (role: UserRole) => hasMinRole(role, 'guest');
  const canCreateDoc = (role: UserRole) => hasMinRole(role, 'member');
  const canUpdateDoc = (role: UserRole) => hasMinRole(role, 'member');
  const canDeleteDoc = (role: UserRole) => hasMinRole(role, 'admin');
  const canCreateReview = (role: UserRole) => hasMinRole(role, 'reviewer');
  const canReadAuditLog = (role: UserRole) => hasMinRole(role, 'reviewer');
  const canChangeRole = (role: UserRole) => hasMinRole(role, 'admin');

  describe('admin 역할', () => {
    const role: UserRole = 'admin';

    it('A-01. admin: 문서 읽기 허용', () => {
      expect(canReadDoc(role)).toBe(true);
    });
    it('A-02. admin: 문서 생성 허용', () => {
      expect(canCreateDoc(role)).toBe(true);
    });
    it('A-03. admin: 문서 수정 허용', () => {
      expect(canUpdateDoc(role)).toBe(true);
    });
    it('A-04. admin: 문서 삭제 허용', () => {
      expect(canDeleteDoc(role)).toBe(true);
    });
    it('A-05. admin: 리뷰 생성 허용', () => {
      expect(canCreateReview(role)).toBe(true);
    });
    it('A-06. admin: 감사 로그 읽기 허용', () => {
      expect(canReadAuditLog(role)).toBe(true);
    });
    it('A-07. admin: 역할 변경 허용', () => {
      expect(canChangeRole(role)).toBe(true);
    });
    it('A-08. admin: ROLE_HIERARCHY 값은 3', () => {
      expect(ROLE_HIERARCHY[role]).toBe(3);
    });
    it('A-09. admin: manage_users 권한 보유', () => {
      expect(ROLE_PERMISSIONS[role]).toContain('manage_users');
    });
    it('A-10. admin: manage_roles 권한 보유', () => {
      expect(ROLE_PERMISSIONS[role]).toContain('manage_roles');
    });
    it('A-11. admin: approve 권한 보유', () => {
      expect(ROLE_PERMISSIONS[role]).toContain('approve');
    });
    it('A-12. admin: review 권한 보유', () => {
      expect(ROLE_PERMISSIONS[role]).toContain('review');
    });
  });

  describe('reviewer 역할', () => {
    const role: UserRole = 'reviewer';

    it('A-13. reviewer: 문서 읽기 허용', () => {
      expect(canReadDoc(role)).toBe(true);
    });
    it('A-14. reviewer: 문서 수정 허용', () => {
      expect(canUpdateDoc(role)).toBe(true);
    });
    it('A-15. reviewer: 리뷰 생성 허용', () => {
      expect(canCreateReview(role)).toBe(true);
    });
    it('A-16. reviewer: 감사 로그 읽기 허용', () => {
      expect(canReadAuditLog(role)).toBe(true);
    });
    it('A-17. reviewer: 역할 변경 거부', () => {
      expect(canChangeRole(role)).toBe(false);
    });
    it('A-18. reviewer: 문서 삭제 거부', () => {
      expect(canDeleteDoc(role)).toBe(false);
    });
    it('A-19. reviewer: ROLE_HIERARCHY 값은 2', () => {
      expect(ROLE_HIERARCHY[role]).toBe(2);
    });
    it('A-20. reviewer: approve 권한 없음', () => {
      expect(ROLE_PERMISSIONS[role]).not.toContain('approve');
    });
    it('A-21. reviewer: review 권한 보유', () => {
      expect(ROLE_PERMISSIONS[role]).toContain('review');
    });
    it('A-22. reviewer: write 권한 보유', () => {
      expect(ROLE_PERMISSIONS[role]).toContain('write');
    });
    it('A-23. reviewer: manage_users 권한 없음', () => {
      expect(ROLE_PERMISSIONS[role]).not.toContain('manage_users');
    });
    it('A-24. reviewer: manage_roles 권한 없음', () => {
      expect(ROLE_PERMISSIONS[role]).not.toContain('manage_roles');
    });
  });

  describe('member 역할', () => {
    const role: UserRole = 'member';

    it('A-25. member: 문서 읽기 허용', () => {
      expect(canReadDoc(role)).toBe(true);
    });
    it('A-26. member: 문서 생성 허용', () => {
      expect(canCreateDoc(role)).toBe(true);
    });
    it('A-27. member: 리뷰 생성 거부', () => {
      expect(canCreateReview(role)).toBe(false);
    });
    it('A-28. member: 감사 로그 읽기 거부', () => {
      expect(canReadAuditLog(role)).toBe(false);
    });
    it('A-29. member: 역할 변경 거부', () => {
      expect(canChangeRole(role)).toBe(false);
    });
    it('A-30. member: 문서 삭제 거부', () => {
      expect(canDeleteDoc(role)).toBe(false);
    });
    it('A-31. member: ROLE_HIERARCHY 값은 1', () => {
      expect(ROLE_HIERARCHY[role]).toBe(1);
    });
    it('A-32. member: read 권한 보유', () => {
      expect(ROLE_PERMISSIONS[role]).toContain('read');
    });
    it('A-33. member: write 권한 보유', () => {
      expect(ROLE_PERMISSIONS[role]).toContain('write');
    });
    it('A-34. member: review 권한 없음', () => {
      expect(ROLE_PERMISSIONS[role]).not.toContain('review');
    });
    it('A-35. member: approve 권한 없음', () => {
      expect(ROLE_PERMISSIONS[role]).not.toContain('approve');
    });
    it('A-36. member: manage_users 권한 없음', () => {
      expect(ROLE_PERMISSIONS[role]).not.toContain('manage_users');
    });
  });

  describe('guest 역할', () => {
    const role: UserRole = 'guest';

    it('A-37. guest: 문서 읽기만 허용', () => {
      expect(canReadDoc(role)).toBe(true);
    });
    it('A-38. guest: 문서 생성 거부', () => {
      expect(canCreateDoc(role)).toBe(false);
    });
    it('A-39. guest: 문서 수정 거부', () => {
      expect(canUpdateDoc(role)).toBe(false);
    });
    it('A-40. guest: 문서 삭제 거부', () => {
      expect(canDeleteDoc(role)).toBe(false);
    });
    it('A-41. guest: 리뷰 생성 거부', () => {
      expect(canCreateReview(role)).toBe(false);
    });
    it('A-42. guest: 감사 로그 읽기 거부', () => {
      expect(canReadAuditLog(role)).toBe(false);
    });
    it('A-43. guest: 역할 변경 거부', () => {
      expect(canChangeRole(role)).toBe(false);
    });
    it('A-44. guest: ROLE_HIERARCHY 값은 0', () => {
      expect(ROLE_HIERARCHY[role]).toBe(0);
    });
    it('A-45. guest: read 권한만 보유', () => {
      expect(ROLE_PERMISSIONS[role]).toContain('read');
      expect(ROLE_PERMISSIONS[role]).not.toContain('write');
    });
    it('A-46. guest: review 권한 없음', () => {
      expect(ROLE_PERMISSIONS[role]).not.toContain('review');
    });
    it('A-47. guest: approve 권한 없음', () => {
      expect(ROLE_PERMISSIONS[role]).not.toContain('approve');
    });
    it('A-48. hasMinRole: guest는 guest 이상 통과, member 이상 거부', () => {
      expect(hasMinRole('guest', 'guest')).toBe(true);
      expect(hasMinRole('guest', 'member')).toBe(false);
    });
  });
});

// ════════════════════════════════════════════════════════════════════════════
// B. 상태 전이 통합 테스트
// ════════════════════════════════════════════════════════════════════════════

describe('B. 상태 전이 통합 테스트', () => {
  it('B-01. 정상 경로 (저위험): draft→in_review→approved (reviewer 1명)', () => {
    // 1단계: draft → in_review
    const status1 = transitionStatus('draft', 'in_review');
    expect(status1).toBe('in_review');

    // 리스크 평가
    const risk = assessRiskLevel('newsletter');
    expect(risk).toBe('low');

    // 승인 판정 (reviewer 1명)
    const reviews = [{ decision: 'approve', reviewerId: 'r1', reviewerRole: 'reviewer' }];
    expect(canApprove(risk, reviews)).toBe(true);

    // 2단계: in_review → approved
    const status2 = transitionStatus(status1, 'approved');
    expect(status2).toBe('approved');
  });

  it('B-02. 정상 경로 (고위험): draft→in_review→approved (reviewer + admin)', () => {
    const status1 = transitionStatus('draft', 'in_review');
    const risk = assessRiskLevel('court_ruling');
    expect(risk).toBe('high');

    // reviewer만으로는 승인 불가
    const reviewsPartial = [{ decision: 'approve', reviewerId: 'r1', reviewerRole: 'reviewer' }];
    expect(canApprove(risk, reviewsPartial)).toBe(false);

    // reviewer + admin 모두 있어야 승인 가능
    const reviewsFull = [
      { decision: 'approve', reviewerId: 'r1', reviewerRole: 'reviewer' },
      { decision: 'approve', reviewerId: 'a1', reviewerRole: 'admin' },
    ];
    expect(canApprove(risk, reviewsFull)).toBe(true);

    const status2 = transitionStatus(status1, 'approved');
    expect(status2).toBe('approved');
  });

  it('B-03. 거절 경로: draft→in_review→rejected→draft→in_review→approved', () => {
    const s1 = transitionStatus('draft', 'in_review');
    const s2 = transitionStatus(s1, 'rejected');
    expect(s2).toBe('rejected');

    // 재제출 (rejected → draft)
    const s3 = transitionStatus(s2, 'draft');
    expect(s3).toBe('draft');

    // 다시 검토
    const s4 = transitionStatus(s3, 'in_review');
    const risk = assessRiskLevel('newsletter');
    const reviews = [{ decision: 'approve', reviewerId: 'r2', reviewerRole: 'reviewer' }];
    expect(canApprove(risk, reviews)).toBe(true);

    const s5 = transitionStatus(s4, 'approved');
    expect(s5).toBe('approved');
  });

  it('B-04. 수정 요청 경로: draft→in_review→revision_requested→draft→in_review', () => {
    const s1 = transitionStatus('draft', 'in_review');
    const s2 = transitionStatus(s1, 'revision_requested');
    expect(s2).toBe('revision_requested');

    // revision_requested에서 draft로
    const s3 = transitionStatus(s2, 'draft');
    expect(s3).toBe('draft');

    // 또는 revision_requested에서 직접 in_review로
    const s3b = transitionStatus(s2, 'in_review');
    expect(s3b).toBe('in_review');
  });

  it('B-05. 재검토 경로: approved→needs_re_review→in_review→approved', () => {
    const s1 = transitionStatus('approved', 'needs_re_review');
    expect(s1).toBe('needs_re_review');

    const s2 = transitionStatus(s1, 'in_review');
    expect(s2).toBe('in_review');

    const risk = assessRiskLevel('newsletter');
    const reviews = [{ decision: 'approve', reviewerId: 'r1', reviewerRole: 'reviewer' }];
    expect(canApprove(risk, reviews)).toBe(true);

    const s3 = transitionStatus(s2, 'approved');
    expect(s3).toBe('approved');
  });

  it('B-06. published 경로: published→needs_re_review→in_review→approved', () => {
    const s1 = transitionStatus('published', 'needs_re_review');
    expect(s1).toBe('needs_re_review');

    const s2 = transitionStatus(s1, 'in_review');
    expect(s2).toBe('in_review');

    const s3 = transitionStatus(s2, 'approved');
    expect(s3).toBe('approved');
  });

  it('B-07. 불법 전이: draft→approved 불가', () => {
    expect(() => transitionStatus('draft', 'approved')).toThrow();
  });

  it('B-08. 불법 전이: draft→rejected 불가', () => {
    expect(() => transitionStatus('draft', 'rejected')).toThrow();
  });

  it('B-09. 불법 전이: in_review→draft 불가', () => {
    expect(() => transitionStatus('in_review', 'draft')).toThrow();
  });

  it('B-10. 불법 전이: approved→draft 불가', () => {
    expect(() => transitionStatus('approved', 'draft')).toThrow();
  });

  it('B-11. 불법 전이: rejected→approved 불가', () => {
    expect(() => transitionStatus('rejected', 'approved')).toThrow();
  });

  it('B-12. 불법 전이: published→approved 불가', () => {
    expect(() => transitionStatus('published', 'approved')).toThrow();
  });

  it('B-13. 고위험 문서에서 admin만 승인 시 canApprove false', () => {
    const risk = assessRiskLevel('regulation');
    const reviews = [{ decision: 'approve', reviewerId: 'a1', reviewerRole: 'admin' }];
    expect(canApprove(risk, reviews)).toBe(false);
  });

  it('B-14. 고위험 문서 순서 무관: admin 먼저 approve 후 reviewer → canApprove true', () => {
    const risk = assessRiskLevel('policy_pdf');
    const reviews = [
      { decision: 'approve', reviewerId: 'a1', reviewerRole: 'admin' },
      { decision: 'approve', reviewerId: 'r1', reviewerRole: 'reviewer' },
    ];
    expect(canApprove(risk, reviews)).toBe(true);
  });
});

// ════════════════════════════════════════════════════════════════════════════
// C. 자기 검토 금지
// ════════════════════════════════════════════════════════════════════════════

describe('C. 자기 검토 금지 테스트', () => {
  it('C-01. authorId가 리뷰어 목록에 포함 → 필터링', () => {
    const authorId = 'author-1';
    const pool = [
      makeReviewer('author-1', 0),
      makeReviewer('reviewer-2', 1),
      makeReviewer('reviewer-3', 2),
    ];
    const result = filterSelfReviewers(pool, [authorId]);
    expect(result.map((r) => r.uid)).not.toContain('author-1');
    expect(result).toHaveLength(2);
  });

  it('C-02. contributorIds에 포함된 사용자 → 필터링', () => {
    const pool = [
      makeReviewer('contributor-1', 0),
      makeReviewer('contributor-2', 1),
      makeReviewer('reviewer-3', 2),
      makeReviewer('reviewer-4', 0),
    ];
    const result = filterSelfReviewers(pool, ['contributor-1', 'contributor-2']);
    expect(result).toHaveLength(2);
    expect(result.map((r) => r.uid)).not.toContain('contributor-1');
    expect(result.map((r) => r.uid)).not.toContain('contributor-2');
  });

  it('C-03. 무관한 사용자 → 통과 (필터링 없음)', () => {
    const pool = [
      makeReviewer('reviewer-1', 0),
      makeReviewer('reviewer-2', 1),
    ];
    const result = filterSelfReviewers(pool, ['author-99']);
    expect(result).toHaveLength(2);
  });

  it('C-04. 리뷰어 풀이 자기 검토자만 → 빈 결과', () => {
    const pool = [
      makeReviewer('author-1', 0),
      makeReviewer('contributor-1', 1),
    ];
    const result = filterSelfReviewers(pool, ['author-1', 'contributor-1']);
    expect(result).toHaveLength(0);
  });

  it('C-05. 빈 contributorIds → authorId만으로 필터링 (직접 Set 사용)', () => {
    // authorId를 contributorIds 배열에 포함시켜 filterSelfReviewers 호출
    const authorId = 'author-only';
    const pool = [
      makeReviewer('author-only', 0),
      makeReviewer('reviewer-2', 1),
      makeReviewer('reviewer-3', 2),
    ];
    // assignReviewer 내부 로직: contributorIds + authorId 병합
    const excludeIds = [authorId];
    const result = filterSelfReviewers(pool, excludeIds);
    expect(result).toHaveLength(2);
    expect(result.map((r) => r.uid)).not.toContain('author-only');
  });

  it('C-06. 빈 pool → 빈 배열 반환', () => {
    const result = filterSelfReviewers([], ['author-1', 'contributor-1']);
    expect(result).toHaveLength(0);
  });
});

// ════════════════════════════════════════════════════════════════════════════
// D. 감사 로그 무결성 테스트
// ════════════════════════════════════════════════════════════════════════════

describe('D. 감사 로그 무결성 테스트', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    mockAuditSet.mockResolvedValue(undefined);
    mockAuditTimestampNow.mockReturnValue({ seconds: 1700000000, nanoseconds: 0 });
  });

  it('D-01. 상태 변경 시 감사 로그 생성 (필드 완전성)', async () => {
    const logId = await writeAuditLog({
      docId: 'doc-001',
      action: 'status_change',
      actorId: 'reviewer-1',
      actorName: '홍길동',
      previousStatus: 'in_review',
      newStatus: 'approved',
      metadata: { riskLevel: 'low' },
    });

    expect(logId).toBe(mockAuditLogId);
    expect(mockAuditSet).toHaveBeenCalledOnce();

    const setArg = mockAuditSet.mock.calls[0][0];
    expect(setArg).toHaveProperty('id', mockAuditLogId);
    expect(setArg).toHaveProperty('docId', 'doc-001');
    expect(setArg).toHaveProperty('action', 'status_change');
    expect(setArg).toHaveProperty('actorId', 'reviewer-1');
    expect(setArg).toHaveProperty('actorName', '홍길동');
    expect(setArg).toHaveProperty('previousStatus', 'in_review');
    expect(setArg).toHaveProperty('newStatus', 'approved');
    expect(setArg).toHaveProperty('createdAt');
    expect(setArg.metadata).toEqual({ riskLevel: 'low' });
  });

  it('D-02. 리뷰 제출 시 감사 로그 생성', async () => {
    const logId = await writeAuditLog({
      docId: 'doc-002',
      action: 'review_submitted',
      actorId: 'reviewer-2',
      actorName: '김검토',
      previousStatus: 'in_review',
      newStatus: 'rejected',
      metadata: { reviewId: 'review-abc', decision: 'reject', riskLevel: 'low' },
    });

    expect(logId).toBe(mockAuditLogId);
    const setArg = mockAuditSet.mock.calls[0][0];
    expect(setArg.action).toBe('review_submitted');
    expect(setArg.newStatus).toBe('rejected');
    expect(setArg.metadata).toHaveProperty('decision', 'reject');
  });

  it('D-03. 경량 수정 면제 시 감사 로그 생성', async () => {
    const logId = await writeAuditLog({
      docId: 'doc-003',
      action: 'lightweight_edit_exempted',
      actorId: 'editor-1',
      metadata: { charDiff: 5 },
    });

    expect(logId).toBe(mockAuditLogId);
    const setArg = mockAuditSet.mock.calls[0][0];
    expect(setArg.action).toBe('lightweight_edit_exempted');
    expect(setArg.metadata).toHaveProperty('charDiff', 5);
  });

  it('D-04. 리스크 레벨 평가 시 감사 로그 생성', async () => {
    const riskLevel = assessRiskLevel('court_ruling');

    const logId = await writeAuditLog({
      docId: 'doc-004',
      action: 'risk_level_assessed',
      actorId: 'system',
      metadata: { riskLevel, sourceType: 'court_ruling' },
    });

    expect(logId).toBe(mockAuditLogId);
    const setArg = mockAuditSet.mock.calls[0][0];
    expect(setArg.action).toBe('risk_level_assessed');
    expect(setArg.metadata).toEqual({ riskLevel: 'high', sourceType: 'court_ruling' });
  });

  it('D-05. 검토자 배정 시 감사 로그 생성', async () => {
    const logId = await writeAuditLog({
      docId: 'doc-005',
      action: 'reviewer_assigned',
      actorId: 'system',
      metadata: { reviewerId: 'reviewer-3', reviewerName: '이리뷰' },
    });

    expect(logId).toBe(mockAuditLogId);
    const setArg = mockAuditSet.mock.calls[0][0];
    expect(setArg.action).toBe('reviewer_assigned');
    expect(setArg.metadata).toHaveProperty('reviewerId', 'reviewer-3');
  });

  it('D-06. Cloud Logging 이중화 확인', async () => {
    await writeAuditLog({
      docId: 'doc-006',
      action: 'status_change',
      actorId: 'admin-1',
      previousStatus: 'draft',
      newStatus: 'in_review',
    });

    expect(mockAuditLoggerInfo).toHaveBeenCalledOnce();
    const [label, payload] = mockAuditLoggerInfo.mock.calls[0];
    expect(label).toBe('AUDIT_LOG');
    expect(payload).toMatchObject({
      docId: 'doc-006',
      action: 'status_change',
      actorId: 'admin-1',
    });
  });
});

// ════════════════════════════════════════════════════════════════════════════
// E. 경량 수정 7조건 통합 테스트 (경계값)
// ════════════════════════════════════════════════════════════════════════════

describe('E. 경량 수정 7조건 통합 테스트', () => {
  // 조건 1: 변경 문자 수 경계값
  describe('E-01~02. 조건1: 변경 문자 수 ≤20자', () => {
    it('E-01. 편집 거리 20자 → 통과', () => {
      const old = 'a'.repeat(100);
      const newContent = 'a'.repeat(80) + 'b'.repeat(20);
      expect(isMinorCharChange(old, newContent)).toBe(true);
    });

    it('E-02. 편집 거리 21자 → 실패', () => {
      const old = 'a'.repeat(100);
      const newContent = 'a'.repeat(79) + 'b'.repeat(21);
      expect(isMinorCharChange(old, newContent)).toBe(false);
    });
  });

  // 조건 2: 숫자/금액 변경 경계값
  describe('E-03~04. 조건2: 숫자/금액 변경 없음', () => {
    it('E-03. 숫자 없는 텍스트만 변경 → 통과', () => {
      const old = '보험료 100만원';
      const newContent = '보험료 100만원 안내';
      expect(hasNoNumericChange(old, newContent)).toBe(true);
    });

    it('E-04. "100만원" → "200만원" → 실패', () => {
      const old = '보험료 100만원';
      const newContent = '보험료 200만원';
      expect(hasNoNumericChange(old, newContent)).toBe(false);
    });
  });

  // 조건 3: 부정어 변경 경계값
  describe('E-05~06. 조건3: 부정어 추가/삭제 없음', () => {
    it('E-05. 부정어 없는 변경 → 통과', () => {
      const old = '보장합니다';
      const newContent = '보장됩니다';
      expect(hasNoNegationChange(old, newContent)).toBe(true);
    });

    it('E-06. "보장한다" → "보장하지 않는다" → 실패', () => {
      const old = '보장한다';
      const newContent = '보장하지 않는다';
      expect(hasNoNegationChange(old, newContent)).toBe(false);
    });
  });

  // 조건 4: URL 변경 경계값
  describe('E-07~08. 조건4: URL 변경 없음', () => {
    it('E-07. URL 없는 텍스트 변경 → 통과', () => {
      const old = '안내 문서';
      const newContent = '상세 안내 문서';
      expect(hasNoUrlChange(old, newContent)).toBe(true);
    });

    it('E-08. URL 추가 → 실패', () => {
      const old = '안내 문서';
      const newContent = '안내 문서 https://example.com';
      expect(hasNoUrlChange(old, newContent)).toBe(false);
    });
  });

  // 조건 5: 법규 번호 변경 경계값
  describe('E-09~10. 조건5: 법규 번호 변경 없음', () => {
    it('E-09. 법규 인용 없는 변경 → 통과', () => {
      const old = '관련 규정에 따라 처리합니다';
      const newContent = '관련 규정에 따라 처리됩니다';
      expect(hasNoLegalRefChange(old, newContent)).toBe(true);
    });

    it('E-10. "제1조" → "제2조" → 실패', () => {
      const old = '약관 제1조에 따라';
      const newContent = '약관 제2조에 따라';
      expect(hasNoLegalRefChange(old, newContent)).toBe(false);
    });
  });

  // 조건 6: 고위험 카테고리
  describe('E-11~12. 조건6: 고위험 카테고리 아님', () => {
    it('E-11. sourceType=newsletter → 통과', () => {
      expect(isNotHighRiskCategory('newsletter')).toBe(true);
    });

    it('E-12. sourceType=court_ruling → 실패', () => {
      expect(isNotHighRiskCategory('court_ruling')).toBe(false);
    });
  });

  // 조건 7: 7일 내 3회 미만
  describe('E-13~14. 조건7: 7일 내 3회 미만 (통합)', () => {
    it('E-13. 7일 내 2회 면제 → isLightweightEditExempt 조건7 통과', async () => {
      const db = makeExemptionDb(2);
      const result = await isLightweightEditExempt({
        oldContent: '보험 안내 문서입니다.',
        newContent: '보험 상세 안내 문서입니다.',
        sourceType: 'newsletter',
        docId: 'doc-e13',
        db,
      });
      expect(result.exempt).toBe(true);
      expect(result.failedConditions).not.toContain('hasLowRecentExemptionCount');
    });

    it('E-14. 7일 내 3번째 면제 시도 → 실패 (조건7)', async () => {
      const db = makeExemptionDb(3);
      const result = await isLightweightEditExempt({
        oldContent: '보험 안내 문서입니다.',
        newContent: '보험 상세 안내 문서입니다.',
        sourceType: 'newsletter',
        docId: 'doc-e14',
        db,
      });
      expect(result.exempt).toBe(false);
      expect(result.failedConditions).toContain('hasLowRecentExemptionCount');
    });
  });

  // 통합: 여러 조건 실패
  it('E-15. 고위험 카테고리 + 부정어 추가 → exempt false, 복수 failedConditions', async () => {
    const db = makeExemptionDb(0);
    const result = await isLightweightEditExempt({
      oldContent: '보장합니다. 제1조 적용.',
      newContent: '보장하지 않습니다. 제1조 적용.',
      sourceType: 'court_ruling',
      docId: 'doc-e15',
      db,
    });
    expect(result.exempt).toBe(false);
    expect(result.failedConditions).toContain('isNotHighRiskCategory');
    expect(result.failedConditions).toContain('hasNoNegationChange');
  });
});

// ════════════════════════════════════════════════════════════════════════════
// F. Custom Claims 발급/갱신 통합 테스트
// ════════════════════════════════════════════════════════════════════════════

describe('F. Custom Claims 통합 테스트', () => {
  beforeEach(() => {
    mockSetCustomUserClaims.mockClear();
    mockCallerDocGet.mockClear();
    mockAuditLoggerInfo.mockClear();
    // 기본: admin 호출자
    mockCallerDocGet.mockResolvedValue({ data: () => ({ role: 'admin' }) });
    mockSetCustomUserClaims.mockResolvedValue(undefined);
  });

  it('F-01. 역할 변경 시 Claims 갱신 (member → reviewer)', async () => {
    const change = {
      before: { exists: true, data: () => ({ role: 'member' }) },
      after: { exists: true, data: () => ({ role: 'reviewer' }) },
    };
    const context = { params: { uid: 'user-f01' } };

    await (syncCustomClaims as any)(change, context);

    expect(mockSetCustomUserClaims).toHaveBeenCalledWith('user-f01', { role: 'reviewer' });
  });

  it('F-02. 유저 삭제 시 Claims 초기화', async () => {
    const change = {
      before: { exists: true, data: () => ({ role: 'reviewer' }) },
      after: { exists: false, data: () => null },
    };
    const context = { params: { uid: 'user-f02' } };

    await (syncCustomClaims as any)(change, context);

    expect(mockSetCustomUserClaims).toHaveBeenCalledWith('user-f02', {});
  });

  it('F-03. 잘못된 역할 → Claims 설정 안함', async () => {
    const change = {
      before: { exists: true, data: () => ({ role: 'member' }) },
      after: { exists: true, data: () => ({ role: 'superadmin' }) },
    };
    const context = { params: { uid: 'user-f03' } };

    await (syncCustomClaims as any)(change, context);

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

  it('F-04. 백필 스크립트: admin 호출 시 전체 유저 Claims 설정', async () => {
    // mockUsersCollection 내 3명(user1: admin, user2: member, user3: guest)
    const result = await (backfillCustomClaims as any)(
      { verbose: true },
      { auth: { uid: 'admin-uid' } }
    );

    expect(result.total).toBe(3);
    expect(result.processed).toBe(3);
    expect(result.errors).toBe(0);
  });
});
