// Standalone tests for reasampler::track_topology — no REAPER, no framework.
// Covers the direct-child walk over a flat I_FOLDERDEPTH delta list: the set a
// ranged item render must silence so a folder parent's children stay out of the
// capture.

#include "../src/core/capture/track_topology.h"

#include <cstddef>
#include <cstdio>
#include <vector>

using namespace reasampler::capture;

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

static bool sameIndices(const std::vector<int>& got, const std::vector<int>& want) {
    return got == want;
}

static void testFlatProjectHasNoChildren() {
    // No track opens a folder, so no track has children — including the one asked
    // about. A flat project must produce an empty silence set, not a stray one.
    const std::vector<int> depths{0, 0, 0};
    CHECK(sameIndices(directChildIndices(depths, 0), {}));
    CHECK(sameIndices(directChildIndices(depths, 1), {}));
}

static void testFolderParentReturnsItsDirectChildren() {
    // 0: folder parent, 1: child, 2: last child (closes the folder), 3: unrelated.
    const std::vector<int> depths{1, 0, -1, 0};
    CHECK(sameIndices(directChildIndices(depths, 0), {1, 2}));
    // Track 3 sits outside the folder entirely and owns nothing.
    CHECK(sameIndices(directChildIndices(depths, 3), {}));
}

static void testGrandchildrenAreExcluded() {
    // 0: outer folder, 1: inner folder (a direct child), 2: grandchild closing the
    // inner folder, 3: second direct child closing the outer. The grandchild reaches
    // track 0 only through track 1, so silencing track 1's send covers it — listing
    // it too would be redundant, and the walk must not.
    const std::vector<int> depths{1, 1, -1, -1};
    CHECK(sameIndices(directChildIndices(depths, 0), {1, 3}));
    // Asked about the inner folder, the grandchild IS the direct child.
    CHECK(sameIndices(directChildIndices(depths, 1), {2}));
}

static void testMultiLevelCloseEndsTheOuterFolderToo() {
    // A -2 closes two folders at once (SDK: "last in the innermost and next-innermost
    // folders"), so track 3 is outside track 0's folder even though no track between
    // them closed it singly.
    const std::vector<int> depths{1, 1, -2, 0};
    CHECK(sameIndices(directChildIndices(depths, 0), {1}));
    CHECK(sameIndices(directChildIndices(depths, 1), {2}));
}

static void testNonFolderAndOutOfRangeReturnEmpty() {
    const std::vector<int> depths{1, 0, -1};
    CHECK(sameIndices(directChildIndices(depths, 1), {}));   // a plain child
    CHECK(sameIndices(directChildIndices(depths, 2), {}));   // the folder's last track
    CHECK(sameIndices(directChildIndices(depths, -1), {}));  // no such track
    CHECK(sameIndices(directChildIndices(depths, 3), {}));   // past the end
    CHECK(sameIndices(directChildIndices({}, 0), {}));       // empty project
}

static void testUnterminatedFolderSwallowsTheRest() {
    // A folder that never closes owns every remaining track, which is how REAPER
    // reads the same list — the walk must not stop early and leave a child audible.
    const std::vector<int> depths{1, 0, 0};
    CHECK(sameIndices(directChildIndices(depths, 0), {1, 2}));
}

static void testSiblingFolderAfterParentClosesIsNotIncluded() {
    // 0: folder parent, 1: child, 2: last child (closes it). 3: an unrelated sibling
    // folder that opens AFTER track 0's folder has already closed, 4: its child, 5:
    // its last child. A walk that fails to stop at track 0's own close would keep
    // consuming and wrongly pull the sibling's children in too.
    const std::vector<int> depths{1, 0, -1, 1, 0, -1};
    CHECK(sameIndices(directChildIndices(depths, 0), {1, 2}));
}

// --- siblingPlacement -------------------------------------------------------
//
// Every case asserts the property that actually matters, not just the numbers: the
// new track sits at the SOURCE's own nesting level, and the delta total is
// unchanged so no track after the insertion moves. `levelsAfter` rebuilds the
// post-insertion list and reads the levels straight off it.

static std::vector<int> depthsAfter(const std::vector<int>& depths,
                                    const SiblingPlacement& p) {
    std::vector<int> out = depths;
    if (p.precedingIndex >= 0) out[static_cast<std::size_t>(p.precedingIndex)] = p.precedingDepth;
    out.insert(out.begin() + p.insertIndex, p.newDepth);
    return out;
}

static int sumOf(const std::vector<int>& v) {
    int s = 0;
    for (int d : v) s += d;
    return s;
}

// Absolute nesting level of track `idx` in a delta list.
static int levelAt(const std::vector<int>& depths, int idx) {
    int level = 0;
    for (int i = 0; i < idx; ++i) level += depths[static_cast<std::size_t>(i)];
    return level;
}

// The whole contract in one call: the new track is a sibling (same level as the
// source) and nothing downstream shifted (delta total preserved).
static void checkIsSibling(const std::vector<int>& before, int srcIdx) {
    const SiblingPlacement p = siblingPlacement(before, srcIdx);
    const std::vector<int> after = depthsAfter(before, p);
    CHECK(sumOf(after) == sumOf(before));
    CHECK(levelAt(after, p.insertIndex) == levelAt(before, srcIdx));
}

static void testSiblingOfANormalTrackGoesDirectlyBelowIt() {
    // Three normal tracks at top level; the source is the middle one.
    const std::vector<int> depths{0, 0, 0};
    const SiblingPlacement p = siblingPlacement(depths, 1);
    CHECK(p.insertIndex == 2);
    CHECK(p.precedingIndex == 1);
    CHECK(p.precedingDepth == 0);   // unchanged
    CHECK(p.newDepth == 0);
    checkIsSibling(depths, 1);
}

static void testSiblingOfAMidFolderTrackStaysInsideTheFolder() {
    // 0: parent, 1: child (the source), 2: last child closing the folder.
    const std::vector<int> depths{1, 0, -1};
    const SiblingPlacement p = siblingPlacement(depths, 1);
    CHECK(p.insertIndex == 2);
    CHECK(p.precedingDepth == 0);
    CHECK(p.newDepth == 0);         // still inside; track 2 still closes the folder
    checkIsSibling(depths, 1);
}

static void testSiblingOfTheLastTrackInAFolderInheritsTheClosingDelta() {
    // The source carries the folder's close, so a naive insert-after would drop the
    // new track OUTSIDE the folder and bypass the folder bus entirely.
    const std::vector<int> depths{1, -1, 0};
    const SiblingPlacement p = siblingPlacement(depths, 1);
    CHECK(p.insertIndex == 2);
    CHECK(p.precedingDepth == 0);   // the source no longer closes the folder
    CHECK(p.newDepth == -1);        // the new track does
    checkIsSibling(depths, 1);
}

static void testSiblingOfTheLastTrackInTwoFoldersMovesTheWholeClose() {
    // 0: outer parent, 1: inner parent, 2: last in BOTH folders (the source).
    const std::vector<int> depths{1, 1, -2};
    const SiblingPlacement p = siblingPlacement(depths, 2);
    CHECK(p.insertIndex == 3);
    CHECK(p.precedingDepth == 0);
    CHECK(p.newDepth == -2);        // the -2 travels intact
    checkIsSibling(depths, 2);
}

static void testSiblingOfAFolderParentLandsAfterTheWholeFolder() {
    // Inserting straight after a folder parent would make the new track its FIRST
    // CHILD, re-summing the render through the parent's FX and fader.
    const std::vector<int> depths{1, 0, -1, 0};
    const SiblingPlacement p = siblingPlacement(depths, 0);
    CHECK(p.insertIndex == 3);      // past the whole folder, not at index 1
    CHECK(p.precedingIndex == 2);
    CHECK(p.precedingDepth == -1);  // unchanged — track 2 still closes the folder
    CHECK(p.newDepth == 0);
    checkIsSibling(depths, 0);
}

static void testSiblingOfTheLastTrackInTheProjectAppends() {
    const std::vector<int> depths{0, 0};
    const SiblingPlacement p = siblingPlacement(depths, 1);
    CHECK(p.insertIndex == 2);      // == count: appended
    CHECK(p.precedingDepth == 0);
    CHECK(p.newDepth == 0);
    checkIsSibling(depths, 1);
}

static void testSiblingOfTheLastTrackInTheProjectInsideAFolder() {
    // The project's last track also closes a folder — the close must still travel.
    const std::vector<int> depths{1, -1};
    const SiblingPlacement p = siblingPlacement(depths, 1);
    CHECK(p.insertIndex == 2);
    CHECK(p.precedingDepth == 0);
    CHECK(p.newDepth == -1);
    checkIsSibling(depths, 1);
}

static void testMalformedDeltaListClampsRatherThanAsserting() {
    // Deltas summing to -3: more closes than opens, which no well-formed project
    // produces. The result must still be a legal in-range placement.
    const std::vector<int> depths{0, -2, -1};
    const SiblingPlacement p = siblingPlacement(depths, 1);
    CHECK(p.insertIndex >= 0 && p.insertIndex <= static_cast<int>(depths.size()));
    CHECK(p.precedingIndex == p.insertIndex - 1);
    // Clamped at zero rather than tracking a negative nesting level.
    CHECK(levelAt(depthsAfter(depths, p), p.insertIndex) >= 0);

    // An unterminated folder (deltas summing to +1) is the other direction.
    const std::vector<int> open{1, 0};
    const SiblingPlacement q = siblingPlacement(open, 1);
    CHECK(q.insertIndex == 2);
    CHECK(q.newDepth <= 0);  // never invents a second folder open
}

static void testOutOfRangeSourceIndexClamps() {
    const std::vector<int> depths{0, 0};
    // Past the end clamps to the last track; negative clamps to the first.
    CHECK(siblingPlacement(depths, 99).insertIndex == 2);
    CHECK(siblingPlacement(depths, -5).insertIndex == 1);
    // An empty project has nothing to precede the new track.
    CHECK(siblingPlacement({}, 0).insertIndex == 0);
    CHECK(siblingPlacement({}, 0).precedingIndex == -1);
}

int main() {
    testFlatProjectHasNoChildren();
    testFolderParentReturnsItsDirectChildren();
    testGrandchildrenAreExcluded();
    testMultiLevelCloseEndsTheOuterFolderToo();
    testNonFolderAndOutOfRangeReturnEmpty();
    testUnterminatedFolderSwallowsTheRest();
    testSiblingFolderAfterParentClosesIsNotIncluded();

    testSiblingOfANormalTrackGoesDirectlyBelowIt();
    testSiblingOfAMidFolderTrackStaysInsideTheFolder();
    testSiblingOfTheLastTrackInAFolderInheritsTheClosingDelta();
    testSiblingOfTheLastTrackInTwoFoldersMovesTheWholeClose();
    testSiblingOfAFolderParentLandsAfterTheWholeFolder();
    testSiblingOfTheLastTrackInTheProjectAppends();
    testSiblingOfTheLastTrackInTheProjectInsideAFolder();
    testMalformedDeltaListClampsRatherThanAsserting();
    testOutOfRangeSourceIndexClamps();

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