#!/usr/bin/env python3
"""
parity_test.py - 펌웨어 wire 계약을 서버(PHP)와 교차검증한다.

검증 항목:
 1. SHT30 본문이 유효한 JSON 이고 서버 필수 필드를 포함한다.
 2. raw-body 서명(HMAC-SHA256(key=API_KEY, msg=body))이 서버 verify_signature_raw 로 통과한다.
 3. 본문/서명 변조 시 서버가 거부한다.
 4. SHT3x CRC8 및 변환식이 데이터시트 벡터와 일치한다.
 5. fixed2() 소수 포맷이 기대 문자열과 일치한다(펌웨어 jb_fixed2 와 동일).

C 펌웨어(jsonbody.c/sig.c/sht30_convert.c)는 reference.py 와 동일 알고리즘으로
작성되어 있어, 본 테스트 통과는 펌웨어 wire 계약이 서버와 호환됨을 의미한다.
"""
from __future__ import annotations

import json
import subprocess
import sys
from pathlib import Path

import reference as ref

HERE = Path(__file__).resolve().parent
PHP_VERIFY = HERE / "php_verify.php"

failures: list[str] = []


def check(cond: bool, msg: str) -> None:
    if not cond:
        failures.append(msg)


def php_api_key() -> str:
    # bytes 모드로 받아 UTF-8 로 디코드 (Windows 로캘 인코딩 영향 제거)
    out = subprocess.run(["php", str(PHP_VERIFY), "--print-key"],
                         capture_output=True, check=True)
    return out.stdout.decode("utf-8")


def php_verify(body: str, sig: str) -> str:
    # 본문은 반드시 UTF-8 바이트로 전달해야 한다. 서명도 동일 UTF-8 본문에 대해
    # HMAC 으로 계산되므로(sign_raw: hmac key=API_KEY, msg=body.encode("utf-8")),
    # 바이트가 일치해야 통과한다.
    out = subprocess.run(["php", str(PHP_VERIFY), sig],
                         input=body.encode("utf-8"), capture_output=True, check=True)
    return out.stdout.decode("utf-8").strip()


REQUIRED = ["device_id", "sensor_id", "event_type", "timestamp"]


def assert_server_accepts(body: str, api_key: str, label: str) -> None:
    # 유효한 JSON + 필수 필드
    try:
        obj = json.loads(body)
    except Exception as exc:  # noqa: BLE001
        check(False, f"{label}: body is not valid JSON: {exc}")
        return
    for field in REQUIRED:
        check(field in obj, f"{label}: missing required field '{field}'")

    # 올바른 서명 -> 서버 통과
    good = ref.sign_raw(api_key, body)
    check(php_verify(body, good) == "OK", f"{label}: valid signature should pass server verify")

    # 본문 1바이트 변조 -> 거부
    check(php_verify(body + " ", good) == "FAIL", f"{label}: tampered body must fail")
    # 서명 변조 -> 거부
    bad = ("0" * 64) if good != "0" * 64 else ("1" * 64)
    check(php_verify(body, bad) == "FAIL", f"{label}: wrong signature must fail")


def main() -> int:
    api_key = php_api_key()
    check(api_key != "", "PHP API_KEY should be readable")

    # ── 1~3. SHT30 이벤트 (startup/periodic, 정상/경계/음수/정수값 float) ─────
    sht_cases = [
        ("startup",  24.0,   48.5,  "normal"),
        ("periodic", 24.0,   48.5,  "normal"),
        ("periodic", -3.245, 0.0,   "normal"),
        ("periodic", 100.0,  100.0, "normal"),
        ("periodic", 125.0,  0.004, "out_of_range"),
    ]
    for ev, t, h, st in sht_cases:
        body = ref.sht30_event_body(
            device_id="stm32-sht30-01",
            device_location="서버실",
            sensor_id=2,
            sensor_name="2번 센서 (SHT30)",
            event_type=ev,
            timestamp=1700000123,
            temperature_c=t,
            humidity_percent=h,
            metric_status=st,
            app_version="v2606-sht30",
        )
        assert_server_accepts(body, api_key, f"sht30/{ev}/{t}/{h}")

    # ── 4. SHT3x CRC8 / 변환 데이터시트 벡터 ─────────────────────────────────
    # 데이터시트: CRC8(0xBE,0xEF) = 0x92
    check(ref.sht30_crc8(bytes([0xBE, 0xEF])) == 0x92, "SHT3x CRC8 datasheet vector (0xBEEF -> 0x92)")

    # raw_t=0x6635 -> 약 25.0C, raw_rh=0x7E66 -> 약 49.4%RH (근사 검증)
    raw_t = 0x6635
    raw_rh = 0x7E66
    frame = bytes([
        (raw_t >> 8) & 0xFF, raw_t & 0xFF, ref.sht30_crc8(bytes([(raw_t >> 8) & 0xFF, raw_t & 0xFF])),
        (raw_rh >> 8) & 0xFF, raw_rh & 0xFF, ref.sht30_crc8(bytes([(raw_rh >> 8) & 0xFF, raw_rh & 0xFF])),
    ])
    temp_c, rh = ref.sht30_parse(frame)
    check(abs(temp_c - (-45.0 + 175.0 * raw_t / 65535.0)) < 1e-9, "SHT30 temp conversion")
    check(abs(rh - (100.0 * raw_rh / 65535.0)) < 1e-9, "SHT30 humidity conversion")
    check(24.0 < temp_c < 26.0, "SHT30 temp in expected ballpark (~25C)")
    # CRC 변조 -> 예외
    bad_frame = bytearray(frame)
    bad_frame[2] ^= 0xFF
    try:
        ref.sht30_parse(bytes(bad_frame))
        check(False, "SHT30 bad CRC should raise")
    except ValueError:
        pass

    # ── 5. fixed2 포맷 (펌웨어 jb_fixed2 와 동일해야 함) ──────────────────────
    fixed2_vectors = {
        24.0: "24.00",
        48.5: "48.50",
        -3.245: "-3.25",   # round half up
        0.0: "0.00",
        100.0: "100.00",
        0.004: "0.00",
        -0.001: "0.00",    # 반올림 결과 0 -> 음수 부호 없음
        9.999: "10.00",
    }
    for v, expected in fixed2_vectors.items():
        got = ref.fixed2(v)
        check(got == expected, f"fixed2({v}) expected {expected} got {got}")

    if failures:
        sys.stderr.write("Parity test FAILURES:\n")
        for f in failures:
            sys.stderr.write(f"- {f}\n")
        return 1

    print("Firmware<->server parity checks passed.")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
