#!/usr/bin/env python3
"""
validate_binary.py — Compare MARTe2 binary input and output files.

Usage:  python3 validate_binary.py <input.bin> <output.bin> [--label NAME]

Reads MARTe2 binary format (header: 4B numSigs, per-sig: 2B type + 32B name + 4B elems),
then compares signal count, element counts, and data rows between input and output.

Output rows are matched against input rows via set membership to handle
async FileWriter flush ordering.
"""
import struct, sys, os

def read_binary(path):
    """Read MARTe2 binary file. Returns (signals, raw_data_bytes)."""
    if not os.path.exists(path):
        return None, None
    with open(path, 'rb') as f:
        data = f.read()
    if len(data) < 4:
        return None, None
    ns = struct.unpack_from('<I', data, 0)[0]
    sigs = []
    off = 4
    for _ in range(ns):
        if off + 38 > len(data):
            break
        tc = struct.unpack_from('<H', data, off)[0]
        nm = data[off+2:off+34].rstrip(b'\x00').decode(errors='replace')
        ne = struct.unpack_from('<I', data, off+34)[0]
        sigs.append((nm, tc, ne))
        off += 38
    raw = data[off:]
    return sigs, raw


def validate(input_path, output_path, label="test"):
    """Validate output against input. Returns (passed, message, details)."""
    in_sigs, in_raw = read_binary(input_path)
    out_sigs, out_raw = read_binary(output_path)

    if in_sigs is None:
        return False, f"[{label}] Cannot read input file: {input_path}", {}
    if out_sigs is None or out_raw is None:
        return False, f"[{label}] Cannot read output file: {output_path}", {}

    # Signal validation
    if len(in_sigs) != len(out_sigs):
        return False, f"[{label}] Signal count mismatch: {len(in_sigs)} in vs {len(out_sigs)} out", {}

    for i, (si, so) in enumerate(zip(in_sigs, out_sigs)):
        if si[2] != so[2]:
            return False, f"[{label}] Signal '{si[0]}' size mismatch: {si[2]} in vs {so[2]} out", {}

    total_elems = sum(ne for _, _, ne in in_sigs)
    in_row_bytes = total_elems * 4
    out_row_bytes = sum(ne for _, _, ne in out_sigs) * 4

    if in_row_bytes != out_row_bytes:
        return False, f"[{label}] Row size mismatch: {in_row_bytes} in vs {out_row_bytes} out", {}

    n_rows_in = len(in_raw) // in_row_bytes if in_row_bytes > 0 else 0
    n_rows_out = len(out_raw) // out_row_bytes if out_row_bytes > 0 else 0

    if n_rows_out == 0:
        return False, f"[{label}] Output file has no data rows", {}

    # Build input row set
    input_rows = set()
    for r in range(n_rows_in):
        input_rows.add(in_raw[r * in_row_bytes:(r + 1) * in_row_bytes])

    # Classify each output row into exactly one of three buckets:
    #   zero        — all-zero row (benign startup transient before first DATA)
    #   matching    — non-zero row that equals some input row (correct transport)
    #   mismatching — non-zero row that matches no input row (data corruption)
    zero_rows = 0
    matching = 0
    mismatching = 0
    first_zero = -1
    first_match = -1
    first_mismatch = -1

    for r in range(n_rows_out):
        row = out_raw[r * out_row_bytes:(r + 1) * out_row_bytes]
        if not any(row):
            zero_rows += 1
            if first_zero < 0:
                first_zero = r
        elif row in input_rows:
            matching += 1
            if first_match < 0:
                first_match = r
        else:
            mismatching += 1
            if first_mismatch < 0:
                first_mismatch = r

    details = {
        "n_signals": len(in_sigs),
        "signal_names": [s[0] for s in in_sigs],
        "signal_sizes": [s[2] for s in in_sigs],
        "row_bytes": in_row_bytes,
        "n_rows_in": n_rows_in,
        "n_rows_out": n_rows_out,
        "zero_rows": zero_rows,
        "matching_rows": matching,
        "mismatching_rows": mismatching,
        "first_zero_row": first_zero,
        "first_matching_row": first_match,
        "first_mismatch_row": first_mismatch,
        "input_bytes": len(in_raw),
        "output_bytes": len(out_raw),
    }

    # Fail threshold:
    #   - any non-zero output row that matches no input row → data corruption → FAIL
    #   - no matching rows at all (incl. all-zero output)            → FAIL
    if matching == 0:
        return False, (f"[{label}] FAIL: no output row matches any input row "
                       f"({n_rows_out} rows: {zero_rows} zero, {mismatching} corrupted)"), details
    if mismatching > 0:
        return False, (f"[{label}] FAIL: {mismatching} non-zero output rows match no input row "
                       f"(first at row {first_mismatch})"), details
    if zero_rows > 0:
        return True, (f"[{label}] WARN: {matching}/{n_rows_out} rows match "
                      f"({zero_rows} startup zero rows; first match at row {first_match})"), details
    return True, f"[{label}] PASS: all {n_rows_out} rows match input", details


def main():
    import argparse
    p = argparse.ArgumentParser(description="Validate MARTe2 binary output against input")
    p.add_argument("input", help="Input binary file")
    p.add_argument("output", help="Output binary file")
    p.add_argument("--label", default="e2e", help="Test label for output messages")
    p.add_argument("--json", default=None, help="Write the metrics (incl. pass/fail) to this JSON file")
    args = p.parse_args()

    passed, msg, details = validate(args.input, args.output, args.label)

    if args.json is not None:
        import json
        record = dict(details)
        record["label"] = args.label
        record["passed"] = passed
        record["status"] = "PASS" if passed else "FAIL"
        record["message"] = msg.strip()
        with open(args.json, "w") as jf:
            json.dump(record, jf, indent=2)

    # Always print details
    if details:
        print(f"  Signals: {details['n_signals']} ({', '.join(f'{n}({s})' for n,s in zip(details['signal_names'], details['signal_sizes']))})")
        print(f"  Row size: {details['row_bytes']} B")
        print(f"  Input:  {details['input_bytes']} B ({details['n_rows_in']} rows)")
        print(f"  Output: {details['output_bytes']} B ({details['n_rows_out']} rows)")
        print(f"  Matching:    {details['matching_rows']}/{details['n_rows_out']}")
        print(f"  Zero rows:   {details['zero_rows']}/{details['n_rows_out']}")
        print(f"  Mismatching: {details['mismatching_rows']}/{details['n_rows_out']} (non-zero, unmatched)")
        if details['first_matching_row'] >= 0:
            print(f"  First matching row:    {details['first_matching_row']}")
        if details['first_zero_row'] >= 0:
            print(f"  First zero row:        {details['first_zero_row']}")
        if details['first_mismatch_row'] >= 0:
            print(f"  First mismatching row: {details['first_mismatch_row']}")

    print(f"  {'✓' if passed else '✗'} {msg}")
    sys.exit(0 if passed else 1)


if __name__ == "__main__":
    main()
