/**
 * migrate-review-fields.ts 단위 테스트
 *
 * firebase-admin을 mock하여 Firestore 의존성 없이 핵심 로직을 검증합니다.
 */

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

// ============================================================
// 상수
// ============================================================

const HIGH_RISK_SOURCE_TYPES = ['court_ruling', 'regulation', 'policy_pdf'] as const;
type HighRiskSourceType = typeof HIGH_RISK_SOURCE_TYPES[number];

// ============================================================
// 순수 함수: getRiskLevel
// ============================================================

function getRiskLevel(sourceType: string | null | undefined): 'high' | 'low' {
  if (!sourceType) return 'low';
  return (HIGH_RISK_SOURCE_TYPES as readonly string[]).includes(sourceType) ? 'high' : 'low';
}

// ============================================================
// mock 타입 정의
// ============================================================

interface MockDocData {
  sourceType?: string | null;
  status?: string;
  riskLevel?: string;
  title?: string;
}

interface MockDocRef {
  id: string;
  update: ReturnType<typeof vi.fn>;
  collection: ReturnType<typeof vi.fn>;
}

interface MockDoc {
  id: string;
  data: () => MockDocData;
  ref: MockDocRef;
}

interface MockRevisionDoc {
  id: string;
  data: () => Record<string, unknown>;
  ref: {
    update: ReturnType<typeof vi.fn>;
  };
}

// versions doc 참조를 재사용 가능하도록 캐싱
const _versionDocCache = new Map<string, { id: string; set: ReturnType<typeof vi.fn> }>();

function makeDocRef(id: string, revisionDocs: MockRevisionDoc[] = [], existingVersionIds: string[] = []): MockDocRef {
  const revSnapshot = {
    docs: revisionDocs,
    empty: revisionDocs.length === 0,
  };

  // versions 컬렉션 mock (doc() 호출 시 캐시된 객체 반환)
  const versionsCollection = {
    get: vi.fn().mockResolvedValue({
      docs: existingVersionIds.map((vid) => ({ id: vid })),
    }),
    doc: vi.fn((docId: string) => {
      const key = `${id}/${docId}`;
      if (!_versionDocCache.has(key)) {
        _versionDocCache.set(key, {
          id: docId,
          set: vi.fn().mockResolvedValue(undefined),
        });
      }
      return _versionDocCache.get(key)!;
    }),
  };

  const revisionsCollection = {
    get: vi.fn().mockResolvedValue(revSnapshot),
  };

  return {
    id,
    update: vi.fn().mockResolvedValue(undefined),
    collection: vi.fn((name: string) => {
      if (name === 'revisions') return revisionsCollection;
      if (name === 'versions') return versionsCollection;
      return {};
    }),
  };
}

function makeDoc(id: string, data: MockDocData, revisionDocs: MockRevisionDoc[] = [], existingVersionIds: string[] = []): MockDoc {
  return {
    id,
    data: () => data,
    ref: makeDocRef(id, revisionDocs, existingVersionIds),
  };
}

function makeRevisionDoc(id: string, data: Record<string, unknown> = {}): MockRevisionDoc {
  return {
    id,
    data: () => data,
    ref: { update: vi.fn().mockResolvedValue(undefined) },
  };
}

// ============================================================
// migrateDocument 로직 (스크립트 핵심 로직 추출)
// ============================================================

interface MigrateDocResult {
  statusAdded: boolean;
  riskLevelAdded: boolean;
}

async function migrateDocument(
  docRef: MockDocRef,
  docData: MockDocData,
  dryRun: boolean,
): Promise<MigrateDocResult> {
  const updates: Record<string, unknown> = {};
  let statusAdded = false;
  let riskLevelAdded = false;

  // status가 없으면 'published' 추가
  if (!docData.status) {
    updates.status = 'published';
    statusAdded = true;
  }

  // riskLevel이 없으면 sourceType 기반 계산
  if (!docData.riskLevel) {
    updates.riskLevel = getRiskLevel(docData.sourceType);
    riskLevelAdded = true;
  }

  if (!dryRun && Object.keys(updates).length > 0) {
    await docRef.update(updates);
  }

  return { statusAdded, riskLevelAdded };
}

// ============================================================
// consolidateRevisions 로직 (스크립트 핵심 로직 추출)
// ============================================================

async function consolidateRevisions(
  docRef: MockDocRef,
  dryRun: boolean,
): Promise<number> {
  const revisionsSnap = await docRef.collection('revisions').get();
  if (revisionsSnap.empty) return 0;

  const versionsSnap = await docRef.collection('versions').get();
  const existingVersionIds = new Set(versionsSnap.docs.map((d: { id: string }) => d.id));

  let consolidated = 0;

  for (const revDoc of revisionsSnap.docs) {
    if (existingVersionIds.has(revDoc.id)) {
      // ID 충돌 → 스킵
      console.log(`[WARN] revisions/${revDoc.id}는 versions에 이미 존재합니다. 스킵.`);
      continue;
    }

    if (!dryRun) {
      // versions/{id}로 복사
      const versionRef = docRef.collection('versions').doc(revDoc.id);
      await versionRef.set(revDoc.data());

      // 원본 revisions에 _migrated: true 마킹
      await revDoc.ref.update({ _migrated: true });
    }

    consolidated++;
  }

  return consolidated;
}

// ============================================================
// 테스트
// ============================================================

// 각 테스트 전에 캐시를 초기화
beforeEach(() => {
  _versionDocCache.clear();
});

describe('status 없는 문서 → published 설정', () => {
  it('status가 없으면 statusAdded=true, update에 published 포함', async () => {
    const doc = makeDoc('doc1', { sourceType: 'youtube' });

    const result = await migrateDocument(doc.ref, doc.data(), false);

    expect(result.statusAdded).toBe(true);
    expect(doc.ref.update).toHaveBeenCalledWith(
      expect.objectContaining({ status: 'published' })
    );
  });

  it('status가 없으면 riskLevel도 함께 설정됨', async () => {
    const doc = makeDoc('doc1', { sourceType: 'youtube' });

    const result = await migrateDocument(doc.ref, doc.data(), false);

    expect(result.statusAdded).toBe(true);
    expect(result.riskLevelAdded).toBe(true);
  });
});

describe('status 있는 문서 → 변경 없음', () => {
  it('status가 이미 있으면 statusAdded=false', async () => {
    const doc = makeDoc('doc1', { sourceType: 'youtube', status: 'published' });

    const result = await migrateDocument(doc.ref, doc.data(), false);

    expect(result.statusAdded).toBe(false);
  });

  it('status가 draft로 있으면 변경되지 않음', async () => {
    const doc = makeDoc('doc1', { sourceType: 'youtube', status: 'draft' });

    const result = await migrateDocument(doc.ref, doc.data(), false);

    expect(result.statusAdded).toBe(false);
    // update 호출 시 status는 포함되지 않아야 함
    if (doc.ref.update.mock.calls.length > 0) {
      const updateArg = doc.ref.update.mock.calls[0][0];
      expect(updateArg).not.toHaveProperty('status');
    }
  });
});

describe('riskLevel 계산 - HIGH_RISK_SOURCE_TYPES', () => {
  it('regulation → high', () => {
    expect(getRiskLevel('regulation')).toBe('high');
  });

  it('court_ruling → high', () => {
    expect(getRiskLevel('court_ruling')).toBe('high');
  });

  it('policy_pdf → high', () => {
    expect(getRiskLevel('policy_pdf')).toBe('high');
  });

  it('youtube → low', () => {
    expect(getRiskLevel('youtube')).toBe('low');
  });

  it('newsletter → low', () => {
    expect(getRiskLevel('newsletter')).toBe('low');
  });

  it('wiki_editorial → low', () => {
    expect(getRiskLevel('wiki_editorial')).toBe('low');
  });

  it('kakao_expert → low', () => {
    expect(getRiskLevel('kakao_expert')).toBe('low');
  });

  it('null → low', () => {
    expect(getRiskLevel(null)).toBe('low');
  });

  it('undefined → low', () => {
    expect(getRiskLevel(undefined)).toBe('low');
  });
});

describe('riskLevel 없음 + sourceType=regulation → high 설정', () => {
  it('riskLevel 없고 regulation → riskLevel=high', async () => {
    const doc = makeDoc('doc1', { sourceType: 'regulation' });

    const result = await migrateDocument(doc.ref, doc.data(), false);

    expect(result.riskLevelAdded).toBe(true);
    expect(doc.ref.update).toHaveBeenCalledWith(
      expect.objectContaining({ riskLevel: 'high' })
    );
  });

  it('riskLevel 없고 court_ruling → riskLevel=high', async () => {
    const doc = makeDoc('doc1', { sourceType: 'court_ruling' });

    const result = await migrateDocument(doc.ref, doc.data(), false);

    expect(result.riskLevelAdded).toBe(true);
    expect(doc.ref.update).toHaveBeenCalledWith(
      expect.objectContaining({ riskLevel: 'high' })
    );
  });
});

describe('riskLevel 없음 + sourceType=youtube → low 설정', () => {
  it('riskLevel 없고 youtube → riskLevel=low', async () => {
    const doc = makeDoc('doc1', { sourceType: 'youtube' });

    const result = await migrateDocument(doc.ref, doc.data(), false);

    expect(result.riskLevelAdded).toBe(true);
    expect(doc.ref.update).toHaveBeenCalledWith(
      expect.objectContaining({ riskLevel: 'low' })
    );
  });
});

describe('riskLevel 이미 있으면 → 변경 없음', () => {
  it('riskLevel이 이미 있으면 riskLevelAdded=false', async () => {
    const doc = makeDoc('doc1', { sourceType: 'youtube', riskLevel: 'low' });

    const result = await migrateDocument(doc.ref, doc.data(), false);

    expect(result.riskLevelAdded).toBe(false);
  });

  it('riskLevel이 이미 있으면 update에 riskLevel 미포함', async () => {
    const doc = makeDoc('doc1', { sourceType: 'regulation', status: 'published', riskLevel: 'high' });

    await migrateDocument(doc.ref, doc.data(), false);

    // 업데이트할 필드가 없으면 update 자체가 호출되지 않음
    expect(doc.ref.update).not.toHaveBeenCalled();
  });
});

describe('revisions 서브컬렉션 → versions로 복사 + _migrated 마킹', () => {
  it('revisions 문서가 versions로 복사되고 _migrated=true 마킹됨', async () => {
    const revDoc = makeRevisionDoc('rev1', { content: '첫 번째 리비전' });
    const doc = makeDoc('doc1', {}, [revDoc]);

    const consolidated = await consolidateRevisions(doc.ref, false);

    expect(consolidated).toBe(1);

    // versions 컬렉션에 set 호출 확인
    const versionsCollection = doc.ref.collection('versions');
    const versionDocRef = versionsCollection.doc('rev1');
    expect(versionDocRef.set).toHaveBeenCalledWith({ content: '첫 번째 리비전' });

    // 원본 revision에 _migrated 마킹
    expect(revDoc.ref.update).toHaveBeenCalledWith({ _migrated: true });
  });

  it('여러 revisions 문서가 모두 versions로 복사됨', async () => {
    const revDocs = [
      makeRevisionDoc('rev1', { content: '리비전 1' }),
      makeRevisionDoc('rev2', { content: '리비전 2' }),
      makeRevisionDoc('rev3', { content: '리비전 3' }),
    ];
    const doc = makeDoc('doc1', {}, revDocs);

    const consolidated = await consolidateRevisions(doc.ref, false);

    expect(consolidated).toBe(3);
    for (const revDoc of revDocs) {
      expect(revDoc.ref.update).toHaveBeenCalledWith({ _migrated: true });
    }
  });

  it('versions에 이미 동일한 ID가 있으면 해당 revision은 복사하지 않음 (ID 충돌 처리)', async () => {
    const revDoc = makeRevisionDoc('rev1', { content: '기존 버전과 충돌' });
    const doc = makeDoc('doc1', {}, [revDoc], ['rev1']); // rev1이 versions에 이미 존재

    const consolidated = await consolidateRevisions(doc.ref, false);

    expect(consolidated).toBe(0); // 충돌로 스킵
    expect(revDoc.ref.update).not.toHaveBeenCalled();
  });

  it('revisions가 없으면 0 반환', async () => {
    const doc = makeDoc('doc1', {}, []); // revisions 없음

    const consolidated = await consolidateRevisions(doc.ref, false);

    expect(consolidated).toBe(0);
  });
});

describe('dry-run 시 쓰기 없음', () => {
  it('dryRun=true이면 migrateDocument에서 update가 호출되지 않음', async () => {
    const doc = makeDoc('doc1', { sourceType: 'youtube' });

    const result = await migrateDocument(doc.ref, doc.data(), true);

    expect(result.statusAdded).toBe(true);
    expect(result.riskLevelAdded).toBe(true);
    expect(doc.ref.update).not.toHaveBeenCalled();
  });

  it('dryRun=true이면 consolidateRevisions에서 set/update가 호출되지 않음', async () => {
    const revDoc = makeRevisionDoc('rev1', { content: '리비전' });
    const doc = makeDoc('doc1', {}, [revDoc]);

    const consolidated = await consolidateRevisions(doc.ref, true);

    // 카운트는 증가하지만 실제 쓰기는 없음
    expect(consolidated).toBe(1);
    expect(revDoc.ref.update).not.toHaveBeenCalled();

    const versionsCollection = doc.ref.collection('versions');
    const versionDocRef = versionsCollection.doc('rev1');
    expect(versionDocRef.set).not.toHaveBeenCalled();
  });

  it('dryRun=false이면 update가 정상 호출됨', async () => {
    const doc = makeDoc('doc1', { sourceType: 'youtube' });

    await migrateDocument(doc.ref, doc.data(), false);

    expect(doc.ref.update).toHaveBeenCalledTimes(1);
  });
});
