"""임시 검수 스크립트 (QA). app/모듈 핵심 로직을 합성/손계산으로 검증."""
import math
import numpy as np
import pandas as pd

import auc, oftt, breath, units, demo_data
import app

fails = []
def check(name, cond, detail=""):
    print(("PASS" if cond else "FAIL"), name, detail)
    if not cond:
        fails.append(name)

# 1) trapezoidal total AUC 손계산
# t=[0,2,4], y=[100,200,100]: 구간1=(2)*(100+200)/2=300, 구간2=(2)*(200+100)/2=300 -> 600
v = auc.total_auc([0,2,4],[100,200,100])
check("total_auc trapz", abs(v-600.0)<1e-9, f"got {v}")

# 2) incremental AUC baseline=100: delta=[0,100,0] -> 구간1=(2)*(0+100)/2=100, 구간2=(2)*(100+0)/2=100 ->200
iv = auc.incremental_auc([0,2,4],[100,200,100])
check("incremental_auc trapz", abs(iv-200.0)<1e-9, f"got {iv}")

# 3) peak/Tmax
p,tm = auc.peak_and_tmax([0,2,4,6],[100,180,150,110])
check("peak", abs(p-180)<1e-9, f"got {p}")
check("tmax", abs(tm-2)<1e-9, f"got {tm}")

# 4) cumulative_auc_to with interpolation: t=[0,2,4] y=[0,100,0], to=3
# interp at 3 -> 50; points (0,0),(2,100),(3,50): seg1=(2)(0+100)/2=100, seg2=(1)(100+50)/2=75 ->175
cv = auc.cumulative_auc_to([0,2,4],[0,100,0],3.0)
check("cumulative_auc_to interp", abs(cv-175.0)<1e-9, f"got {cv}")

# 5) return_to_baseline: baseline=100,tol10%->threshold 110; y peak then crosses
rtb = auc.return_to_baseline_time([0,2,4,6],[100,200,150,90])
# between t=4(150) and t=6(90): cross 110 -> frac=(150-110)/(150-90)=40/60=0.6667; t=4+0.6667*2=5.333
check("return_to_baseline", abs(rtb-5.3333)<1e-2, f"got {rtb}")

# 6) OFTT analyze on demo normal (no NaN/errors)
dfn = demo_data.make_oftt_normal()
res = oftt.analyze_oftt(dfn[dfn.subject_id=="N01"].sort_values("time_h")["time_h"].tolist(),
                        dfn[dfn.subject_id=="N01"].sort_values("time_h")["tg"].tolist(),
                        fat_load_g=65, expected_times=[0,2,4,6,8])
d = res.as_dict()
check("OFTT no NaN core", all(not (isinstance(d[k],float) and math.isnan(d[k]))
      for k in ["peak","tmax_h","total_auc","incremental_auc"]), str(d))
check("OFTT iauc_per_g_fat present", d["iauc_per_g_fat"] is not None and d["iauc_per_g_fat"]==res.incremental_auc/65)

# 7) delta_to_pdr hand calc
# DOB at single point: delta=[-21,-11] baseline -21 -> DOB=[0,10]; VCO2=1000, dose=4
# PDR = 10/1000 *1000 *0.0112372 /4 *100 = 0.01*1000*0.0112372/4*100
# = 0.0112372*1000... let's compute: (10/1000)=0.01; *1000=10; *0.0112372=0.112372; /4=0.028093; *100=2.8093
th,pdr = breath.delta_to_pdr([0,60],[-21,-11],dose_mmol_13c=4.0,vco2_mmol_h=1000.0)
check("delta_to_pdr hand", abs(pdr[1]-2.8093)<1e-3, f"got {pdr[1]}")
check("delta_to_pdr baseline0", abs(pdr[0]-0.0)<1e-12, f"got {pdr[0]}")
check("times_h conv", abs(th[1]-1.0)<1e-12, f"got {th[1]}")

# 8) BSA + VCO2
bsa = breath.du_bois_bsa(70,175)
check("BSA du bois ~1.84", abs(bsa-1.846)<0.02, f"got {bsa}")
vco2 = breath.estimate_vco2(70,175,300)
check("VCO2 estimate", abs(vco2-300*bsa)<1e-6, f"got {vco2}")

# 9) full breath analyze on demo, no NaN in cPDR
bdf = demo_data.make_breath_curves()
sub = bdf[bdf.subject_id=="BN01"].sort_values("time_min")
bres = breath.analyze_breath(sub["time_min"].tolist(), sub["delta13c"].tolist(),
        dose_mmol_13c=4.0, weight_kg=float(sub.weight_kg.iloc[0]),
        height_cm=float(sub.height_cm.iloc[0]),
        expected_times_min=[0,10,20,30,40,60,90,120,150,180,240])
bd = bres.as_dict()
check("breath cpdr40 finite", np.isfinite(bd["cpdr_40min_pct"]), str(bd["cpdr_40min_pct"]))
check("breath cpdr120 finite", np.isfinite(bd["cpdr_120min_pct"]))
check("breath halflife finite", np.isfinite(bd["half_life_h"]), str(bd["half_life_h"]))
check("breath fit method nonlinear-or-loglin", bres.fit_method in ("nonlinear_monoexp","loglinear"), bres.fit_method)
# normal oxidation should have faster kel than impaired
subL = bdf[bdf.subject_id=="BL01"].sort_values("time_min")
bresL = breath.analyze_breath(subL["time_min"].tolist(), subL["delta13c"].tolist(),
        dose_mmol_13c=4.0, weight_kg=float(subL.weight_kg.iloc[0]),
        height_cm=float(subL.height_cm.iloc[0]))
check("normal kel > impaired kel", bres.kel_per_h > bresL.kel_per_h,
      f"N={bres.kel_per_h:.3f} L={bresL.kel_per_h:.3f}")

# 10) units round-trip
check("TG roundtrip", abs(units.tg_mmoll_to_mgdl(units.tg_mgdl_to_mmoll(150))-150)<1e-6)
check("delta roundtrip", abs(units.ratio_to_delta(units.delta_to_ratio(-21))-(-21))<1e-9)

# 11) app helper tables
ot = app.run_oftt_table(dfn, fat_load_g=65)
check("app.run_oftt_table rows", len(ot)==dfn.subject_id.nunique(), f"{len(ot)}")
bt = app.run_breath_table(bdf)
check("app.run_breath_table rows", len(bt)==bdf.subject_id.nunique(), f"{len(bt)}")
check("app breath has group col", "group" in bt.columns)

# 12) QC flag triggers: missing timepoint + negative
dfbad = pd.DataFrame({"subject_id":["X","X","X"],"time_h":[0,2,4],"tg":[100,-5,120]})
rb = oftt.analyze_oftt(dfbad.time_h.tolist(), dfbad.tg.tolist(), expected_times=[0,2,4,6,8])
check("QC missing flag", any("누락" in f for f in rb.flags), str(rb.flags))
check("QC negative flag", any("음수" in f for f in rb.flags), str(rb.flags))

# 13) nonlinear fallback path (scipy import-fail simulation via use_nonlinear=False)
fitlin = breath.fit_kel_halflife(th_list:=[0,0.5,1,1.5,2], [5,3,1.8,1.1,0.7], use_nonlinear=False)
check("loglinear kel positive", fitlin["kel_per_h"]>0 and fitlin["method"]=="loglinear", str(fitlin))

print("\n=== SUMMARY ===")
print("FAILS:", fails if fails else "NONE")
import sys
sys.exit(1 if fails else 0)
