#!/usr/bin/env python3
# $Id: pptest_build_instructions 201 2026-10-12 12:03:51Z peter $
#
# pptest_build_instructions --- checks build_instructions.py's own
# real pipeline (far-leg resolution, ordering strategies, step
# generation, all four output formatters) against KNOWN, real ground
# truth -- this project's own real, already-established test
# circuits, not synthetic, invented fixtures.
#
# ADDED 2026-08-26, alongside build_instructions.py itself, matching
# the SAME discipline test_breadboard_config.py established: every
# check here was first confirmed by hand, in-session, before being
# written down as an automated assertion.
#
# Usage: python3 test_build_instructions.py --kicad-cli CMD [--quiet]
# See test_build_instructions.1, or --help, for the full option list.
# Needs kicad-cli: unlike test_breadboard_config.py, this exercises
# the full real pipeline (netlist -> layout -> render -> instructions),
# the same real dependency test_netlist.py already has.

import sys
import os

sys.path.insert(0, os.path.dirname(__file__))
from build_instructions import (
    component_type, is_generic_passive, hole_name, far_leg_position,
    order_position, order_netlist, generate_steps, describe_step,
    format_plain, format_markdown, format_html, format_orgmode,
    WIKIPEDIA_LINKS, TYPE_NAMES,
)
import breadboard_config as bc

FAILURES = []

# A real, explicit, sufficient board config for this test script's
# own purposes -- CORRECTED 2026-08-27, closing a real, direct
# consequence of default_config()'s own rows_per_half correction
# (10 -> 5, Péter's own real, physical breadboard observation; see
# breadboard_lib.py's own ROWS_PER_HALF for the full real account):
# this script's own real job is checking build_instructions.py's own
# logic, given a real, VALID placement -- not re-verifying whether
# the real, default board is adequate for these SPECIFIC, already-
# known test circuits (a real, separate concern render.py's/
# pprender's own bounds_ok check already covers). Confirmed directly,
# not assumed: packpay-example-circuit1 genuinely does not fit the
# new, smaller, more physically accurate default at all -- an earlier
# version of this test called render() on an invalid placement
# without checking first, crashing with a bare AssertionError instead
# of a real, honest test failure. rows_per_half=10 here matches the
# OLD default exactly, a real, already-confirmed-sufficient value for
# every real circuit this script exercises.
TEST_BOARD_CONFIG = bc.BoardConfig(sector=bc.SectorConfig(
    columns=bc.SECTOR_COL_STRIDE - 1, rows_per_half=10))

# A real, SEPARATE, deliberately tighter config was previously used
# here, just for check_unplaced_handling() below (ADDED 2026-09-06,
# closing a real regression this project's own resistor/diode
# footprint fix caused there) -- REMOVED 2026-09-11, closing a
# SECOND, real regression the SAME way: this project's own rail-
# attachment fallback fix (see ResearchJournal.org's own 2026-09-11
# entry) made the room search genuinely thorough enough that no
# board-size value at all still produced the intended, narrow
# "unplaced, but otherwise valid" case against knight-roller -- the
# second time this exact tuning broke from an unrelated, real
# placement improvement. check_unplaced_handling() itself now uses a
# synthetic Netlist immune to this by construction, not a real
# circuit tuned against today's placement behavior -- see that
# function's own comment for the full account.


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


def _version():
    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 _load(schematic, kicad_cli, board_config=None):
    from netlist import load_from_schematic
    from auto_layout import layout
    from render import render
    import breadboard_lib
    import contextlib
    import io
    import tempfile

    # reset_module_state(), CORRECTED 2026-09-17, closing a real,
    # genuine gap found directly, not assumed, investigating a
    # confusing, seemingly non-deterministic "accidental short"
    # render() failure: this script's own _load() calls render()
    # multiple times per real process (once per real check function
    # that needs a placement), but never cleared
    # breadboard_lib.py's own module-level, mutable render state
    # (_occupied, _SPATIAL_INDEX, etc.) between calls at all --
    # pptest_regression and pptest_breadboard_config already,
    # correctly call this before every one of their own real render()
    # calls, for the exact same, already-documented reason
    # (reset_module_state()'s own docstring: a second real render()
    # call, uncleared, silently corrupts collision/spatial-index
    # state left over from the first). Confirmed directly: this
    # single, missing call was the real, full explanation for a
    # placement that looked randomly, unpredictably wrong depending
    # on which OTHER, unrelated circuits' own checks had already run
    # earlier in the same real process -- not a genuine placement bug
    # in auto_layout.py/render.py's own real algorithm at all.
    breadboard_lib.reset_module_state()
    nl = load_from_schematic(schematic, kicad_cli=kicad_cli)
    p, anchors, passives, rails = layout(nl, board_config=board_config)
    with tempfile.NamedTemporaryFile(suffix=".svg", delete=False) as tf:
        tmp = tf.name
    try:
        with contextlib.redirect_stdout(io.StringIO()):
            render(nl, p, anchors, passives, rails, tmp)
    finally:
        try:
            os.unlink(tmp)
        except OSError:
            pass
    return nl, p, anchors, passives, rails


def check_component_type(quiet):
    if not quiet:
        print("\ncomponent_type()/is_generic_passive(): known lib:part pairs:")

    class FakeC:
        def __init__(self, lib, part):
            self.lib, self.part = lib, part

    check("Device:R -> resistor",
          component_type(FakeC("Device", "R")) == "resistor")
    check("Device:C -> ceramic capacitor",
          component_type(FakeC("Device", "C")) == "ceramic capacitor")
    check("Device:D_Schottky -> Schottky diode",
          component_type(FakeC("Device", "D_Schottky")) == "Schottky diode")
    check("an unrecognized lib:part falls back to the real part name itself",
          component_type(FakeC("Interface_USB", "STUSB4500QTR")) == "STUSB4500QTR")
    check("every real TYPE_NAMES entry is a generic passive",
          is_generic_passive(FakeC("Device", "R")))
    check("an unrecognized lib:part is NOT a generic passive",
          not is_generic_passive(FakeC("Interface_USB", "STUSB4500QTR")))
    check("every real TYPE_NAMES value has its own real Wikipedia link",
          all(name in WIKIPEDIA_LINKS for name in set(TYPE_NAMES.values())))


def check_hole_name(quiet):
    if not quiet:
        print("\nhole_name(): row letter + real, LOCAL column number:")
    check("column 1, N-half, row 0 -> a1",
          hole_name(1, "N", 0, TEST_BOARD_CONFIG) == "a1")
    check("column 5, S-half, row 2 -> (rows_per_half + 2)th letter, local col 5",
          hole_name(5, "S", 2, TEST_BOARD_CONFIG).endswith("5"))


def check_far_leg_and_steps(quiet, kicad_cli):
    if not quiet:
        print("\nfar_leg_position()/generate_steps(): against a real, "
              "already-known circuit (packpay-example-circuit1):")
    if not os.path.exists("packpay-example-circuit1.kicad_sch"):
        # ppexample_circuit1's own real generation code lives inside
        # its own `if __name__ == "__main__":` guard (matching this
        # project's own real CLI convention) -- a plain `import`
        # would NOT run it. Confirmed directly, not assumed: an
        # earlier version of this fallback tried exactly that (a
        # `hasattr(..., "main")` check against a script with no real
        # `main()` at all), and would have silently done nothing had
        # this branch ever actually been exercised. A real subprocess
        # call, matching how this project's own Makefile.am itself
        # invokes the same script, is what actually generates the
        # file.
        import subprocess
        subprocess.run([sys.executable, "ppexample_circuit1"], check=True)
    nl, p, anchors, passives, rails = _load(
        "packpay-example-circuit1.kicad_sch", kicad_cli,
        board_config=TEST_BOARD_CONFIG)

    check("p.passive_far_xy is populated after render() (render.py's own "
          "real exposure site)",
          hasattr(p, "passive_far_xy") and len(p.passive_far_xy) == len(passives))

    for ref in passives:
        far = far_leg_position(p, ref)
        check(f"far_leg_position({ref}) resolves to a real (col, row) pair",
              far is not None and len(far) == 2)
        far_col, far_row = far
        near_row = p.col_row[ref]
        size = p.size_rows.get(ref, 1)
        check(f"{ref}'s own far row is exactly near_row + size_rows "
              f"({near_row}+{size}={near_row + size})",
              far_row == near_row + size)
        check(f"{ref}'s own far column is genuinely DIFFERENT from its "
              f"own near column (leg()'s own real, established constraint)",
              far_col != p.passive_col[ref])

    steps_pos = generate_steps(nl, p, anchors, passives, rails, order="position")
    steps_net = generate_steps(nl, p, anchors, passives, rails, order="netlist")
    steps_grp = generate_steps(nl, p, anchors, passives, rails, order="net-grouped")
    check("generate_steps() produces at least one real step per real "
          "anchor and passive",
          len(steps_pos) >= len(anchors) + len(passives))
    check("position order and netlist order produce the SAME real count "
          "of steps (same real components, different real order only)",
          len(steps_pos) == len(steps_net))
    check("net-grouped order produces the SAME real count of steps too",
          len(steps_pos) == len(steps_grp))

    # a rail wire (0.72.66) follows the step of the part it belongs to and is not a part: the order that matters is the parts'
    parts_of = lambda steps_: [s for s in steps_ if s.kind != "rail_wire"]
    pos_anchor_refs = [s.ref for s in steps_pos if s.kind == "anchor"]
    net_anchor_refs = [s.ref for s in steps_net if s.kind == "anchor"]
    grp_anchor_refs = [s.ref for s in steps_grp if s.kind == "anchor"]
    check("both real orderings place every real anchor before any real "
          "passive",
          pos_anchor_refs == sorted(pos_anchor_refs, key=lambda r: 0) and
          all(s.kind == "anchor" for s in parts_of(steps_pos)[:len(anchors)]) and
          all(s.kind == "anchor" for s in parts_of(steps_net)[:len(anchors)]))
    check("netlist order places anchors in plain reference order",
          net_anchor_refs == sorted(net_anchor_refs))
    check("net-grouped order also places every real anchor before any "
          "real passive",
          all(s.kind == "anchor" for s in parts_of(steps_grp)[:len(anchors)]))


def check_net_grouped_ordering(quiet, kicad_cli):
    if not quiet:
        print("\norder_net_grouped(): against a real, MCU-centric circuit "
              "(rp2040-basic-m1) -- the exact real case this whole "
              "strategy exists for:")
    from breadboard_config import BoardConfig, SectorConfig
    from build_instructions import order_net_grouped, order_position
    # A real, adequate board_config -- rp2040-basic-m1 genuinely
    # overflows the bare default (rows_per_half=5); matches this
    # same file's own established pattern for a dense real circuit.
    cfg = BoardConfig(sectors=1, sector=SectorConfig(columns=999999, rows_per_half=15))
    nl, p, anchors, passives, rails = _load(
        "TestSchemes/jck-RP2040-basic-m1/rp2040-basic-m1.kicad_sch",
        kicad_cli, board_config=cfg)

    ordered = order_net_grouped(nl, p, anchors, passives)
    unplaced = getattr(p, "unplaced", ())
    placeable = [ref for ref in passives if ref not in unplaced]

    check("order_net_grouped() returns every real, placeable passive "
          "exactly once, none dropped or duplicated",
          sorted(s for s in ordered if s not in anchors) == sorted(placeable))

    # The real, core property this whole strategy exists for
    # (FutureResearch.org's own account, 2026-08-26): a naive "shares
    # ANY net with the same anchor" heuristic collapses nearly every
    # real passive into ONE group on an MCU-centric circuit, since
    # the MCU (U1) connects to almost everything. Confirmed directly
    # this is genuinely avoided: no single group should hold more
    # than half of this real circuit's own placeable passives.
    from auto_layout import high_degree_nets
    anchor_set = set(anchors)
    rail_nets = high_degree_nets(nl)

    def anchor_pins_touched(ref):
        touched = set()
        for net in nl.nets:
            if net.name in rail_nets or "unconnected" in net.name:
                continue
            my_pins = [pin for (r, pin) in net.nodes if r == ref]
            if not my_pins:
                continue
            for (other_ref, other_pin) in net.nodes:
                if other_ref != ref and other_ref in anchor_set:
                    touched.add((other_ref, other_pin))
        return tuple(sorted(touched))

    groups = {}
    for ref in placeable:
        groups.setdefault(anchor_pins_touched(ref), []).append(ref)
    largest_group_size = max(len(refs) for refs in groups.values())
    check(f"no single group (largest: {largest_group_size} of "
          f"{len(placeable)} real, placeable passives) holds more than "
          "half of this real circuit's own passives -- confirming the "
          "MCU-collapse failure mode this strategy exists to avoid is "
          "genuinely absent, not just theoretically addressed",
          largest_group_size <= len(placeable) / 2)

    # The real, cited example (FutureResearch.org's own account): "the
    # crystal and its own two load capacitors" should end up as one
    # real, contiguous group -- confirmed directly for this real
    # circuit's own Y1 (crystal) and C1 (its own load cap, both on
    # U1's own pin 20).
    y1_idx = ordered.index("Y1") if "Y1" in ordered else None
    c1_idx = ordered.index("C1") if "C1" in ordered else None
    check("Y1 (crystal) and C1 (its own load capacitor, both touching "
          "the SAME real anchor pin) land immediately adjacent in the "
          "real, net-grouped order",
          y1_idx is not None and c1_idx is not None and
          abs(y1_idx - c1_idx) == 1)

    # A real, direct comparison against position-based ordering,
    # confirming this is a genuinely DIFFERENT strategy, not a
    # relabeled duplicate of the existing default.
    pos_ordered = order_position(nl, p, anchors, passives)
    check("net-grouped order is genuinely DIFFERENT from position-based "
          "order for this real circuit, not a duplicate under a new name",
          ordered != pos_ordered)


def check_unplaced_handling(quiet, kicad_cli):
    if not quiet:
        print("\ngenerate_steps(): a circuit with a genuinely, "
              "permanently UNPLACEABLE component doesn't crash:")
    # A synthetic Netlist, not a real, board-size-tuned circuit --
    # CHANGED 2026-09-11, closing a real, recurring maintenance
    # burden found directly, not assumed: this check previously
    # relied on knight-roller under a deliberately tight
    # UNPLACED_TEST_BOARD_CONFIG to produce a genuinely unplaced
    # component -- but this project's own two, real, same-day
    # placement fixes (see ResearchJournal.org's own 2026-09-11
    # entry: leg()'s own far-leg registration gap, and the rail-
    # attachment fallback's own missing wider retry) made the room
    # search genuinely thorough enough that EVERY rows_per_half value
    # tried either placed every real component (too loose) or also
    # overflowed the ones that WERE placed (too tight) -- no config
    # value at all still produced the intended, narrow "unplaced, but
    # otherwise valid" case, the second time this exact tuning has
    # broken from an unrelated, real placement improvement. A
    # component with NO real, non-rail connection to anything AND no
    # real rail net at all (R1 below, both of its own two, real pins
    # on their own, single-member nets) can never be placed by ANY
    # search this file has, or could ever gain -- genuinely immune to
    # future placement improvements, not merely tuned against
    # today's.
    from netlist import Netlist, Component, Net
    from auto_layout import layout
    from render import render
    import contextlib
    import io
    import tempfile

    components = {
        "U1": Component(ref="U1", value="Generic MCU", lib="", part=""),
        "R1": Component(ref="R1", value="10k", lib="Device", part="R"),
    }
    nets = [
        Net(name="N1", code="0", nodes=[("U1", "1")]),
        Net(name="N2", code="1", nodes=[("U1", "2")]),
        Net(name="N3", code="2", nodes=[("U1", "3")]),
        Net(name="Isolated-A", code="3", nodes=[("R1", "1")]),
        Net(name="Isolated-B", code="4", nodes=[("R1", "2")]),
    ]
    nl = Netlist(components=components, nets=nets, pin_names={})
    p, anchors, passives, rails = layout(nl)
    with tempfile.NamedTemporaryFile(suffix=".svg", delete=False) as tf:
        tmp = tf.name
    try:
        with contextlib.redirect_stdout(io.StringIO()):
            render(nl, p, anchors, passives, rails, tmp)
    finally:
        try:
            os.unlink(tmp)
        except OSError:
            pass
    check("this synthetic circuit genuinely has an unplaced component "
          "(confirming this check exercises the real, intended case)",
          len(p.unplaced) > 0)
    try:
        steps = generate_steps(nl, p, anchors, passives, rails)
        ok = True
    except KeyError:
        ok = False
        steps = []
    check("generate_steps() does not raise on a real, unplaced component "
          "-- skips it honestly instead",
          ok)
    step_refs = {s.ref for s in steps if s.ref}
    check("no real, unplaced component's own ref appears in the real, "
          "generated steps",
          not (step_refs & set(p.unplaced)))


def check_anchor_rail_and_power_role(quiet, kicad_cli):
    if not quiet:
        print("\nanchor_rail_sides/power_role (build_instructions.py) and "
              "their real, reverse round-trip (parse_instructions.py) -- "
              "FutureResearch.org's own \"Option A\" item, closed "
              "2026-09-15, per Péter's own, direct instruction:")
    from breadboard_config import BoardConfig, SectorConfig
    from parse_instructions import parse_instructions, reconstruct

    # Same real, adequate board_config already established by
    # check_net_grouped_ordering() above, for the exact same real,
    # dense, MCU-centric circuit.
    cfg = BoardConfig(sectors=1, sector=SectorConfig(columns=999999, rows_per_half=15))
    nl, p, anchors, passives, rails = _load(
        "TestSchemes/jck-RP2040-basic-m1/rp2040-basic-m1.kicad_sch",
        kicad_cli, board_config=cfg)
    steps = generate_steps(nl, p, anchors, passives, rails)
    by_ref = {s.ref: s for s in steps if s.kind == "anchor"}

    # U1 (RP2040): sits on both real rails, and this project's own
    # power_sources.py classifies it "secondary" (a regulated,
    # derived source, not the circuit's own external one) --
    # confirmed directly, by hand, before writing this assertion.
    check("U1 (RP2040) is on both real rails ('+','-')",
          by_ref["U1"].anchor_rail_sides == ("+", "-"))
    check("U1 (RP2040) is classified a SECONDARY real power source",
          by_ref["U1"].power_role == "secondary")
    # J1 (USB_C_Receptacle): this circuit's own real, external,
    # PRIMARY power source.
    check("J1 (USB_C_Receptacle) is classified a PRIMARY real power source",
          by_ref["J1"].power_role == "primary")
    # J3 (Conn_02x04_Odd_Even): sits on the ground rail alone (no
    # real power-rail pin), and is not itself a power source at all.
    check("J3 (a real connector, ground-rail-only) has NO real power role",
          by_ref["J3"].power_role is None)
    check("J3 sits on the ground rail alone ('-'), not both",
          by_ref["J3"].anchor_rail_sides == ("-",))

    n = list(steps).index(by_ref["U1"]) + 1
    text = describe_step(by_ref["U1"], n)
    check("U1's own real describe_step() text states BOTH real rails, "
          "in this module's own fixed, real order (power then ground)",
          "connects to the power (+) and ground (-) rails" in text)
    check("U1's own real describe_step() text states its own SECONDARY "
          "real power-source role",
          "this circuit's secondary power source" in text)

    # The real, reverse direction: generate the FULL, real instruction
    # text, parse it back, and confirm the anchor's own rail pins now
    # genuinely land on the SAME real, named net a rail-connected
    # passive already does -- closing resolve_components.py's own,
    # previously-honest, documented limit (see that module's own
    # resolve_components() docstring): an anchor's own GND/VBUS pins
    # used to be genuinely absent from any real net at all after a
    # round trip.
    #
    # rp2040-basic-m1 again -- REVERTED 2026-09-29, back to this same
    # real circuit: briefly switched to a synthetic Netlist (see this
    # same file's own real history, if consulted) after this check's
    # own, direct verification found a genuine, PRE-EXISTING placement
    # defect on THIS real circuit (C8's own near leg and C10's/C11's
    # own far legs sharing one real breadboard column across two
    # different rails) -- now CLOSED (render.py/auto_layout.py's own
    # far_net computation, FutureResearch.org's own record), so this
    # real circuit is genuinely clean again; reverted rather than left
    # on a synthetic substitute with no real reason left to justify it.
    full_text = format_plain(steps)
    parsed = parse_instructions(full_text)
    nl2, p2 = reconstruct(parsed, board_config=cfg)
    u1_nets = set(nl2.nets_for_ref("U1"))
    check("the reconstructed netlist's own U1 is now genuinely on a "
          "real GND net (previously absent entirely -- see "
          "resolve_components.py's own resolve_components() docstring)",
          "GND" in u1_nets)
    check("the reconstructed netlist's own U1 is now genuinely on a "
          "real VBUS net too",
          "VBUS" in u1_nets)
    gnd_refs = {ref for ref, _ in nl2.pins_on_net("GND")}
    check("U1 (an anchor) and at least one real, rail-connected passive "
          "now genuinely share the SAME real GND net -- confirming this "
          "is one, single, real net, not two, separate, same-named ones",
          "U1" in gnd_refs and len(gnd_refs) > 1)

    # The real, honest edge case: an anchor with NO real rail
    # connection at all gets no trailing sentence whatsoever, not an
    # empty or malformed one -- a synthetic Netlist, immune to this
    # project's own real circuits' own coincidental rail habits,
    # matching check_unplaced_handling()'s own, established pattern
    # just above for the identical, real reason (genuinely immune to
    # future placement/classification changes, not merely tuned
    # against today's real test circuits).
    from netlist import Netlist, Component, Net
    from auto_layout import layout
    from render import render
    components = {
        "U1": Component(ref="U1", value="Generic MCU", lib="", part=""),
        "R1": Component(ref="R1", value="10k", lib="Device", part="R"),
    }
    nets = [
        Net(name="DATA1", code="0", nodes=[("U1", "1"), ("R1", "1")]),
        Net(name="DATA2", code="1", nodes=[("U1", "2"), ("R1", "2")]),
        # A third, real, isolated pin of U1's own -- needed so
        # classify()'s own real anchor_min_pins=3 default threshold
        # genuinely counts U1 as an anchor at all (2 real pins alone
        # would classify it a passive instead, defeating this real
        # check's own entire point).
        Net(name="DATA3", code="2", nodes=[("U1", "3")]),
    ]
    nl3 = Netlist(components=components, nets=nets, pin_names={})
    p3, anchors3, passives3, rails3 = layout(nl3)
    import contextlib, io, tempfile
    with tempfile.NamedTemporaryFile(suffix=".svg", delete=False) as tf:
        tmp = tf.name
    try:
        with contextlib.redirect_stdout(io.StringIO()):
            render(nl3, p3, anchors3, passives3, rails3, tmp)
    finally:
        try:
            os.unlink(tmp)
        except OSError:
            pass
    steps3 = generate_steps(nl3, p3, anchors3, passives3, rails3)
    u1_step3 = next(s for s in steps3 if s.ref == "U1")
    check("a real anchor with NO rail connection at all has an empty "
          "anchor_rail_sides, not a spurious real value",
          u1_step3.anchor_rail_sides == ())
    check("a real anchor that is not a power source at all has power_role "
          "None, not a spurious real value",
          u1_step3.power_role is None)
    text3 = describe_step(u1_step3, 1)
    check("its own real describe_step() text has NO trailing rail/power "
          "sentence at all -- confirming these are genuinely OPTIONAL, "
          "not always emitted",
          "connects to" not in text3 and "power source" not in text3)


def check_plain_format_title_round_trips(quiet, kicad_cli):
    if not quiet:
        print("\nformat_plain()'s own real title header (the title itself, "
              "then a line of \"=\" characters, then a blank separator) "
              "now round-trips through parse_instructions() -- CORRECTED "
              "2026-09-16, closing a real, genuine gap Péter found "
              "directly, running the actual CLI tools (not this project's "
              "own internal functions) end-to-end: ppbuild_instructions(1)'s "
              "own DEFAULT format (plain) could never be read back by "
              "ppparse_instructions(1)/ppresolve_components(1) at all, out "
              "of the box, before this fix:")
    from parse_instructions import parse_instructions, ParseError

    nl, p, anchors, passives, rails = _load(
        "packpay-example-circuit1.kicad_sch", kicad_cli,
        board_config=TEST_BOARD_CONFIG)
    steps = generate_steps(nl, p, anchors, passives, rails)

    # A real title genuinely produces this exact header -- confirmed
    # directly against format_plain()'s own source, not assumed:
    # title itself, "=" * len(title), then a blank line.
    text = format_plain(steps, title="Build instructions: test.kicad_sch")
    try:
        parsed = parse_instructions(text)
        ok = True
    except ParseError:
        ok = False
    check("format_plain()'s own real title header no longer raises "
          "ParseError -- the exact real failure ppresolve_components(1) "
          "hit directly, against this project's own real altimeter "
          "circuit, before this fix",
          ok)
    if ok:
        check("every real step still parses back, title header aside "
              "(same real count as generate_steps() itself produced)",
              len(parsed) == len(steps))

    # A real, deliberate check this fix stays NARROW: a genuinely
    # malformed line that happens to be followed by a real "="-only
    # line, but is NOT the very first line of the document (a valid
    # step precedes it here), must still raise ParseError -- confirmed
    # directly, not assumed, this fix doesn't quietly swallow a real
    # parse error anywhere else in the document, only the one, real,
    # specific position format_plain() itself ever emits this pattern
    # at (the very first line).
    bad_text = ("1. Insert the 1k Resistor (R1): one lead into hole a5, "
                "the other into hole a7.\n"
                "2. Genuinely not a real step line at all.\n"
                "========================================\n")
    try:
        parse_instructions(bad_text)
        raised = False
    except ParseError:
        raised = True
    check("a genuinely malformed line NOT at the very start of the "
          "document, even one followed by a real \"=\"-only line, still "
          "raises ParseError -- confirming this fix stays narrow, not a "
          "general \"skip anything before an underline\" rule",
          raised)


def check_rail_direct_power_source(quiet, kicad_cli):
    if not quiet:
        print("\np.rail_direct_power_source (auto_layout.py) and its own real "
              "build_instructions.py phrasing -- closing Péter's own, "
              "direct, real-world placement observation, 2026-09-16: a "
              "genuine, simple power source should never sit \"in plain "
              "on the board... very close to a major and complex "
              "component\" the way even the existing rail-attachment "
              "fallback still does (a real, brand-new, but still main-"
              "grid, column):")
    from breadboard_config import BoardConfig, SectorConfig, RailConfig
    from auto_layout import layout, classify

    # Camera-trap: this project's own real, existing TestSchemes
    # circuit whose own J2 is already, directly confirmed (by hand,
    # this same session) to be classified a real passive AND a real
    # power source, with BOTH of its own real pins genuinely on
    # rail-like nets (GND, VCC) -- no synthetic circuit needed.
    nl, p, anchors, passives, rails = _load(
        "TestSchemes/jck-camera-trap-r1/Camera-trap.kicad_sch", kicad_cli,
        board_config=BoardConfig(sector=SectorConfig(columns=999, rows_per_half=10),
                                  rail=RailConfig(present=True)))
    check("J2 (a real power source, both pins on rail-like nets) is NOT "
          "in the ordinary passives list at all once a real rail is "
          "present",
          "J2" not in passives)
    check("J2 genuinely lands on the north rail directly (the only real "
          "rail this board_config has)",
          p.rail_direct_power_source.get("J2") == "north")
    # CORRECTED 2026-09-17, closing FutureResearch.org's own new issue
    # set's own item 2 -- SW1 also classifies a real power source
    # (classify_power_sources()'s own, real, pre-existing structural
    # ambiguity: a single real pin on VCC alone), but genuinely has a
    # SECOND, real, non-rail pin too (a real connection to J1's own
    # Pin_1, confirmed directly, by hand, not merely asserted) -- it
    # must stay an ordinary passive, never wired "directly to the
    # rail" like J2, since that would silently drop its own real,
    # other connection entirely (exactly the real harm this same
    # item closed).
    check("SW1 (also power-source-classified, but with a real, SECOND, "
          "non-rail connection of its own) correctly stays an ordinary "
          "passive, NOT rail-direct",
          "SW1" in passives and "SW1" not in p.rail_direct_power_source)

    steps = generate_steps(nl, p, anchors, passives, rails)
    rail_source_steps = [s for s in steps if s.kind == "rail_power_source"]
    rail_jumper_steps = [s for s in steps if s.kind == "rail_jumper"]
    check("generate_steps() emits one real rail_power_source step for "
          "J2, the one real, qualifying source here",
          len(rail_source_steps) == 1 and rail_source_steps[0].ref == "J2")
    check("...and one real north-south jumper for it",
          len(rail_jumper_steps) == 1 and
          {rail_jumper_steps[0].rail_from, rail_jumper_steps[0].rail_to} == {"north", "south"})

    n = list(steps).index(rail_source_steps[0]) + 1
    text = describe_step(rail_source_steps[0], n)
    check("a rail_power_source step's own real text states which rail "
          "directly, with NO specific hole at all -- Péter's own, "
          "direct simplification, when asked",
          "directly to the" in text and "rail" in text and "hole" not in text)

    # A real, direct, synthetic control for jumper DEDUPLICATION
    # specifically -- confirmed directly, no real, existing
    # TestSchemes circuit has TWO, separate, genuinely-qualifying
    # rail-direct sources (both of their own real pins on rail nets)
    # sharing the same real rail at once, after item 2's own new,
    # narrower check; a synthetic netlist is the real, honest way to
    # exercise this specific real case, matching
    # check_unplaced_handling()'s own, already-established pattern.
    from netlist import Netlist, Component, Net
    components = {
        "U1": Component(ref="U1", value="Generic IC", lib="", part=""),
        "BT1": Component(ref="BT1", value="Battery", lib="Device", part="Battery_Cell"),
        "BT2": Component(ref="BT2", value="Battery", lib="Device", part="Battery_Cell"),
    }
    nets = [
        Net(name="VCC", code="0", nodes=[("U1", "1"), ("BT1", "1"), ("BT2", "1")]),
        Net(name="GND", code="1", nodes=[("U1", "2"), ("BT1", "2"), ("BT2", "2")]),
        Net(name="DATA", code="2", nodes=[("U1", "3")]),
    ]
    nl_dedup = Netlist(components=components, nets=nets, pin_names={})
    p_dedup, anchors_dedup, passives_dedup, rails_dedup = layout(
        nl_dedup, board_config=BoardConfig(sector=SectorConfig(columns=999, rows_per_half=10),
                                            rail=RailConfig(present=True)))
    check("both real, synthetic sources (BT1, BT2), each genuinely "
          "qualifying on its own, both land rail-direct",
          p_dedup.rail_direct_power_source.get("BT1") == "north" and
          p_dedup.rail_direct_power_source.get("BT2") == "north")
    steps_dedup = generate_steps(nl_dedup, p_dedup, anchors_dedup, passives_dedup, rails_dedup)
    jumpers_dedup = [s for s in steps_dedup if s.kind == "rail_jumper"]
    check("generate_steps() DEDUPLICATES the real north-south jumper -- "
          "both real sources share the SAME real north rail, so only "
          "ONE real jumper step is needed, not two identical ones",
          len(jumpers_dedup) == 1)

    # A real vertical (left) rail present too -- Péter's own, direct
    # account: a source should prefer connecting to that rail
    # instead, distributing to BOTH horizontal rails from there.
    nl2, p2, anchors2, passives2, rails2 = _load(
        "TestSchemes/jck-camera-trap-r1/Camera-trap.kicad_sch", kicad_cli,
        board_config=BoardConfig(sector=SectorConfig(columns=999, rows_per_half=10),
                                  rail=RailConfig(present=True),
                                  rail_left=RailConfig(present=True)))
    check("with a real, vertical left rail ALSO present, a real power "
          "source prefers it over the horizontal rail",
          p2.rail_direct_power_source.get("J2") == "left")
    steps2 = generate_steps(nl2, p2, anchors2, passives2, rails2)
    jumper_pairs2 = {frozenset((s.rail_from, s.rail_to))
                      for s in steps2 if s.kind == "rail_jumper"}
    check("a real, vertical-rail source gets TWO real jumpers (left-to-"
          "north AND left-to-south), not one -- Péter's own, direct "
          "account: \"it distributes by wire jumping the northern and "
          "the southern rails\"",
          frozenset(("left", "north")) in jumper_pairs2 and
          frozenset(("left", "south")) in jumper_pairs2)

    # No real rails at all configured -- Péter's own, direct
    # confirmation: fall back to placing the source directly in a
    # sector, the same real way any other passive already would.
    nl3, p3, anchors3, passives3, rails3 = _load(
        "TestSchemes/jck-camera-trap-r1/Camera-trap.kicad_sch", kicad_cli,
        board_config=BoardConfig(sector=SectorConfig(columns=999, rows_per_half=10)))
    check("with NO real rails configured at all, J2/SW1 stay in the "
          "ordinary passives list -- the real, honest fallback",
          "J2" in passives3 and "SW1" in passives3)
    check("...and p.rail_direct_power_source stays genuinely empty",
          not p3.rail_direct_power_source)


def check_far_leg_unresolvable_is_honest(quiet, kicad_cli):
    if not quiet:
        print("\ndescribe_step()'s own real \"far leg unresolvable\" honesty -- "
              "CORRECTED 2026-09-17, closing FutureResearch.org's own new "
              "issue set's own item 1: a passive whose own far leg lands on "
              "a genuinely DIFFERENT real board than its own near leg (a "
              "real, already-documented gap in leg()'s own board-unaware "
              "pixel-space search -- see generate_steps()'s own real, "
              "2026-08-28 comment) used to get a silent, misleading "
              "fallback to its own near hole repeated (\"one lead into "
              "hole c5, the other into hole c5\"), which then corrupted "
              "the reconstructed netlist too (a real, genuine short) -- "
              "confirmed directly, reusing this project's own real, "
              "already-established rp2040-basic-m1 two-board scenario "
              "(pptest_regression's own 2026-09-06 crash-regression "
              "check):")
    from parse_instructions import parse_instructions, reconstruct
    from breadboard_config import BoardConfig, SectorConfig, RailConfig, MultiboardConfig
    import breadboard_lib

    board1 = BoardConfig(name="board1", sectors=1, sector=SectorConfig(columns=40, rows_per_half=10),
                          rail=RailConfig(present=False), rail_left=RailConfig(present=True))
    board2 = BoardConfig(name="board2", sectors=1, sector=SectorConfig(columns=40, rows_per_half=10),
                          rail=RailConfig(present=False), rail_left=RailConfig(present=True))
    multi = MultiboardConfig(name="far-leg-unresolvable-check", boards=[board1, board2])
    nl, p, anchors, passives, rails = _load(
        "TestSchemes/jck-RP2040-basic-m1/rp2040-basic-m1.kicad_sch", kicad_cli,
        board_config=multi)
    steps = generate_steps(nl, p, anchors, passives, rails)
    unresolvable = next((s for s in steps if s.kind == "passive" and
                          s.far_hole is None), None)
    check("a real passive with a genuinely unresolvable far leg exists "
          "here -- confirming this check exercises the real, intended "
          "case (confirmed by hand, 20/20 real, separate runs, before "
          "writing this check)",
          unresolvable is not None)
    if unresolvable is None:
        return
    ref = unresolvable.ref
    n = list(steps).index(unresolvable) + 1
    text = describe_step(unresolvable, n)
    check(f"{ref}'s own real describe_step() text says its far leg could "
          f"not be placed, honestly -- not the near hole repeated a "
          f"second time",
          "far leg could not be placed" in text and
          text.count(f"hole {unresolvable.near_hole}") <= 1)

    # parse_instructions.py's own new _PASSIVE_NO_FAR pattern, tested
    # directly against a minimal, synthetic instructions snippet --
    # not this same, real, multi-board circuit's own full, real
    # output, since ppparse_instructions(1)/parse_instructions.py
    # itself refuses multi-board instructions entirely, at an
    # earlier, unrelated real step (the real "connect ... to board N"
    # offboard-jumper line, confirmed directly, not this check's own
    # concern at all) -- this synthetic snippet isolates the ONE,
    # real behavior this check needs: does a genuinely unresolvable
    # far leg register only its own near pin, on the reconstructed
    # netlist, rather than two pins genuinely shorted together.
    snippet = ("Build instructions: synthetic\n"
               "=============================\n\n"
               "1. Place the Generic IC (IC, ref U1) straddling the "
               "trench, with its leftmost pin in hole a5.\n"
               "2. Insert the 1k Resistor (R1): one lead into hole a10. "
               "Its own far leg could not be placed within this board's "
               "own row limit.\n")
    parsed = parse_instructions(snippet)
    nl2, p2 = reconstruct(parsed)
    r1_pins = [pin for r, pin in nl2.pins_on_net(nl2.nets_for_ref("R1")[0])
               if r == "R1"]
    check("a synthetic passive whose own far leg text states it could "
          "not be placed reconstructs with only ONE real pin registered "
          "-- not two pins genuinely shorted onto the identical net",
          len(r1_pins) == 1)

    # A real, direct control: an ordinary passive whose own far leg IS
    # resolvable must still work exactly as before -- this fix must
    # not have silently broken the common, real case while fixing the
    # rare one.
    nl3, p3, anchors3, passives3, rails3 = _load(
        "packpay-example-circuit1.kicad_sch", kicad_cli,
        board_config=TEST_BOARD_CONFIG)
    steps3 = generate_steps(nl3, p3, anchors3, passives3, rails3)
    ordinary = next((s for s in steps3 if s.kind == "passive" and
                      s.far_hole is not None), None)
    check("a real, ordinary passive with a genuinely resolvable far leg "
          "still exists, in a real, separate, already-known-good circuit",
          ordinary is not None)
    if ordinary is not None:
        n2 = list(steps3).index(ordinary) + 1
        text2 = describe_step(ordinary, n2)
        check(f"{ordinary.ref}'s own real describe_step() text still "
              f"states BOTH real holes, the existing, unchanged real "
              f"grammar",
              "the other into hole" in text2)


def check_formatters(quiet, kicad_cli):
    if not quiet:
        print("\nOutput formatters: all four produce real, non-empty text "
              "containing every real step's own reference:")
    nl, p, anchors, passives, rails = _load(
        "packpay-example-circuit1.kicad_sch", kicad_cli,
        board_config=TEST_BOARD_CONFIG)
    steps = generate_steps(nl, p, anchors, passives, rails)

    for name, fn in (("plain", format_plain), ("markdown", format_markdown),
                      ("html", format_html), ("orgmode", format_orgmode)):
        text = fn(steps, title="test")
        check(f"{name} formatter produces real, non-empty output",
              len(text) > 0)
        refs_with_kind = [s.ref for s in steps if s.ref]
        check(f"{name} formatter mentions every real component ref at "
              f"least once",
              all(ref in text for ref in refs_with_kind))

    child_text = format_plain(steps, child_level=True)
    adult_text = format_plain(steps, child_level=False)
    check("child-level and adult-level phrasing produce genuinely "
          "DIFFERENT real text (not the same wording twice)",
          child_text != adult_text)


def main(argv=None):
    """CLI entry point, matching test_breadboard_config.py's own real,
    established style for this project's own real test scripts."""
    import argparse

    parser = argparse.ArgumentParser(
        prog="pptest_build_instructions",
        description="Check build_instructions.py's own real pipeline "
                     "(far-leg resolution, both ordering strategies, step "
                     "generation, all four output formatters) against "
                     "this project's own real, already-established test "
                     "circuits. Run this after any change to "
                     "build_instructions.py or to render.py's own "
                     "p.passive_far_xy exposure.")
    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)")
    parser.add_argument("--version", action="version",
                         version=f"%(prog)s {_version()}")
    parser.add_argument("--quiet", action="store_true",
                         help="suppress the section header lines; still "
                              "print every [ok]/[FAIL] check result and "
                              "the final summary")
    args = parser.parse_args(argv)

    check_component_type(args.quiet)
    check_hole_name(args.quiet)
    check_far_leg_and_steps(args.quiet, args.kicad_cli)
    check_net_grouped_ordering(args.quiet, args.kicad_cli)
    check_anchor_rail_and_power_role(args.quiet, args.kicad_cli)
    check_plain_format_title_round_trips(args.quiet, args.kicad_cli)
    check_rail_direct_power_source(args.quiet, args.kicad_cli)
    check_far_leg_unresolvable_is_honest(args.quiet, args.kicad_cli)
    check_unplaced_handling(args.quiet, args.kicad_cli)
    check_formatters(args.quiet, args.kicad_cli)

    print()
    if FAILURES:
        print(f"{len(FAILURES)} check(s) failed:")
        for f in FAILURES:
            print(f"  - {f}")
        return 1
    print("All checks passed.")
    return 0


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