#include "rm_control/modules/trajectory/reversed_vendor_trajectory_store.h"

#include <chrono>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <iterator>
#include <string>
#include <system_error>

#ifndef RM_TEST_OUTPUT_DIR
#define RM_TEST_OUTPUT_DIR "build/test_outputs"
#endif

namespace {

struct TestContext {
    int failures = 0;

    void Expect(bool condition, const std::string &message) {
        if (!condition) {
            ++failures;
            std::cerr << "[FAIL] " << message << '\n';
        }
    }
};

const std::string &FirstLine() {
    static const std::string line = "{\"point\":[0,0,0,0,0,0,0]}";
    return line;
}

const std::string &SecondLine() {
    static const std::string line = "{\"point\":[100,200,300,400,500,600,700]}";
    return line;
}

const std::string &ThirdLine() {
    static const std::string line = "{\"point\":[1000,2000,3000,4000,5000,6000,7000]}";
    return line;
}

std::string ForwardCrLfContent() {
    return FirstLine() + "\r\n" + SecondLine() + "\r\n" + ThirdLine() + "\r\n";
}

std::string ReversedCrLfContent() {
    return ThirdLine() + "\r\n" + SecondLine() + "\r\n" + FirstLine() + "\r\n";
}

std::filesystem::path TestOutputRoot() {
    static const std::filesystem::path root = [] {
        const auto unique_suffix =
            std::chrono::steady_clock::now().time_since_epoch().count();
        const std::filesystem::path path = std::filesystem::path(RM_TEST_OUTPUT_DIR) /
            ("reversed_vendor_trajectory_store_" + std::to_string(unique_suffix));
        std::error_code filesystem_error;
        std::filesystem::create_directories(path, filesystem_error);
        return path;
    }();
    return root;
}

bool WriteBinaryFile(const std::filesystem::path &path, const std::string &content) {
    std::ofstream output(path, std::ios::binary | std::ios::out | std::ios::trunc);
    output.write(content.data(), static_cast<std::streamsize>(content.size()));
    output.close();
    return static_cast<bool>(output);
}

std::string ReadBinaryFile(const std::filesystem::path &path) {
    std::ifstream input(path, std::ios::binary);
    return std::string(std::istreambuf_iterator<char>(input),
                       std::istreambuf_iterator<char>());
}

bool SetReadOnly(const std::filesystem::path &path) {
    std::error_code filesystem_error;
    const auto permissions = std::filesystem::perms::owner_read |
        std::filesystem::perms::group_read | std::filesystem::perms::others_read;
    std::filesystem::permissions(
        path, permissions, std::filesystem::perm_options::replace, filesystem_error);
    return !filesystem_error;
}

bool HasNoWritePermission(const std::filesystem::path &path) {
    std::error_code filesystem_error;
    const auto status = std::filesystem::symlink_status(path, filesystem_error);
    const auto writes = std::filesystem::perms::owner_write |
        std::filesystem::perms::group_write | std::filesystem::perms::others_write;
    return !filesystem_error && (status.permissions() & writes) == std::filesystem::perms::none;
}

rm_control::modules::trajectory::VendorDragTrajectoryParseResult CreateForwardFixture(
    const std::string &filename,
    const std::string &content = ForwardCrLfContent()) {
    const auto path = TestOutputRoot() / filename;
    if (!WriteBinaryFile(path, content)) {
        std::cerr << "[FAIL] failed to write forward fixture " << path << '\n';
        return {};
    }
    const auto parsed =
        rm_control::modules::trajectory::ParseVendorDragTrajectoryJsonLines(path, 100);
    if (!parsed.ok) {
        std::cerr << "[FAIL] failed to parse forward fixture: "
                  << parsed.error_message << '\n';
        return {};
    }
    if (!SetReadOnly(path)) {
        std::cerr << "[FAIL] failed to protect forward fixture " << path << '\n';
        return {};
    }
    return parsed.value;
}

std::size_t DirectoryEntryCount(const std::filesystem::path &path) {
    std::error_code filesystem_error;
    std::size_t count = 0U;
    for (std::filesystem::directory_iterator iterator(path, filesystem_error), end;
         !filesystem_error && iterator != end;
         iterator.increment(filesystem_error)) {
        ++count;
    }
    return filesystem_error ? static_cast<std::size_t>(-1) : count;
}

void TestCreatesExactReadOnlyReverseWithoutOverwrite(TestContext *context) {
    const auto forward = CreateForwardFixture("success_forward.txt");
    const auto output_root = TestOutputRoot() / "successful_artifacts";
    const auto first = rm_control::modules::trajectory::
        CreateReadOnlyReversedVendorTrajectoryWithoutOverwrite(forward, output_root);
    context->Expect(first.ok, "valid read-only source should create a reversed artifact");
    if (!first.ok) {
        return;
    }
    context->Expect(first.value.source_file != forward.source_file,
                    "reversed artifact must not reuse or overwrite the source path");
    context->Expect(ReadBinaryFile(first.value.source_file) == ReversedCrLfContent(),
                    "generated file must preserve CRLF and reverse exact physical lines");
    context->Expect(HasNoWritePermission(first.value.source_file),
                    "generated reversed trajectory must have no write bits");
    context->Expect(ReadBinaryFile(forward.source_file) == ForwardCrLfContent(),
                    "creating a reverse must leave the source byte-for-byte unchanged");
    context->Expect(first.value.samples.front().joints_deg ==
                        forward.samples.back().joints_deg &&
                    first.value.samples.back().joints_deg ==
                        forward.samples.front().joints_deg,
                    "parsed reversed endpoints must exactly invert forward endpoints");

    const auto second = rm_control::modules::trajectory::
        CreateReadOnlyReversedVendorTrajectoryWithoutOverwrite(forward, output_root);
    context->Expect(second.ok && second.value.source_file != first.value.source_file,
                    "a second call must allocate a unique path without overwriting the first");
    context->Expect(ReadBinaryFile(first.value.source_file) == ReversedCrLfContent(),
                    "a later reverse must not change the first generated artifact");
}

void TestRejectsWritableAndTamperedSources(TestContext *context) {
    const auto writable_path = TestOutputRoot() / "writable_forward.txt";
    context->Expect(WriteBinaryFile(writable_path, ForwardCrLfContent()),
                    "writable source fixture should be created");
    const auto writable =
        rm_control::modules::trajectory::ParseVendorDragTrajectoryJsonLines(writable_path, 100);
    const auto writable_result = rm_control::modules::trajectory::
        CreateReadOnlyReversedVendorTrajectoryWithoutOverwrite(
            writable.value, TestOutputRoot() / "writable_rejection");
    context->Expect(!writable_result.ok &&
                        writable_result.error_message.find("read-only") != std::string::npos,
                    "a writable source must fail before creating a reverse");

    auto tampered = CreateForwardFixture("tampered_forward.txt");
    std::error_code filesystem_error;
    std::filesystem::permissions(
        tampered.source_file, std::filesystem::perms::owner_write,
        std::filesystem::perm_options::add, filesystem_error);
    std::string changed = ForwardCrLfContent();
    changed[changed.find("1000") + 3U] = '1';
    context->Expect(!filesystem_error && WriteBinaryFile(tampered.source_file, changed) &&
                        SetReadOnly(tampered.source_file),
                    "tampered source fixture should retain stale parsed metadata");
    const auto tampered_result = rm_control::modules::trajectory::
        CreateReadOnlyReversedVendorTrajectoryWithoutOverwrite(
            tampered, TestOutputRoot() / "tampered_rejection");
    context->Expect(!tampered_result.ok &&
                        tampered_result.error_message.find("SHA-256") != std::string::npos,
                    "same-size source tampering must be rejected by SHA-256");
}

void TestRejectsSymbolicLinkSource(TestContext *context) {
    const auto parsed = CreateForwardFixture("symlink_target.txt");
    const auto symbolic_link = TestOutputRoot() / "symlink_source.txt";
    std::error_code filesystem_error;
    std::filesystem::create_symlink(parsed.source_file, symbolic_link, filesystem_error);
    auto linked_metadata = parsed;
    linked_metadata.source_file = symbolic_link;
    const auto result = rm_control::modules::trajectory::
        CreateReadOnlyReversedVendorTrajectoryWithoutOverwrite(
            linked_metadata, TestOutputRoot() / "symlink_rejection");
    context->Expect(!filesystem_error && !result.ok &&
                        result.error_message.find("non-symlink regular file") != std::string::npos,
                    "symbolic-link source must fail the regular-file check");
}

void TestFailureCleansOnlyNewArtifact(TestContext *context) {
    auto inconsistent = CreateForwardFixture("inconsistent_samples_forward.txt");
    inconsistent.samples.front().joints_deg.front() = 99.0F;
    const auto output_root = TestOutputRoot() / "cleanup_scope";
    std::error_code filesystem_error;
    std::filesystem::create_directories(output_root, filesystem_error);
    const auto sentinel = output_root / "preexisting_sentinel.txt";
    context->Expect(!filesystem_error && WriteBinaryFile(sentinel, "keep"),
                    "cleanup test sentinel should be created");
    const std::size_t entries_before = DirectoryEntryCount(output_root);

    const auto result = rm_control::modules::trajectory::
        CreateReadOnlyReversedVendorTrajectoryWithoutOverwrite(inconsistent, output_root);

    context->Expect(!result.ok && result.error_message.find("sample") != std::string::npos,
                    "inconsistent in-memory samples must fail after output verification");
    context->Expect(DirectoryEntryCount(output_root) == entries_before &&
                        ReadBinaryFile(sentinel) == "keep",
                    "failure cleanup must remove only this call's new artifact");
    context->Expect(ReadBinaryFile(inconsistent.source_file) == ForwardCrLfContent(),
                    "failure cleanup must never remove or modify the forward source");
}

void TestDefaultDirectoryIsDedicated(TestContext *context) {
    const auto path =
        rm_control::modules::trajectory::DefaultReversedVendorTrajectoryDirectory();
    context->Expect(path.filename() == "reversed_vendor_trajectories",
                    "default reverse directory must be dedicated to derived artifacts");
}

} // namespace

int main() {
    TestContext context;
    TestCreatesExactReadOnlyReverseWithoutOverwrite(&context);
    TestRejectsWritableAndTamperedSources(&context);
    TestRejectsSymbolicLinkSource(&context);
    TestFailureCleansOnlyNewArtifact(&context);
    TestDefaultDirectoryIsDedicated(&context);

    std::error_code cleanup_error;
    std::filesystem::remove_all(TestOutputRoot(), cleanup_error);
    if (context.failures == 0) {
        std::cout << "[PASS] rm_reversed_vendor_trajectory_store_tests\n";
        return 0;
    }
    std::cerr << context.failures << " test(s) failed\n";
    return 1;
}
