// Standalone tests for reasampler::view::solo_cache — no REAPER, no test framework.
//
// Covers: the soloed-subset filter (zeros and empty GUIDs dropped, raw values kept),
// the cache's store/query/clear lifecycle including the empty-set-removes rule, GUID
// pruning on reconcile, and the restore plan's two drop rules (dead GUID, not visible
// in the incoming mode — covering both a parked leaf and a derived-invisible parent).

#include "../src/core/view/solo_cache.h"

#include <cstdio>

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)

// --- soloedTracks: the filter ------------------------------------------------

static void testUnsoloedProjectYieldsNothingToCache() {
    const std::map<std::string, int> soloed =
        soloedTracks({{"{A}", 0}, {"{B}", 0}, {"{C}", 0}});
    CHECK(soloed.empty());
}

static void testSoloedTracksKeepsRawValueNotABoolean() {
    const std::map<std::string, int> soloed =
        soloedTracks({{"{A}", 1}, {"{B}", 0}, {"{C}", 2}, {"{D}", 6}});

    CHECK(soloed.size() == 3);
    CHECK(soloed.at("{A}") == 1);   // plain solo
    CHECK(soloed.at("{C}") == 2);   // solo-in-place survives
    CHECK(soloed.at("{D}") == 6);   // safe solo-in-place survives
    CHECK(soloed.count("{B}") == 0);
}

static void testSoloedTracksDropsEmptyGuids() {
    const std::map<std::string, int> soloed = soloedTracks({{"", 1}, {"{A}", 1}});
    CHECK(soloed.size() == 1);
    CHECK(soloed.count("{A}") == 1);
}

// --- SoloCache: store / query / clear ----------------------------------------

static void testStoreThenQueryReturnsTheStoredSet() {
    SoloCache cache;
    CHECK(cache.store("arrange", {{"{A}", 1}, {"{B}", 2}}));

    const std::map<std::string, int>* got = cache.query("arrange");
    CHECK(got != nullptr);
    CHECK(got->size() == 2);
    CHECK(got->at("{A}") == 1);
    CHECK(got->at("{B}") == 2);
    CHECK(cache.query("design") == nullptr);
}

static void testStoringAnEmptySetRemovesTheModeEntry() {
    SoloCache cache;
    cache.store("arrange", {{"{A}", 1}});
    CHECK(cache.query("arrange") != nullptr);

    // Unsoloing everything and switching away must leave no record behind — an
    // empty record would not survive the serialize round trip.
    CHECK(cache.store("arrange", {}));
    CHECK(cache.query("arrange") == nullptr);
    CHECK(cache.empty());
}

static void testStoreReplacesRatherThanMerges() {
    SoloCache cache;
    cache.store("design", {{"{A}", 1}, {"{B}", 1}});
    cache.store("design", {{"{C}", 2}});

    const std::map<std::string, int>* got = cache.query("design");
    CHECK(got != nullptr);
    CHECK(got->size() == 1);
    CHECK(got->count("{C}") == 1);
}

static void testStoreRejectsAnEmptyModeId() {
    SoloCache cache;
    CHECK(!cache.store("", {{"{A}", 1}}));
    CHECK(cache.empty());
}

static void testClearConsumesOnlyTheNamedMode() {
    SoloCache cache;
    cache.store("arrange", {{"{A}", 1}});
    cache.store("design", {{"{B}", 1}});

    CHECK(cache.clear("arrange"));
    CHECK(cache.query("arrange") == nullptr);
    CHECK(cache.query("design") != nullptr);
    CHECK(!cache.clear("arrange"));  // already consumed
}

// --- SoloCache::reconcile ----------------------------------------------------

static void testReconcileDropsDeadGuidsAcrossEveryMode() {
    SoloCache cache;
    cache.store("arrange", {{"{LIVE}", 1}, {"{DEAD}", 2}});
    cache.store("design", {{"{DEAD}", 1}});

    CHECK(cache.reconcile({"{LIVE}"}) == 2);

    const std::map<std::string, int>* arrange = cache.query("arrange");
    CHECK(arrange != nullptr);
    CHECK(arrange->size() == 1);
    CHECK(arrange->count("{LIVE}") == 1);
    // The design entry lost its only track, so the mode record goes with it.
    CHECK(cache.query("design") == nullptr);
}

static void testReconcileWithEveryGuidLiveRemovesNothing() {
    SoloCache cache;
    cache.store("arrange", {{"{A}", 1}, {"{B}", 5}});
    CHECK(cache.reconcile({"{A}", "{B}", "{UNRELATED}"}) == 0);
    CHECK(cache.query("arrange")->size() == 2);
}

// --- planSoloRestore ---------------------------------------------------------

static void testRestorePlanReplaysEveryLiveVisibleEntryVerbatim() {
    const std::vector<SoloOp> ops =
        planSoloRestore({{"{A}", 1}, {"{B}", 2}}, {"{A}", "{B}"}, {"{A}", "{B}"});

    CHECK(ops.size() == 2);
    CHECK(ops[0] == (SoloOp{"{A}", 1}));
    CHECK(ops[1] == (SoloOp{"{B}", 2}));
}

static void testRestorePlanSkipsAGuidThatNoLongerExists() {
    const std::vector<SoloOp> ops =
        planSoloRestore({{"{GONE}", 1}, {"{HERE}", 1}}, {"{HERE}"}, {"{HERE}"});

    CHECK(ops.size() == 1);
    CHECK(ops[0].guid == "{HERE}");
}

static void testRestorePlanSkipsAParkedLeafNotVisibleInTheIncomingMode() {
    // Soloing a parked track would silence the mix while contributing nothing
    // audible, and the track is hidden, so the user could not undo it.
    const std::vector<SoloOp> ops =
        planSoloRestore({{"{PARKED}", 1}, {"{VISIBLE}", 2}}, {"{PARKED}", "{VISIBLE}"},
                        {"{VISIBLE}"});

    CHECK(ops.size() == 1);
    CHECK(ops[0] == (SoloOp{"{VISIBLE}", 2}));
}

static void testRestorePlanSkipsAFolderParentHiddenInTheIncomingMode() {
    // The parent is never parked (parents are never parked — see planToggle), but a
    // derived-invisible parent is hidden all the same: replaying its cached solo
    // would silence the mix onto a track the user cannot see or unsolo. `visibleGuids`
    // (not the park plan) is the drop set precisely so this case is caught too.
    const std::vector<SoloOp> ops =
        planSoloRestore({{"{HIDDEN_PARENT}", 1}}, {"{HIDDEN_PARENT}"}, {});

    CHECK(ops.empty());
}

static void testRestorePlanOfAnEmptyCacheWritesNothing() {
    CHECK(planSoloRestore({}, {"{A}"}, {}).empty());
}

int main() {
    testUnsoloedProjectYieldsNothingToCache();
    testSoloedTracksKeepsRawValueNotABoolean();
    testSoloedTracksDropsEmptyGuids();

    testStoreThenQueryReturnsTheStoredSet();
    testStoringAnEmptySetRemovesTheModeEntry();
    testStoreReplacesRatherThanMerges();
    testStoreRejectsAnEmptyModeId();
    testClearConsumesOnlyTheNamedMode();

    testReconcileDropsDeadGuidsAcrossEveryMode();
    testReconcileWithEveryGuidLiveRemovesNothing();

    testRestorePlanReplaysEveryLiveVisibleEntryVerbatim();
    testRestorePlanSkipsAGuidThatNoLongerExists();
    testRestorePlanSkipsAParkedLeafNotVisibleInTheIncomingMode();
    testRestorePlanSkipsAFolderParentHiddenInTheIncomingMode();
    testRestorePlanOfAnEmptyCacheWritesNothing();

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