#!/usr/bin/env python3
"""test_crescent_detect.py — test crescent moon detection on a camera frame.

Runs detect_moon() twice on the same image:
  1. Standard parameters (full/quarter moon defaults)
  2. Crescent parameters (relaxed roundness, lower brightness threshold)

Saves annotated debug images so you can see exactly what each pass found.
Optionally renders the full composite if detection succeeds and a reference
moon image (or cached NASA dial-a-moon) is available.

Usage:

  # Basic detection test against any east camera frame:
  python3 test_crescent_detect.py PATH/TO/FRAME.jpg

  # Also render the composite (needs ref moon image):
  python3 test_crescent_detect.py PATH/TO/FRAME.jpg --ref PATH/TO/MOON.jpg

  # Specify the capture time for parallactic-angle rotation:
  python3 test_crescent_detect.py PATH/TO/FRAME.jpg --ref PATH/TO/MOON.jpg \\
      --when 2026-05-08T05:30:00Z --phase waning-crescent

  # Quick self-test using the bundled east frame (no crescent, but confirms
  # the standard detector still works after the crescent changes):
  python3 test_crescent_detect.py
"""
from __future__ import annotations

import argparse
import pathlib
import sys
from datetime import datetime, timezone

HERE = pathlib.Path(__file__).resolve().parent
sys.path.insert(0, str(HERE))

# ── Default self-test frame ───────────────────────────────────────────────────
_DEFAULT_FRAME = HERE / '21-07-00.jpg'

# ── Crescent thresholds (mirrors moon_phase_monthly.CRESCENT_* defaults) ─────
CRESCENT_SATURATED_THR  = 180
CRESCENT_MIN_ROUNDNESS  = 0.15
CRESCENT_MAX_HALO_RATIO = 12.0
CRESCENT_MIN_QUALITY    = 0.35


def _label(det, min_q: float) -> str:
    if det is None:
        return 'NOT DETECTED'
    verdict = 'PASS' if det.quality >= min_q else f'FAIL (quality {det.quality:.3f} < {min_q})'
    return (
        f'{verdict}  cx={det.centroid_xy[0]:.0f} cy={det.centroid_xy[1]:.0f}  '
        f'diam={det.diameter_px:.0f}px  roundness={det.roundness:.2f}  '
        f'halo_ratio={det.halo_ratio:.1f}  quality={det.quality:.3f}'
    )


def run(frame: str, ref_moon: str | None, when_utc: datetime | None,
        phase: str, out_dir: pathlib.Path) -> int:
    from moon_detect import detect_moon, _draw_debug
    import moon_phase_monthly as mpm

    frame_p = pathlib.Path(frame)
    if not frame_p.exists():
        print(f'ERROR: frame not found: {frame}', file=sys.stderr)
        return 1

    out_dir.mkdir(parents=True, exist_ok=True)
    stem = frame_p.stem

    print(f'frame : {frame_p}')
    print(f'phase : {phase}')
    print()

    # ── Pass 1: standard detection ────────────────────────────────────────────
    det_std = detect_moon(str(frame_p))
    print(f'standard  (thr=240 round≥0.55 halo≤6.0)  →  {_label(det_std, mpm.MIN_QUALITY)}')
    debug_std = out_dir / f'{stem}_debug_standard.jpg'
    _draw_debug(str(frame_p), det_std, str(debug_std))
    print(f'  debug → {debug_std}')

    print()

    # ── Pass 2: crescent detection ────────────────────────────────────────────
    det_cre = detect_moon(
        str(frame_p),
        saturated_threshold=CRESCENT_SATURATED_THR,
        min_roundness=CRESCENT_MIN_ROUNDNESS,
        max_halo_ratio=CRESCENT_MAX_HALO_RATIO,
    )
    print(f'crescent  (thr=180 round≥0.15 halo≤12.0) →  {_label(det_cre, CRESCENT_MIN_QUALITY)}')
    debug_cre = out_dir / f'{stem}_debug_crescent.jpg'
    _draw_debug(str(frame_p), det_cre, str(debug_cre))
    print(f'  debug → {debug_cre}')

    # ── Optional composite render ─────────────────────────────────────────────
    det_for_render = det_cre if phase in ('waxing-crescent', 'waning-crescent') else det_std
    min_q = CRESCENT_MIN_QUALITY if phase in ('waxing-crescent', 'waning-crescent') else mpm.MIN_QUALITY

    if ref_moon and det_for_render is not None and det_for_render.quality >= min_q:
        print()
        if when_utc is None:
            print('note: --when not supplied; skipping parallactic rotation')
            when_utc = datetime.now(timezone.utc)

        # Monkeypatch moon_phase if skyfield is unavailable
        try:
            import moon_phase as _mp
            _mp.illumination(when_utc)          # probe — raises if no ephemeris
        except Exception:
            print('skyfield/ephemeris not available — using stub moon_phase values')
            import moon_phase as _mp
            illum = {'waxing-crescent': 0.28, 'waning-crescent': 0.28,
                     'full': 1.0, 'first-quarter': 0.50, 'third-quarter': 0.50}
            _mp.illumination        = lambda w: illum.get(phase, 0.50)
            _mp.phase_angle         = lambda w: {'waxing-crescent': 110,
                                                  'waning-crescent': 110,
                                                  'full': 0, 'first-quarter': 90,
                                                  'third-quarter': 90}.get(phase, 90)
            _mp.waxing              = lambda w: phase == 'waxing-crescent'
            _mp.parallactic_angle   = lambda w, **kw: 0.0

        from moon_composite import composite_full_moon
        out_img = out_dir / f'{stem}_{phase}_composite.jpg'
        composite_full_moon(
            source_jpg=str(frame_p),
            detection=det_for_render,
            when_utc=when_utc,
            ref_moon_path=ref_moon,
            out_path=str(out_img),
            output_size=(1920, 1080),
            moon_height_pct=0.70,
            caption=(
                f'{phase.replace("-", " ").title()} — '
                f'{when_utc.strftime("%B %Y")} — sky-cam east — NASA SVS Dial-a-Moon'
            ),
        )
        print(f'composite → {out_img}')
    elif ref_moon:
        print()
        print('composite skipped — detection did not pass quality threshold')

    return 0


def main() -> int:
    p = argparse.ArgumentParser(description=__doc__,
                                formatter_class=argparse.RawDescriptionHelpFormatter)
    p.add_argument('frame', nargs='?', default=str(_DEFAULT_FRAME),
                   help='east camera JPEG to test (default: bundled 21-07-00.jpg)')
    p.add_argument('--ref', metavar='MOON_JPG',
                   help='reference moon image for composite render '
                        '(NASA dial-a-moon PNG or any moon photo)')
    p.add_argument('--when', metavar='ISO_UTC',
                   help='capture time, e.g. 2026-05-08T05:30:00Z  '
                        '(used for parallactic rotation; defaults to now)')
    p.add_argument('--phase',
                   choices=['waxing-crescent', 'waning-crescent',
                             'full', 'first-quarter', 'third-quarter'],
                   default='waning-crescent',
                   help='which phase to simulate (affects detection thresholds '
                        'and composite labelling, default: waning-crescent)')
    p.add_argument('--out-dir', metavar='DIR', default=str(HERE),
                   help='directory for debug and composite output (default: sky-cam dir)')
    args = p.parse_args()

    when = None
    if args.when:
        when = datetime.fromisoformat(args.when.replace('Z', '+00:00'))
        if when.tzinfo is None:
            when = when.replace(tzinfo=timezone.utc)

    return run(
        frame=args.frame,
        ref_moon=args.ref,
        when_utc=when,
        phase=args.phase,
        out_dir=pathlib.Path(args.out_dir),
    )


if __name__ == '__main__':
    sys.exit(main())
