#include "rm_control/config/simple_json.h"
#include "rm_control/modules/trajectory/raw_trajectory_store.h"
#include "rm_control/modules/trajectory/sha256_file_checksum.h"

#include <filesystem>
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>

#ifndef RM_TEST_OUTPUT_DIR
#error "RM_TEST_OUTPUT_DIR must be defined so tests cannot write into the source tree"
#endif

namespace {

namespace trajectory = rm_control::modules::trajectory;

class RawTrajectoryStoreTestSuite {
public:
    int Run() {
        std::error_code filesystem_error;
        std::filesystem::create_directories(TestOutputRoot(), filesystem_error);
        Expect(!filesystem_error,
               "test output root should be created under RM_TEST_OUTPUT_DIR: " +
                   filesystem_error.message());

        TestSanitizeSessionName();
        TestSha256StandardVectors();
        TestUniqueSessionLayoutDoesNotOverwrite();
        TestFinalizeSuccessfulVendorArtifact();
        TestFinalizeVendorSaveFailureStillWritesManifest();
        return failures_;
    }

private:
    int failures_ = 0;

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

    std::filesystem::path TestOutputRoot() {
        return std::filesystem::path(RM_TEST_OUTPUT_DIR) / "raw_trajectory_store_tests";
    }

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

    std::string ReadTextFile(const std::filesystem::path &path) {
        std::ifstream input(path);
        std::ostringstream contents;
        contents << input.rdbuf();
        return contents.str();
    }

    trajectory::RawTrajectorySessionPaths CreateTestSession(const std::string &group,
                                                            const std::string &name) {
        const auto result = trajectory::CreateUniqueRawTrajectorySession(
            TestOutputRoot() / group, name);
        Expect(result.ok, "unique raw trajectory session should be created: " +
                              result.error_message);
        return result.value;
    }

    void TestSanitizeSessionName() {
        Expect(trajectory::SanitizeRawTrajectorySessionName("  flight::path // alpha  ") ==
                   "flight_path_alpha",
               "illegal character runs should collapse to one underscore");
        Expect(trajectory::SanitizeRawTrajectorySessionName("con") == "trajectory_con",
               "Windows reserved names should receive a safe prefix");
        Expect(trajectory::SanitizeRawTrajectorySessionName("飞行轨迹") == "trajectory",
               "a name without ASCII filename characters should use the fallback");
        Expect(trajectory::SanitizeRawTrajectorySessionName(std::string(80U, 'a')).size() == 64U,
               "sanitized session names should be limited to 64 characters");
    }

    void TestSha256StandardVectors() {
        const auto paths = CreateTestSession("sha256", "standard vectors");
        if (paths.directory.empty()) {
            return;
        }

        const std::filesystem::path empty_file = paths.directory / "empty.bin";
        const std::filesystem::path abc_file = paths.directory / "abc.bin";
        Expect(WriteBinaryFile(empty_file, ""), "empty SHA-256 vector should be written");
        Expect(WriteBinaryFile(abc_file, "abc"), "abc SHA-256 vector should be written");

        const auto empty_digest = trajectory::CalculateFileSha256(empty_file);
        Expect(empty_digest.ok, "empty file SHA-256 should succeed");
        if (empty_digest.ok) {
            Expect(empty_digest.value ==
                       "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
                   "empty file SHA-256 should match the standard vector");
        }

        const auto abc_digest = trajectory::CalculateFileSha256(abc_file);
        Expect(abc_digest.ok, "abc SHA-256 should succeed");
        if (abc_digest.ok) {
            Expect(abc_digest.value ==
                       "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
                   "abc SHA-256 should match the standard vector");
        }
    }

    void TestUniqueSessionLayoutDoesNotOverwrite() {
        const auto first = CreateTestSession("unique", "same / session");
        const auto second = CreateTestSession("unique", "same / session");
        if (first.directory.empty() || second.directory.empty()) {
            return;
        }

        Expect(first.directory != second.directory,
               "two reservations for the same name should use different directories");
        Expect(std::filesystem::is_directory(first.directory),
               "first unique session directory should exist");
        Expect(std::filesystem::is_directory(second.directory),
               "second unique session directory should exist");
        Expect(first.raw_trajectory.filename() == "raw_trajectory.txt",
               "raw trajectory filename should be fixed");
        Expect(first.synchronized_csv.filename() == "synchronized_samples.csv",
               "synchronized sample filename should be fixed");
        Expect(first.manifest.filename() == "manifest.json",
               "manifest filename should be fixed");
        Expect(first.directory.parent_path().filename() == "sessions",
               "session directories should be placed under the sessions folder");
    }

    void TestFinalizeSuccessfulVendorArtifact() {
        const auto paths = CreateTestSession("finalize_success", "RM75 raw success");
        if (paths.directory.empty()) {
            return;
        }
        Expect(WriteBinaryFile(paths.raw_trajectory, "abc"),
               "raw vendor trajectory should be written by the test fixture");
        Expect(WriteBinaryFile(paths.synchronized_csv, "sample_index,timestamp\n"),
               "synchronized CSV should be written by the test fixture");

        trajectory::RawTrajectoryManifestMetadata metadata;
        metadata.robot_model = "RM75-6F";
        metadata.sdk_version = "RM_API2 test";
        metadata.firmware_version = "controller test";
        metadata.trajectory_point_count = 12U;
        metadata.recorded_sample_count = 2U;
        metadata.vendor_save_api_called = true;
        metadata.vendor_save_return_code = 0;
        metadata.synchronized_csv_write_succeeded = true;

        const auto result = trajectory::FinalizeRawTrajectorySession(paths, metadata);
        Expect(result.ok, "successful raw artifact should be finalized: " +
                              result.error_message);
        if (!result.ok) {
            return;
        }

        Expect(result.value.raw_file_present, "successful raw file should be marked present");
        Expect(result.value.raw_file_size_bytes == 3U, "raw file size should be recorded");
        Expect(result.value.raw_file_sha256 ==
                   "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
               "raw file SHA-256 should be recorded");
        Expect(std::filesystem::is_regular_file(paths.manifest),
               "manifest should be atomically published");
        Expect(!std::filesystem::exists(paths.directory / "manifest.json.tmp"),
               "temporary manifest should not remain after publication");

        const std::string manifest = ReadTextFile(paths.manifest);
        const auto parsed_manifest = rm_control::config::parseJson(manifest);
        Expect(parsed_manifest.ok && parsed_manifest.value.isObject(),
               "manifest should be valid JSON");
        Expect(manifest.find("\"robot_model\": \"RM75-6F\"") != std::string::npos,
               "manifest should record robot model");
        Expect(manifest.find("\"trajectory_point_count\": 12") != std::string::npos,
               "manifest should record trajectory point count");
        Expect(manifest.find("\"raw_file_size_bytes\": 3") != std::string::npos,
               "manifest should record raw file size");
        Expect(manifest.find("\"save_interface_return_code\": 0") != std::string::npos,
               "manifest should record vendor save return code");
        Expect(manifest.find("\"synchronized_timestamp_is_derived\": false") !=
                   std::string::npos,
               "manifest should record the synchronized timestamp provenance");

        constexpr std::filesystem::perms kAllWritePermissions =
            std::filesystem::perms::owner_write | std::filesystem::perms::group_write |
            std::filesystem::perms::others_write;
        const std::filesystem::perms raw_permissions =
            std::filesystem::status(paths.raw_trajectory).permissions();
        Expect((raw_permissions & kAllWritePermissions) == std::filesystem::perms::none,
               "finalized raw trajectory should have all write permissions removed");

        const auto second_finalize = trajectory::FinalizeRawTrajectorySession(paths, metadata);
        Expect(!second_finalize.ok, "finalization should refuse to overwrite an existing manifest");
        Expect(ReadTextFile(paths.manifest) == manifest,
               "refused second finalization should leave the original manifest unchanged");
    }

    void TestFinalizeVendorSaveFailureStillWritesManifest() {
        const auto paths = CreateTestSession("finalize_failure", "RM75 raw failure");
        if (paths.directory.empty()) {
            return;
        }

        trajectory::RawTrajectoryManifestMetadata metadata;
        metadata.robot_model = "RM75-6F";
        metadata.sdk_version = "RM_API2 test";
        metadata.firmware_version = "controller test";
        metadata.recorded_sample_count = 5U;
        metadata.vendor_save_api_called = true;
        metadata.vendor_save_return_code = -2;
        metadata.vendor_save_error_message = "mock timeout";

        const auto result = trajectory::FinalizeRawTrajectorySession(paths, metadata);
        Expect(result.ok, "failed vendor save attempt should still finalize its manifest: " +
                              result.error_message);
        if (!result.ok) {
            return;
        }
        Expect(!result.value.raw_file_present,
               "failed vendor save without output should be marked absent");
        Expect(result.value.raw_file_size_bytes == 0U,
               "absent raw file should have zero recorded size");
        Expect(result.value.raw_file_sha256.empty(),
               "absent raw file should have an empty SHA-256 field");

        const std::string manifest = ReadTextFile(paths.manifest);
        Expect(manifest.find("\"raw_file_present\": false") != std::string::npos,
               "failure manifest should record that the raw file is absent");
        Expect(manifest.find("\"save_interface_return_code\": -2") != std::string::npos,
               "failure manifest should preserve the vendor return code");
    }

};

} // namespace

int main() {
    RawTrajectoryStoreTestSuite suite;
    const int failures = suite.Run();
    if (failures == 0) {
        std::cout << "[PASS] rm_raw_trajectory_store_tests\n";
        return 0;
    }

    std::cerr << failures << " test(s) failed\n";
    return 1;
}
