/**
 * Cloud Functions Custom Claims 단위 테스트
 *
 * 테스트 대상:
 *  - syncCustomClaims  : Firestore 트리거 (setCustomClaims.ts)
 *  - backfillCustomClaims : HTTPS Callable (backfillClaims.ts)
 *
 * Firebase 에뮬레이터 없이 vi.mock 기반으로 동작
 */

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

// ── firebase-admin mock ──────────────────────────────────────────────────────
const mockGetUser = vi.fn().mockResolvedValue({});
const mockSetCustomUserClaims = vi.fn().mockResolvedValue(undefined);

const mockUsersCollection = {
  docs: [
    { id: 'user1', data: () => ({ role: 'admin' }) },
    { id: 'user2', data: () => ({ role: 'member' }) },
    { id: 'user3', data: () => ({ role: 'guest' }) },
  ],
};

const mockCallerDocGet = vi.fn().mockResolvedValue({
  data: () => ({ role: 'admin' }),
});

const mockFirestore = {
  collection: vi.fn((_name: string) => ({
    doc: vi.fn(() => ({ get: mockCallerDocGet })),
    get: vi.fn().mockResolvedValue(mockUsersCollection),
  })),
};

vi.mock('firebase-admin', () => ({
  auth: vi.fn(() => ({
    getUser: mockGetUser,
    setCustomUserClaims: mockSetCustomUserClaims,
  })),
  firestore: vi.fn(() => mockFirestore),
  apps: [{}],
  initializeApp: vi.fn(),
}));

// ── firebase-functions mock ──────────────────────────────────────────────────
vi.mock('firebase-functions', () => {
  class MockHttpsError extends Error {
    code: string;
    constructor(code: string, message: string) {
      super(message);
      this.code = code;
    }
  }

  return {
    logger: {
      info: vi.fn(),
      error: vi.fn(),
    },
    firestore: {
      document: vi.fn(() => ({
        onWrite: vi.fn((handler: any) => handler),
      })),
    },
    https: {
      onCall: vi.fn((handler: any) => handler),
      HttpsError: MockHttpsError,
    },
  };
});

// ── 모듈 import (mock 선언 후) ───────────────────────────────────────────────
import * as admin from 'firebase-admin';

// syncCustomClaims: functions.firestore.document().onWrite()의 반환값이 handler 자체
// backfillCustomClaims: functions.https.onCall()의 반환값이 handler 자체
import { syncCustomClaims } from '../setCustomClaims';
import { backfillCustomClaims } from '../backfillClaims';

// ── syncCustomClaims 테스트 ──────────────────────────────────────────────────
describe('syncCustomClaims', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    mockSetCustomUserClaims.mockResolvedValue(undefined);
  });

  it('should set claims when role changes', async () => {
    const change = {
      before: { exists: true, data: () => ({ role: 'member' }) },
      after: { exists: true, data: () => ({ role: 'reviewer' }) },
    };
    const context = { params: { uid: 'test-uid' } };

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

    expect(admin.auth().setCustomUserClaims).toHaveBeenCalledWith('test-uid', { role: 'reviewer' });
  });

  it('should skip when role unchanged', async () => {
    const change = {
      before: { exists: true, data: () => ({ role: 'member' }) },
      after: { exists: true, data: () => ({ role: 'member' }) },
    };
    const context = { params: { uid: 'test-uid' } };

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

    expect(admin.auth().setCustomUserClaims).not.toHaveBeenCalled();
  });

  it('should clear claims when user deleted', async () => {
    const change = {
      before: { exists: true, data: () => ({ role: 'member' }) },
      after: { exists: false, data: () => null },
    };
    const context = { params: { uid: 'test-uid' } };

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

    expect(admin.auth().setCustomUserClaims).toHaveBeenCalledWith('test-uid', {});
  });

  it('should reject invalid role', async () => {
    const change = {
      before: { exists: true, data: () => ({ role: 'member' }) },
      after: { exists: true, data: () => ({ role: 'superadmin' }) },
    };
    const context = { params: { uid: 'test-uid' } };

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

    expect(admin.auth().setCustomUserClaims).not.toHaveBeenCalled();
  });
});

// ── backfillCustomClaims 테스트 ──────────────────────────────────────────────
describe('backfillCustomClaims', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    mockGetUser.mockResolvedValue({});
    mockSetCustomUserClaims.mockResolvedValue(undefined);
    // 기본: admin 호출자
    mockCallerDocGet.mockResolvedValue({ data: () => ({ role: 'admin' }) });
  });

  it('should reject unauthenticated calls', async () => {
    await expect(
      (backfillCustomClaims as any)({}, { auth: null })
    ).rejects.toThrow('Authentication required');
  });

  it('should reject non-admin calls', async () => {
    mockCallerDocGet.mockResolvedValueOnce({ data: () => ({ role: 'member' }) });

    await expect(
      (backfillCustomClaims as any)({}, { auth: { uid: 'non-admin' } })
    ).rejects.toThrow('Admin access required');
  });

  it('should process all users when called by admin', async () => {
    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);
    expect(result.results).toHaveLength(3);
  });

  it('should skip users not found in Auth', async () => {
    const notFoundError = Object.assign(new Error('There is no user record'), {
      code: 'auth/user-not-found',
    });
    // user2만 Auth에 없는 상황
    mockGetUser
      .mockResolvedValueOnce({})           // user1: 존재
      .mockRejectedValueOnce(notFoundError) // user2: 없음
      .mockResolvedValueOnce({});           // user3: 존재

    const result = await (backfillCustomClaims as any)(
      { verbose: true },
      { auth: { uid: 'admin-uid' } }
    );

    expect(result.total).toBe(3);
    expect(result.processed).toBe(2);
    expect(result.skipped).toBe(1);
    expect(result.errors).toBe(0);

    const skippedEntry = result.results.find((r: any) => r.status === 'skipped_no_user');
    expect(skippedEntry).toBeDefined();
    expect(skippedEntry.uid).toBe('user2');
  });

  it('should coerce invalid role to guest via resolveRole', async () => {
    // CF 내부에서 collection()은 두 번 호출:
    //   1차: collection('users').doc(callerUid).get()  → caller role 확인
    //   2차: collection('users').get()                 → 전체 유저 조회
    const invalidRoleCollection = {
      docs: [{ id: 'user-invalid', data: () => ({ role: 'superadmin' }) }],
    };
    mockFirestore.collection
      // 1차 호출: caller doc 조회 (기존 mockCallerDocGet 유지)
      .mockImplementationOnce((_name: string) => ({
        doc: vi.fn(() => ({ get: mockCallerDocGet })),
        get: vi.fn(),
      }))
      // 2차 호출: invalid role 유저만 있는 컬렉션
      .mockImplementationOnce((_name: string) => ({
        doc: vi.fn(),
        get: vi.fn().mockResolvedValue(invalidRoleCollection),
      }));

    const result = await (backfillCustomClaims as any)(
      { verbose: true },
      { auth: { uid: 'admin-uid' } }
    );

    expect(result.processed).toBe(1);
    expect(result.results[0].role).toBe('guest');
    expect(result.results[0].status).toBe('success');
  });
});
