/**
 * gardenfly.selftest.js — the audit's garden engine audits itself.  [Lane B, SPRINT13]
 *
 * Browser-only (skyfx needs `document`), so unlike sweep.selftest.js this is an
 * ASYNC factory: b.test.js awaits it and registers the returned [name, fn]
 * pairs. The flights happen here, once; the tests assert on captured results —
 * the same Suite-cannot-await shape a.test.js's pinned-separation flight uses.
 *
 * What it pins, and why each can fail:
 *
 *  1. TWO HARNESSES, ONE PIN. a.test flies backyard_01's `separation` block
 *     through the RiggingSession shop path; this flies the SAME block through
 *     the audit's own chain (gardenfly: windForSite, direct attach, named
 *     hardware). Both must land on A's ruling — held FULL and winning, bare
 *     losing. If a retune moves the wild night, both go red together and the
 *     argument happens in THREADS, not in a drifted tool.
 *
 *  2. THE VENTURI REACHES THE FLOWN WIND. Three harnesses shipped the same
 *     bug (site_audit SPRINT11, C's bench, D's first harness): the funnel
 *     lives in the SITE def and a harness that doesn't wire it flies an
 *     easier yard than ships. Same synthetic-yard trick as sweep.selftest:
 *     starve gardenfly of the siteDef's venturi and this goes red.
 *
 *  3. The shop has no mystery tier: hardwareByName throws on a stranger
 *     rather than quietly handing back nothing (the setHardware lesson).
 */

import * as THREE from '../../web/world/vendor/three.module.js';
import { createWorld, loadSite } from '../../web/world/js/world.js';
import { loadStorm, windForSite } from '../../web/world/js/weather.js';
import { SailRig } from '../../web/world/js/sail.js';
import { createSkyFx } from '../../web/world/js/skyfx.js';
import { createGarden } from '../../web/world/js/garden.js';
import { FIXED_DT } from '../../web/world/js/contracts.js';
import { flyGarden, flySeparation, hardwareByName } from './gardenfly.js';

const assert = (cond, msg) => { if (!cond) throw new Error(msg); };

/** sweep.selftest's synthetic yard: one quad, storm dead along +Z, funnel on it. */
const SYN_ANCHORS = [
  { id: 'a1', type: 'post', pos: { x: -3, y: 3.9, z: -3 } },
  { id: 'a2', type: 'post', pos: { x: 3, y: 3.9, z: -3 } },
  { id: 'a3', type: 'post', pos: { x: 3, y: 3.9, z: 3 } },
  { id: 'a4', type: 'post', pos: { x: -3, y: 3.9, z: 3 } },
].map((a) => ({ ...a, sway: () => a.pos }));
const SYN_BED = { x: 0, z: 0, w: 4, d: 4 };
const SYN_STORM = { id: 'gardenfly_selftest_storm', duration: 8, dir: Math.PI / 2, base: 14,
  gusts: { every: 3, peak: 1.6, downdraftOfTotal: 0.2 } };
const SYN_FUNNEL = [{ x: 0, z: 0, axis: Math.PI / 2, gain: 2.0, radius: 12, sharp: 1 }];

export async function buildGardenflyTests() {
  // The real yard, the way the game builds it — on a re-pointable wind proxy
  // (C's bench pattern) so world.anchors fly THEMSELVES with live sway (a
  // static remap froze the gum tree, C's landmine 2). Fail LOUD if dress
  // fails — an undressed yard has no fascia hint, and a garden number
  // measured on it is about a different game (C's bare-specifier gun).
  const site = await loadSite('backyard_01');
  let currentWind = {
    sample: (p, t, o) => (o || new THREE.Vector3()).set(0, 0, 4),
    speedAt: () => 4, rainAt: () => 0, rainMmPerHour: () => 0,
    gustTelegraph: () => null, eventsBetween: () => [],
  };
  const windProxy = {
    sample: (p, t, o) => currentWind.sample(p, t, o || new THREE.Vector3()),
    speedAt: (p, t) => currentWind.speedAt(p, t),
    rainAt: (t) => currentWind.rainAt(t),
    rainMmPerHour: (t) => (currentWind.rainMmPerHour ? currentWind.rainMmPerHour(t) : 0),
    gustTelegraph: (t) => currentWind.gustTelegraph(t),
    eventsBetween: (a, b) => currentWind.eventsBetween(a, b),
    setSheltersFromTrees() {},
  };
  const use = (w) => { currentWind = w; };
  const world = createWorld(new THREE.Scene(), { wind: windProxy, site });
  await world.dress();

  let sepRun = null, skewRun = null, sepErr = null;
  if (site.separation) {
    try {
      const stormDef = await loadStorm(site.separation.stormKey);
      sepRun = flySeparation({
        anchors: world.anchors, bed: world.gardenBed,
        separation: site.separation, stormDef, siteDef: site, use,
      });
      // The OTHER harness's chain, reproduced on purpose: a.test settles 12 s
      // on the calm day before the storm, and SailRig samples wind at its
      // INTERNAL clock — so its storm flies 12 s off the authored curve
      // against the sky's t. Measured (2026-07-18, tmp probe → THREADS): the
      // skew is worth +4.6 HP on the pinned line (63.8 game-true vs 68.4
      // skewed; A's 68.3 to within 0.1, peaks to the digit). This flight
      // exists so BOTH harnesses are pinned from one file: if either chain
      // drifts, the agreement asserts below go red and name which one.
      const calmDef = await loadStorm('storm_01_gentle');
      const sep = site.separation;
      const wind = windForSite(stormDef, site, world.anchors);
      const calm = windForSite(calmDef, site, world.anchors);
      use(calm);
      const rig = new SailRig({ anchors: world.anchors, gridN: 10, porosity: 0.30 })
        .attach(sep.line.map((l) => l.anchor), sep.line.map((l) => hardwareByName(l.hw)), sep.tension ?? 1);
      for (let i = 0, n = Math.round(12 / FIXED_DT); i < n; i++) rig.step(FIXED_DT, calm, (i * FIXED_DT) % 3);
      use(wind);
      const sky = createSkyFx({ wind, night: true });
      const garden = createGarden({ setPlants() {} });
      const bed = world.gardenBed;
      for (let i = 0, n = Math.round(stormDef.duration / FIXED_DT); i < n; i++) {
        const t = i * FIXED_DT;
        rig.step(FIXED_DT, wind, t);
        sky.step(FIXED_DT, t, { sail: rig });
        garden.step(FIXED_DT, sky.gardenHailExposure(bed, t), sky.gardenExposure(bed, t));
      }
      sky.dispose?.();
      skewRun = { hp: +garden.hp.toFixed(1), lost: rig.corners.filter((c) => c.broken).length };
    } catch (err) { sepErr = String((err && err.stack) || err); }
  }

  let venRun = null, venErr = null;
  try {
    const fly = (venturi) => flyGarden({
      anchors: SYN_ANCHORS, bed: SYN_BED, stormDef: SYN_STORM,
      siteDef: { wind: { venturi } },
      ids: ['a1', 'a2', 'a3', 'a4'], hw: hardwareByName('rated shackle'),
    });
    venRun = { bare: fly([]), funnelled: fly(SYN_FUNNEL) };
  } catch (err) { venErr = String((err && err.stack) || err); }

  return [
    ['gardenfly: the pinned separation line, flown in BOTH chains — the 12s-skew disagreement, pinned', () => {
      // ⚠ THE STATE OF PLAY (2026-07-18, [B] THREADS, receipts in the entry):
      // the pinned recipe HOLDS and rigging matters by a full state's width —
      // but the >66 FULL half of the target is only met in a.test's chain,
      // whose 12 s calm settle runs the storm 12 s off the authored curve
      // (SailRig samples wind at its INTERNAL clock). The game-true chain
      // (commit→attach→storm at t=0, this module) reads ~63.8 TATTERED.
      // Ruled numbers are A's; the game is D's to play; this test pins BOTH
      // measurements so any drift in either chain goes red and names itself.
      // When A re-rules (fix a.test's harness, restate the threshold, or
      // re-steel the recipe), the constants below move WITH the ruling.
      if (!site.separation) throw new Error('backyard_01 lost its separation block — the audit has no target to read');
      if (sepErr) throw new Error(`separation flight died: ${sepErr}`);
      const s = site.separation;
      assert(sepRun.held.cost <= 80, `the pinned recipe costs $${sepRun.held.cost} — must be night-1 buyable`);
      assert(sepRun.held.cornersLost === 0, `pinned line lost ${sepRun.held.cornersLost}/4 corners — it must HOLD the wild night`);
      assert(sepRun.bareOk, `bare bed ${sepRun.bare.hp} vs pinned <${s.bareMustLoseBelow} — bare must LOSE the night`);
      assert(sepRun.held.hp - sepRun.bare.hp > 25,
        `separation ${(sepRun.held.hp - sepRun.bare.hp).toFixed(1)} — rigging must matter by a full state's width`);
      // RESOLVED at SPRINT13 integration, per this assert's own instruction: A
      // re-ruled heldMustExceed 66 → 60 on the game-true number (their THREADS
      // entry, "restated, not nudged"), so the two-chain disagreement this used
      // to pin is closed and the pin is asserted directly. B pre-authorized the
      // edit in the message above; A verified 357/0/0 on a scratch merge first.
      assert(sepRun.heldOk,
        `game-true chain reads ${sepRun.held.hp} vs pinned >${s.heldMustExceed} — the held bed must ` +
        `clear the site's separation block for the RIGHT reason (no settle skew)`);
      assert(skewRun.lost === 0 && skewRun.hp > s.heldMustExceed,
        `a.test's settle-skewed chain reads ${skewRun.hp} — measured 68.4 at landing (a.test's own 68.3). ` +
        `If this dropped, a.test's pin is about to go red too: the wild night itself moved`);
      assert(sepRun.ok === (sepRun.heldOk && sepRun.heldWin && sepRun.bareOk),
        'flySeparation.ok must agree with its own parts');
    }],
    ['gardenfly: the SITE venturi reaches the flown wind (the bench landmine, third time charmed)', () => {
      if (venErr) throw new Error(`venturi flight died: ${venErr}`);
      const { bare, funnelled } = venRun;
      assert(bare.peaks.length === 4 && funnelled.peaks.length === 4, 'both flights fly 4 corners');
      for (const b of bare.peaks) {
        // align by anchorId — peaks are ring-ordered, never input-ordered (C's warning)
        const f = funnelled.peaks.find((p) => p.id === b.id);
        assert(f.peakN > b.peakN,
          `corner ${b.id}: funnelled peak ${f.peakN} N is not above bare ${b.peakN} N — ` +
          `the siteDef's venturi is not reaching gardenfly's wind (windForSite must receive the SITE def)`);
      }
    }],
    ['gardenfly: unknown hardware names throw — the shop has no mystery tier', () => {
      let threw = false;
      try { hardwareByName('titanium dream'); } catch { threw = true; }
      assert(threw, 'hardwareByName("titanium dream") must throw, not hand back undefined');
      assert(hardwareByName('rated shackle').rating === 6500, 'and the real names still resolve');
    }],
  ];
}
