#!/usr/bin/env python3
# $Id: pptest_netlist 72 2026-08-27 08:32:33Z peter $
#
# pptest_netlist --- checks netlist.py's own real, kicad-cli-based
# loading pipeline (pin names, pin types, net membership) against
# KNOWN, self-authored ground truth: this project's own example_
# circuit1.py, generated fresh by this test itself, not a pre-built
# fixture file. Run this after any change to netlist.py itself, or
# after a KiCad version upgrade that might change kicad-cli's own
# netlist export format.
#
# EXTRACTED 2026-08-14 as part of packpay's own creation: the
# ORIGINAL version of this file (still in the dudhya project, which
# this one was extracted from) checks netlist.py against facts about
# a real, physically-designed circuit (dudhya's own Pixel 3a power
# stage) -- genuinely valuable there, but not portable to an
# independent project with no such circuit of its own. This version
# checks the exact same real pipeline (nothing about netlist.py
# itself changed) against this project's own self-contained example
# circuit instead, so packpay's own test suite has no dependency on
# any downstream project's own data.
#
# Usage: python3 test_netlist.py [--kicad-cli CMD] [--debug] [--quiet]
# Defaults to $KICAD_CLI if set, else a plain `kicad-cli` on PATH --
# see test_netlist.py's own --help, or test_netlist(1).

import sys
import os
import subprocess

sys.path.insert(0, os.path.dirname(__file__))
from netlist import load_from_schematic

FAILURES = []


def check(label, condition):
    status = "ok" if condition else "FAIL"
    print(f"  [{status}] {label}")
    if not condition:
        FAILURES.append(label)


def same_members(actual, expected_ref_pins):
    """True if `actual` (a list of (ref, pin) tuples) contains exactly
    the given (ref, pin) pairs, in any order."""
    return set(actual) == set(expected_ref_pins)


def _version():
    """This project's own current Dewey-decimal version, read from
    VERSION -- see netlist.py's own identical helper for the full
    reasoning. Duplicated per-script rather than factored into a
    shared module -- see render.py's own identical note on why."""
    here = os.path.dirname(os.path.abspath(__file__))
    try:
        with open(os.path.join(here, "VERSION")) as f:
            for line in f:
                line = line.strip()
                if line and not line.startswith("#"):
                    return line
    except OSError:
        pass
    return "unknown"


def main(argv=None):
    """CLI entry point, ADDED 2026-08-18, this project's own FOURTH
    script (after netlist.py, render.py, and footprint.py) brought up
    to DevelopmentStandard.org's own real "Options and Arguments" bar
    -- see test_netlist.1 (this program's own new manual page)."""
    import argparse

    parser = argparse.ArgumentParser(
        prog="pptest_netlist",
        description="Regenerate this project's own example_circuit1.py "
                     "fresh, then check netlist.py's own real, kicad-cli-"
                     "backed loading pipeline (component values, key nets, "
                     "real pin electrical types, query methods) against "
                     "that self-authored ground truth. Run this after any "
                     "change to netlist.py itself, or after a KiCad "
                     "version upgrade that might change kicad-cli's own "
                     "netlist export format.")
    parser.add_argument("--kicad-cli", metavar="CMD",
                         default=os.environ.get("KICAD_CLI", "kicad-cli"),
                         help="kicad-cli command (default: $KICAD_CLI if "
                              "set, else 'kicad-cli' on PATH -- same "
                              "convention as this project's own Makefile, "
                              "ppnetlist(1), and pprender(1))")
    parser.add_argument("--version", action="version",
                         version=f"%(prog)s {_version()}")
    parser.add_argument("--debug", action="store_true",
                         help="also print every real component and net "
                              "loaded, not just the values each check "
                              "spot-checks")
    parser.add_argument("--quiet", action="store_true",
                         help="suppress the section header lines and the "
                              "leading component/net-count line; still "
                              "print every [ok]/[FAIL] check result and "
                              "the final summary")
    args = parser.parse_args(argv)

    cli = args.kicad_cli

    # Generate this project's own example circuit fresh, so this test
    # has no dependency on a pre-existing fixture file staying in
    # sync with ppexample_circuit1's own definition.
    try:
        subprocess.run([sys.executable, os.path.join(os.path.dirname(__file__), "ppexample_circuit1")],
                        check=True, capture_output=True, text=True,
                        env={**os.environ, "KICAD_CLI": cli})
    except subprocess.CalledProcessError as e:
        print(f"pptest_netlist: ppexample_circuit1 failed:\n{e.stderr}", file=sys.stderr)
        raise SystemExit(1)
    sch = os.path.join(os.path.dirname(__file__), "packpay-example-circuit1.kicad_sch")

    try:
        nl = load_from_schematic(sch, kicad_cli=cli)
    except (OSError, subprocess.CalledProcessError, ValueError) as e:
        print(f"pptest_netlist: {sch}: {e}", file=sys.stderr)
        raise SystemExit(1)

    if not args.quiet:
        print(f"Loaded {len(nl.components)} components, {len(nl.nets)} nets from {sch}\n")
    if args.debug:
        for ref in nl.refs():
            print(f"  {ref}: {nl.components[ref].value}")
        for net in nl.nets:
            print(f"  {net.name}: " + ", ".join(f"{r}.{p}" for r, p in net.nodes))
        print()

    if not args.quiet:
        print("Component count and a few spot-check values:")
    check("10 components total", len(nl.components) == 10)
    check("U1 is the STUSB4500QTR", "STUSB4500" in nl.components.get("U1", type("", (), {"value": ""})()).value)
    check("U2 is the XL4015", nl.components.get("U2", type("", (), {"value": ""})()).value == "XL4015")

    if not args.quiet:
        print("\nKey nets, checked against example_circuit1.py's own real, generated output:")
    check("GNDTEST has 6 members (this example's own widest net)",
          len(nl.pins_on_net("GNDTEST")) == 6)
    check("PWRA = {C30.2, U1.24, U2.5}",
          same_members(nl.pins_on_net("PWRA"), [("C30", "2"), ("U1", "24"), ("U2", "5")]))
    check("Z = {C10.1, R10.2} (a real, drawn direct wire, not two labels)",
          same_members(nl.pins_on_net("Z"), [("C10", "1"), ("R10", "2")]))
    check("SWTEST = {L20.1, U2.3}",
          same_members(nl.pins_on_net("SWTEST"), [("L20", "1"), ("U2", "3")]))

    if not args.quiet:
        print("\nReal pin electrical types (additive netlist.py extension):")
    check("U1 pin 24 (VDD) is power_in", nl.pin_types.get(("U1", "24")) == "power_in")
    check("U1 pin 21 (VREG_1V2) is power_out", nl.pin_types.get(("U1", "21")) == "power_out")
    check("nets_needing_pwr_flag() includes PWRA and PWRB",
          {"PWRA", "PWRB"} <= set(nl.nets_needing_pwr_flag()))

    if not args.quiet:
        print("\nQuery methods:")
    check("net_for_pin(U1, 24) == PWRA", nl.net_for_pin("U1", "24") == "PWRA")
    check("net_for_pin(nonexistent, 1) is None", nl.net_for_pin("ZZ99", "1") is None)
    check("pins_on_net(nonexistent) == []", nl.pins_on_net("NO_SUCH_NET") == [])

    print(f"\n{len(FAILURES)} failure(s) out of a full check pass." if FAILURES
          else "\nAll checks passed.")
    if FAILURES:
        for f in FAILURES:
            print(f"  FAILED: {f}")
        raise SystemExit(1)


if __name__ == "__main__":
    main()
