
    i /                     ~   d Z ddlZddlZddlZddlZddlZej                  j                  dej                  j                  ej                  j                  e
      d             ddlmZmZmZmZmZmZ g dZg dZdZdZ G d	 d
      Z G d d      Z G d d      Z G d d      Z G d d      Z G d d      Zy)u   conversion_tracker.py 단위 테스트.

TDD RED→GREEN 방식으로 작성된 테스트.
모든 테스트 데이터는 명시적으로 "샘플 데이터"로 표기됨.
    Nz..)calculate_funnelcalculate_dropoffanalyze_ai_sourcesgenerate_insights	parse_csvbuild_report))   AI 검색 도착  )   콘텐츠 소비^  )   인터랙션x   )   관심<   )   전환   )	   재방문
   )   추천   ))r	      ChatGPT)r	   r   
Perplexity)r	      Gemini)r      r   )r      r   )r      r   uq   stage,users
AI 검색 도착,500
콘텐츠 소비,350
인터랙션,120
관심,60
전환,25
재방문,10
추천,3
u   stage,users,source
AI 검색 도착,180,ChatGPT
AI 검색 도착,120,Perplexity
AI 검색 도착,200,Gemini
전환,12,ChatGPT
전환,8,Perplexity
전환,5,Gemini
c                   $    e Zd Zd Zd Zd Zd Zy)TestParseCsvc                     t        t        j                  t                    }t	        |      dk(  sJ |d   d   dk(  sJ |d   d   dk(  sJ |d   j                  d      J y)	u5   소스 컬럼 없는 CSV 파싱 — 샘플 데이터.   r   stager	   usersr
   sourceN)r   ioStringIOSAMPLE_CSV_NO_SOURCElengetselfrowss     H/home/jay/workspace/tools/geo-analytics/tests/test_conversion_tracker.pytest_parse_csv_no_source_columnz,TestParseCsv.test_parse_csv_no_source_columnM   sn    %9:;4yA~~Aww#5555Aww3&&&Aw{{8$,,,    c                     t        t        j                  t                    }t	        |      dk(  sJ |d   d   dk(  sJ |d   d   dk(  sJ y)u5   소스 컬럼 있는 CSV 파싱 — 샘플 데이터.   r   r%   r   r$   r   N)r   r&   r'   SAMPLE_CSV_WITH_SOURCEr)   r+   s     r.   !test_parse_csv_with_source_columnz.TestParseCsv.test_parse_csv_with_source_columnU   sT    %;<=4yA~~Awx I---Aww3&&&r0   c                     t        t        j                  t                    }|D ]  }t	        |d   t
              rJ  y)uC   users 값이 정수로 파싱되는지 확인 — 샘플 데이터.r$   N)r   r&   r'   r(   
isinstanceint)r,   r-   rows      r.   !test_parse_csv_users_are_integersz.TestParseCsv.test_parse_csv_users_are_integers\   s9    %9:; 	1Cc'lC000	1r0   c                 N    t        t        j                  d            }|g k(  sJ y)u    빈 CSV 파일 엣지 케이스.zstage,users
N)r   r&   r'   r+   s     r.   test_parse_csv_empty_filez&TestParseCsv.test_parse_csv_empty_fileb   s!    _56rzzr0   N)__name__
__module____qualname__r/   r4   r9   r;    r0   r.   r    r    L   s    -'1r0   r    c                   6    e Zd Zd Zd Zd Zd Zd Zd Zd Z	y)	TestCalculateFunnelc                 N    t        t              }|d   d   dk(  sJ d|d   vsJ y)u4   첫 번째 단계는 rate=100% — 샘플 데이터.r   ratez100%dropoffNr   SAMPLE_STAGE_DATAr,   funnels     r.    test_funnel_first_stage_rate_100z4TestCalculateFunnel.test_funnel_first_stage_rate_100n   s6    !"34ay F***q	)))r0   c                 B    t        t              }t        |      dk(  sJ y)u$   7단계 퍼널 — 샘플 데이터.r"   N)r   rF   r)   rG   s     r.   test_funnel_stages_countz,TestCalculateFunnel.test_funnel_stages_countt   s    !"346{ar0   c                 V    t        t              }|d   d   dk(  sJ |d   d   dk(  sJ y)u&   users 값 보존 — 샘플 데이터.r   r$   r
      r   NrE   rG   s     r.   test_funnel_users_preservedz/TestCalculateFunnel.test_funnel_users_preservedy   s;    !"34ay!S(((ay!R'''r0   c                 V    t        t              }|d   d   dk(  sJ |d   d   dk(  sJ y)u)   stage 이름 보존 — 샘플 데이터.r   r#   r	   rM   r   NrE   rG   s     r.   test_funnel_stage_namesz+TestCalculateFunnel.test_funnel_stage_names   s<    !"34ay!%7777ay!X---r0   c                 <    t        t              }|d   d   dk(  sJ y)u@   두 번째 단계 rate = 350/500*100 = 70% — 샘플 데이터.   rC   z70.0%NrE   rG   s     r.   test_funnel_second_stage_ratez1TestCalculateFunnel.test_funnel_second_stage_rate   s$    !"34ay G+++r0   c                 B    t        t              }|dd D ]  }d|v rJ  y)uG   첫 번째 제외 모든 단계에 dropoff 존재 — 샘플 데이터.rR   NrD   rE   )r,   rH   r#   s      r.   )test_funnel_all_have_dropoff_except_firstz=TestCalculateFunnel.test_funnel_all_have_dropoff_except_first   s0    !"34ABZ 	&E%%%	&r0   c                 (    t        g       }|g k(  sJ y)u   빈 데이터 엣지 케이스.N)r   rG   s     r.   test_funnel_empty_dataz*TestCalculateFunnel.test_funnel_empty_data   s    !"%||r0   N)
r<   r=   r>   rI   rK   rN   rP   rS   rU   rW   r?   r0   r.   rA   rA   m   s%    * 
(.,
&r0   rA   c                   *    e Zd Zd Zd Zd Zd Zd Zy)TestCalculateDropoffc                 P    t        dd      }|t        j                  d      k(  sJ y)uE   기본 드롭오프율: (500-350)/500*100 = 30% — 샘플 데이터.r
   r   g      >@Nr   pytestapproxr,   results     r.   test_dropoff_basicz'TestCalculateDropoff.test_dropoff_basic   s%    "3,t,,,,r0   c                 *    t        dd      }|dk(  sJ y)uD   이전 단계 users=0 엣지 케이스 — ZeroDivisionError 방지.r   d           N)r   r^   s     r.   test_dropoff_zero_previousz/TestCalculateDropoff.test_dropoff_zero_previous   s    "1c*}}r0   c                 P    t        dd      }|t        j                  d      k(  sJ y)uD   드롭오프 없는 경우 (이전=현재): 0% — 샘플 데이터.rb   rc   Nr[   r^   s     r.   test_dropoff_no_dropoffz,TestCalculateDropoff.test_dropoff_no_dropoff   s%    "3,s++++r0   c                 P    t        dd      }|t        j                  d      k(  sJ y)u)   전체 이탈: 100% — 샘플 데이터.rb   r   g      Y@Nr[   r^   s     r.   test_dropoff_full_dropoffz.TestCalculateDropoff.test_dropoff_full_dropoff   s%    "3*u----r0   c                 P    t        dd      }|t        j                  d      k(  sJ y)uP   인터랙션→관심 드롭오프: (120-60)/120*100 = 50% — 샘플 데이터.r   r   g      I@Nr[   r^   s     r.   &test_dropoff_interaction_to_conversionz;TestCalculateDropoff.test_dropoff_interaction_to_conversion   s%    "3+t,,,,r0   N)r<   r=   r>   r`   rd   rf   rh   rj   r?   r0   r.   rY   rY      s    -

,
.
-r0   rY   c                   0    e Zd Zd Zd Zd Zd Zd Zd Zy)TestAnalyzeAiSourcesc                 F    t        t              }d|v sJ d|v sJ d|v sJ y)uS   샘플 소스 데이터에 ChatGPT, Perplexity, Gemini 포함 — 샘플 데이터.r   r   r   Nr   SAMPLE_SOURCE_DATAr^   s     r.   test_sources_presentz)TestAnalyzeAiSources.test_sources_present   s7    #$67F"""v%%%6!!!r0   c                 <    t        t              }|d   d   dk(  sJ y)u*   ChatGPT arrivals=180 — 샘플 데이터.r   arrivalsr   Nrn   r^   s     r.   test_arrivals_correctz*TestAnalyzeAiSources.test_arrivals_correct   s%    #$67i ,333r0   c                 <    t        t              }|d   d   dk(  sJ y)u,   ChatGPT conversions=12 — 샘플 데이터.r   conversionsr   Nrn   r^   s     r.   test_conversions_correctz-TestAnalyzeAiSources.test_conversions_correct   s%    #$67i /2555r0   c                     t        t              }t        |d   d   j                  dd            }|t	        j
                  dd      k(  sJ y)	u6   ChatGPT CVR = 12/180*100 = 6.67% — 샘플 데이터.r   cvr% gGz@g{Gz?)relN)r   ro   floatreplacer\   r]   )r,   r_   cvr_vals      r.   test_cvr_calculationz)TestAnalyzeAiSources.test_cvr_calculation   sG    #$67y)%088bAB&--$7777r0   c                 (    t        g       }|i k(  sJ y)u&   빈 소스 데이터 엣지 케이스.Nr   r^   s     r.   test_empty_source_dataz+TestAnalyzeAiSources.test_empty_source_data   s    #B'||r0   c                 T    dg}t        |      }|d   d   dk(  sJ |d   d   dk(  sJ y)uB   전환 단계 없는 소스는 conversions=0 — 샘플 데이터.)r	   rb   Clauder   ru   r   rx   z0.0%Nr   )r,   datar_   s      r.   $test_source_without_conversion_stagez9TestAnalyzeAiSources.test_source_without_conversion_stage   sD    34#D)h.!333h&&000r0   N)	r<   r=   r>   rp   rs   rv   r   r   r   r?   r0   r.   rl   rl      s     "4
6
8
1r0   rl   c                   6    e Zd Zd Zd Zd Zd Zd Zd Zd Z	y)	TestGenerateInsightsc                 T    t        t              | _        t        t              | _        y)u4   샘플 데이터로 퍼널 및 소스 분석 준비.N)r   rF   rH   r   ro   sources)r,   s    r.   setup_methodz!TestGenerateInsights.setup_method   s    &'89)*<=r0   c                 h    t        | j                  | j                        }t        |t              sJ y)u6   인사이트 결과가 리스트 — 샘플 데이터.N)r   rH   r   r6   listr,   insightss     r.   test_insights_is_listz*TestGenerateInsights.test_insights_is_list   s&    $T[[$,,?(D)))r0   c                 l    t        | j                  | j                        }t        d |D              sJ y)u>   최대 이탈 지점 인사이트 포함 — 샘플 데이터.c              3   $   K   | ]  }d |v  
 yw)u   최대 이탈Nr?   .0is     r.   	<genexpr>zETestGenerateInsights.test_insights_has_max_dropoff.<locals>.<genexpr>   s     :A?a':   Nr   rH   r   anyr   s     r.   test_insights_has_max_dropoffz2TestGenerateInsights.test_insights_has_max_dropoff   s*    $T[[$,,?:::::r0   c                 l    t        | j                  | j                        }t        d |D              sJ y)u>   최고 전환 소스 인사이트 포함 — 샘플 데이터.c              3   $   K   | ]  }d |v  
 yw)u   최고 전환 소스Nr?   r   s     r.   r   zETestGenerateInsights.test_insights_has_best_source.<locals>.<genexpr>        A1)Q.Ar   Nr   r   s     r.   test_insights_has_best_sourcez2TestGenerateInsights.test_insights_has_best_source   *    $T[[$,,?AAAAAr0   c                 l    t        | j                  | j                        }t        d |D              sJ y)u>   최저 전환 소스 인사이트 포함 — 샘플 데이터.c              3   $   K   | ]  }d |v  
 yw)u   최저 전환 소스Nr?   r   s     r.   r   zFTestGenerateInsights.test_insights_has_worst_source.<locals>.<genexpr>   r   r   Nr   r   s     r.   test_insights_has_worst_sourcez3TestGenerateInsights.test_insights_has_worst_source   r   r0   c                 @    t        g i       }t        |t              sJ y)u8   빈 퍼널 엣지 케이스 — 크래시 없어야 함.N)r   r6   r   r   s     r.   test_insights_empty_funnelz/TestGenerateInsights.test_insights_empty_funnel   s    $R,(D)))r0   c                 d    t        | j                  | j                        }t        |      dk\  sJ y)u5   인사이트 최소 1개 이상 — 샘플 데이터.rR   N)r   rH   r   r)   r   s     r.   test_insights_min_countz,TestGenerateInsights.test_insights_min_count  s(    $T[[$,,?8}!!!r0   N)
r<   r=   r>   r   r   r   r   r   r   r   r?   r0   r.   r   r      s'    >
*
;
B
B
*
"r0   r   c                   6    e Zd Zd Zd Zd Zd Zd Zd Zd Z	y)	TestBuildReportc                 8    t        t        t              }d|v sJ y)u4   리포트에 funnel 키 존재 — 샘플 데이터.rH   Nr   rF   ro   r,   reports     r.   test_report_has_funnel_keyz*TestBuildReport.test_report_has_funnel_key  s    /1CD6!!!r0   c                 8    t        t        t              }d|v sJ y)uA   리포트에 ai_source_breakdown 키 존재 — 샘플 데이터.ai_source_breakdownNr   r   s     r.   'test_report_has_ai_source_breakdown_keyz7TestBuildReport.test_report_has_ai_source_breakdown_key  s    /1CD$...r0   c                 8    t        t        t              }d|v sJ y)u6   리포트에 insights 키 존재 — 샘플 데이터.r   Nr   r   s     r.   test_report_has_insights_keyz,TestBuildReport.test_report_has_insights_key  s    /1CDV###r0   c                     t        t        t              }t        j                  |d      }t        j
                  |      }|d   d   d   dk(  sJ y)u8   리포트가 JSON 직렬화 가능 — 샘플 데이터.F)ensure_asciirH   r   r#   r	   N)r   rF   ro   jsondumpsloads)r,   r   dumpedloadeds       r.   test_report_json_serializablez-TestBuildReport.test_report_json_serializable  sL    /1CDF7F#h"7+/AAAAr0   c                 R    t        t        t              }t        |d         dk(  sJ y)u$   퍼널 7단계 — 샘플 데이터.rH   r"   N)r   rF   ro   r)   r   s     r.   test_report_funnel_lengthz)TestBuildReport.test_report_funnel_length&  s(    /1CD6(#$)))r0   c                 8    t        t        g       }|d   i k(  sJ y)uB   소스 데이터 없을 때 리포트 생성 — 샘플 데이터.r   N)r   rF   r   s     r.   test_report_no_source_dataz*TestBuildReport.test_report_no_source_data+  s#    /4+,222r0   c                 N   g d}t        |g       }|d   }t        d |D              }t        |d   j                  dd            t	        j
                  d      k(  sJ t        d |D              }t        |d	   j                  dd            t	        j
                  d
      k(  sJ y)u;   보험 도메인 샘플 데이터 — 실제 비율 검증.))r	   i  )r   i  )r   r   )r   P   )r      )r      )r   rM   rH   c              3   2   K   | ]  }|d    dk(  s|  yw)r#   r   Nr?   r   fs     r.   r   zFTestBuildReport.test_report_insurance_domain_sample.<locals>.<genexpr>?  s     LaQwZ85KL   rC   ry   rz   g      @c              3   2   K   | ]  }|d    dk(  s|  yw)r#   r   Nr?   r   s     r.   r   zFTestBuildReport.test_report_insurance_domain_sample.<locals>.<genexpr>B  s     S1!G*@R2RQSr   rD   g     A@N)r   nextr|   r}   r\   r]   )r,   insurance_stagesr   rH   conversion_stagecontent_stages         r.   #test_report_insurance_domain_samplez3TestBuildReport.test_report_insurance_domain_sample0  s    
 .3!L6LL%f-55c2>?6==QTCUUUUSSS]9-55c2>?6==QUCVVVVr0   N)
r<   r=   r>   r   r   r   r   r   r   r   r?   r0   r.   r   r     s'    "
/
$
B*
3
Wr0   r   )__doc__r&   r   sysosr\   pathinsertjoindirname__file__conversion_trackerr   r   r   r   r   r   rF   ro   r(   r3   r    rA   rY   rl   r   r   r?   r0   r.   <module>r      s    
  
 	  277<< 94@ A 	     B& &\- -@"1 "1T"" ""T4W 4Wr0   