"""TIASshot の実機テストのランナー（PLAN_06．ユーザーが会話の中で実機テストを合図したときだけ実行する）.

実機のカメラ（Lumenera / ImagingSource）と光源（シリアルポート）を操作する．撮影の本番中に実行しないこと．
NUnit テスト・セルフテストは実行しない（それぞれ run_tests.py・run_selftest.py）．

手順:
  1. ランナーがビルドする構成（Debug|x64）の出力 bin/x64/Debug・obj/x64/Debug を削除する（--no-clean で省略）
  2. Debug|x64 をビルドする（HWTEST フラグ入り）
  3. TIASshot.exe が起動していないことを確かめる（起動中はカメラ・光源を使用中＝撮影の本番中の可能性があるため実行しない）
  4. TIASshot.exe --hwtest --camera <機種> を実行ファイルのフォルダで実行し，終了コードと結果ファイルで判定する
     （実際のフォームが表示される．保存先は %TEMP%\\TIASshot-hwtest\\work-* に差し替えられる）

--with-chart を付けると，チャート不要の検査（preview〜reconnect）の代わりにチャートが必要な検査
（calibrate・shot・close-saving・close-calibrating．校正を止めない．カメラの前に舌診チャートがはまっている前提）を行う．
チャート不要の検査と続けて実行する（connect・cleanup は両方で行う）．

出力: .verify/hwtest/run-<日時>/ に build.log・hwtest-result.txt・summary.json
終了コード: 0=全項目合格，1=不合格あり（ビルド失敗・検査の不合格・タイムアウト），
            2=前提条件を満たさない（ツール未導入・TIASshot 起動中・指定機種が見つからない・引数不正）

使い方（リポジトリルートで実行）:
  python scripts/verify/run_hwtest.py --camera lucam
  python scripts/verify/run_hwtest.py --camera iscam
  python scripts/verify/run_hwtest.py --camera iscam --with-chart   # チャートが必要な検査
  python scripts/verify/run_hwtest.py --camera iscam --no-clean   # Debug 出力を消さない（開発時の短縮用）
"""

from __future__ import annotations

import argparse
import datetime as dt
import json
import subprocess
import sys

from run_selftest import (BUILD_TIMEOUT_SEC, EXE, EXIT_FAIL, EXIT_OK, EXIT_PRECONDITION, REPO, SLN,
                          PreconditionError, clean_build_outputs, find_vs, run_logged)

# 実機テスト内部のウォッチドッグ（全体: チャート不要 10 分・--with-chart 20 分）より長くする．超えたらランナーがプロセスを打ち切る
HWTEST_TIMEOUT_SEC = 720
HWTEST_WITH_CHART_TIMEOUT_SEC = 1320

# 実機テスト（TIASshot.exe --hwtest）の終了コード
HW_PASS = 0
HW_FAIL = 1
HW_ERROR = 2
HW_TIMEOUT = 3


def is_tiasshot_running() -> bool:
    """TIASshot.exe が起動しているか（人間が撮影中ならカメラ・光源を使用中のため実機テストを行わない）."""
    proc = subprocess.run(["tasklist", "/FI", "IMAGENAME eq TIASshot.exe", "/FO", "CSV", "/NH"],
                          capture_output=True, check=False)
    out = proc.stdout.decode("cp932", errors="replace")
    return "tiasshot.exe" in out.lower()


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    parser.add_argument("--camera", required=True, choices=["lucam", "iscam"], help="検査するカメラの機種")
    parser.add_argument("--with-chart", action="store_true",
                        help="チャートが必要な検査（calibrate・shot・close-saving・close-calibrating．PLAN_06 フェーズ 2）を行う"
                             "（チャート不要の検査は行わない．舌診チャートをはめてから実行する）")
    parser.add_argument("--keep", action="store_true",
                        help="合格時も作業フォルダ（%%TEMP%%/TIASshot-hwtest/work-*）を残し，--with-chart では証拠の画像（evidence/*.png）も保存する（TIASshot.exe の --hwtest-keep）")
    parser.add_argument("--no-clean", action="store_true",
                        help="Debug 出力（bin/obj の x64/Debug）を削除しない（開発時の短縮用）")
    args = parser.parse_args()
    # エージェント（Git Bash 経由）が読めるよう，本ランナーの出力は UTF-8 に固定する
    if hasattr(sys.stdout, "reconfigure"):
        sys.stdout.reconfigure(encoding="utf-8")

    stamp = dt.datetime.now().strftime("%Y%m%d-%H%M%S")
    run_dir = REPO / ".verify" / "hwtest" / f"run-{stamp}"
    run_dir.mkdir(parents=True, exist_ok=True)
    summary: dict = {"run_dir": str(run_dir), "camera": args.camera, "with_chart": args.with_chart, "keep": args.keep,
                     "steps": [], "result": None}

    def step(name: str, ok: bool, detail: str) -> None:
        summary["steps"].append({"name": name, "ok": ok, "detail": detail})
        print(f"[{'OK' if ok else 'NG'}] {name}: {detail}", flush=True)

    exit_code = EXIT_OK
    try:
        vs = find_vs()
        msbuild = vs / "MSBuild" / "Current" / "Bin" / "MSBuild.exe"
        if not msbuild.exists():
            raise PreconditionError(f"MSBuild.exe が見つかりません: {msbuild}")

        if is_tiasshot_running():
            raise PreconditionError("TIASshot.exe が起動しています（撮影の本番中の可能性．カメラ・光源を使用中のため実機テストを行いません）")

        if not args.no_clean:
            clean_build_outputs()
            step("clean", True, "Debug 出力（bin/obj の x64/Debug）を削除")

        code = run_logged([str(msbuild), str(SLN), "/t:Build", "/p:Configuration=Debug", "/p:Platform=x64",
                           "/nologo", "/v:minimal"], run_dir / "build.log", BUILD_TIMEOUT_SEC)
        step("build", code == 0, f"Debug|x64 ビルド exit {code}（build.log）")
        if code != 0:
            exit_code = EXIT_FAIL
            raise RuntimeError("ビルド失敗")

        result_file = run_dir / "hwtest-result.txt"
        cmd = [str(EXE), "--hwtest", "--camera", args.camera, "--hwtest-out", str(result_file)]
        if args.keep:
            cmd.append("--hwtest-keep")
        if args.with_chart:
            cmd.append("--with-chart")
        summary["command"] = " ".join(cmd)
        timeout_sec = HWTEST_WITH_CHART_TIMEOUT_SEC if args.with_chart else HWTEST_TIMEOUT_SEC
        try:
            # デバイス状態ファイル（{DeviceName}.xml）は相対パスで読まれるため，通常起動と同じく実行ファイルのフォルダで実行する
            proc = subprocess.run(cmd, cwd=EXE.parent, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
                                  timeout=timeout_sec, check=False)
            code = proc.returncode
        except subprocess.TimeoutExpired:
            code = None
        text = result_file.read_text(encoding="utf-8-sig", errors="replace") if result_file.exists() else ""
        heads = [ln for ln in text.splitlines() if ln.startswith("[") or ln.startswith("RESULT:")]
        summary["hwtest_exit_code"] = code
        summary["hwtest_lines"] = heads
        if code is None:
            detail = f"{timeout_sec} 秒で終わらないためランナーが打ち切りました（hwtest-result.txt）"
        else:
            detail = f"exit {code}（hwtest-result.txt）"
        step("hwtest", code == HW_PASS, detail)
        for ln in heads:
            print(f"    {ln}", flush=True)
        if code == HW_ERROR:
            # 指定機種が見つからない・引数不正 → 実機テスト自体を行えない（検証不能）
            exit_code = EXIT_PRECONDITION
        elif code != HW_PASS:
            exit_code = EXIT_FAIL
    except PreconditionError as ex:
        step("precondition", False, str(ex))
        exit_code = EXIT_PRECONDITION
    except RuntimeError as ex:
        step("abort", False, str(ex))
        exit_code = EXIT_FAIL
    except subprocess.TimeoutExpired as ex:
        step("timeout", False, f"{ex.cmd} が {ex.timeout} 秒で終わりません")
        exit_code = EXIT_FAIL

    summary["result"] = {EXIT_OK: "PASS", EXIT_FAIL: "FAIL", EXIT_PRECONDITION: "PRECONDITION"}[exit_code]
    (run_dir / "summary.json").write_text(json.dumps(summary, ensure_ascii=False, indent=2), encoding="utf-8")
    print(f"RESULT: {summary['result']}  (出力: {run_dir})", flush=True)
    return exit_code


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