// Standalone tests for reasampler::ViewModeModel — no REAPER, no test framework.
// Mirror of test_bank_model: iterate the hard logic outside the DAW.
//
// Covers (PLAN.md D1 test cases):
//  1. N-mode proven — >=3 modes, membership + derivation still correct.
//  2. Parent derivation — a folder with descendant leaves in different modes is
//     visible in each of those modes.
//  3. Restore round-trip — snapshot -> park -> restore returns every driven flag to
//     its captured value; includes the "flag already at 0 stays 0" (no default).
//  4. show-both leaf never appears in a park op-list and is visible in all modes.
//  5. Unknown/stale GUID tolerated (ignore-and-prune, no crash).
//  6. JSON round-trip lossless: modes + membership + show-both + snapshots + active.
//  7. planToggle park path: fxOffline is empty (shell-expands-FX contract).
//  8. Per-FX offline is keyed by FX identity, and the v1 (slot-keyed) blob lifts
//     into that keying without losing its restore.
//  9. Nested-folder toggle: the snapshot store/clear lifecycle survives a re-park
//     (park-while-parked) so untagged leaves return to visible after toggling back;
//     guards the in-DAW "all leaves hidden after toggling twice" regression.

#include "../src/core/view/view_mode_model.h"
#include "../src/core/view/lane_keys.h"  // laneNameForMode — assert the minting plan's durable keys

#include <algorithm>
#include <cstdio>
#include <string>

using namespace reasampler;
using namespace reasampler::view;

static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
    std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)

// -- helpers -----------------------------------------------------------------

static bool visibleHas(const std::set<std::string>& v, const std::string& g) {
    return v.count(g) > 0;
}

// Does any park TrackPlan in the plan target `guid`?
static bool parkTargets(const TogglePlan& plan, const std::string& guid) {
    for (const auto& p : plan.park)
        for (const auto& f : p.flags)
            if (f.guid == guid) return true;
    return false;
}

// Find the single restore plan for `guid`, or nullptr.
static const TrackPlan* restoreFor(const TogglePlan& plan, const std::string& guid) {
    for (const auto& p : plan.restore)
        if (!p.flags.empty() && p.flags.front().guid == guid) return &p;
    return nullptr;
}

static int flagValue(const TrackPlan& p, Flag f) {
    for (const auto& op : p.flags)
        if (op.flag == f) return op.value;
    return -999;  // sentinel: flag absent
}

// Golden byte-literal (Q-W1 follow-up): pins the EXACT serialized bytes for the
// default-seeded model (Arrange + Design, no membership), not just self-
// consistent re-serialization — a format drift that both writer and reader
// agree on would slip past the round-trip tests but not this. The format is
// frozen as-shipped; the literal below is the captured current output.
static void testSerializeGoldenLiteral() {
    ViewModeModel vm;
    CHECK(vm.serialize() ==
          "{\"version\":2,\"activeMode\":\"arrange\",\"modes\":[{\"id\":\"arrange\","
          "\"displayName\":\"Arrange\",\"ordinal\":0},{\"id\":\"design\","
          "\"displayName\":\"Design\",\"ordinal\":1}],\"membership\":[],"
          "\"snapshots\":[],\"lanes\":[],\"soloCache\":[]}");
}

// -- 1. N-mode proven --------------------------------------------------------

static void testNModeRegistryAndMembership() {
    ViewModeModel vm;
    // Seeded: Arrange + Design.
    CHECK(vm.modes().size() == 2);
    CHECK(vm.modes().contains(kArrangeModeId));
    CHECK(vm.modes().contains(kDesignModeId));
    CHECK(vm.activeModeId() == kArrangeModeId);

    // Add a third mode — proves N-mode, not boolean.
    CHECK(vm.modes().add(Mode{"mixdown", "Mixdown", 2}));
    CHECK(vm.modes().size() == 3);
    CHECK(vm.modes().contains("mixdown"));

    // Duplicate id and empty id are rejected without mutation.
    CHECK(!vm.modes().add(Mode{"mixdown", "Dup", 5}));
    CHECK(!vm.modes().add(Mode{"", "Empty", 6}));
    CHECK(vm.modes().size() == 3);

    // Membership across three modes.
    CHECK(vm.membership().tag("{A}", kArrangeModeId));
    CHECK(vm.membership().tag("{D}", kDesignModeId));
    CHECK(vm.membership().tag("{M}", "mixdown"));

    // Leaf-belongs rule holds in each mode.
    CHECK(vm.leafBelongsToMode("{D}", kDesignModeId));
    CHECK(!vm.leafBelongsToMode("{D}", kArrangeModeId));
    CHECK(vm.leafBelongsToMode("{M}", "mixdown"));
    CHECK(!vm.leafBelongsToMode("{M}", kDesignModeId));

    // Untagged leaf defaults to Arrange, and only Arrange.
    CHECK(vm.leafBelongsToMode("{UNTAGGED}", kArrangeModeId));
    CHECK(!vm.leafBelongsToMode("{UNTAGGED}", kDesignModeId));

    // Retag moves the leaf (single-mode semantics).
    CHECK(vm.membership().tag("{D}", "mixdown"));
    CHECK(vm.leafBelongsToMode("{D}", "mixdown"));
    CHECK(!vm.leafBelongsToMode("{D}", kDesignModeId));

    // Untag returns to the Arrange default.
    CHECK(vm.membership().untag("{D}"));
    CHECK(vm.leafBelongsToMode("{D}", kArrangeModeId));
    CHECK(!vm.membership().untag("{D}"));  // second untag is a no-op

    // setActiveMode rejects an unregistered id, accepts a registered one.
    CHECK(!vm.setActiveMode("nope"));
    CHECK(vm.activeModeId() == kArrangeModeId);
    CHECK(vm.setActiveMode("mixdown"));
    CHECK(vm.activeModeId() == "mixdown");
}

// -- 2. Parent derivation ----------------------------------------------------

static void testParentDerivationMultiMode() {
    ViewModeModel vm;
    // Folder {F} holds two leaves: {L1} in Arrange (untagged default), {L2} in Design.
    FolderTree tree;
    tree.nodes.push_back(FolderNode{"{F}", "", /*isParent=*/true});
    tree.nodes.push_back(FolderNode{"{L1}", "{F}", false});
    tree.nodes.push_back(FolderNode{"{L2}", "{F}", false});
    vm.membership().tag("{L2}", kDesignModeId);
    // {L1} stays untagged ⇒ Arrange.

    auto arrange = vm.visibleTracks(tree, kArrangeModeId);
    auto design  = vm.visibleTracks(tree, kDesignModeId);

    // The parent is visible in BOTH modes because it has a descendant in each.
    CHECK(visibleHas(arrange, "{F}"));
    CHECK(visibleHas(design, "{F}"));

    // Leaves appear only in their own mode.
    CHECK(visibleHas(arrange, "{L1}") && !visibleHas(arrange, "{L2}"));
    CHECK(visibleHas(design, "{L2}") && !visibleHas(design, "{L1}"));

    // Nested folder chain: grandparent {G} > parent {F2} > leaf {L3} (Design).
    // The whole chain up to the root must be visible in Design.
    FolderTree nested;
    nested.nodes.push_back(FolderNode{"{G}", "", true});
    nested.nodes.push_back(FolderNode{"{F2}", "{G}", true});
    nested.nodes.push_back(FolderNode{"{L3}", "{F2}", false});
    ViewModeModel vm2;
    vm2.membership().tag("{L3}", kDesignModeId);
    auto d2 = vm2.visibleTracks(nested, kDesignModeId);
    CHECK(visibleHas(d2, "{L3}"));
    CHECK(visibleHas(d2, "{F2}"));
    CHECK(visibleHas(d2, "{G}"));
    // In Arrange: the Design leaf {L3} stays hidden (leaf visibility unchanged), but
    // the untagged parents {F2}/{G} are Arrange members by their own default, so they
    // are visible in Arrange under the corrected own-membership OR derived rule.
    // (See testParentOwnMembershipVisibility for the full case matrix.)
    auto a2 = vm2.visibleTracks(nested, kArrangeModeId);
    CHECK(!visibleHas(a2, "{L3}"));
    CHECK(visibleHas(a2, "{F2}") && visibleHas(a2, "{G}"));

    // A parent is NEVER parked, in either mode.
    auto planD = vm.planToggle(tree, kDesignModeId);
    auto planA = vm.planToggle(tree, kArrangeModeId);
    CHECK(!parkTargets(planD, "{F}"));
    CHECK(!parkTargets(planA, "{F}"));
}

// -- 2b. Parent own-membership visibility (untagged folder + own FX/media) ---
//
// Corrected rule: a parent is visible in mode M if EITHER a descendant leaf is
// visible in M (existing derived rule) OR the parent belongs to M by its OWN
// membership (leafBelongsToMode on the parent's own GUID; untagged ⇒ Arrange).
// The reported case: an untagged folder whose leaves are all Design vanished in
// Arrange despite carrying its own FX/media. It must now show in Arrange (own
// default) AND Design (derived from children). Parents remain never parked.

static void testParentOwnMembershipVisibility() {
    // Case A [reported]: untagged folder {F}, all leaves Design ⇒ folder visible in
    // BOTH Arrange (own default) and Design (derived from children).
    {
        ViewModeModel vm;
        FolderTree tree;
        tree.nodes.push_back(FolderNode{"{F}", "", /*isParent=*/true});
        tree.nodes.push_back(FolderNode{"{L1}", "{F}", false});
        tree.nodes.push_back(FolderNode{"{L2}", "{F}", false});
        vm.membership().tag("{L1}", kDesignModeId);
        vm.membership().tag("{L2}", kDesignModeId);
        // {F} itself untagged ⇒ Arrange member by default.

        auto arrange = vm.visibleTracks(tree, kArrangeModeId);
        auto design  = vm.visibleTracks(tree, kDesignModeId);
        CHECK(visibleHas(arrange, "{F}"));   // own default (would FAIL under derived-only rule)
        CHECK(visibleHas(design, "{F}"));    // derived from Design children
        // Leaves appear only in Design; neither is visible in Arrange.
        CHECK(visibleHas(design, "{L1}") && visibleHas(design, "{L2}"));
        CHECK(!visibleHas(arrange, "{L1}") && !visibleHas(arrange, "{L2}"));
        // Still never parked, in either mode.
        CHECK(!parkTargets(vm.planToggle(tree, kArrangeModeId), "{F}"));
        CHECK(!parkTargets(vm.planToggle(tree, kDesignModeId), "{F}"));
    }

    // Case B: untagged folder, all leaves Arrange ⇒ folder visible in Arrange only.
    {
        ViewModeModel vm;
        FolderTree tree;
        tree.nodes.push_back(FolderNode{"{F}", "", true});
        tree.nodes.push_back(FolderNode{"{L1}", "{F}", false});  // untagged ⇒ Arrange
        tree.nodes.push_back(FolderNode{"{L2}", "{F}", false});  // untagged ⇒ Arrange

        CHECK(visibleHas(vm.visibleTracks(tree, kArrangeModeId), "{F}"));
        CHECK(!visibleHas(vm.visibleTracks(tree, kDesignModeId), "{F}"));
    }

    // Case C: untagged folder, mixed Arrange + Design leaves ⇒ visible in both.
    {
        ViewModeModel vm;
        FolderTree tree;
        tree.nodes.push_back(FolderNode{"{F}", "", true});
        tree.nodes.push_back(FolderNode{"{LA}", "{F}", false});  // untagged ⇒ Arrange
        tree.nodes.push_back(FolderNode{"{LD}", "{F}", false});
        vm.membership().tag("{LD}", kDesignModeId);

        CHECK(visibleHas(vm.visibleTracks(tree, kArrangeModeId), "{F}"));
        CHECK(visibleHas(vm.visibleTracks(tree, kDesignModeId), "{F}"));
    }

    // Case D: nested untagged grandparent {G} > untagged parent {F} > Design leaves.
    // Both intermediate folders visible in BOTH modes: own-default Arrange (they are
    // untagged), and derived Design (a Design leaf lives under each).
    {
        ViewModeModel vm;
        FolderTree tree;
        tree.nodes.push_back(FolderNode{"{G}", "", true});
        tree.nodes.push_back(FolderNode{"{F}", "{G}", true});
        tree.nodes.push_back(FolderNode{"{L1}", "{F}", false});
        tree.nodes.push_back(FolderNode{"{L2}", "{F}", false});
        vm.membership().tag("{L1}", kDesignModeId);
        vm.membership().tag("{L2}", kDesignModeId);

        auto arrange = vm.visibleTracks(tree, kArrangeModeId);
        auto design  = vm.visibleTracks(tree, kDesignModeId);
        CHECK(visibleHas(arrange, "{G}") && visibleHas(arrange, "{F}"));  // own default
        CHECK(visibleHas(design, "{G}") && visibleHas(design, "{F}"));    // derived
        // The Design leaves stay Design-only; not visible in Arrange.
        CHECK(!visibleHas(arrange, "{L1}") && !visibleHas(arrange, "{L2}"));
    }

    // Case E: existing behavior unchanged — a TAGGED-Design folder holding a visible
    // Design leaf still shows in Design; and a folder made visible only by a visible
    // tagged-Design descendant (own membership Arrange) still derives into Design.
    // Leaf visibility itself is unchanged: a Design leaf is Design-only.
    {
        ViewModeModel vm;
        FolderTree tree;
        tree.nodes.push_back(FolderNode{"{F}", "", true});
        tree.nodes.push_back(FolderNode{"{LD}", "{F}", false});
        vm.membership().tag("{F}", kDesignModeId);   // folder tagged into Design itself
        vm.membership().tag("{LD}", kDesignModeId);

        auto design = vm.visibleTracks(tree, kDesignModeId);
        CHECK(visibleHas(design, "{F}"));    // own Design membership + derived from {LD}
        CHECK(visibleHas(design, "{LD}"));
        // A Design-tagged folder is NOT an Arrange member ⇒ not visible in Arrange
        // unless a child is; here the only child is Design, so folder hidden in Arrange.
        auto arrange = vm.visibleTracks(tree, kArrangeModeId);
        CHECK(!visibleHas(arrange, "{F}"));
        CHECK(!visibleHas(arrange, "{LD}"));   // leaf visibility unchanged
    }
}

// -- 3. Restore round-trip (the trust anchor) --------------------------------

static void testRestoreRoundTripSnapshotValues() {
    // Direct planner check: park is fixed zeros; restore is snapshot verbatim.
    TrackSnapshot snap;
    snap.showInTcp   = 1;
    snap.showInMixer = 1;
    snap.mainSend    = 0;   // user had it OUT of the mix for their own reason
    snap.fxEnable    = 1;
    // Slot 1's plugin was already offline before parking; each entry carries the
    // identity of the FX it came from.
    snap.fxOffline   = {{"{FX-A}", 0}, {"{FX-B}", 1}, {"{FX-C}", 0}};

    TrackPlan park = makeParkPlan("{T}", /*fxCount=*/3);
    CHECK(flagValue(park, Flag::ShowInTcp) == 0);
    CHECK(flagValue(park, Flag::ShowInMixer) == 0);
    CHECK(flagValue(park, Flag::MainSend) == 0);
    CHECK(flagValue(park, Flag::FxEnable) == 0);
    CHECK(park.fxOffline.size() == 3);
    for (const auto& op : park.fxOffline) CHECK(op.offline == true);

    TrackPlan restore = makeRestorePlan("{T}", snap);
    // Every flag returns to its CAPTURED value — not a hardcoded "on".
    CHECK(flagValue(restore, Flag::ShowInTcp) == 1);
    CHECK(flagValue(restore, Flag::ShowInMixer) == 1);
    CHECK(flagValue(restore, Flag::MainSend) == 0);   // the "already at 0 stays 0" case
    CHECK(flagValue(restore, Flag::FxEnable) == 1);
    CHECK(restore.fxOffline.size() == 3);
    CHECK(restore.fxOffline[0].offline == false);
    CHECK(restore.fxOffline[1].offline == true);   // was offline pre-park ⇒ stays offline
    CHECK(restore.fxOffline[2].offline == false);
    // Each restore op names the FX it was captured from, not just a position —
    // resolveFxRestore has something to key on even if the chain moved.
    CHECK(restore.fxOffline[1].keying == FxKeying::Identity);
    CHECK(restore.fxOffline[1].fxGuid == "{FX-B}");

    // A snapshot entirely at 0 must restore entirely to 0 (no default leaks in).
    TrackSnapshot zero;  // all zeros, empty fxOffline
    TrackPlan rz = makeRestorePlan("{Z}", zero);
    CHECK(flagValue(rz, Flag::ShowInTcp) == 0);
    CHECK(flagValue(rz, Flag::ShowInMixer) == 0);
    CHECK(flagValue(rz, Flag::MainSend) == 0);
    CHECK(flagValue(rz, Flag::FxEnable) == 0);
    CHECK(rz.fxOffline.empty());

    // End-to-end via planToggle: a leaf tagged Design, snapshotted, parked while in
    // Arrange, then restored when we toggle back to Design.
    ViewModeModel vm;
    FolderTree tree;
    tree.nodes.push_back(FolderNode{"{DES}", "", false});
    vm.membership().tag("{DES}", kDesignModeId);
    vm.storeSnapshot("{DES}", snap);

    // Toggle to Arrange: {DES} is inactive ⇒ parked.
    auto toArrange = vm.planToggle(tree, kArrangeModeId);
    CHECK(parkTargets(toArrange, "{DES}"));
    CHECK(restoreFor(toArrange, "{DES}") == nullptr);  // not restored while inactive

    // Toggle to Design: {DES} is active AND has a snapshot ⇒ restored from it.
    auto toDesign = vm.planToggle(tree, kDesignModeId);
    CHECK(!parkTargets(toDesign, "{DES}"));
    const TrackPlan* r = restoreFor(toDesign, "{DES}");
    CHECK(r != nullptr);
    if (r) {
        CHECK(flagValue(*r, Flag::MainSend) == 0);   // captured 0 comes back 0
        CHECK(flagValue(*r, Flag::ShowInTcp) == 1);
    }
}

// -- 4. show-both leaf --------------------------------------------------------

static void testShowBothNeverParkedVisibleEverywhere() {
    ViewModeModel vm;
    CHECK(vm.modes().add(Mode{"mixdown", "Mixdown", 2}));

    FolderTree tree;
    tree.nodes.push_back(FolderNode{"{SB}", "", false});
    // Tag into Design, then pin show-both.
    vm.membership().tag("{SB}", kDesignModeId);
    CHECK(vm.membership().setShowBoth("{SB}", true));
    CHECK(vm.membership().isShowBoth("{SB}"));

    // Visible in EVERY mode.
    CHECK(visibleHas(vm.visibleTracks(tree, kArrangeModeId), "{SB}"));
    CHECK(visibleHas(vm.visibleTracks(tree, kDesignModeId), "{SB}"));
    CHECK(visibleHas(vm.visibleTracks(tree, "mixdown"), "{SB}"));

    // Never parked, in any mode — even a mode it isn't tagged into.
    CHECK(!parkTargets(vm.planToggle(tree, kArrangeModeId), "{SB}"));
    CHECK(!parkTargets(vm.planToggle(tree, kDesignModeId), "{SB}"));
    CHECK(!parkTargets(vm.planToggle(tree, "mixdown"), "{SB}"));

    // Clearing show-both restores normal one-mode parking: now in Arrange it parks.
    CHECK(vm.membership().setShowBoth("{SB}", false));
    CHECK(parkTargets(vm.planToggle(tree, kArrangeModeId), "{SB}"));
    CHECK(!parkTargets(vm.planToggle(tree, kDesignModeId), "{SB}"));
}

// -- 5. Unknown/stale GUID tolerated -----------------------------------------

static void testStaleGuidTolerated() {
    ViewModeModel vm;
    // Tag two leaves, but the tree only knows one — the other GUID is stale (its
    // track was deleted / restructured while parked).
    vm.membership().tag("{LIVE}", kDesignModeId);
    vm.membership().tag("{GHOST}", kDesignModeId);
    vm.storeSnapshot("{GHOST}", TrackSnapshot{});  // stale snapshot too

    FolderTree tree;
    tree.nodes.push_back(FolderNode{"{LIVE}", "", false});
    // {GHOST} absent from the tree.

    // No crash; the stale GUID is simply ignored (prune-safe).
    auto plan = vm.planToggle(tree, kArrangeModeId);
    CHECK(parkTargets(plan, "{LIVE}"));       // live leaf still planned
    CHECK(!parkTargets(plan, "{GHOST}"));     // stale leaf never emitted

    // Visibility derivation also ignores the stale GUID without incident.
    auto vis = vm.visibleTracks(tree, kDesignModeId);
    CHECK(visibleHas(vis, "{LIVE}"));
    CHECK(!visibleHas(vis, "{GHOST}"));

    // An empty tree with tagged members: nothing planned, no crash.
    FolderTree empty;
    auto emptyPlan = vm.planToggle(empty, kDesignModeId);
    CHECK(emptyPlan.park.empty() && emptyPlan.restore.empty());
}

// -- 6. JSON round-trip lossless ---------------------------------------------

static void testJsonRoundTrip() {
    ViewModeModel vm;
    // Modes: seeded pair + a third; also an out-of-order ordinal to prove sorting
    // survives round-trip.
    CHECK(vm.modes().add(Mode{"print", "Print \"stem\"\n", 5}));
    CHECK(vm.modes().add(Mode{"mixdown", "Mixdown", 2}));

    // Membership: a plain Design leaf, a show-both leaf, an Arrange leaf, and a
    // leaf carrying multiple modes (representable via restore; exercises the set).
    vm.membership().tag("{A}", kArrangeModeId);
    vm.membership().tag("{D}", kDesignModeId);
    vm.membership().tag("{SB}", "mixdown");
    vm.membership().setShowBoth("{SB}", true);
    Membership multi;
    multi.modeIds = {kDesignModeId, "mixdown"};
    multi.showBoth = false;
    CHECK(vm.membership().restore("{MULTI}", multi));

    // Snapshots: one full, one with a per-FX vector, including the tricky 0-values.
    TrackSnapshot s1; s1.showInTcp = 1; s1.showInMixer = 0; s1.mainSend = 1;
    s1.fxEnable = 0;
    s1.fxOffline = {{"{FX-1}", 1}, {"{FX-2}", 0}, {"{FX-3}", 1}, {"{FX-4}", 1}};
    vm.storeSnapshot("{D}", s1);
    TrackSnapshot s2;  // all zeros, empty fx vector
    vm.storeSnapshot("{A}", s2);

    // Active mode set to a non-default.
    CHECK(vm.setActiveMode("mixdown"));

    std::string json = vm.serialize();
    auto back = ViewModeModel::deserialize(json);
    CHECK(back.has_value());
    CHECK(back && *back == vm);
    // String form is stable across a second round-trip.
    if (back) CHECK(back->serialize() == json);

    // Spot-check the load-bearing bits survived.
    if (back) {
        CHECK(back->activeModeId() == "mixdown");
        CHECK(back->modes().size() == 4);
        const Mode* print = back->modes().query("print");
        CHECK(print && print->displayName == "Print \"stem\"\n" && print->ordinal == 5);
        CHECK(back->membership().isShowBoth("{SB}"));
        const Membership* mm = back->membership().query("{MULTI}");
        CHECK(mm && mm->modeIds.size() == 2 && mm->modeIds.count("mixdown"));
        const TrackSnapshot* snap = back->snapshot("{D}");
        CHECK(snap && snap->mainSend == 1 && snap->fxEnable == 0);
        CHECK(snap && snap->fxOffline.size() == 4 && snap->fxOffline[1].offline == 0);
        // The FX identities survive the round-trip — without them the restore is
        // back to guessing at slots.
        CHECK(snap && snap->fxKeying == FxKeying::Identity);
        CHECK(snap && snap->fxOffline[3].fxGuid == "{FX-4}");
    }
}

static void testEmptyModelRoundTrip() {
    ViewModeModel vm;  // default: Arrange + Design seeded, active = Arrange, no members
    std::string json = vm.serialize();
    auto back = ViewModeModel::deserialize(json);
    CHECK(back.has_value());
    CHECK(back && *back == vm);

    // Lenient empty root ⇒ a default-seeded model.
    auto empty = ViewModeModel::deserialize("{}");
    CHECK(empty.has_value());
    CHECK(empty && empty->modes().size() == 2);
    CHECK(empty && empty->activeModeId() == kArrangeModeId);
    CHECK(empty && empty->membership().empty());
}

static void testMalformedJson() {
    const char* bad[] = {
        "",
        "{",
        "not json",
        "{\"modes\":[",
        "{\"modes\":[{\"id\":\"x\"",                       // truncated mode
        "{\"activeMode\":\"ghost\"}",                       // active mode not registered
        "{\"modes\":[{\"id\":\"a\",\"ordinal\":0},{\"id\":\"a\",\"ordinal\":1}]}",  // dup id
        "{\"membership\":[{\"guid\":\"\"}]}",               // empty guid
        "{\"snapshots\":[{\"showInTcp\":1}]}",              // snapshot without guid
        "{\"snapshots\":[{\"guid\":\"x\",\"fxOffline\":[1,notanumber]}]}",
        "{\"snapshots\":[{\"guid\":\"x\",\"fx\":[{\"guid\":\"{F}\"}]}]}",       // fx without offline
        "{\"snapshots\":[{\"guid\":\"x\",\"fx\":[{\"offline\":1}]}]}",          // fx without guid
        "{\"snapshots\":[{\"guid\":\"x\",\"fx\":[",                             // truncated fx
        "{\"modes\":[]}trailing",                           // trailing garbage
    };
    for (const char* j : bad) {
        auto r = ViewModeModel::deserialize(j);
        CHECK(!r.has_value());
    }
}

// -- 7. planToggle park path: fxOffline is empty (shell-expands-FX contract) --
//
// planToggle calls makeParkPlan(guid, /*fxCount=*/0) for each inactive leaf.
// The D2 shell is responsible for expanding per-FX offline ops using
// TrackFX_GetCount — the pure model has no access to REAPER FX counts at plan
// time. This test pins that contract so a regression that passes a non-zero
// count (and emits FX ops prematurely) is caught immediately.
// The direct makeParkPlan(guid, 3) path (non-zero fxCount) is covered by
// testRestoreRoundTripSnapshotValues above.

static void testPlanToggleParkHasEmptyFxOffline() {
    ViewModeModel vm;
    FolderTree tree;
    tree.nodes.push_back(FolderNode{"{LEAF}", "", false});
    vm.membership().tag("{LEAF}", kDesignModeId);

    // Toggle to Arrange: {LEAF} is inactive ⇒ park plan emitted.
    auto plan = vm.planToggle(tree, kArrangeModeId);
    CHECK(plan.park.size() == 1);
    // The park plan must have an empty fxOffline — the shell expands FX ops.
    CHECK(plan.park[0].fxOffline.empty());
    // Scalar flags must still be present (the four park zeros).
    CHECK(plan.park[0].flags.size() == 4);
}

// -- 7b. Untagged leaves are managed by the mode system ----------------------
//
// The core semantic fix: an untagged leaf is an Arrange member. planToggle must
// enumerate EVERY leaf in the tree (not just membership_.all()), so an untagged
// leaf — absent from the membership index — parks in every non-Arrange mode and
// restores in Arrange, identically to a tagged leaf. Parents and show-both leaves
// remain never-parked. Tagged-leaf behavior is unchanged.

static void testUntaggedLeavesManagedByModeSystem() {
    ViewModeModel vm;

    // A tree of leaves NONE of which are in the membership index (all untagged),
    // plus a parent folder and a show-both leaf to prove they stay untouched.
    FolderTree tree;
    tree.nodes.push_back(FolderNode{"{P}", "", /*isParent=*/true});
    tree.nodes.push_back(FolderNode{"{U1}", "{P}", false});  // untagged leaf
    tree.nodes.push_back(FolderNode{"{U2}", "{P}", false});  // untagged leaf
    tree.nodes.push_back(FolderNode{"{SB}", "", false});     // show-both leaf
    vm.membership().setShowBoth("{SB}", true);

    // (iii) Enumeration covers leaves absent from the membership index: {U1}/{U2}
    // are NOT in membership_.all(), yet the planner reaches them.
    CHECK(vm.membership().query("{U1}") == nullptr);
    CHECK(vm.membership().query("{U2}") == nullptr);

    // (i) Toggling to Design (non-Arrange): every untagged leaf is parked.
    auto toDesign = vm.planToggle(tree, kDesignModeId);
    CHECK(parkTargets(toDesign, "{U1}"));
    CHECK(parkTargets(toDesign, "{U2}"));

    // (iv) The parent and the show-both leaf are NEVER parked, in either mode.
    CHECK(!parkTargets(toDesign, "{P}"));
    CHECK(!parkTargets(toDesign, "{SB}"));

    // (ii) Toggling to Arrange: the untagged leaves are Arrange members ⇒ active and
    // NOT parked. (No snapshot stored yet ⇒ no restore op either; just not parked.)
    auto toArrange = vm.planToggle(tree, kArrangeModeId);
    CHECK(!parkTargets(toArrange, "{U1}"));
    CHECK(!parkTargets(toArrange, "{U2}"));
    CHECK(restoreFor(toArrange, "{U1}") == nullptr);
    CHECK(!parkTargets(toArrange, "{P}"));
    CHECK(!parkTargets(toArrange, "{SB}"));

    // (vi) Restore-from-snapshot fidelity for a previously-parked UNTAGGED leaf:
    // an untagged leaf parked while in Design carries a snapshot; toggling back to
    // Arrange restores it from that snapshot verbatim, never a hardcoded default.
    TrackSnapshot snap;
    snap.showInTcp = 1; snap.showInMixer = 1; snap.mainSend = 0; snap.fxEnable = 1;
    snap.fxOffline = {{"{FX-A}", 0}, {"{FX-B}", 1}};
    vm.storeSnapshot("{U1}", snap);  // as the shell would, before parking it in Design
    auto backToArrange = vm.planToggle(tree, kArrangeModeId);
    const TrackPlan* r = restoreFor(backToArrange, "{U1}");
    CHECK(r != nullptr);
    if (r) {
        CHECK(flagValue(*r, Flag::ShowInTcp) == 1);
        CHECK(flagValue(*r, Flag::ShowInMixer) == 1);
        CHECK(flagValue(*r, Flag::MainSend) == 0);   // captured 0 comes back 0
        CHECK(flagValue(*r, Flag::FxEnable) == 1);
        CHECK(r->fxOffline.size() == 2);
        CHECK(r->fxOffline[0].offline == false);
        CHECK(r->fxOffline[1].offline == true);
    }
}

// (v) Tagged-leaf park/restore behavior is unchanged after the untagged fix: a leaf
// tagged Design parks in Arrange and is active (not parked) in Design, and a mix of
// tagged + untagged leaves each land on the correct side of the toggle.

static void testTaggedLeafBehaviorUnchangedWithUntagged() {
    ViewModeModel vm;
    FolderTree tree;
    tree.nodes.push_back(FolderNode{"{DES}", "", false});  // tagged into Design
    tree.nodes.push_back(FolderNode{"{UNT}", "", false});  // untagged ⇒ Arrange
    vm.membership().tag("{DES}", kDesignModeId);

    // In Design: {DES} active (not parked); {UNT} inactive ⇒ parked.
    auto design = vm.planToggle(tree, kDesignModeId);
    CHECK(!parkTargets(design, "{DES}"));
    CHECK(parkTargets(design, "{UNT}"));

    // In Arrange: {DES} inactive ⇒ parked; {UNT} active (not parked).
    auto arrange = vm.planToggle(tree, kArrangeModeId);
    CHECK(parkTargets(arrange, "{DES}"));
    CHECK(!parkTargets(arrange, "{UNT}"));
}

// -- 10. reconcile: prune orphaned snapshots on track delete -----------------
//
// Closes the "reconcile on delete/restructure" hardening item. reconcile prunes a
// snapshot whose GUID is not in the live set (its track was deleted while parked),
// preventing both the slow snapshot leak and an incorrect restore if REAPER reuses
// the GUID. Membership is deliberately KEPT (undo-delete restores the same GUID, so
// dropping the tag would silently lose it). A full live set is a no-op — this is why
// folder RESTRUCTURE, which leaves every GUID live, needs no special handling.

static void testReconcilePrunesOrphanedSnapshots() {
    ViewModeModel vm;
    // Two parked tracks (both snapshotted + tagged); {DEL} is about to be deleted.
    vm.membership().tag("{LIVE}", kDesignModeId);
    vm.membership().tag("{DEL}", kDesignModeId);
    TrackSnapshot sLive; sLive.showInTcp = 1;
    sLive.fxOffline = {{"{FX-A}", 0}, {"{FX-B}", 1}};
    TrackSnapshot sDel;  sDel.showInTcp  = 1; sDel.fxEnable  = 1;
    vm.storeSnapshot("{LIVE}", sLive);
    vm.storeSnapshot("{DEL}", sDel);
    CHECK(vm.snapshots().size() == 2);

    // {DEL} is deleted from the project ⇒ absent from the live GUID set.
    std::set<std::string> liveGuids{"{LIVE}"};
    std::size_t removed = vm.reconcile(liveGuids);

    // The orphaned snapshot is pruned; the live one is retained verbatim.
    CHECK(removed == 1);
    CHECK(vm.snapshot("{DEL}") == nullptr);
    const TrackSnapshot* kept = vm.snapshot("{LIVE}");
    CHECK(kept != nullptr);
    if (kept) CHECK(kept->showInTcp == 1 && kept->fxOffline.size() == 2);

    // Membership is NOT pruned — the deleted GUID keeps its Design tag so an
    // undo-delete (which restores the same GUID) brings the track back correctly
    // tagged. This is the load-bearing design call.
    CHECK(vm.membership().query("{DEL}") != nullptr);
    CHECK(vm.leafBelongsToMode("{DEL}", kDesignModeId));
    CHECK(vm.membership().query("{LIVE}") != nullptr);
}

static void testReconcileFullLiveSetIsNoOp() {
    // The restructure case: tracks moved between folders but none deleted ⇒ every
    // GUID stays live ⇒ reconcile prunes nothing.
    ViewModeModel vm;
    vm.storeSnapshot("{A}", TrackSnapshot{});
    vm.storeSnapshot("{B}", TrackSnapshot{});
    vm.storeSnapshot("{C}", TrackSnapshot{});

    std::set<std::string> liveGuids{"{A}", "{B}", "{C}"};
    std::size_t removed = vm.reconcile(liveGuids);

    CHECK(removed == 0);
    CHECK(vm.snapshots().size() == 3);
    CHECK(vm.snapshot("{A}") && vm.snapshot("{B}") && vm.snapshot("{C}"));

    // A superset of live GUIDs (tracks exist that were never parked) is also a no-op:
    // reconcile only ever removes, never adds.
    std::set<std::string> superset{"{A}", "{B}", "{C}", "{NEVER_PARKED}"};
    CHECK(vm.reconcile(superset) == 0);
    CHECK(vm.snapshots().size() == 3);

    // Empty live set (whole project emptied) prunes everything.
    CHECK(vm.reconcile(std::set<std::string>{}) == 3);
    CHECK(vm.snapshots().empty());
}

static void testReconcileThenReparkLifecycleIntact() {
    // No regression to the park/restore lifecycle: after reconcile prunes a deleted
    // track's snapshot, a still-live tagged leaf toggled back to its mode still
    // restores from its retained snapshot, and a re-park recaptures fresh state.
    ViewModeModel vm;
    FolderTree tree;
    tree.nodes.push_back(FolderNode{"{DES}", "", false});
    vm.membership().tag("{DES}", kDesignModeId);

    TrackSnapshot snap; snap.showInTcp = 1; snap.mainSend = 0; snap.fxEnable = 1;
    vm.storeSnapshot("{DES}", snap);           // parked while in Arrange
    vm.storeSnapshot("{ORPHAN}", TrackSnapshot{});  // a since-deleted parked track

    // Reconcile with {DES} live, {ORPHAN} gone.
    CHECK(vm.reconcile(std::set<std::string>{"{DES}"}) == 1);
    CHECK(vm.snapshot("{ORPHAN}") == nullptr);

    // Toggle back to Design: {DES} restores from its retained snapshot verbatim.
    auto toDesign = vm.planToggle(tree, kDesignModeId);
    const TrackPlan* r = restoreFor(toDesign, "{DES}");
    CHECK(r != nullptr);
    if (r) {
        CHECK(flagValue(*r, Flag::ShowInTcp) == 1);
        CHECK(flagValue(*r, Flag::MainSend) == 0);
        CHECK(flagValue(*r, Flag::FxEnable) == 1);
    }
}

// -- 8. nextModeId cycle (D4 toggle helper) ----------------------------------

static void testNextModeIdCycles() {
    ModeRegistry seeded;  // Arrange(0) + Design(1)

    // Two-mode cycle: Arrange -> Design -> Arrange (wraps past the last).
    CHECK(nextModeId(seeded, kArrangeModeId) == kDesignModeId);
    CHECK(nextModeId(seeded, kDesignModeId) == kArrangeModeId);

    // Extends to cycle-through-all with >2 modes, in ordinal order.
    ModeRegistry three;
    CHECK(three.add(Mode{"mixdown", "Mixdown", 2}));
    CHECK(nextModeId(three, kArrangeModeId) == kDesignModeId);
    CHECK(nextModeId(three, kDesignModeId) == "mixdown");
    CHECK(nextModeId(three, "mixdown") == kArrangeModeId);  // wraps

    // Unknown/stale current id -> first mode (a sane home, not "").
    CHECK(nextModeId(seeded, "does-not-exist") == kArrangeModeId);

    // Empty registry -> "" (nothing to cycle to).
    ModeRegistry empty = ModeRegistry::makeEmpty();
    CHECK(nextModeId(empty, kArrangeModeId).empty());
}

// -- 9. Nested-folder toggle: snapshot lifecycle survives a re-park -----------
//
// Regression for the in-DAW bug: a nested structure (root parent > intermediate
// parent > leaves), ONE leaf tagged Design, toggled twice, hid ALL leaves for good.
//
// Root cause: the D2 shell's park loop stored a fresh snapshot on EVERY park. If a
// track is parked again while already parked — which happens on any redundant
// same-mode re-apply (a re-activate of the current mode, the segmented switch, a
// tag/untag reapply) — the second snapshot captures the track's already-HIDDEN
// flags, so a later restore returns it to hidden and the leaf vanishes permanently.
//
// The pure model can't run the REAPER shell, but the corruption is entirely in the
// snapshot store/clear lifecycle, which is model state. This harness mirrors
// applyMode's park/restore loops faithfully: it maintains a per-track live "visible"
// flag (proxy for B_SHOWINTCP), and for each toggle it runs planToggle, then for
// each park op it (a) snapshots the live flag BEFORE parking — GUARDED to only
// capture when no snapshot exists yet, exactly like the fixed shell — and (b) hides
// the track; for each restore op it writes the snapshot's flag back and clears the
// snapshot. Asserting the live flags after the sequence proves the fix; a parallel
// UNGUARDED run reproduces the original corruption.

namespace {

// A tiny stand-in for the shell's live REAPER flag reads/writes: guid -> visible.
using LiveFlags = std::map<std::string, int>;

// Runs one applyMode-equivalent toggle against `vm` + `live`. `guard` selects the
// fixed (snapshot-once) behavior vs. the original buggy (snapshot-every-park) one.
// Returns nothing; mutates `vm` snapshots/active mode and `live` flags in place,
// exactly mirroring view.cpp's park then restore then setActiveMode ordering.
void simulateApplyMode(ViewModeModel& vm, LiveFlags& live, const FolderTree& tree,
                       const std::string& target, bool guard) {
    TogglePlan plan = vm.planToggle(tree, target);

    // PARK: snapshot-before-hide (guarded or not), then hide.
    for (const auto& tp : plan.park) {
        if (tp.flags.empty()) continue;
        const std::string& guid = tp.flags.front().guid;
        if (!guard || vm.snapshot(guid) == nullptr) {
            TrackSnapshot snap;
            snap.showInTcp = live[guid];   // capture the LIVE visible flag
            vm.storeSnapshot(guid, snap);
        }
        live[guid] = 0;                    // park hides it
    }

    // RESTORE: write snapshot flag back, then drop the snapshot.
    for (const auto& tp : plan.restore) {
        if (tp.flags.empty()) continue;
        const std::string& guid = tp.flags.front().guid;
        if (const TrackSnapshot* snap = vm.snapshot(guid))
            live[guid] = snap->showInTcp;  // restore the captured visible flag
        vm.clearSnapshot(guid);
    }

    vm.setActiveMode(target);
}

// The nested tree Daniel reported: root parent {R} > intermediate parent {I} >
// leaves {L1} (tagged Design) and {L2}, {L3} (untagged => Arrange).
FolderTree nestedTree() {
    FolderTree t;
    t.nodes.push_back(FolderNode{"{R}", "", /*isParent=*/true});
    t.nodes.push_back(FolderNode{"{I}", "{R}", /*isParent=*/true});
    t.nodes.push_back(FolderNode{"{L1}", "{I}", false});
    t.nodes.push_back(FolderNode{"{L2}", "{I}", false});
    t.nodes.push_back(FolderNode{"{L3}", "{I}", false});
    return t;
}

}  // namespace

static void testNestedToggleSnapshotSurvivesRepark() {
    const FolderTree tree = nestedTree();

    // Structural preconditions: buildFolderTree-shaped 3-level tree is classified
    // correctly and the intermediate node is a parent (never parked), and its
    // visibility derives from its descendant leaves.
    {
        ViewModeModel probe;
        probe.membership().tag("{L1}", kDesignModeId);
        // {I} and {R} are parents => never parked, in either mode.
        auto pd = probe.planToggle(tree, kDesignModeId);
        auto pa = probe.planToggle(tree, kArrangeModeId);
        CHECK(!parkTargets(pd, "{I}") && !parkTargets(pd, "{R}"));
        CHECK(!parkTargets(pa, "{I}") && !parkTargets(pa, "{R}"));
        // Design: {L1} (its only Design leaf) is visible => {I} and {R} derive visible.
        auto vd = probe.visibleTracks(tree, kDesignModeId);
        CHECK(visibleHas(vd, "{I}") && visibleHas(vd, "{R}"));
        // Arrange: {L2}/{L3} are Arrange leaves => {I} and {R} still derive visible.
        auto va = probe.visibleTracks(tree, kArrangeModeId);
        CHECK(visibleHas(va, "{I}") && visibleHas(va, "{R}"));
    }

    // GUARDED (fixed shell): drive the reported sequence and assert every leaf is
    // returned to its correct visibility. Include a redundant same-mode re-apply
    // (the real trigger) between toggles to force a park-while-parked.
    {
        ViewModeModel vm;
        vm.membership().tag("{L1}", kDesignModeId);
        LiveFlags live{{"{L1}", 1}, {"{L2}", 1}, {"{L3}", 1}};  // all visible at start

        simulateApplyMode(vm, live, tree, kDesignModeId, /*guard=*/true);
        // In Design: {L2}/{L3} parked (hidden), {L1} visible.
        CHECK(live["{L1}"] == 1 && live["{L2}"] == 0 && live["{L3}"] == 0);

        // Redundant re-apply of the CURRENT mode (segmented switch / re-activate).
        // With the guard this must NOT recapture the now-hidden snapshots.
        simulateApplyMode(vm, live, tree, kDesignModeId, /*guard=*/true);
        CHECK(live["{L1}"] == 1 && live["{L2}"] == 0 && live["{L3}"] == 0);

        simulateApplyMode(vm, live, tree, kArrangeModeId, /*guard=*/true);
        // Back in Arrange: {L2}/{L3} RESTORED to visible; {L1} parked.
        CHECK(live["{L2}"] == 1 && live["{L3}"] == 1 && live["{L1}"] == 0);

        // A second full round to match "toggle twice" exactly.
        simulateApplyMode(vm, live, tree, kDesignModeId, /*guard=*/true);
        CHECK(live["{L1}"] == 1 && live["{L2}"] == 0 && live["{L3}"] == 0);
        simulateApplyMode(vm, live, tree, kArrangeModeId, /*guard=*/true);
        CHECK(live["{L2}"] == 1 && live["{L3}"] == 1 && live["{L1}"] == 0);
    }

    // UNGUARDED (original shell): the same sequence corrupts — the redundant re-apply
    // recaptures {L2}/{L3}'s hidden flags, so toggling back to Arrange restores them
    // to HIDDEN and they never return. This pins the exact regression the guard fixes.
    {
        ViewModeModel vm;
        vm.membership().tag("{L1}", kDesignModeId);
        LiveFlags live{{"{L1}", 1}, {"{L2}", 1}, {"{L3}", 1}};

        simulateApplyMode(vm, live, tree, kDesignModeId, /*guard=*/false);
        simulateApplyMode(vm, live, tree, kDesignModeId, /*guard=*/false);  // re-park corrupts
        simulateApplyMode(vm, live, tree, kArrangeModeId, /*guard=*/false);

        // The bug: Arrange leaves stay hidden after returning to Arrange.
        CHECK(live["{L2}"] == 0 && live["{L3}"] == 0);
    }
}

// ===========================================================================
// D2 two-canvas lane extension tests
// ===========================================================================

// Does the plan emit a lane op for (trackGuid, laneKey)? Returns its lanePlays, or a
// sentinel if absent.
static int lanePlaysFor(const TogglePlan& plan, const std::string& trackGuid,
                        const std::string& laneKey) {
    for (const auto& op : plan.lanes)
        if (op.trackGuid == trackGuid && op.laneKey == laneKey) return op.lanePlays;
    return -999;  // sentinel: no op for this lane
}

static bool laneTouched(const std::set<LaneRef>& s, const std::string& g, const std::string& k) {
    return s.count(LaneRef{g, k}) > 0;
}

// -- D2.1 Lane↔mode mapping + C_LANEPLAYS values -----------------------------
//
// A managed lane owned by the ACTIVE mode plays exclusively (1); every inactive-mode
// managed lane is silenced+hidden (C_LANEPLAYS = 0). Covers the direct laneModeState
// decision and the planToggle op values.

static void testLaneModeStateAndPlayValues() {
    // Direct decision: active mode's lane plays exclusively; others silent.
    CHECK(laneModeState(kArrangeModeId, kArrangeModeId) == kLanePlaysExclusive);
    CHECK(laneModeState(kDesignModeId, kArrangeModeId) == kLaneSilent);
    CHECK(laneModeState(kDesignModeId, kDesignModeId) == kLanePlaysExclusive);
    CHECK(laneModeState(kArrangeModeId, kDesignModeId) == kLaneSilent);
    CHECK(kLanePlaysExclusive == 1 && kLaneSilent == 0);  // SDK C_LANEPLAYS values

    // Via planToggle: a shared track {T} with an Arrange lane and a Design lane.
    ViewModeModel vm;
    CHECK(vm.lanes().setManaged("{T}", "laneA", kArrangeModeId));
    CHECK(vm.lanes().setManaged("{T}", "laneD", kDesignModeId));
    FolderTree tree;
    tree.nodes.push_back(FolderNode{"{T}", "", false});

    // Toggle to Design: the Design lane plays (1); the Arrange lane is silenced (0).
    auto design = vm.planToggle(tree, kDesignModeId);
    CHECK(lanePlaysFor(design, "{T}", "laneD") == kLanePlaysExclusive);
    CHECK(lanePlaysFor(design, "{T}", "laneA") == kLaneSilent);

    // Toggle to Arrange: mirror image.
    auto arrange = vm.planToggle(tree, kArrangeModeId);
    CHECK(lanePlaysFor(arrange, "{T}", "laneA") == kLanePlaysExclusive);
    CHECK(lanePlaysFor(arrange, "{T}", "laneD") == kLaneSilent);

    // A D1-only project (no fixed lanes) emits no lane ops — plan unchanged from before.
    ViewModeModel plain;
    FolderTree t2; t2.nodes.push_back(FolderNode{"{L}", "", false});
    CHECK(plain.planToggle(t2, kDesignModeId).lanes.empty());
}

// -- D2.2 Lane-ownership index: managed vs manual, add/query/remove -----------

static void testLaneOwnershipIndex() {
    LaneOwnershipIndex idx;
    CHECK(idx.empty());

    // A lane ABSENT from the index is manual-by-default (never minted by the tool).
    CHECK(idx.query("{T}", "l0") == nullptr);
    CHECK(!idx.isManaged("{T}", "l0"));

    // Managed lane names its owning mode.
    CHECK(idx.setManaged("{T}", "l0", kDesignModeId));
    const LaneOwnership* o = idx.query("{T}", "l0");
    CHECK(o != nullptr);
    if (o) {
        CHECK(o->isManaged() && !o->isManual());
        CHECK(o->managedMode && *o->managedMode == kDesignModeId);
    }
    CHECK(idx.isManaged("{T}", "l0"));

    // Manual lane carries no mode.
    CHECK(idx.setManual("{T}", "l1"));
    const LaneOwnership* m = idx.query("{T}", "l1");
    CHECK(m != nullptr);
    if (m) CHECK(m->isManual() && !m->isManaged());
    CHECK(!idx.isManaged("{T}", "l1"));

    // (guid, laneKey) is a composite key: same laneKey on a different track is distinct.
    CHECK(idx.setManaged("{U}", "l0", kArrangeModeId));
    CHECK(idx.size() == 3);
    CHECK(idx.isManaged("{U}", "l0"));

    // setManaged replaces a prior manual entry (retag a lane the tool now owns).
    CHECK(idx.setManaged("{T}", "l1", kArrangeModeId));
    CHECK(idx.isManaged("{T}", "l1"));

    // Empty args are rejected without mutation.
    CHECK(!idx.setManaged("", "l0", kDesignModeId));
    CHECK(!idx.setManaged("{T}", "", kDesignModeId));
    CHECK(!idx.setManaged("{T}", "l0", ""));
    CHECK(!idx.setManual("", "l0"));
    CHECK(!idx.setManual("{T}", ""));
    CHECK(idx.size() == 3);

    // remove drops the entry (⇒ manual-by-default again); second remove is a no-op.
    CHECK(idx.remove("{T}", "l0"));
    CHECK(idx.query("{T}", "l0") == nullptr);
    CHECK(!idx.isManaged("{T}", "l0"));
    CHECK(!idx.remove("{T}", "l0"));
}

// -- D2.2b Last-writer-wins ownership replace (round-trip) --------------------
//
// setManual then setManaged on the SAME (guid, laneKey) must leave EXACTLY ONE
// managed entry — the ownership record is replaced, not accumulated. Guards the
// "retag a lane the tool now owns" contract and its persistence: the replace must
// survive a serialize/deserialize round-trip with no stray manual duplicate.
static void testLaneOwnershipLastWriterWins() {
    ViewModeModel vm;

    // Manual first, then managed on the same lane — the managed write replaces.
    CHECK(vm.lanes().setManual("{T}", "l0"));
    CHECK(vm.lanes().setManaged("{T}", "l0", kDesignModeId));
    CHECK(vm.lanes().size() == 1);                       // one entry, not two
    const LaneOwnership* o = vm.lanes().query("{T}", "l0");
    CHECK(o && o->isManaged() && *o->managedMode == kDesignModeId);

    // The reverse also replaces: managed -> manual leaves exactly one manual entry.
    CHECK(vm.lanes().setManual("{T}", "l0"));
    CHECK(vm.lanes().size() == 1);
    const LaneOwnership* m = vm.lanes().query("{T}", "l0");
    CHECK(m && m->isManual());

    // Back to managed, then round-trip: exactly one managed entry survives, no stray
    // manual duplicate resurrected by (de)serialization.
    CHECK(vm.lanes().setManaged("{T}", "l0", kArrangeModeId));
    auto back = ViewModeModel::deserialize(vm.serialize());
    CHECK(back.has_value());
    if (back) {
        CHECK(back->lanes().size() == 1);
        const LaneOwnership* r = back->lanes().query("{T}", "l0");
        CHECK(r && r->isManaged() && *r->managedMode == kArrangeModeId);
    }
}

// -- D2.3/D2.4 Managed-only: planner + query never emit a manual lane --------
//
// Required case: a track with a manual lane + managed mode lanes — neither the planner
// nor the "which lanes may this toggle touch" query ever emit an op for the manual lane.

static void testManagedOnlyPlannerAndQuery() {
    ViewModeModel vm;
    FolderTree tree;
    tree.nodes.push_back(FolderNode{"{T}", "", false});

    // Two managed lanes + one manual comp lane on the same track.
    CHECK(vm.lanes().setManaged("{T}", "arr", kArrangeModeId));
    CHECK(vm.lanes().setManaged("{T}", "des", kDesignModeId));
    CHECK(vm.lanes().setManual("{T}", "comp"));    // user's own comp take

    // Planner: emits ops for the two managed lanes only; the manual lane is untouched.
    auto plan = vm.planToggle(tree, kDesignModeId);
    CHECK(plan.lanes.size() == 2);
    CHECK(lanePlaysFor(plan, "{T}", "des") == kLanePlaysExclusive);
    CHECK(lanePlaysFor(plan, "{T}", "arr") == kLaneSilent);
    CHECK(lanePlaysFor(plan, "{T}", "comp") == -999);   // NEVER emitted for a manual lane

    // Query: managed lanes only; the manual lane is never in the result.
    auto touched = vm.lanesTouchedByToggle();
    CHECK(touched.size() == 2);
    CHECK(laneTouched(touched, "{T}", "arr"));
    CHECK(laneTouched(touched, "{T}", "des"));
    CHECK(!laneTouched(touched, "{T}", "comp"));

    // The query is target-mode-independent: the SET of touchable lanes is every managed
    // lane regardless of which mode we would toggle to (the mode only sets the VALUE).
    auto touchedA = vm.lanesTouchedByToggle();
    CHECK(touchedA == touched);

    // A lane absent from the index entirely is also never touched (manual by default).
    CHECK(!laneTouched(touched, "{T}", "never-indexed"));
}

// -- D2.5 Auto-tag decision --------------------------------------------------
//
// New track/item GUIDs + active mode ⇒ membership writes; a new item on a manual lane
// is EXEMPT (no tag); pre-existing content (not reported new) stays Arrange by default.

static bool hasTag(const std::vector<AutoTag>& tags, const std::string& guid,
                   const std::string& mode) {
    for (const auto& t : tags)
        if (t.guid == guid && t.modeId == mode) return true;
    return false;
}

static void testAutoTagDecision() {
    // A new track + a new item, active mode = Design ⇒ both tagged to Design.
    {
        std::vector<std::string> tracks{"{NT}"};
        std::vector<NewItem> items{ NewItem{"{NI}", /*onManualLane=*/false} };
        auto tags = autoTagNewContent(tracks, items, kDesignModeId);
        CHECK(tags.size() == 2);
        CHECK(hasTag(tags, "{NT}", kDesignModeId));
        CHECK(hasTag(tags, "{NI}", kDesignModeId));
    }

    // A new item on a MANUAL lane is exempt — no tag emitted for it.
    {
        std::vector<NewItem> items{
            NewItem{"{NORMAL}", false},
            NewItem{"{MANUAL}", true},   // landed on a hand-managed lane ⇒ exempt
        };
        auto tags = autoTagNewContent({}, items, kDesignModeId);
        CHECK(tags.size() == 1);
        CHECK(hasTag(tags, "{NORMAL}", kDesignModeId));
        CHECK(!hasTag(tags, "{MANUAL}", kDesignModeId));  // manual-lane exemption
    }

    // Active mode = Arrange ⇒ new content is tagged to Arrange (the active-mode rule,
    // even for the default stance). Empty GUIDs are skipped.
    {
        std::vector<std::string> tracks{"{NT}", ""};
        auto tags = autoTagNewContent(tracks, {}, kArrangeModeId);
        CHECK(tags.size() == 1);
        CHECK(hasTag(tags, "{NT}", kArrangeModeId));
    }

    // Empty active mode ⇒ no tags at all (nothing to tag into).
    {
        auto tags = autoTagNewContent({"{NT}"}, {NewItem{"{NI}", false}}, "");
        CHECK(tags.empty());
    }

    // Pre-existing content resolves to Arrange: a GUID the shell does NOT report as new
    // is never passed here, so it never gets tagged and stays untagged ⇒ Arrange by the
    // membership default. Prove the default directly on a fresh model.
    {
        ViewModeModel vm;
        CHECK(vm.membership().query("{PREEXISTING}") == nullptr);   // absent from index
        CHECK(vm.leafBelongsToMode("{PREEXISTING}", kArrangeModeId));  // ⇒ Arrange
        CHECK(!vm.leafBelongsToMode("{PREEXISTING}", kDesignModeId));
    }

    // ADOPTION / STRAND GUARD: a new item dropped onto a track whose pre-existing content
    // resolves to a SINGLE mode adopts THAT mode, NOT the (different) active mode — so the
    // track never becomes multi-mode and no silencing lane split is triggered. This is the
    // exact drop-onto-tagged-track repro at the auto-tag boundary: active mode = Design,
    // the track already carries Arrange content ⇒ the drop is tagged Arrange (adopted),
    // keeping the pre-existing Arrange items on the visible/playing surface.
    {
        NewItem dropped{"{DROP}", /*onManualLane=*/false, /*trackModes=*/{kArrangeModeId}};
        auto tags = autoTagNewContent({}, {dropped}, kDesignModeId);
        CHECK(tags.size() == 1);
        CHECK(hasTag(tags, "{DROP}", kArrangeModeId));   // adopted, NOT Design
        CHECK(!hasTag(tags, "{DROP}", kDesignModeId));
    }

    // Adoption is symmetric: pre-existing Design content + active Arrange ⇒ adopt Design.
    {
        NewItem dropped{"{DROP}", false, {kDesignModeId}};
        auto tags = autoTagNewContent({}, {dropped}, kArrangeModeId);
        CHECK(tags.size() == 1);
        CHECK(hasTag(tags, "{DROP}", kDesignModeId));
    }

    // No pre-existing content (empty trackModes — a brand-new/empty track) ⇒ the item
    // takes the ACTIVE mode (unchanged behaviour; adoption only fires with prior content).
    {
        NewItem dropped{"{DROP}", false, /*trackModes=*/{}};
        auto tags = autoTagNewContent({}, {dropped}, kDesignModeId);
        CHECK(tags.size() == 1);
        CHECK(hasTag(tags, "{DROP}", kDesignModeId));
    }

    // Pre-existing content ALREADY spans >1 mode (a deliberate split) ⇒ the new item
    // takes the ACTIVE mode and joins the active lane; adoption does not fire (no single
    // mode to adopt), and the existing split — with both lanes present — cannot strand.
    {
        NewItem dropped{"{DROP}", false, {kArrangeModeId, kDesignModeId}};
        auto tags = autoTagNewContent({}, {dropped}, kDesignModeId);
        CHECK(tags.size() == 1);
        CHECK(hasTag(tags, "{DROP}", kDesignModeId));    // active mode, not adopted
    }

    // Adoption composes with the manual-lane exemption: a manual-lane item is still exempt
    // regardless of its track's pre-existing modes (no tag emitted at all).
    {
        NewItem manual{"{MANUAL}", /*onManualLane=*/true, {kArrangeModeId}};
        auto tags = autoTagNewContent({}, {manual}, kDesignModeId);
        CHECK(tags.empty());
    }

    // A new TRACK still takes the active mode — adoption is an ITEM rule only (a track has
    // no "pre-existing content on the same track" notion).
    {
        auto tags = autoTagNewContent({"{NT}"}, {}, kDesignModeId);
        CHECK(tags.size() == 1);
        CHECK(hasTag(tags, "{NT}", kDesignModeId));
    }
}

// -- Drop-onto-tagged-track STRAND repro (end-to-end at the pure-model level) --
//
// The reported bug: a Design-tagged track carries pre-existing (untagged ⇒ Arrange)
// items; while Design is the active mode the user drops a capture onto the track. The
// old auto-tag rule tagged the drop into the ACTIVE mode (Design) even though the
// track's own content was Arrange; the track went multi-mode; planLaneMinting split it;
// planToggle drove the Arrange lane C_LANEPLAYS=0 — stranding the pre-existing,
// previously-visible items on a silenced lane with no user intent.
//
// This test drives the WHOLE decision chain the shell runs on a drop tick — detect the
// new item, resolve its track's pre-existing modes, autoTagNewContent, apply the tag,
// then planLaneMinting + planToggle — and asserts the invariant directly: NO lane
// holding a pre-existing item ends silenced under the active mode.
static void testDropOntoTaggedTrackDoesNotStrand() {
    const std::string track = "{T}";
    const std::string preA = "{arr-pre-1}";   // pre-existing untagged ⇒ Arrange
    const std::string preB = "{arr-pre-2}";   // pre-existing untagged ⇒ Arrange
    const std::string drop = "{drop}";        // the capture just dropped onto the track

    ViewModeModel vm;
    vm.membership().tag(track, kDesignModeId); // the TRACK is tagged Design (leaf tag)
    vm.setActiveMode(kDesignModeId);           // user is viewing Design when they drop
    // Pre-existing items are UNTAGGED (they resolve to Arrange) — never auto-tagged (they
    // predate the baseline). Leave them absent from the membership index.

    // Shell resolves the drop's track pre-existing modes: both siblings are untagged ⇒
    // {arrange}. Exactly one mode ⇒ the adoption guard fires.
    NewItem dropped{drop, /*onManualLane=*/false, /*trackModes=*/{kArrangeModeId}};
    const std::vector<AutoTag> tags =
        autoTagNewContent({}, {dropped}, vm.activeModeId());
    for (const AutoTag& t : tags) vm.membership().tag(t.guid, t.modeId);

    // FIX ASSERTION 1: the drop adopted Arrange, so the track's OWN items are all one mode.
    CHECK(vm.membership().modesOf(drop) == std::set<std::string>{kArrangeModeId});

    // Run the lane-minting decision exactly as the shell does after the tag.
    FolderTree tree;
    tree.nodes.push_back(FolderNode{track, "", false});
    std::vector<LaneTrack> tracks{
        LaneTrack{track, {
            LaneItem{preA, kArrangeModeId, false},
            LaneItem{preB, kArrangeModeId, false},
            LaneItem{drop, kArrangeModeId, false},  // adopted ⇒ Arrange
        }},
    };
    const LaneMintPlan plan = planLaneMinting(vm, tree, tracks);

    // FIX ASSERTION 2: single-mode track ⇒ NO split at all. Nothing is minted, nothing is
    // reassigned, so the pre-existing items stay exactly where they were and visible.
    CHECK(plan.empty());

    // FIX ASSERTION 3 (the invariant, stated positively): apply whatever lanes the plan
    // WOULD mint into the ownership index, then toggle to the active mode and assert NO
    // lane carrying a pre-existing item is silenced. With no split the ownership index is
    // empty and the toggle emits no silencing op — the pre-existing items cannot be
    // stranded. (Belt-and-braces: the same assertion would catch a regression that
    // re-introduced the split.)
    for (const LaneMint& m : plan.mints)
        vm.lanes().setManaged(m.trackGuid, m.laneKey, m.modeId);
    const TogglePlan toggle = vm.planToggle(tree, vm.activeModeId());
    // The Arrange lane (if it existed) would be laneNameForMode(kArrangeModeId). Under a
    // correct fix it never exists; assert it is not driven to silent either way.
    CHECK(lanePlaysFor(toggle, track, laneNameForMode(kArrangeModeId)) != kLaneSilent);

    // CONTRAST — the OLD (buggy) behaviour, reproduced by forcing the active-mode tag: if
    // the drop had been tagged Design (active) instead of adopting Arrange, the track WOULD
    // split and the Arrange lane WOULD be silenced under Design. This proves the test can
    // disprove the bug — it is not tautological.
    ViewModeModel buggy;
    buggy.membership().tag(track, kDesignModeId);
    buggy.setActiveMode(kDesignModeId);
    buggy.membership().tag(drop, kDesignModeId);  // the old active-mode tag
    std::vector<LaneTrack> buggyTracks{
        LaneTrack{track, {
            LaneItem{preA, kArrangeModeId, false},
            LaneItem{preB, kArrangeModeId, false},
            LaneItem{drop, kDesignModeId, false},   // Design (active) ⇒ 2nd mode
        }},
    };
    const LaneMintPlan buggyPlan = planLaneMinting(buggy, tree, buggyTracks);
    CHECK(!buggyPlan.empty());                       // the old path DID split
    for (const LaneMint& m : buggyPlan.mints)
        buggy.lanes().setManaged(m.trackGuid, m.laneKey, m.modeId);
    const TogglePlan buggyToggle = buggy.planToggle(tree, kDesignModeId);
    CHECK(lanePlaysFor(buggyToggle, track, laneNameForMode(kArrangeModeId)) == kLaneSilent);
}

// -- D2 W3-B item-level mode-move decision -----------------------------------
//
// planItemRetag: the pure decision behind the three item actions. A non-empty target
// tags each eligible selected item into it; an EMPTY target untags (→ Arrange default).
// Manual-lane items are EXEMPT (no op) and empty-GUID items are skipped.

// Find the single op for `guid`, or nullptr.
static const ItemRetagOp* retagOpFor(const std::vector<ItemRetagOp>& ops,
                                      const std::string& guid) {
    for (const auto& o : ops)
        if (o.guid == guid) return &o;
    return nullptr;
}

static void testPlanItemRetag() {
    // Move -> Design: a managed-lane / normal item is tagged into Design (untag=false).
    {
        std::vector<RetagItem> sel{
            RetagItem{"{A}", /*onManualLane=*/false},
            RetagItem{"{B}", false},
        };
        auto ops = planItemRetag(sel, kDesignModeId);
        CHECK(ops.size() == 2);
        const ItemRetagOp* a = retagOpFor(ops, "{A}");
        CHECK(a != nullptr);
        CHECK(a && !a->untag);                 // a tag, not an untag
        CHECK(a && a->modeId == kDesignModeId); // into Design specifically
        const ItemRetagOp* b = retagOpFor(ops, "{B}");
        CHECK(b && !b->untag && b->modeId == kDesignModeId);
    }

    // Empty target ⇒ UNTAG each item (Move -> Arrange / Untag items collapse to this).
    // untag must be true and modeId empty — NOT a tag into "arrange".
    {
        std::vector<RetagItem> sel{ RetagItem{"{A}", false} };
        auto ops = planItemRetag(sel, std::string{});
        CHECK(ops.size() == 1);
        const ItemRetagOp* a = retagOpFor(ops, "{A}");
        CHECK(a != nullptr);
        CHECK(a && a->untag);                  // an untag
        CHECK(a && a->modeId.empty());         // no target mode carried on an untag
    }

    // Manual-lane exemption: a manual-lane item yields NO op — not for Move nor for Untag.
    {
        std::vector<RetagItem> sel{
            RetagItem{"{NORMAL}", false},
            RetagItem{"{MANUAL}", true},   // on a hand-managed lane ⇒ EXEMPT
        };
        auto design = planItemRetag(sel, kDesignModeId);
        CHECK(design.size() == 1);
        CHECK(retagOpFor(design, "{NORMAL}") != nullptr);
        CHECK(retagOpFor(design, "{MANUAL}") == nullptr);  // exempt — never retagged

        auto untag = planItemRetag(sel, std::string{});
        CHECK(untag.size() == 1);
        CHECK(retagOpFor(untag, "{NORMAL}") != nullptr);
        CHECK(retagOpFor(untag, "{MANUAL}") == nullptr);   // exempt — never untagged
    }

    // Empty-GUID items are skipped defensively; empty selection ⇒ no ops.
    {
        std::vector<RetagItem> sel{ RetagItem{"", false}, RetagItem{"{A}", false} };
        auto ops = planItemRetag(sel, kDesignModeId);
        CHECK(ops.size() == 1);
        CHECK(retagOpFor(ops, "{A}") != nullptr);

        CHECK(planItemRetag({}, kDesignModeId).empty());
        CHECK(planItemRetag({}, std::string{}).empty());
    }
}

// -- D2 W3-B reconcile unregistered-mode guard (pure decision) ---------------
//
// reconcileManagedLanes (shell) recovers a lane's managed ownership from its durable
// name, but must NOT record ownership for a mode the registry no longer knows — a lane
// keyed to an unregistered mode can never become the active mode's lane and would stay
// silenced+hidden forever, orphaning its items. The guard's pure decision is exactly
// modeIdFromLaneName(name) ∈ modes(): this locks that composition so the shell's guard
// (which calls model.modes().contains(*mode)) cannot silently drift.

static void testReconcileUnregisteredModeGuardDecision() {
    ViewModeModel vm;  // seeds Arrange + Design only

    // A managed lane naming a REGISTERED mode: mode decodes and IS contained ⇒ record.
    {
        const std::string name = laneNameForMode(kDesignModeId);
        auto mode = modeIdFromLaneName(name);
        CHECK(mode.has_value());
        CHECK(vm.modes().contains(*mode));   // guard passes ⇒ shell records ownership
    }

    // A managed lane naming an UNREGISTERED mode: mode decodes but is NOT contained ⇒
    // the guard rejects it and the shell leaves the lane off the index (manual-by-default).
    {
        const std::string name = laneNameForMode("removed_mode");
        auto mode = modeIdFromLaneName(name);
        CHECK(mode.has_value());
        CHECK(*mode == "removed_mode");
        CHECK(!vm.modes().contains(*mode));  // guard fails ⇒ shell must skip
    }
}

// -- D2.7 Lane minting decision (Wave 3) -------------------------------------
//
// planLaneMinting: a track with content of only ONE mode is NOT split (D1 unchanged);
// a track that holds >1 mode's content mints one managed lane per mode and assigns EVERY
// managed-eligible item (incl. pre-existing) to its mode's lane; manual-lane items are
// exempt (never counted, never reassigned, their lane never minted-over).

static bool hasMint(const LaneMintPlan& p, const std::string& track,
                    const std::string& mode) {
    for (const auto& m : p.mints)
        if (m.trackGuid == track && m.modeId == mode &&
            m.laneKey == laneNameForMode(mode))
            return true;
    return false;
}

static bool hasAssign(const LaneMintPlan& p, const std::string& item,
                      const std::string& track, const std::string& mode) {
    for (const auto& a : p.assigns)
        if (a.itemGuid == item && a.trackGuid == track &&
            a.laneKey == laneNameForMode(mode))
            return true;
    return false;
}

static int splitLaneCount(const LaneMintPlan& p, const std::string& track) {
    for (const auto& s : p.splits)
        if (s.trackGuid == track) return s.laneCount;
    return -1;  // no split for this track
}

// A plain LEAF track (not a folder) carrying its own items, with no tree derivation:
// an empty model + empty tree means visibleTracks contributes nothing, so the ONLY
// trigger is the track's own-item mode span — exactly the W3-A behavior. These helpers
// keep the W3-A leaf tests reading against a neutral model/tree.
static const ViewModeModel& bareModel() { static ViewModeModel m; return m; }
static const FolderTree&    emptyTree() { static FolderTree t; return t; }

static void testLaneMintingSingleModeNoSplit() {
    // A track whose items all belong to ONE mode is NOT lane-split — D1 whole-track
    // parking still separates the stances. No split, no mint, no assignment.
    std::vector<LaneTrack> tracks{
        LaneTrack{"{T}", {
            LaneItem{"{i1}", kArrangeModeId, false},
            LaneItem{"{i2}", kArrangeModeId, false},
        }},
    };
    const LaneMintPlan plan = planLaneMinting(bareModel(), emptyTree(), tracks);
    CHECK(plan.empty());
    CHECK(splitLaneCount(plan, "{T}") == -1);

    // An empty track (no items) is likewise never split.
    CHECK(planLaneMinting(bareModel(), emptyTree(), {LaneTrack{"{E}", {}}}).empty());
}

static void testLaneMintingMultiModeMintsAndAssignsAll() {
    // A track that gained a second mode's item: it now holds Arrange + Design content.
    // Both modes get a managed lane; ALL managed-eligible items are assigned — including
    // the pre-existing Arrange item (retroactive lane assignment), not only the new one.
    std::vector<LaneTrack> tracks{
        LaneTrack{"{T}", {
            LaneItem{"{arr1}", kArrangeModeId, false},  // pre-existing single-mode item
            LaneItem{"{arr2}", kArrangeModeId, false},  // pre-existing single-mode item
            LaneItem{"{des1}", kDesignModeId, false},   // the newly-added 2nd-mode item
        }},
    };
    const LaneMintPlan plan = planLaneMinting(bareModel(), emptyTree(), tracks);
    CHECK(!plan.empty());

    // One split with two managed lanes (one per involved mode).
    CHECK(splitLaneCount(plan, "{T}") == 2);
    CHECK(plan.mints.size() == 2);
    CHECK(hasMint(plan, "{T}", kArrangeModeId));
    CHECK(hasMint(plan, "{T}", kDesignModeId));

    // EVERY managed-eligible item assigned to its mode's lane — pre-existing included.
    CHECK(plan.assigns.size() == 3);
    CHECK(hasAssign(plan, "{arr1}", "{T}", kArrangeModeId));  // retroactive
    CHECK(hasAssign(plan, "{arr2}", "{T}", kArrangeModeId));  // retroactive
    CHECK(hasAssign(plan, "{des1}", "{T}", kDesignModeId));   // the new item
}

static void testLaneMintingManualLaneExempt() {
    // A track with Arrange + Design managed-eligible content AND an item the user placed
    // on a manual lane: the manual item is EXEMPT — it is not counted, not assigned, and
    // its lane is never minted-over. The managed split proceeds around it.
    std::vector<LaneTrack> tracks{
        LaneTrack{"{T}", {
            LaneItem{"{arr}", kArrangeModeId, false},
            LaneItem{"{des}", kDesignModeId, false},
            LaneItem{"{comp}", kDesignModeId, /*onManualLane=*/true},  // user's comp take
        }},
    };
    const LaneMintPlan plan = planLaneMinting(bareModel(), emptyTree(), tracks);

    // Split for the two managed modes; the manual item never appears in assigns.
    CHECK(splitLaneCount(plan, "{T}") == 2);
    CHECK(plan.assigns.size() == 2);
    CHECK(hasAssign(plan, "{arr}", "{T}", kArrangeModeId));
    CHECK(hasAssign(plan, "{des}", "{T}", kDesignModeId));
    for (const auto& a : plan.assigns)
        CHECK(a.itemGuid != "{comp}");  // manual-lane item NEVER reassigned

    // Manual-lane exemption can also SUPPRESS a split: if the ONLY second mode is
    // supplied by a manual-lane item, the managed-eligible items are single-mode ⇒ NO
    // split (the user's manual lane is not a mode the tool separates).
    std::vector<LaneTrack> t2{
        LaneTrack{"{U}", {
            LaneItem{"{a}", kArrangeModeId, false},
            LaneItem{"{d}", kDesignModeId, /*onManualLane=*/true},  // only 2nd mode, exempt
        }},
    };
    // managed-eligible content is single-mode ⇒ no split (leaf, no tree derivation).
    CHECK(planLaneMinting(bareModel(), emptyTree(), t2).empty());
}

static void testLaneMintingThreeModesAndOwnershipKeys() {
    // N-mode proof + the ownership writes the shell will apply: three modes on one track
    // mint three managed lanes, each keyed by its durable name (== laneNameForMode), each
    // owning the right mode. Applying the mints to a real ownership index reproduces the
    // managed classification the toggle planner then gates on.
    ViewModeModel vm;
    CHECK(vm.modes().add(Mode{"mixdown", "Mixdown", 2}));

    std::vector<LaneTrack> tracks{
        LaneTrack{"{T}", {
            LaneItem{"{a}", kArrangeModeId, false},
            LaneItem{"{d}", kDesignModeId, false},
            LaneItem{"{m}", "mixdown", false},
        }},
    };
    // Own items span three modes (leaf; empty tree ⇒ own-item-span is the sole trigger).
    const LaneMintPlan plan = planLaneMinting(vm, FolderTree{}, tracks);
    CHECK(splitLaneCount(plan, "{T}") == 3);
    CHECK(plan.mints.size() == 3);

    // Apply the mints exactly as the shell does — record managed ownership — then assert
    // the ownership index classifies each lane managed-for-its-mode and the toggle
    // planner would drive exactly these three lanes (managed-only invariant intact).
    for (const auto& m : plan.mints)
        CHECK(vm.lanes().setManaged(m.trackGuid, m.laneKey, m.modeId));
    CHECK(vm.lanes().size() == 3);
    CHECK(vm.lanes().isManaged("{T}", laneNameForMode(kArrangeModeId)));
    CHECK(vm.lanes().isManaged("{T}", laneNameForMode(kDesignModeId)));
    CHECK(vm.lanes().isManaged("{T}", laneNameForMode("mixdown")));
    CHECK(vm.lanesTouchedByToggle().size() == 3);

    // Persist round-trip of the just-minted lane-split project: the ownership index (and
    // the whole model) survives serialize/deserialize unchanged, so a saved lane-split
    // project restores its managed classification without re-minting.
    auto back = ViewModeModel::deserialize(vm.serialize());
    CHECK(back.has_value());
    CHECK(back && *back == vm);
    if (back) CHECK(back->lanes().size() == 3);
}

// -- Fix: content-bearing folder derived-visible in >1 mode splits its own media ----
//
// The exact failing case. A folder {F} has descendant leaves in BOTH modes ({LD} Design,
// {LA} Arrange) and carries ONE OWN item ({own}) tagged Design. W3-A's own-item-span test
// alone would NOT split {F} (its own content is single-mode Design), so the item leaked
// into every mode the folder was derived-visible in. The visibility-aware decision splits
// {F} and lanes {own} onto the Design lane — so it hides+silences whenever Arrange is
// active. LAZY-MINT: {F} mints ONLY the Design lane (holding the item), NOT an empty
// reserved Arrange lane — confinement holds via C_LANEPLAYS=0 on the lone Design lane when
// Arrange is active. This is the load-bearing fix; assert it hard.
static void testLaneMintingFolderDerivedVisibleSplitsOwnMedia() {
    ViewModeModel vm;
    vm.membership().tag("{LD}", kDesignModeId);   // a Design leaf under the folder
    // {LA} left untagged ⇒ Arrange member; both stances thus live under {F}.
    vm.membership().tag("{own}", kDesignModeId);  // the folder's OWN dropped item (Design)

    FolderTree tree;
    tree.nodes.push_back(FolderNode{"{F}", "", /*isParent=*/true});
    tree.nodes.push_back(FolderNode{"{LD}", "{F}", false});
    tree.nodes.push_back(FolderNode{"{LA}", "{F}", false});

    // Sanity: the folder really is derived-visible in BOTH modes (the precondition the
    // W3-A trigger ignored). If this ever stops holding, the fix's premise is gone.
    CHECK(vm.visibleTracks(tree, kArrangeModeId).count("{F}") == 1);
    CHECK(vm.visibleTracks(tree, kDesignModeId).count("{F}") == 1);

    // The folder track {F} carries its own single Design item; its child leaves are the
    // separate leaf tracks (not reported as items on {F}).
    std::vector<LaneTrack> tracks{
        LaneTrack{"{F}", {LaneItem{"{own}", kDesignModeId, false}}},
    };

    const LaneMintPlan plan = planLaneMinting(vm, tree, tracks);

    // {F} MUST split even though its own item is single-mode: it is visible in 2 modes.
    CHECK(!plan.empty());

    // LAZY-MINT: ONE lane only — the Design lane that holds the item. No empty reserved
    // Arrange lane is minted, even though {F} is derived-visible in Arrange. The Arrange
    // lane appears on demand when an Arrange item first lands on {F}.
    CHECK(splitLaneCount(plan, "{F}") == 1);           // Design lane only — no reserved lane
    CHECK(plan.mints.size() == 1);
    CHECK(hasMint(plan, "{F}", kDesignModeId));        // Design lane (holds the item)
    CHECK(!hasMint(plan, "{F}", kArrangeModeId));      // NO empty reserved Arrange lane

    // The own item is confined to its tagged (Design) lane — the exact hide-in-Arrange fix.
    // With only the Design lane present, toggling to Arrange sets its C_LANEPLAYS to 0, so
    // the item hides+silences and the track reads as an empty normal track (no leak).
    CHECK(plan.assigns.size() == 1);
    CHECK(hasAssign(plan, "{own}", "{F}", kDesignModeId));
    for (const auto& a : plan.assigns)
        CHECK(!(a.itemGuid == "{own}" && a.laneKey == laneNameForMode(kArrangeModeId)));
}

// LAZY-MINT confinement proof. Same single-own-mode / dual-visibility folder, but instead
// of asserting the mint COUNT we prove the FUNCTIONAL confinement the lazy split preserves:
// apply the minted lane's ownership to a live model, then drive the toggle planner and show
// the lone Design lane SILENCES when Arrange is active (C_LANEPLAYS = 0). That is the whole
// point — a single managed lane still hides its item in every other mode, so removing the
// empty reserved Arrange lane costs nothing functionally. Fails if the split ever leaves the
// Design item audible in Arrange (the leak the D2 fix closed) or mints a spurious lane.
static void testLaneMintingLazySingleLaneStillConfines() {
    ViewModeModel vm;
    vm.membership().tag("{LD}", kDesignModeId);   // Design leaf ⇒ folder visible in Design
    // {LA} untagged ⇒ Arrange member ⇒ folder ALSO derived-visible in Arrange.
    vm.membership().tag("{own}", kDesignModeId);  // the folder's one own item (Design)

    FolderTree tree;
    tree.nodes.push_back(FolderNode{"{F}", "", /*isParent=*/true});
    tree.nodes.push_back(FolderNode{"{LD}", "{F}", false});
    tree.nodes.push_back(FolderNode{"{LA}", "{F}", false});

    std::vector<LaneTrack> tracks{
        LaneTrack{"{F}", {LaneItem{"{own}", kDesignModeId, false}}},
    };
    const LaneMintPlan plan = planLaneMinting(vm, tree, tracks);

    // Exactly one lane minted (the Design lane) — no empty reserved Arrange lane.
    CHECK(plan.mints.size() == 1);
    CHECK(hasMint(plan, "{F}", kDesignModeId));

    // Apply the mint's ownership exactly as the shell does, then drive the toggle planner.
    for (const auto& m : plan.mints)
        CHECK(vm.lanes().setManaged(m.trackGuid, m.laneKey, m.modeId));
    CHECK(vm.lanes().size() == 1);  // one managed lane on {F}, not two

    const std::string designLane = laneNameForMode(kDesignModeId);

    // Active = Design: the lone Design lane PLAYS (item visible+audible in its own mode).
    const auto design = vm.planToggle(tree, kDesignModeId);
    CHECK(lanePlaysFor(design, "{F}", designLane) == kLanePlaysExclusive);

    // Active = Arrange: the lone Design lane SILENCES — with no lane playing, the track
    // reads as an empty normal track and the Design item does NOT leak. This is the
    // confinement guarantee that lets us drop the reserved Arrange lane.
    const auto arrange = vm.planToggle(tree, kArrangeModeId);
    CHECK(lanePlaysFor(arrange, "{F}", designLane) == kLaneSilent);
}

// A folder carrying its OWN items that already span both modes → still split (the two
// triggers OR: own-item span AND derived visibility both point the same way here). Both
// own items separate to their tagged lanes.
static void testLaneMintingFolderOwnItemsSpanBothModes() {
    ViewModeModel vm;
    vm.membership().tag("{LD}", kDesignModeId);
    vm.membership().tag("{d}", kDesignModeId);
    // {a} untagged ⇒ Arrange.

    FolderTree tree;
    tree.nodes.push_back(FolderNode{"{F}", "", true});
    tree.nodes.push_back(FolderNode{"{LD}", "{F}", false});
    tree.nodes.push_back(FolderNode{"{LA}", "{F}", false});  // untagged ⇒ Arrange

    std::vector<LaneTrack> tracks{
        LaneTrack{"{F}", {
            LaneItem{"{a}", kArrangeModeId, false},
            LaneItem{"{d}", kDesignModeId, false},
        }},
    };
    const LaneMintPlan plan = planLaneMinting(vm, tree, tracks);
    CHECK(splitLaneCount(plan, "{F}") == 2);
    CHECK(plan.assigns.size() == 2);
    CHECK(hasAssign(plan, "{a}", "{F}", kArrangeModeId));
    CHECK(hasAssign(plan, "{d}", "{F}", kDesignModeId));
}

// -- Fix (pd2): item inserted onto an ALREADY-split folder still yields a non-empty plan --
//
// Regression guard for the "inserted item invisible until a manual toggle" bug. When a new
// item lands (via insert/capture) on a folder that is ALREADY lane-split and derived-visible
// in >1 mode, and REAPER placed it on the active mode's currently-playing lane, the shell's
// assignItemToLane sees I_FIXEDLANE unchanged and writes nothing — applyMintPlan reports
// changed==false. The shell must STILL treat this tick as "content landed on a managed track"
// and refresh the arrange (so the item draws immediately, no toggle). The pure signal the
// shell keys on is: planLaneMinting returns a NON-EMPTY plan carrying an assign for the new
// item. This test locks that signal; if planLaneMinting ever went empty here, the shell would
// have nothing to refresh on and the bug would return.
static void testLaneMintingNewItemOnAlreadySplitFolderYieldsPlan() {
    ViewModeModel vm;
    vm.membership().tag("{LD}", kDesignModeId);   // Design leaf ⇒ folder derived-visible Design
    // {LA} untagged ⇒ Arrange ⇒ folder ALSO derived-visible in Arrange (dual-visible).
    vm.membership().tag("{own}", kDesignModeId);  // the pre-existing own Design item
    vm.membership().tag("{new}", kDesignModeId);  // the JUST-INSERTED item (auto-tagged Design)

    FolderTree tree;
    tree.nodes.push_back(FolderNode{"{F}", "", /*isParent=*/true});
    tree.nodes.push_back(FolderNode{"{LD}", "{F}", false});
    tree.nodes.push_back(FolderNode{"{LA}", "{F}", false});

    // The folder is already split for Design (its lane exists + is owned). This mirrors the
    // live "already auto-split" track the bug reproduces on.
    CHECK(vm.lanes().setManaged("{F}", laneNameForMode(kDesignModeId), kDesignModeId));

    // {F} now carries its original own item PLUS the freshly-inserted one, both Design.
    std::vector<LaneTrack> tracks{
        LaneTrack{"{F}", {
            LaneItem{"{own}", kDesignModeId, false},
            LaneItem{"{new}", kDesignModeId, false},
        }},
    };
    const LaneMintPlan plan = planLaneMinting(vm, tree, tracks);

    // The plan is NON-EMPTY (folder is dual-visible ⇒ splits) and carries an assign for the
    // new item onto the Design lane. In the shell this is the exact branch that must force a
    // redraw even when the assign is an idempotent no-op (item already on the playing lane).
    CHECK(!plan.empty());
    CHECK(hasAssign(plan, "{new}", "{F}", kDesignModeId));
    CHECK(hasAssign(plan, "{own}", "{F}", kDesignModeId));
}

// SHOW-BOTH escape hatch: a show-both track carrying its own items is visible in every
// mode ON PURPOSE and must NOT be force-split — its content stays cross-mode-visible.
// Even with own items that would otherwise span modes, the decision skips it entirely.
static void testLaneMintingShowBothNotForceSplit() {
    ViewModeModel vm;
    vm.membership().setShowBoth("{SB}", true);

    // A show-both track whose OWN items even span two modes — the W3-A own-span trigger
    // would fire, but show-both must override it (its items are meant to play everywhere).
    std::vector<LaneTrack> tracks{
        LaneTrack{"{SB}", {
            LaneItem{"{a}", kArrangeModeId, false},
            LaneItem{"{d}", kDesignModeId, false},
        }},
    };
    const LaneMintPlan plan = planLaneMinting(vm, FolderTree{}, tracks);
    CHECK(plan.empty());                     // NOT split — the escape hatch holds
    CHECK(splitLaneCount(plan, "{SB}") == -1);

    // And a show-both FOLDER derived-visible in both modes carrying an own item: still not
    // split. Visibility is the deliberate point of show-both.
    ViewModeModel vm2;
    vm2.membership().setShowBoth("{F}", true);
    vm2.membership().tag("{LD}", kDesignModeId);
    vm2.membership().tag("{own}", kDesignModeId);
    FolderTree tree;
    tree.nodes.push_back(FolderNode{"{F}", "", true});
    tree.nodes.push_back(FolderNode{"{LD}", "{F}", false});
    tree.nodes.push_back(FolderNode{"{LA}", "{F}", false});
    std::vector<LaneTrack> t2{LaneTrack{"{F}", {LaneItem{"{own}", kDesignModeId, false}}}};
    CHECK(planLaneMinting(vm2, tree, t2).empty());
}

// A single-mode LEAF visible in exactly one mode is still never split — the D1 whole-track
// parking case. A leaf under a folder, tagged Design, whose sibling is also Design: the
// leaf is visible in one mode only, carries its own Design item, and must NOT lane-split.
static void testLaneMintingSingleModeLeafVisibleOnceNoSplit() {
    ViewModeModel vm;
    vm.membership().tag("{L}", kDesignModeId);
    vm.membership().tag("{own}", kDesignModeId);

    FolderTree tree;
    tree.nodes.push_back(FolderNode{"{F}", "", true});
    tree.nodes.push_back(FolderNode{"{L}", "{F}", false});  // the leaf under test

    // The leaf {L} is visible only in Design (its one tagged mode).
    CHECK(vm.visibleTracks(tree, kDesignModeId).count("{L}") == 1);
    CHECK(vm.visibleTracks(tree, kArrangeModeId).count("{L}") == 0);

    std::vector<LaneTrack> tracks{
        LaneTrack{"{L}", {LaneItem{"{own}", kDesignModeId, false}}},
    };
    const LaneMintPlan plan = planLaneMinting(vm, tree, tracks);
    CHECK(plan.empty());  // single-mode, visible once ⇒ D1 whole-track parking, no split
}

// A content-EMPTY folder derived-visible in many modes carries NO own media, so there is
// nothing to lane-separate: it stays visibility-only (D1 parent handling), never split.
static void testLaneMintingEmptyFolderNotSplit() {
    ViewModeModel vm;
    vm.membership().tag("{LD}", kDesignModeId);
    // {LA} untagged ⇒ Arrange; folder derived-visible in both modes but holds no own item.

    FolderTree tree;
    tree.nodes.push_back(FolderNode{"{F}", "", true});
    tree.nodes.push_back(FolderNode{"{LD}", "{F}", false});
    tree.nodes.push_back(FolderNode{"{LA}", "{F}", false});

    CHECK(vm.visibleTracks(tree, kArrangeModeId).count("{F}") == 1);
    CHECK(vm.visibleTracks(tree, kDesignModeId).count("{F}") == 1);

    std::vector<LaneTrack> tracks{LaneTrack{"{F}", {}}};  // no own media
    CHECK(planLaneMinting(vm, tree, tracks).empty());
}

// -- D2.6 JSON round-trip with lane index + membership -----------------------

// An EXPLICIT Arrange record is new: the shipped "tag selected tracks -> Arrange"
// action untags instead, so until now Arrange was only ever represented by absence.
// The render-in-place verb writes one, because the record — not the behaviour — is
// what the panel's auto-tag detector defers to. It must be indistinguishable from
// absence everywhere else.
static void testExplicitArrangeRecordRoundTripsAndBehavesLikeAbsence() {
    ViewModeModel vm;
    vm.membership().tag("{TAGGED-ARRANGE}", kArrangeModeId);
    // "{UNTAGGED}" is deliberately never tagged — the comparison partner.

    const std::string json = vm.serialize();
    const auto back = ViewModeModel::deserialize(json);
    CHECK(back.has_value());
    CHECK(back && *back == vm);
    if (back) CHECK(back->serialize() == json);

    // The record survives as a record, not collapsed away on the round-trip.
    if (back) {
        const Membership* m = back->membership().query("{TAGGED-ARRANGE}");
        CHECK(m != nullptr);
        CHECK(m && m->modeIds == std::set<std::string>{kArrangeModeId});
        CHECK(back->membership().query("{UNTAGGED}") == nullptr);
    }

    // Membership answers identically for the record and for its absence, in BOTH
    // modes — that equivalence is what makes writing the record free of behaviour.
    const auto checkEquivalent = [](const ViewModeModel& m) {
        CHECK(m.leafBelongsToMode("{TAGGED-ARRANGE}", kArrangeModeId) ==
              m.leafBelongsToMode("{UNTAGGED}", kArrangeModeId));
        CHECK(m.leafBelongsToMode("{TAGGED-ARRANGE}", kArrangeModeId));
        CHECK(m.leafBelongsToMode("{TAGGED-ARRANGE}", kDesignModeId) ==
              m.leafBelongsToMode("{UNTAGGED}", kDesignModeId));
        CHECK(!m.leafBelongsToMode("{TAGGED-ARRANGE}", kDesignModeId));
    };
    checkEquivalent(vm);
    if (back) checkEquivalent(*back);  // and after a save/reload round-trip

    // untag() still returns it to absence, so the existing way out still works.
    CHECK(vm.membership().untag("{TAGGED-ARRANGE}"));
    CHECK(vm.membership().query("{TAGGED-ARRANGE}") == nullptr);
}

static void testLaneJsonRoundTrip() {
    ViewModeModel vm;
    CHECK(vm.modes().add(Mode{"mixdown", "Mixdown", 2}));

    // Membership populated (item + track GUIDs share the index).
    vm.membership().tag("{TRACK}", kDesignModeId);
    vm.membership().tag("{ITEM}", "mixdown");

    // Lane ownership: managed lanes for two modes + a manual lane; a lane key with
    // characters that exercise the string escaper.
    CHECK(vm.lanes().setManaged("{T}", "lane:0", kArrangeModeId));
    CHECK(vm.lanes().setManaged("{T}", "lane\"1\"", kDesignModeId));
    CHECK(vm.lanes().setManual("{T}", "comp"));
    CHECK(vm.lanes().setManaged("{U}", "lane:0", "mixdown"));  // same key, other track
    CHECK(vm.setActiveMode("mixdown"));

    std::string json = vm.serialize();
    auto back = ViewModeModel::deserialize(json);
    CHECK(back.has_value());
    CHECK(back && *back == vm);                    // deserialize(serialize(x)) == x
    if (back) CHECK(back->serialize() == json);    // stable second round-trip

    if (back) {
        CHECK(back->lanes().size() == 4);
        const LaneOwnership* a = back->lanes().query("{T}", "lane:0");
        CHECK(a && a->isManaged() && *a->managedMode == kArrangeModeId);
        const LaneOwnership* d = back->lanes().query("{T}", "lane\"1\"");
        CHECK(d && d->isManaged() && *d->managedMode == kDesignModeId);
        const LaneOwnership* c = back->lanes().query("{T}", "comp");
        CHECK(c && c->isManual());
        const LaneOwnership* u = back->lanes().query("{U}", "lane:0");
        CHECK(u && u->isManaged() && *u->managedMode == "mixdown");
    }

    // A model with an EMPTY lane index still round-trips (D1-only project on D2 code).
    ViewModeModel d1only;
    d1only.membership().tag("{X}", kDesignModeId);
    auto b2 = ViewModeModel::deserialize(d1only.serialize());
    CHECK(b2.has_value());
    CHECK(b2 && *b2 == d1only);
    CHECK(b2 && b2->lanes().empty());
}

static void testLaneMalformedJson() {
    const char* bad[] = {
        "{\"lanes\":[{\"trackGuid\":\"{T}\",\"laneKey\":\"l0\"}]}",           // missing "managed"
        "{\"lanes\":[{\"trackGuid\":\"{T}\",\"managed\":true,\"mode\":\"design\"}]}",  // missing laneKey
        "{\"lanes\":[{\"laneKey\":\"l0\",\"managed\":false}]}",               // missing trackGuid
        "{\"lanes\":[{\"trackGuid\":\"\",\"laneKey\":\"l0\",\"managed\":false}]}",     // empty trackGuid
        "{\"lanes\":[{\"trackGuid\":\"{T}\",\"laneKey\":\"\",\"managed\":false}]}",    // empty laneKey
        "{\"lanes\":[{\"trackGuid\":\"{T}\",\"laneKey\":\"l0\",\"managed\":true}]}",   // managed w/o mode
        "{\"lanes\":[{\"trackGuid\":\"{T}\",\"laneKey\":\"l0\",\"managed\":true,\"mode\":\"\"}]}",  // managed empty mode
        "{\"lanes\":[{\"trackGuid\":\"{T}\",\"laneKey\":\"l0\",\"managed\":false,\"mode\":\"design\"}]}",  // manual w/ mode
        "{\"lanes\":[",                                                        // truncated
    };
    for (const char* j : bad) {
        auto r = ViewModeModel::deserialize(j);
        CHECK(!r.has_value());
    }
}

// -- Per-mode solo cache: persistence + reconcile participation ---------------

static void testSoloCacheJsonRoundTrip() {
    ViewModeModel vm;
    CHECK(vm.modes().add(Mode{"mixdown", "Mixdown", 2}));
    vm.membership().tag("{T}", kDesignModeId);
    vm.storeSnapshot("{T}", TrackSnapshot{1, 1, 1, 1, {{"{FX-A}", 0}, {"{FX-B}", 1}}});
    CHECK(vm.lanes().setManaged("{T}", "lane:0", kArrangeModeId));

    // Every non-zero I_SOLO variant, across more than one mode, plus a GUID that
    // exercises the string escaper.
    CHECK(vm.soloCache().store(kArrangeModeId, {{"{A}", 1}, {"{B\"q\"}", 2}}));
    CHECK(vm.soloCache().store("mixdown", {{"{C}", 5}, {"{D}", 6}}));
    CHECK(vm.setActiveMode(kDesignModeId));

    const std::string json = vm.serialize();
    auto back = ViewModeModel::deserialize(json);
    CHECK(back.has_value());
    CHECK(back && *back == vm);                  // deserialize(serialize(x)) == x
    if (back) CHECK(back->serialize() == json);  // stable second round-trip

    if (back) {
        const std::map<std::string, int>* arrange = back->soloCache().query(kArrangeModeId);
        CHECK(arrange != nullptr);
        CHECK(arrange && arrange->at("{A}") == 1);
        CHECK(arrange && arrange->at("{B\"q\"}") == 2);
        const std::map<std::string, int>* mix = back->soloCache().query("mixdown");
        CHECK(mix != nullptr);
        CHECK(mix && mix->at("{C}") == 5);
        CHECK(mix && mix->at("{D}") == 6);
        CHECK(back->soloCache().query(kDesignModeId) == nullptr);
    }
}

static void testBlobWithoutSoloCacheKeyStillParses() {
    // The compatibility case both ways: a project saved by a build that predates the
    // key parses to an empty cache, and its own output stays readable here.
    const char* older =
        "{\"version\":1,\"activeMode\":\"design\",\"modes\":[{\"id\":\"arrange\","
        "\"displayName\":\"Arrange\",\"ordinal\":0},{\"id\":\"design\","
        "\"displayName\":\"Design\",\"ordinal\":1}],\"membership\":[{\"guid\":\"{T}\","
        "\"modes\":[\"design\"],\"showBoth\":false}],\"snapshots\":[],\"lanes\":[]}";

    auto back = ViewModeModel::deserialize(older);
    CHECK(back.has_value());
    CHECK(back && back->soloCache().empty());
    CHECK(back && back->activeModeId() == kDesignModeId);
    CHECK(back && back->membership().query("{T}") != nullptr);
}

static void testSoloCacheMalformedJson() {
    const char* bad[] = {
        "{\"soloCache\":[{\"tracks\":[{\"guid\":\"{A}\",\"solo\":1}]}]}",   // missing mode
        "{\"soloCache\":[{\"mode\":\"\",\"tracks\":[{\"guid\":\"{A}\",\"solo\":1}]}]}",  // empty mode
        "{\"soloCache\":[{\"mode\":\"arrange\"}]}",                          // missing tracks
        "{\"soloCache\":[{\"mode\":\"arrange\",\"tracks\":[]}]}",            // empty tracks
        "{\"soloCache\":[{\"mode\":\"arrange\",\"tracks\":[{\"solo\":1}]}]}",  // missing guid
        "{\"soloCache\":[{\"mode\":\"arrange\",\"tracks\":[{\"guid\":\"{A}\"}]}]}",  // missing solo
        "{\"soloCache\":[{\"mode\":\"arrange\",\"tracks\":[{\"guid\":\"\",\"solo\":1}]}]}",  // empty guid
        "{\"soloCache\":[",                                                  // truncated
    };
    for (const char* j : bad) {
        auto r = ViewModeModel::deserialize(j);
        CHECK(!r.has_value());
    }
}

static void testReconcilePrunesTheSoloCacheAlongsideSnapshots() {
    ViewModeModel vm;
    vm.storeSnapshot("{LIVE}", TrackSnapshot{1, 1, 1, 1, {}});
    vm.storeSnapshot("{DEAD}", TrackSnapshot{1, 1, 1, 1, {}});
    vm.soloCache().store(kDesignModeId, {{"{LIVE}", 1}, {"{DEAD}", 2}});

    // The return stays the SNAPSHOT count; the solo cache is pruned by the same call.
    CHECK(vm.reconcile({"{LIVE}"}) == 1);

    const std::map<std::string, int>* design = vm.soloCache().query(kDesignModeId);
    CHECK(design != nullptr);
    CHECK(design && design->size() == 1);
    CHECK(design && design->count("{LIVE}") == 1);
}

// -- Per-FX offline: identity keying and the v1 -> v2 snapshot ladder ---------
//
// The restore path end-to-end, at the seam the shell actually uses: a stored
// snapshot -> planToggle -> makeRestorePlan ops -> resolveFxRestore against the
// chain as it stands now. The chain mutations happen while the track is parked,
// which is the whole reason a slot cannot be the key.

namespace {

// The plan's restore ops for one parked-then-reactivated leaf.
std::vector<FxOfflineOp> restoreOpsFor(ViewModeModel& vm, const std::string& guid) {
    FolderTree tree;
    tree.nodes.push_back(FolderNode{guid, "", false});
    const TogglePlan plan = vm.planToggle(tree, kDesignModeId);
    const TrackPlan* r = restoreFor(plan, guid);
    return r ? r->fxOffline : std::vector<FxOfflineOp>{};
}

bool writeAt(const FxRestoreResolution& res, int fxIndex, bool offline) {
    for (const FxOfflineWrite& w : res.writes)
        if (w.fxIndex == fxIndex) return w.offline == offline;
    return false;
}

bool anyWriteAt(const FxRestoreResolution& res, int fxIndex) {
    for (const FxOfflineWrite& w : res.writes)
        if (w.fxIndex == fxIndex) return true;
    return false;
}

// A leaf tagged Design, parked with three identified FX — the state every case
// below starts from.
ViewModeModel parkedWithThreeFx() {
    ViewModeModel vm;
    vm.membership().tag("{T}", kDesignModeId);
    TrackSnapshot snap;
    snap.showInTcp = 1; snap.showInMixer = 1; snap.mainSend = 1; snap.fxEnable = 1;
    snap.fxOffline = {{"{FX-A}", 0}, {"{FX-B}", 1}, {"{FX-C}", 0}};
    vm.storeSnapshot("{T}", snap);
    return vm;
}

}  // namespace

static void testParkReorderRestoreLandsEachPluginItsOwnState() {
    ViewModeModel vm = parkedWithThreeFx();
    // Reordered to C, A, B while parked.
    const FxRestoreResolution res =
        resolveFxRestore(restoreOpsFor(vm, "{T}"), {"{FX-C}", "{FX-A}", "{FX-B}"});

    CHECK(res.writes.size() == 3);
    CHECK(res.drops.total() == 0);
    CHECK(writeAt(res, 1, false));  // A
    CHECK(writeAt(res, 2, true));   // B's captured offline followed B, not slot 1
    CHECK(writeAt(res, 0, false));  // C
}

static void testParkDeleteFxRestoreDropsExplicitly() {
    ViewModeModel vm = parkedWithThreeFx();
    // B deleted while parked.
    const FxRestoreResolution res =
        resolveFxRestore(restoreOpsFor(vm, "{T}"), {"{FX-A}", "{FX-C}"});

    CHECK(res.writes.size() == 2);
    CHECK(res.drops.missingIdentity == 1);
    CHECK(writeAt(res, 0, false));  // A
    CHECK(writeAt(res, 1, false));  // C — and NOT B's captured `true`
    CHECK(!describeFxRestoreDrops(res.drops, 1).empty());
}

static void testParkAddFxRestoreLeavesItAlone() {
    ViewModeModel vm = parkedWithThreeFx();
    // A new plugin inserted at the head while parked.
    const FxRestoreResolution res = resolveFxRestore(
        restoreOpsFor(vm, "{T}"), {"{FX-NEW}", "{FX-A}", "{FX-B}", "{FX-C}"});

    CHECK(res.writes.size() == 3);
    CHECK(res.drops.total() == 0);
    CHECK(!anyWriteAt(res, 0));     // the added FX is never written
    CHECK(writeAt(res, 1, false));
    CHECK(writeAt(res, 2, true));
    CHECK(writeAt(res, 3, false));
}

static void testV2WritesTheLegacySlotArrayBesideIdentities() {
    // The downgrade half of the ladder: a build that predates identity keying
    // reads "fxOffline" and skips "fx", so it keeps exactly the behavior it had
    // instead of losing every captured FX state to an unknown key.
    ViewModeModel vm = parkedWithThreeFx();
    const std::string json = vm.serialize();

    CHECK(json.find("\"fxOffline\":[0,1,0]") != std::string::npos);
    CHECK(json.find("\"fx\":[{\"guid\":\"{FX-A}\",\"offline\":0},"
                    "{\"guid\":\"{FX-B}\",\"offline\":1},"
                    "{\"guid\":\"{FX-C}\",\"offline\":0}]") != std::string::npos);

    auto back = ViewModeModel::deserialize(json);
    CHECK(back.has_value());
    CHECK(back && *back == vm);

    // The downgrade path itself, at the only point it can be reached from here:
    // an unknown array-of-objects key beside "fxOffline" is skipped and the slot
    // array is still read — the same skipValue branch an older build takes on
    // "fx". (An actual older binary is not runnable from this test.)
    auto asOlder = ViewModeModel::deserialize(
        "{\"snapshots\":[{\"guid\":\"{T}\",\"fxOffline\":[0,1,0],"
        "\"futureKey\":[{\"guid\":\"{FX-A}\",\"offline\":0}]}]}");
    CHECK(asOlder.has_value());
    CHECK(asOlder && asOlder->snapshot("{T}") &&
          asOlder->snapshot("{T}")->fxOffline.size() == 3);
    CHECK(asOlder && asOlder->snapshot("{T}") &&
          asOlder->snapshot("{T}")->fxOffline[1].offline == 1);
}

static void testLegacyBlobLiftsToSlotKeyingAndStillRestores() {
    // A view_state written before FX identity existed: no "fx" key anywhere.
    const char* v1 =
        "{\"version\":1,\"activeMode\":\"arrange\",\"modes\":[{\"id\":\"arrange\","
        "\"displayName\":\"Arrange\",\"ordinal\":0},{\"id\":\"design\","
        "\"displayName\":\"Design\",\"ordinal\":1}],\"membership\":[{\"guid\":\"{T}\","
        "\"modes\":[\"design\"],\"showBoth\":false}],\"snapshots\":[{\"guid\":\"{T}\","
        "\"showInTcp\":1,\"showInMixer\":1,\"mainSend\":1,\"fxEnable\":1,"
        "\"fxOffline\":[0,1,0]}],\"lanes\":[]}";

    auto loaded = ViewModeModel::deserialize(v1);
    CHECK(loaded.has_value());
    if (!loaded) return;

    const TrackSnapshot* snap = loaded->snapshot("{T}");
    CHECK(snap != nullptr);
    CHECK(snap && snap->fxKeying == FxKeying::Slot);   // no identities to key on
    CHECK(snap && snap->fxOffline.size() == 3);
    CHECK(snap && snap->fxOffline[1].offline == 1);
    CHECK(snap && snap->fxOffline[1].fxGuid.empty());
    CHECK(snap && snap->showInTcp == 1 && snap->mainSend == 1);

    // It still restores — by position, which is all its bytes can support, and
    // is exactly what the pre-change build would have done with them.
    const std::vector<FxOfflineOp> ops = restoreOpsFor(*loaded, "{T}");
    CHECK(ops.size() == 3);
    CHECK(!ops.empty() && ops[0].keying == FxKeying::Slot);
    const FxRestoreResolution res = resolveFxRestore(ops, {"{FX-X}", "{FX-Y}", "{FX-Z}"});
    CHECK(res.writes.size() == 3);
    CHECK(res.drops.total() == 0);
    CHECK(writeAt(res, 0, false));
    CHECK(writeAt(res, 1, true));
    CHECK(writeAt(res, 2, false));

    // Re-saving a lifted snapshot does NOT invent identities for it: the "fx"
    // key stays absent, and a second load reads the same slot-keyed shape.
    const std::string resaved = loaded->serialize();
    CHECK(resaved.find("\"fx\":") == std::string::npos);
    CHECK(resaved.find("\"fxOffline\":[0,1,0]") != std::string::npos);
    auto again = ViewModeModel::deserialize(resaved);
    CHECK(again.has_value());
    CHECK(again && *again == *loaded);
    CHECK(again && again->snapshot("{T}") &&
          again->snapshot("{T}")->fxKeying == FxKeying::Slot);

    // And the lift is one-shot: a restore consumes the snapshot, so the next park
    // captures identities and the project leaves the legacy shape behind.
    loaded->clearSnapshot("{T}");
    TrackSnapshot fresh;
    fresh.fxOffline = {{"{FX-X}", 1}};
    loaded->storeSnapshot("{T}", fresh);
    CHECK(loaded->serialize().find("\"fx\":[{\"guid\":\"{FX-X}\",\"offline\":1}]")
          != std::string::npos);
}

int main() {
    testSerializeGoldenLiteral();
    testNModeRegistryAndMembership();
    testParentDerivationMultiMode();
    testParentOwnMembershipVisibility();
    testRestoreRoundTripSnapshotValues();
    testShowBothNeverParkedVisibleEverywhere();
    testStaleGuidTolerated();
    testJsonRoundTrip();
    testEmptyModelRoundTrip();
    testMalformedJson();
    testPlanToggleParkHasEmptyFxOffline();
    testUntaggedLeavesManagedByModeSystem();
    testTaggedLeafBehaviorUnchangedWithUntagged();
    testNestedToggleSnapshotSurvivesRepark();
    testReconcilePrunesOrphanedSnapshots();
    testReconcileFullLiveSetIsNoOp();
    testReconcileThenReparkLifecycleIntact();
    testNextModeIdCycles();

    // D2 two-canvas lane extension
    testLaneModeStateAndPlayValues();
    testLaneOwnershipIndex();
    testLaneOwnershipLastWriterWins();
    testManagedOnlyPlannerAndQuery();
    testAutoTagDecision();
    testDropOntoTaggedTrackDoesNotStrand();
    testPlanItemRetag();
    testReconcileUnregisteredModeGuardDecision();
    testLaneMintingSingleModeNoSplit();
    testLaneMintingMultiModeMintsAndAssignsAll();
    testLaneMintingManualLaneExempt();
    testLaneMintingThreeModesAndOwnershipKeys();
    testLaneMintingFolderDerivedVisibleSplitsOwnMedia();
    testLaneMintingLazySingleLaneStillConfines();
    testLaneMintingFolderOwnItemsSpanBothModes();
    testLaneMintingNewItemOnAlreadySplitFolderYieldsPlan();
    testLaneMintingShowBothNotForceSplit();
    testLaneMintingSingleModeLeafVisibleOnceNoSplit();
    testLaneMintingEmptyFolderNotSplit();
    testExplicitArrangeRecordRoundTripsAndBehavesLikeAbsence();
    testLaneJsonRoundTrip();
    testLaneMalformedJson();

    // Per-mode solo cache
    testSoloCacheJsonRoundTrip();
    testBlobWithoutSoloCacheKeyStillParses();
    testSoloCacheMalformedJson();
    testReconcilePrunesTheSoloCacheAlongsideSnapshots();

    // Per-FX offline identity keying + the v1 -> v2 snapshot ladder
    testParkReorderRestoreLandsEachPluginItsOwnState();
    testParkDeleteFxRestoreDropsExplicitly();
    testParkAddFxRestoreLeavesItAlone();
    testV2WritesTheLegacySlotArrayBesideIdentities();
    testLegacyBlobLiftsToSlotKeyingAndStillRestores();

    if (g_fail == 0) std::printf("All tests passed.\n");
    return g_fail ? 1 : 0;
}
