// Standalone tests for shell/package/package_io — no REAPER, no framework. Failure is
// injected at the writer seam (abandonment, open failure, rename failure) rather than
// simulated, and the streaming claim is asserted against PayloadBuffer::alive().

#include "../src/shell/package/package_io.h"
#include "../src/shell/package/package_path.h"

#include <algorithm>
#include <cstdio>
#include <filesystem>
#include <fstream>
#include <string>
#include <utility>
#include <vector>

using namespace reasampler;
namespace fs = std::filesystem;

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 std::vector<std::uint8_t> patternBytes(std::size_t n, std::uint8_t seed) {
    std::vector<std::uint8_t> v(n);
    for (std::size_t i = 0; i < n; ++i)
        v[i] = static_cast<std::uint8_t>(seed + i * 7u);
    return v;
}

static bool sameBytes(const PayloadBuffer& p, const std::vector<std::uint8_t>& want) {
    return p.size() == want.size() &&
           (want.empty() || std::equal(want.begin(), want.end(), p.data()));
}

static void writeScratchFile(const std::string& path,
                             const std::vector<std::uint8_t>& bytes) {
    std::ofstream f(utf8Path(path), std::ios::binary | std::ios::trunc);
    f.write(reinterpret_cast<const char*>(bytes.data()),
            static_cast<std::streamsize>(bytes.size()));
}

static std::vector<std::uint8_t> readAll(const std::string& path) {
    std::ifstream f(utf8Path(path), std::ios::binary);
    return std::vector<std::uint8_t>(std::istreambuf_iterator<char>(f),
                                     std::istreambuf_iterator<char>());
}

static bool exists(const std::string& path) { return fs::exists(utf8Path(path)); }

static void removeQuietly(const std::string& path) {
    std::error_code ec;
    fs::remove(utf8Path(path), ec);
}

static void testPayloadCounterTracksMovesNotCopies() {
    CHECK(PayloadBuffer::alive() == 0);
    {
        PayloadBuffer a(patternBytes(4, 1));
        CHECK(PayloadBuffer::alive() == 1);
        PayloadBuffer b = std::move(a);
        CHECK(PayloadBuffer::alive() == 1);  // the count moved with the bytes
        PayloadBuffer c;
        c = std::move(b);
        CHECK(PayloadBuffer::alive() == 1);
        CHECK(c.size() == 4);
    }
    CHECK(PayloadBuffer::alive() == 0);
    {
        PayloadBuffer empty;
        CHECK(PayloadBuffer::alive() == 0);  // holding nothing counts as nothing
    }
}

static void testStreamingRoundTripHoldsOnePayload() {
    std::error_code destEc;
    const std::string dest =
        (fs::temp_directory_path(destEc) / "pkg_io_scratch.rsbank").generic_string();
    const std::vector<std::uint8_t> header = patternBytes(16, 0xA0);
    const std::vector<std::vector<std::uint8_t>> entries = {
        patternBytes(1000, 1), patternBytes(500, 2), patternBytes(1, 3)};

    std::vector<std::string> srcs;
    for (std::size_t i = 0; i < entries.size(); ++i) {
        srcs.push_back("pkg_io_src" + std::to_string(i) + ".bin");
        writeScratchFile(srcs[i], entries[i]);
    }

    CHECK(PayloadBuffer::alive() == 0);
    std::vector<std::pair<std::uint64_t, std::uint64_t>> layout;  // offset, length
    {
        PackageFileWriter writer(dest);
        CHECK(writer.ok());
        CHECK(writer.appendRaw(header.data(), header.size()));
        std::uint64_t offset = header.size();
        for (std::size_t i = 0; i < entries.size(); ++i) {
            PayloadBuffer p = readFilePayload(srcs[i]);
            CHECK(sameBytes(p, entries[i]));
            CHECK(PayloadBuffer::alive() == 1);  // exactly one entry in memory
            CHECK(writer.appendPayload(p));
            layout.emplace_back(offset, p.size());
            offset += p.size();
        }
        CHECK(PayloadBuffer::alive() == 0);  // each released before the next
        CHECK(writer.commit());
        CHECK(!writer.commit());  // a second commit is refused
    }
    CHECK(!exists(dest + ".rsbanktmp"));
    CHECK(exists(dest));

    {
        // Scoped: the reader holds the file open, and Windows refuses to delete an
        // open file — cleanup below needs it closed first.
        PackageFileReader reader(dest);
        CHECK(reader.ok());
        CHECK(reader.fileSize() == header.size() + 1000 + 500 + 1);
        for (std::size_t i = 0; i < entries.size(); ++i) {
            PayloadBuffer p = reader.readRange(layout[i].first, layout[i].second);
            CHECK(PayloadBuffer::alive() == 1);  // one entry per readRange, no more
            CHECK(sameBytes(p, entries[i]));
        }
        CHECK(PayloadBuffer::alive() == 0);
    }

    for (const std::string& s : srcs) removeQuietly(s);
    removeQuietly(dest);
}

static void testEmptyPayloadIsRefusedAndPoisonsTheWriter() {
    // The failure this guards: an unreadable source yields an empty payload, and a
    // verb that trusted a `true` here would commit framing claiming bytes nobody wrote.
    const std::string dest = "pkg_io_emptypayload.rsbank";
    {
        PackageFileWriter writer(dest);
        const std::vector<std::uint8_t> head = patternBytes(4, 1);
        CHECK(writer.appendRaw(head.data(), head.size()));
        CHECK(!writer.appendPayload(PayloadBuffer{}));
        CHECK(!writer.ok());
        CHECK(!writer.commit());  // the short stream can never reach the destination
    }
    CHECK(!exists(dest));
    CHECK(!exists(dest + ".rsbanktmp"));
    // A zero-length appendRaw stays tolerated — framing has legitimate empty edges.
    {
        PackageFileWriter writer(dest);
        CHECK(writer.appendRaw(nullptr, 0));
        CHECK(writer.ok());
        writer.abort();
    }
}

static void testAbandonedWriteLeavesNoDestination() {
    const std::string dest = "pkg_io_abandon.rsbank";
    {
        PackageFileWriter writer(dest);
        const std::vector<std::uint8_t> some = patternBytes(64, 9);
        CHECK(writer.appendRaw(some.data(), some.size()));
        // no commit — destruction is the injected interruption
    }
    CHECK(!exists(dest));
    CHECK(!exists(dest + ".rsbanktmp"));
}

static void testAbortPreservesPriorContents() {
    const std::string dest = "pkg_io_prior.rsbank";
    const std::vector<std::uint8_t> prior = patternBytes(32, 0x40);
    writeScratchFile(dest, prior);
    {
        PackageFileWriter writer(dest);
        const std::vector<std::uint8_t> some = patternBytes(64, 9);
        CHECK(writer.appendRaw(some.data(), some.size()));
        writer.abort();
        CHECK(!writer.appendRaw(some.data(), some.size()));  // dead after abort
        CHECK(!writer.commit());
    }
    CHECK(readAll(dest) == prior);
    CHECK(!exists(dest + ".rsbanktmp"));
    removeQuietly(dest);
}

static void testCommitReplacesAnExistingFile() {
    // The module's one deliberate asymmetry against LandedFileJournal's never-overwrite
    // rule: the save dialog's overwrite confirm is the consent, so commit() replaces.
    const std::string dest = "pkg_io_replace.rsbank";
    const std::vector<std::uint8_t> prior = patternBytes(40, 0x11);
    const std::vector<std::uint8_t> fresh = patternBytes(7, 0x22);
    writeScratchFile(dest, prior);
    {
        PackageFileWriter writer(dest);
        CHECK(writer.ok());
        CHECK(writer.appendRaw(fresh.data(), fresh.size()));
        CHECK(writer.commit());
    }
    CHECK(readAll(dest) == fresh);
    CHECK(!exists(dest + ".rsbanktmp"));
    removeQuietly(dest);
}

static void testOpenFailureIsInert() {
    const std::string dest = "pkg_io_no_such_dir/x.rsbank";
    PackageFileWriter writer(dest);
    CHECK(!writer.ok());
    const std::vector<std::uint8_t> some = patternBytes(8, 1);
    CHECK(!writer.appendRaw(some.data(), some.size()));
    CHECK(!writer.commit());
    CHECK(!exists("pkg_io_no_such_dir"));
}

static void testCommitRenameFailureSelfCleans() {
    // A directory squatting on the destination makes the final rename fail — a real
    // injected commit failure, not a simulated one.
    const std::string dest = "pkg_io_dir.rsbank";
    std::error_code ec;
    fs::create_directory(utf8Path(dest), ec);
    CHECK(!ec);
    {
        PackageFileWriter writer(dest);
        CHECK(writer.ok());
        const std::vector<std::uint8_t> some = patternBytes(8, 1);
        CHECK(writer.appendRaw(some.data(), some.size()));
        CHECK(!writer.commit());
    }
    CHECK(fs::is_directory(utf8Path(dest)));  // prior state intact
    CHECK(!exists(dest + ".rsbanktmp"));
    fs::remove(utf8Path(dest), ec);
}

static void testReaderEdges() {
    PackageFileReader missing("pkg_io_no_such_file.rsbank");
    CHECK(!missing.ok());
    CHECK(missing.readRange(0, 1).empty());

    const std::string path = "pkg_io_edges.bin";
    const std::vector<std::uint8_t> bytes = patternBytes(10, 5);
    writeScratchFile(path, bytes);
    {
        // Scoped: the reader must be closed before the cleanup remove below.
        PackageFileReader reader(path);
        CHECK(reader.ok());
        CHECK(reader.fileSize() == 10);
        CHECK(reader.readRange(5, 10).empty());   // past the end
        CHECK(reader.readRange(10, 1).empty());   // starts at the end
        CHECK(reader.readRange(0, 0).empty());    // zero length is failure, one branch
        CHECK(sameBytes(reader.readRange(2, 3),
                        std::vector<std::uint8_t>(bytes.begin() + 2, bytes.begin() + 5)));
    }
    removeQuietly(path);
}

static void testFileStatusSeparatesAbsentFromUnreadable() {
    CHECK(fileStatus("pkg_io_no_such_file.bin") == FileStatus::Absent);

    const std::string path = "pkg_io_status.bin";
    writeScratchFile(path, patternBytes(4, 1));
    CHECK(fileStatus(path) == FileStatus::Present);
    removeQuietly(path);

    // A directory in a file's place is not a readable file — the export message must
    // not report it as simply missing.
    const std::string dir = "pkg_io_status_dir";
    std::error_code ec;
    fs::create_directory(utf8Path(dir), ec);
    CHECK(fileStatus(dir) == FileStatus::Unreadable);
    fs::remove(utf8Path(dir), ec);
}

static void testNonAsciiPathsRoundTripAsUtf8() {
    // "café" in UTF-8. Windows decodes a narrow std::filesystem path through the ANSI
    // code page, so an unconverted seam lands "cafÃ©" or fails outright.
    const std::string dir = "pkg_io_caf\xC3\xA9_dir";
    const std::string name = "caf\xC3\xA9.rsbank";
    std::error_code ec;
    fs::create_directory(utf8Path(dir), ec);
    CHECK(!ec);

    const std::string dest = dir + "/" + name;
    const std::vector<std::uint8_t> bytes = patternBytes(24, 0x5A);
    {
        PackageFileWriter writer(dest);
        CHECK(writer.ok());
        CHECK(writer.appendRaw(bytes.data(), bytes.size()));
        CHECK(writer.commit());
    }
    CHECK(exists(dest));
    CHECK(fileStatus(dest) == FileStatus::Present);
    CHECK(sameBytes(readFilePayload(dest), bytes));
    // The listing must hand the name back in the same encoding it was given.
    CHECK(listFolderFileNames(dir) == (std::vector<std::string>{name}));

    fs::remove_all(utf8Path(dir), ec);
}

static void testWriteFileExclusiveRefusesAnOccupiedPath() {
    const std::string path = "pkg_io_excl.bin";
    const std::vector<std::uint8_t> mine = patternBytes(12, 3);
    CHECK(writeFileExclusive(path, PayloadBuffer(mine)));
    CHECK(readAll(path) == mine);
    // The create IS the check: a second call cannot replace the first file's bytes.
    // (this pins the refusal, not O_EXCL's atomicity — a genuine race isn't portable)
    CHECK(!writeFileExclusive(path, PayloadBuffer(patternBytes(9, 8))));
    CHECK(readAll(path) == mine);
    CHECK(!writeFileExclusive("pkg_io_excl_empty.bin", PayloadBuffer{}));
    CHECK(!exists("pkg_io_excl_empty.bin"));
    removeQuietly(path);

    // writeFileExclusive is the one call site using the wstring()/c_str() Windows
    // open form rather than the fstream(fs::path) overload every other test here
    // exercises — the only conversion whose reversion this file would otherwise miss.
    const std::string cafePath = "pkg_io_excl_caf\xC3\xA9.bin";
    const std::vector<std::uint8_t> cafeBytes = patternBytes(6, 4);
    CHECK(writeFileExclusive(cafePath, PayloadBuffer(cafeBytes)));
    CHECK(readAll(cafePath) == cafeBytes);
    CHECK(!writeFileExclusive(cafePath, PayloadBuffer(patternBytes(3, 9))));
    removeQuietly(cafePath);
}

static void testListFolderFileNames() {
    const std::string dir = "pkg_io_listdir";
    std::error_code ec;
    fs::create_directory(utf8Path(dir), ec);
    writeScratchFile(dir + "/b.bin", patternBytes(2, 1));
    writeScratchFile(dir + "/a.bin", patternBytes(2, 2));
    fs::create_directory(utf8Path(dir + "/sub"), ec);
    writeScratchFile(dir + "/sub/c.bin", patternBytes(2, 3));

    const std::vector<std::string> names = listFolderFileNames(dir);
    CHECK(names == (std::vector<std::string>{"a.bin", "b.bin"}));  // sorted, bare, non-recursive
    CHECK(listFolderFileNames("pkg_io_no_such_dir").empty());

    fs::remove_all(utf8Path(dir), ec);
}

int main() {
    testPayloadCounterTracksMovesNotCopies();
    testStreamingRoundTripHoldsOnePayload();
    testEmptyPayloadIsRefusedAndPoisonsTheWriter();
    testAbandonedWriteLeavesNoDestination();
    testAbortPreservesPriorContents();
    testCommitReplacesAnExistingFile();
    testOpenFailureIsInert();
    testCommitRenameFailureSelfCleans();
    testReaderEdges();
    testFileStatusSeparatesAbsentFromUnreadable();
    testNonAsciiPathsRoundTripAsUtf8();
    testWriteFileExclusiveRefusesAnOccupiedPath();
    testListFolderFileNames();

    if (g_fail == 0) std::printf("package_io: all tests passed\n");
    else             std::printf("package_io: %d CHECK(s) FAILED\n", g_fail);
    return g_fail == 0 ? 0 : 1;
}
