#include "GeneratorTests.h"
#include "PluginProcessor.h"
#include "Presets/DeepPluckPresets.h"

#include <cmath>
#include <iostream>
#include <map>
#include <set>

namespace deeppluck::presets
{
// Fixture injection is available only in this developer test translation unit.
// It exercises the real load/navigation/host-state code, not a second model.
struct IntegrationTestAccess
{
    static void install (DeepPluckAudioProcessor& processor, const FactoryLibrary& library)
    { processor.factoryLibrary = library; }
};

namespace
{
constexpr std::uint64_t rootSeed = UINT64_C(0x44504c55434b1200);
void nearValues (const juce::AudioProcessorValueTreeState& state,
                 const DeepPluckParameterValues& a, const DeepPluckParameterValues& b)
{
    for (std::size_t i = 0; i < a.size(); ++i)
    {
        const auto* parameter = state.getParameter (allParameterIds[i]);
        require (std::abs (parameter->convertTo0to1 (a[i]) - parameter->convertTo0to1 (b[i])) <= 0.000001f,
                 "Sound differs at " + juce::String (allParameterIds[i]));
    }
}

template <typename Function> void rejects (Function&& function, const juce::String& label)
{
    bool rejected = false;
    try { function(); } catch (const std::exception&) { rejected = true; }
    require (rejected, "Negative case accepted: " + label);
}

juce::var clone (const juce::var& value) { return juce::JSON::parse (juce::JSON::toString (value)); }
juce::String encode (const juce::AudioProcessorValueTreeState& state, const GeneratedCandidate& candidate,
                     const juce::String& name = {})
{
    UserPresetData data {name.isEmpty() ? candidate.name : name, "Tonetrax", DEEPPLUCK_PRESET_TOOL_VERSION,
                         "Synthetic test fixture; never a user approval.", candidate.values};
    juce::String json, error;
    require (encodePreset (state, data, json, error, true), error);
    return json;
}

void profilesTest (juce::AudioProcessorValueTreeState& state, const juce::File& root)
{
    // Independent expectations transcribed from the approved profile table.
    constexpr std::array<std::array<int,5>,8> waves {{{45,25,5,10,15},{15,40,0,30,15},
        {50,5,30,0,15},{35,30,5,15,15},{25,30,5,20,20},{50,15,20,5,10},{35,30,10,10,15},{20,15,20,10,35}}};
    constexpr std::array<std::array<int,4>,8> unison {{{25,60,10,5},{25,60,10,5},{40,50,8,2},
        {15,60,15,10},{10,55,20,15},{55,40,5,0},{15,65,5,15},{20,35,20,25}}};
    constexpr std::array<std::array<float,2>,8> cutoff {{{450,2400},{800,3500},{2200,8500},{650,2800},
        {600,3200},{1000,5500},{1000,4200},{500,6500}}};
    constexpr std::array<int,8> osc2 {70,75,55,90,90,45,75,90};
    constexpr std::array<int,8> transient {65,25,85,45,35,90,45,65};
    constexpr std::array<int,8> noise {25,20,25,35,65,30,20,75};
    constexpr std::array<int,8> lfo {20,25,10,55,75,10,20,80};
    constexpr std::array<std::array<int,3>,8> fx {{{25,25,45},{55,25,65},{20,30,35},{65,65,65},
        {75,80,85},{15,60,20},{40,40,50},{60,65,65}}};
    juce::Array<juce::var> results;
    const auto defaults = captureParameterDefaults (state);
    for (int category = 0; category < 8; ++category)
    {
        juce::MemoryOutputStream raw;
        std::array<int,5> waveCounts {};
        std::array<int,4> unisonCounts {};
        int coreCount = 0, enabledOsc2 = 0, exploration = 0, zeroSustain = 0;
        std::array<int,6> enabledLayers {};
        std::set<juce::String> unique;
        for (int seed = 0; seed < 1024; ++seed)
        {
            const auto candidate = generateCandidate (state, category, rootSeed + static_cast<std::uint64_t> (seed), 1);
            const auto repeat = generateCandidate (state, category, rootSeed + static_cast<std::uint64_t> (seed), 1);
            require (candidate.values == repeat.values && candidate.id == repeat.id
                     && candidate.derivedSeed == repeat.derivedSeed, "Seed repeat changed");
            require (candidate.values[0] == -3 && candidate.values[1] == 440, "Output/Tune randomized");
            require (candidate.values[6] == 1 && candidate.values[29] >= 1 && candidate.values[29] <= 4,
                     "Invalid musical core");
            if (candidate.values[29] == 1) require (candidate.values[39] == 0, "Inaudible single-slot Spread route");
            juce::MemoryOutputStream one;
            for (std::size_t i = 0; i < candidate.values.size(); ++i)
            {
                const auto value = candidate.values[i];
                const auto* parameter = state.getParameter (allParameterIds[i]);
                const auto& range = parameter->getNormalisableRange();
                require (std::isfinite (value) && value >= range.start && value <= range.end, "Range/nonfinite failure");
                if (parameter->isDiscrete()) require (value == std::round (value), "Fractional discrete value");
                raw.writeFloat (value); one.writeFloat (value);
            }
            unique.insert (juce::SHA256 (one.getData(), one.getDataSize()).toHexString());
            const auto json = encode (state, candidate);
            UserPresetData decoded;
            juce::String error;
            require (decodePreset (state, defaults, json, decoded, error), error);
            nearValues (state, candidate.values, decoded.values);
            if (candidate.exploration) ++exploration;
            else
            {
                ++coreCount;
                ++waveCounts[static_cast<std::size_t> (candidate.values[40])];
                ++unisonCounts[static_cast<std::size_t> (candidate.values[29] - 1)];
                require (candidate.values[7] >= cutoff[static_cast<std::size_t> (category)][0]
                         && candidate.values[7] <= cutoff[static_cast<std::size_t> (category)][1], "Core cutoff band");
            }
            if (candidate.values[24] == 1) ++enabledOsc2;
            if (candidate.values[16] == 1) ++enabledLayers[0];
            if (candidate.values[20] == 1) ++enabledLayers[1];
            if (candidate.values[37] != 0 || candidate.values[38] != 0 || candidate.values[39] != 0) ++enabledLayers[2];
            if (candidate.values[44] == 0) ++enabledLayers[3];
            if (candidate.values[49] == 0) ++enabledLayers[4];
            if (candidate.values[56] == 0) ++enabledLayers[5];
            if (candidate.values[4] == 0) ++zeroSustain;
            require (candidate.values[12] <= candidate.values[2] + 0.00001f
                     && candidate.values[13] <= candidate.values[3] + 0.00001f
                     && candidate.values[15] <= candidate.values[5] + 0.00001f, "Envelope correlation");
            for (const std::size_t mix : {48u,55u,60u}) require (candidate.values[mix] <= .321f, "Excess FX mix");
            require (candidate.values[53] <= .501f && candidate.values[57] <= .851f, "Unbounded FX feedback/size");
        }
        require (exploration >= 105 && exploration <= 205 && unique.size() == 1024, "Exploration/diversity property");
        for (std::size_t i = 0; i < waveCounts.size(); ++i)
            require (std::abs (100.0 * waveCounts[i] / coreCount - waves[static_cast<std::size_t> (category)][i]) < 6,
                     "Waveform weights");
        for (std::size_t i = 0; i < unisonCounts.size(); ++i)
            require (std::abs (100.0 * unisonCounts[i] / coreCount - unison[static_cast<std::size_t> (category)][i]) < 6,
                     "Unison weights");
        require (std::abs (100.0 * enabledOsc2 / 1024 - osc2[static_cast<std::size_t> (category)]) < 6, "Osc2 weight");
        require (std::abs (100.0 * zeroSustain / 1024 - (category == 5 ? 90 : 65)) < 6, "Zero sustain weight");
        const auto cat=static_cast<std::size_t>(category);
        const std::array<int,6> layerWeights {transient[cat],noise[cat],lfo[cat],fx[cat][0],fx[cat][1],fx[cat][2]};
        for(std::size_t i=0;i<enabledLayers.size();++i)
            require(std::abs(100.0*enabledLayers[i]/1024-layerWeights[i])<6,"Layer/LFO/FX activation weight");
        auto record = object();
        set (record, "category", factoryCategories[static_cast<std::size_t> (category)].id);
        set (record, "count", 1024); set (record, "exploration", exploration);
        set (record, "coreCount", coreCount);
        juce::Array<juce::var> waveObserved, unisonObserved, activationObserved;
        for(const auto count:waveCounts) waveObserved.add(count);
        for(const auto count:unisonCounts) unisonObserved.add(count);
        for(const auto count:enabledLayers) activationObserved.add(count);
        set (record,"coreWaveformCounts",waveObserved); set(record,"coreUnisonCounts",unisonObserved);
        set (record,"activationCountsTransientNoiseLfoChorusDelayReverb",activationObserved);
        set (record,"osc2Enabled",enabledOsc2); set(record,"zeroSustain",zeroSustain);
        set (record, "sha256FloatBits", juce::SHA256 (raw.getData(), raw.getDataSize()).toHexString());
        results.add (record);
    }
    auto result = object(); set (result, "categories", results);
    writeJson (root.getChildFile ("profile-goldens.json"), result);
}

struct Fixture
{
    juce::var manifest;
    std::map<juce::String,juce::String> files;
};

Fixture finalFixture (const juce::AudioProcessorValueTreeState& state)
{
    Fixture fixture {object(),{}};
    set (fixture.manifest, "format", "DeepPluckFactoryLibrary"); set (fixture.manifest, "schemaVersion", 1);
    set (fixture.manifest, "status", "approved");
    set (fixture.manifest, "expectedActiveCount", static_cast<int> (finalFactoryPresetCount));
    juce::Array<juce::var> reviews, entries;
    for (int i = 0; i < 4; ++i)
    {
        auto review = object(); set (review,"id","factory:"+juce::String(i));
        set (review,"decision","replaced"); set (review,"evidence","SYNTHETIC TEST ONLY"); reviews.add (review);
    }
    for (int category = 0; category < 8; ++category)
        for (int index = 1; index <= finalFactoryPresetCounts[static_cast<std::size_t> (category)]; ++index)
        {
            const auto candidate = generateCandidate (state, category, rootSeed, index);
            const auto name = "Fixture " + candidate.name;
            const auto json = encode (state, candidate, name);
            const auto path = juce::String (factoryCategories[static_cast<std::size_t> (category)].name) + "/" + name + ".deeppluckpreset";
            const auto hash = juce::SHA256 (json.toRawUTF8(), json.getNumBytesAsUTF8()).toHexString();
            fixture.files.emplace (path, json);
            auto entry = object(), approval = object();
            set (entry,"id","factory12:fixture-"+juce::String(category)+"-"+juce::String(index));
            set (entry,"category",factoryCategories[static_cast<std::size_t> (category)].id); set (entry,"name",name);
            set (entry,"contentVersion","fixture-only"); set (entry,"path",path); set (entry,"sha256",hash);
            set (approval,"by","user"); set (approval,"decision","approved");
            set (approval,"evidence","SYNTHETIC TEST ONLY — NOT A REAL APPROVAL"); set (approval,"sha256",hash);
            set (entry,"approval",approval); entries.add (entry);
        }
    set (fixture.manifest,"legacyReviews",reviews); set (fixture.manifest,"entries",entries);
    return fixture;
}

FactoryLibrary validated (const juce::AudioProcessorValueTreeState& state, const Fixture& fixture, bool final = true)
{
    FactoryLibrary library;
    require (validateFactoryLibrary (state, juce::JSON::toString (fixture.manifest), [&] (const auto& path)
    {
        const auto found = fixture.files.find (path);
        return found == fixture.files.end() ? juce::String {} : found->second;
    }, final, library), library.error);
    return library;
}

void factoryTest (juce::AudioProcessorValueTreeState& state, const juce::File& root)
{
    auto fixture = finalFixture (state);
    require (validated (state, fixture).entries.size() == finalFactoryPresetCount,
             "Positive synthetic approved-count fixture");
    const auto negative = [&] (auto mutation, const juce::String& label)
    {
        auto broken = Fixture {clone (fixture.manifest),fixture.files}; mutation (broken);
        rejects ([&] { juce::ignoreUnused (validated (state, broken)); }, label);
    };
    negative ([] (auto& f) { f.manifest["entries"].getArray()->removeLast(); }, "one missing entry");
    negative ([] (auto& f) { auto entry = clone ((*f.manifest["entries"].getArray())[0]); f.manifest["entries"].getArray()->add (entry); }, "one extra entry");
    negative ([] (auto& f) { set (f.manifest,"expectedActiveCount",115); }, "wrong declared active count");
    for (const auto& field : {"id","name","path"})
        negative ([&] (auto& f) { auto entry = (*f.manifest["entries"].getArray())[1]; set (entry,field,(*f.manifest["entries"].getArray())[0][field]); }, "duplicate " + juce::String(field));
    negative ([] (auto& f) { auto entry=(*f.manifest["entries"].getArray())[0]; set(entry,"name","CON"); }, "reserved name");
    negative ([] (auto& f) { auto entry=(*f.manifest["entries"].getArray())[1]; set(entry,"name",(*f.manifest["entries"].getArray())[0]["name"].toString().toUpperCase()); }, "case-insensitive name collision");
    negative ([] (auto& f) { auto entry=(*f.manifest["entries"].getArray())[0]; set(entry,"name"," Fixture DEEP_001 "); }, "outer whitespace");
    negative ([] (auto& f) { auto entry=(*f.manifest["entries"].getArray())[0]; set(entry,"name","Fixture  DEEP_001"); }, "duplicate whitespace spelling");
    negative ([] (auto& f) { auto entry=(*f.manifest["entries"].getArray())[0]; set(entry,"category","warm"); }, "category mismatch");
    negative ([] (auto& f) { auto entry=(*f.manifest["entries"].getArray())[0]; set(entry,"category","ninth"); }, "unknown ninth category");
    negative ([] (auto& f) { auto entry=(*f.manifest["entries"].getArray())[0]; set(entry,"path","Deep Plucks/../escape.deeppluckpreset"); }, "path traversal");
    negative ([] (auto& f) { auto approval=(*f.manifest["entries"].getArray())[0]["approval"]; set(approval,"decision","Keep"); }, "Keep is not approval");
    for(const auto& decision:{"unreviewed","Maybe","Reject"})
        negative ([&] (auto& f) { auto approval=(*f.manifest["entries"].getArray())[0]["approval"]; set(approval,"decision",decision); }, "Non-approval status");
    negative ([] (auto& f) { auto approval=(*f.manifest["entries"].getArray())[0]["approval"]; set(approval,"sha256","changed"); }, "stale approval");
    negative ([] (auto& f) { f.files.erase(f.files.begin()); }, "missing content");
    negative ([] (auto& f) { f.files.begin()->second += " "; }, "changed content");
    negative ([] (auto& f) { auto review=(*f.manifest["legacyReviews"].getArray())[0]; set(review,"decision","OPEN"); }, "incomplete Legacy review");
    // Equal total is insufficient: move one valid file into the wrong category.
    negative ([] (auto& f)
    {
        auto entry=(*f.manifest["entries"].getArray())[0]; const auto old=entry["path"].toString();
        const auto path="Warm & Soft/"+old.fromLastOccurrenceOf("/",false,false);
        f.files.emplace(path,f.files.at(old)); f.files.erase(old); set(entry,"path",path); set(entry,"category","warm");
    }, "wrong category split");
    const auto embedded = loadEmbeddedFactoryLibrary (state);
    require (embedded.error.isEmpty(), "Invalid actual embedded catalogue");
    // The real content gate is reported separately, never passed by fixtures.
    std::cout << "Actual embedded final count: " << embedded.entries.size() << "/"
              << finalFactoryPresetCount << "; "
              << (embedded.awaitingCuration ? "OPEN" : "approved manifest present") << '\n';
    const auto reviewed=root.getChildFile("synthetic-reviewed"), staged=root.getChildFile("synthetic-staged");
    for(const auto& [relative,json]:fixture.files) writeNew(containedFile(reviewed,relative),json);
    writeJson(reviewed.getChildFile("catalog.json"),fixture.manifest);
    stagePromotion(state,reviewed,staged);
    for(const auto& [relative,json]:fixture.files)
    {
        juce::ignoreUnused(json);
        require(hashFile(containedFile(reviewed,relative))==hashFile(containedFile(staged,relative)),"Promotion changed file");
    }
    rejects([&]{stagePromotion(state,reviewed,staged);},"Promotion collision");
    const auto pending=root.getChildFile("synthetic-pending"); auto pendingManifest=clone(fixture.manifest);
    set(pendingManifest,"status","awaiting-user-curation"); set(pendingManifest,"entries",juce::Array<juce::var>{});
    for(auto& review:*pendingManifest["legacyReviews"].getArray()) set(review,"decision","OPEN");
    writeJson(pending.getChildFile("catalog.json"),pendingManifest);
    rejects([&]{stagePromotion(state,pending,root.getChildFile("unapproved"));},"Pending is not promotable");
    require(!root.getChildFile("unapproved").exists(),"Rejected promotion wrote output");
}

void filesTest (juce::AudioProcessorValueTreeState& state, const juce::File& root)
{
    const auto a=root.getChildFile("a"), b=root.getChildFile("b");
    generateRun(state,{0,1,2,3,4,5,6,7},2,1,rootSeed,a);
    generateRun(state,{7,6,5,4,3,2,1,0},2,1,rootSeed,b);
    const auto single=root.getChildFile("single"); generateRun(state,{0},1,2,rootSeed,single);
    const auto singleManifest=validateRun(state,single);
    const auto singlePath=(*singleManifest["entries"].getArray())[0]["path"].toString();
    require(hashFile(containedFile(a,singlePath))==hashFile(containedFile(single,singlePath)),"Batch size/start-index independence");
    const auto manifest=validateRun(state,a); juce::ignoreUnused(validateRun(state,b));
    for (const auto& entry : *manifest["entries"].getArray())
        require(hashFile(containedFile(a,entry["path"].toString()))==hashFile(containedFile(b,entry["path"].toString())),"Batch order/path determinism");
    rejects([&]{generateRun(state,{0},1,1,rootSeed,a);},"existing run");
    for (const auto& path : {"../escape","/absolute","User/../../escape","x:y","a//b"})
        rejects([&]{juce::ignoreUnused(containedFile(root,path));},"unsafe path");
    rejects([&]{createRunRoot(juce::File(DEEPPLUCK_FACTORY_SOURCE_ROOT));},"Factory output root");
    const auto input=encode(state,generateCandidate(state,0,rootSeed,1));
    auto parsed=juce::JSON::parse(input); const auto defaults=captureParameterDefaults(state);
    UserPresetData data; juce::String error;
    for (const auto& field : {"format","schemaVersion","name","author","pluginVersion","parameters"})
    {
        auto invalid=clone(parsed); invalid.getDynamicObject()->removeProperty(field);
        require(!decodePreset(state,defaults,juce::JSON::toString(invalid),data,error),"Missing required codec field");
    }
    auto invalid=clone(parsed); auto params=invalid["parameters"]; set(params,"filterCutoff","NaN");
    require(!decodePreset(state,defaults,juce::JSON::toString(invalid),data,error),"Non-numeric known parameter");
    params.getDynamicObject()->removeProperty("filterCutoff"); set(params,"futureParameter",object());
    require(decodePreset(state,defaults,juce::JSON::toString(invalid),data,error),"Legacy missing + future unknown parameter");
    require(data.values[7]==defaults[7],"Missing default migration");
    const auto audition=root.getChildFile("audition"); prepareAudition(state,a,audition);
    DeepPluckAudioProcessor processor(audition,true);
    require(processor.getPresetManager().waitForScanForTests(10000),"Audition scan timeout");
    const auto catalog=processor.getUserPresetCatalog(); require(catalog.entries.size()==16,"Isolated pack count");
    for(const auto& entry:catalog.entries) require(!entry.editable && entry.source==PresetSource::pack,"Candidate not read-only pack");
    require(processor.getFactoryLibrary().entries.size()==loadEmbeddedFactoryLibrary(state).entries.size(),"Candidates leaked into actual Factory");
    const auto blank=root.getChildFile("unrelated-root"); DeepPluckAudioProcessor isolated(blank,true);
    require(isolated.getPresetManager().waitForScanForTests(10000),"Blank scan timeout");
    require(isolated.getUserPresetCatalog().entries.empty(),"Candidate leakage across roots");
    auto corrupt=clone(manifest); auto entry=(*corrupt["entries"].getArray())[0]; set(entry,"derivedSeed","0x0000000000000000");
    const auto damaged=root.getChildFile("corrupt"); require(a.copyDirectoryTo(damaged),"Fixture copy");
    require(damaged.getChildFile("candidate-manifest.json").replaceWithText(juce::JSON::toString(corrupt,false)),"Fixture mutation");
    rejects([&]{juce::ignoreUnused(validateRun(state,damaged));},"seed provenance corruption");
}

void integrationTest (juce::AudioProcessorValueTreeState& state, const juce::File& root)
{
    const auto fixture=finalFixture(state); const auto library=validated(state,fixture);
    DeepPluckAudioProcessor first(root.getChildFile("one"),true), second(root.getChildFile("two"),true);
    IntegrationTestAccess::install(first,library); IntegrationTestAccess::install(second,library);
    juce::String error;
    const auto untouched=captureParameterValues(second.getValueTreeState());
    for(const auto& entry:library.entries)
    {
        require(first.loadPresetByStableId(entry.stableId,error),error);
        nearValues(state,captureParameterValues(first.getValueTreeState()),entry.values);
        const auto display=first.getPresetDisplayState(); require(display.stableId==entry.stableId && !display.editable,"Factory identity/read-only");
        juce::MemoryBlock hostState; first.getStateInformation(hostState);
        DeepPluckAudioProcessor restored; IntegrationTestAccess::install(restored,library);
        restored.setStateInformation(hostState.getData(),static_cast<int>(hostState.getSize()));
        nearValues(state,captureParameterValues(restored.getValueTreeState()),entry.values);
        require(restored.getPresetDisplayState().stableId==entry.stableId,"Content fingerprint after rootless restore");
        auto& random=first.getRandomization(); random.setAmount(.42f); random.setModuleLocked(RandomizationModule::effectsRack,true);
        const auto beforeRandom=captureParameterValues(first.getValueTreeState());
        require(first.randomizeAll(),"Random candidate integration");
        auto changed=captureParameterValues(first.getValueTreeState());
        for(const auto i:getRandomizationParameterIndices(RandomizationModule::effectsRack))
            require(changed[static_cast<std::size_t>(i)]==beforeRandom[static_cast<std::size_t>(i)],"Locked FX changed");
        require(first.undoRandomization() && !first.undoRandomization(),"Single undo");
        nearValues(state,captureParameterValues(first.getValueTreeState()),entry.values);
        require(first.getPresetDisplayState().stableId==entry.stableId,"Undo identity");
        require(first.randomizeAll(),"Random before boundary");
        require(first.loadPresetByStableId(entry.stableId,error),error);
        require(!random.canUndo() && random.getAmount()==.42f && random.isModuleLocked(RandomizationModule::effectsRack),"Load boundary contracts");
    }
    nearValues(state,captureParameterValues(second.getValueTreeState()),untouched);
    require(first.loadPresetByStableId(library.entries.front().stableId,error),error);
    for(std::size_t i=1;i<library.entries.size();++i)
    {
        require(first.loadRelativePreset(1,error),error);
        require(first.getPresetDisplayState().stableId==library.entries[i].stableId,"Active catalogue navigation");
    }
    require(first.loadRelativePreset(1,error),error);
    require(first.getPresetDisplayState().stableId==library.entries.front().stableId,
            "Active-only navigation wrap excludes the four Legacy entries");
    for(int index=0;index<4;++index)
    {
        require(first.loadPresetByStableId("factory:"+juce::String(index),error),error);
        DeepPluckAudioProcessor legacy; require(legacy.loadFactoryPreset(index),"Legacy fixture");
        nearValues(state,captureParameterValues(first.getValueTreeState()),captureParameterValues(legacy.getValueTreeState()));
    }
    const auto before=captureParameterValues(first.getValueTreeState());
    require(!first.loadPresetByStableId("factory:garbage",error),"Malformed Legacy id accepted");
    nearValues(state,before,captureParameterValues(first.getValueTreeState()));
    require(first.loadPresetByStableId(library.entries.front().stableId,error),error);
    require(first.saveUserPreset("Fine Tune Fixture","Tonetrax",false,false,error)==PresetSaveResult::saved,error);
    require(first.getPresetDisplayState().editable,"Save As read-only Factory to isolated User");
}
}

void runGeneratorTests (juce::AudioProcessorValueTreeState& state, const juce::String& suite, const juce::File& output)
{
    require(juce::StringArray{"profiles","files","factory","integration"}.contains(suite),"Unknown test suite");
    createRunRoot(output);
    if(suite=="profiles") profilesTest(state,output);
    else if(suite=="files") filesTest(state,output);
    else if(suite=="factory") factoryTest(state,output);
    else integrationTest(state,output);
    auto receipt=object(); set(receipt,"suite",suite); set(receipt,"pass",true);
    set(receipt,"version",DEEPPLUCK_PRESET_TOOL_VERSION); writeJson(output.getChildFile("result.json"),receipt);
    std::cout << "PASS: " << suite << '\n';
}
}
