/**
 * @file package_archive_test.cpp
 * @brief Tests for the Package Archive library.
 *
 * Tests the Ogre::PackageArchive implementation including:
 * - Creating packages
 * - Adding files
 * - Listing files
 * - Finding files
 * - Opening/reading files
 * - Removing files
 * - Extracting files
 * - Error handling
 * - Edge cases (empty packages, duplicate files, etc.)
 */

#include "OgrePackageArchive.h"
#include "OgreException.h"

#include <cassert>
#include <cstdio>
#include <cstring>
#include <fstream>
#include <iostream>
#include <string>
#include <vector>

// Test utilities
static int testsPassed = 0;
static int testsFailed = 0;

#define TEST(name)                     \
	static void test_##name();     \
	struct TestRegister_##name {   \
		TestRegister_##name()  \
		{                      \
			test_##name(); \
		}                      \
	} testReg_##name;              \
	static void test_##name()

#define ASSERT(cond)                                                         \
	do {                                                                 \
		if (!(cond)) {                                               \
			std::cerr << "FAIL: " << #cond << " at " << __FILE__ \
				  << ":" << __LINE__ << std::endl;           \
			testsFailed++;                                       \
			return;                                              \
		}                                                            \
	} while (0)

#define ASSERT_EQ(a, b)                                                        \
	do {                                                                   \
		if ((a) != (b)) {                                              \
			std::cerr << "FAIL: " << #a << " == " << #b << " at "  \
				  << __FILE__ << ":" << __LINE__ << " ("       \
				  << (a) << " != " << (b) << ")" << std::endl; \
			testsFailed++;                                         \
			return;                                                \
		}                                                              \
	} while (0)

#define ASSERT_THROW(expr, exc_code)                                            \
	do {                                                                   \
		bool caught = false;                                           \
		try {                                                          \
			expr;                                                  \
		} catch (const Ogre::Exception &) {                            \
			caught = true;                                         \
		} catch (...) {                                                \
		}                                                              \
		if (!caught) {                                                 \
			std::cerr << "FAIL: expected Ogre::Exception at "       \
				  << __FILE__ << ":" << __LINE__ << std::endl; \
			testsFailed++;                                         \
			return;                                                \
		}                                                              \
	} while (0)

// Helper: create a test file on disk
static void createTestFile(const std::string &path, const std::string &content)
{
	std::ofstream f(path.c_str(), std::ios::binary);
	f.write(content.data(), content.size());
	f.close();
}

// Helper: read a file from disk
static std::string readTestFile(const std::string &path)
{
	std::ifstream f(path.c_str(), std::ios::binary | std::ios::ate);
	if (!f.is_open())
		return "";
	std::streamsize size = f.tellg();
	f.seekg(0, std::ios::beg);
	std::string content(static_cast<size_t>(size), '\0');
	f.read(&content[0], size);
	return content;
}

// Helper: remove a file
static void removeTestFile(const std::string &path)
{
	std::remove(path.c_str());
}

// -----------------------------------------------------------------------
// Test: Create an empty package
// -----------------------------------------------------------------------
TEST(createEmptyPackage)
{
	const std::string pkgPath = "/tmp/test_empty.package";

	// Create empty package via the factory
	Ogre::PackageArchive archive(pkgPath, "Package");
	archive.load();

	// Should be loaded and writable
	ASSERT_EQ(archive.isReadOnly(), false);

	// List should be empty
	auto files = archive.list(true, false);
	ASSERT(files);
	ASSERT_EQ(files->size(), 0u);

	archive.unload();
	removeTestFile(pkgPath);

	std::cout << "  PASS: createEmptyPackage" << std::endl;
	testsPassed++;
}

// -----------------------------------------------------------------------
// Test: Create package with files
// -----------------------------------------------------------------------
TEST(createPackageWithFiles)
{
	const std::string pkgPath = "/tmp/test_with_files.package";
	const std::string data1 = "Hello World";
	const std::string data2 = "Test file 2 content here";

	Ogre::PackageArchive archive(pkgPath, "Package");
	archive.load();

	archive.addFile("file1.txt", data1.data(), data1.size());
	archive.addFile("subdir/file2.txt", data2.data(), data2.size());

	// List all files (recursive)
	auto files = archive.list(true, false);
	ASSERT(files);
	ASSERT_EQ(files->size(), 2u);

	// List non-recursive should only show top-level
	auto topFiles = archive.list(false, false);
	ASSERT(topFiles);
	ASSERT_EQ(topFiles->size(), 1u);
	ASSERT_EQ((*topFiles)[0], "file1.txt");

	// List file info
	auto infoList = archive.listFileInfo(true, false);
	ASSERT(infoList);
	ASSERT_EQ(infoList->size(), 2u);

	archive.unload();
	removeTestFile(pkgPath);

	std::cout << "  PASS: createPackageWithFiles" << std::endl;
	testsPassed++;
}

// -----------------------------------------------------------------------
// Test: Open and read files from package
// -----------------------------------------------------------------------
TEST(openAndReadFiles)
{
	const std::string pkgPath = "/tmp/test_read.package";
	const std::string data1 = "Content of file 1";
	const std::string data2 = "Content of file 2 with more data";

	Ogre::PackageArchive archive(pkgPath, "Package");
	archive.load();

	archive.addFile("readme.txt", data1.data(), data1.size());
	archive.addFile("data.bin", data2.data(), data2.size());

	// Open and read file 1
	auto stream1 = archive.open("readme.txt");
	ASSERT(stream1);
	ASSERT_EQ(stream1->size(), data1.size());

	std::string read1(static_cast<size_t>(stream1->size()), '\0');
	stream1->read(&read1[0], static_cast<size_t>(stream1->size()));
	ASSERT_EQ(read1, data1);

	// Open and read file 2
	auto stream2 = archive.open("data.bin");
	ASSERT(stream2);
	ASSERT_EQ(stream2->size(), data2.size());

	std::string read2(static_cast<size_t>(stream2->size()), '\0');
	stream2->read(&read2[0], static_cast<size_t>(stream2->size()));
	ASSERT_EQ(read2, data2);

	archive.unload();
	removeTestFile(pkgPath);

	std::cout << "  PASS: openAndReadFiles" << std::endl;
	testsPassed++;
}

// -----------------------------------------------------------------------
// Test: Find files by pattern
// -----------------------------------------------------------------------
TEST(findFiles)
{
	const std::string pkgPath = "/tmp/test_find.package";

	Ogre::PackageArchive archive(pkgPath, "Package");
	archive.load();

	archive.addFile("alpha.txt", "a", 1);
	archive.addFile("beta.txt", "b", 1);
	archive.addFile("gamma.dat", "c", 1);
	archive.addFile("subdir/delta.txt", "d", 1);

	// Find by exact name
	auto found = archive.find("beta.txt", false, false);
	ASSERT(found);
	ASSERT_EQ(found->size(), 1u);
	ASSERT_EQ((*found)[0], "beta.txt");

	// Find by wildcard
	auto wildFound = archive.find("*.txt", true, false);
	ASSERT(wildFound);
	ASSERT_EQ(wildFound->size(), 3u);

	// Find by wildcard non-recursive
	auto wildTop = archive.find("*.txt", false, false);
	ASSERT(wildTop);
	ASSERT_EQ(wildTop->size(), 2u);

	// Find non-existent
	auto notFound = archive.find("nonexistent.txt", false, false);
	ASSERT(notFound);
	ASSERT_EQ(notFound->size(), 0u);

	archive.unload();
	removeTestFile(pkgPath);

	std::cout << "  PASS: findFiles" << std::endl;
	testsPassed++;
}

// -----------------------------------------------------------------------
// Test: Exists
// -----------------------------------------------------------------------
TEST(exists)
{
	const std::string pkgPath = "/tmp/test_exists.package";

	Ogre::PackageArchive archive(pkgPath, "Package");
	archive.load();

	archive.addFile("present.txt", "data", 4);

	ASSERT(archive.exists("present.txt"));
	ASSERT(!archive.exists("missing.txt"));

	archive.unload();
	removeTestFile(pkgPath);

	std::cout << "  PASS: exists" << std::endl;
	testsPassed++;
}

// -----------------------------------------------------------------------
// Test: Remove files
// -----------------------------------------------------------------------
TEST(removeFiles)
{
	const std::string pkgPath = "/tmp/test_remove.package";

	Ogre::PackageArchive archive(pkgPath, "Package");
	archive.load();

	archive.addFile("keep.txt", "keep", 4);
	archive.addFile("remove.txt", "remove", 6);

	ASSERT_EQ(archive.list(true, false)->size(), 2u);

	archive.remove("remove.txt");

	ASSERT_EQ(archive.list(true, false)->size(), 1u);
	ASSERT(archive.exists("keep.txt"));
	ASSERT(!archive.exists("remove.txt"));

	archive.unload();
	removeTestFile(pkgPath);

	std::cout << "  PASS: removeFiles" << std::endl;
	testsPassed++;
}

// -----------------------------------------------------------------------
// Test: Extract files
// -----------------------------------------------------------------------
TEST(extractFiles)
{
	const std::string pkgPath = "/tmp/test_extract.package";
	const std::string data = "Extract this content!";
	const std::string extractPath = "/tmp/extracted_test.txt";

	Ogre::PackageArchive archive(pkgPath, "Package");
	archive.load();

	archive.addFile("extract_me.txt", data.data(), data.size());

	bool result = archive.extractFile("extract_me.txt", extractPath);
	ASSERT(result);

	std::string extracted = readTestFile(extractPath);
	ASSERT_EQ(extracted, data);

	removeTestFile(extractPath);
	archive.unload();
	removeTestFile(pkgPath);

	std::cout << "  PASS: extractFiles" << std::endl;
	testsPassed++;
}

// -----------------------------------------------------------------------
// Test: Add file from disk
// -----------------------------------------------------------------------
TEST(addFileFromDisk)
{
	const std::string pkgPath = "/tmp/test_add_disk.package";
	const std::string srcPath = "/tmp/source_file.txt";
	const std::string srcData = "File from disk content";

	createTestFile(srcPath, srcData);

	Ogre::PackageArchive archive(pkgPath, "Package");
	archive.load();

	archive.addFileFromDisk("stored_file.txt", srcPath);

	ASSERT(archive.exists("stored_file.txt"));

	auto stream = archive.open("stored_file.txt");
	ASSERT(stream);
	ASSERT_EQ(stream->size(), srcData.size());

	removeTestFile(srcPath);
	archive.unload();
	removeTestFile(pkgPath);

	std::cout << "  PASS: addFileFromDisk" << std::endl;
	testsPassed++;
}

// -----------------------------------------------------------------------
// Test: Persistence (save and reload)
// -----------------------------------------------------------------------
TEST(persistence)
{
	const std::string pkgPath = "/tmp/test_persist.package";
	const std::string data1 = "Persistent data 1";
	const std::string data2 = "Persistent data 2";

	// Create and add files
	{
		Ogre::PackageArchive archive(pkgPath, "Package");
		archive.load();
		archive.addFile("persist1.txt", data1.data(), data1.size());
		archive.addFile("persist2.txt", data2.data(), data2.size());
		archive.unload();
	}

	// Re-open and verify
	{
		Ogre::PackageArchive archive(pkgPath, "Package");
		archive.load();

		ASSERT_EQ(archive.list(true, false)->size(), 2u);
		ASSERT(archive.exists("persist1.txt"));
		ASSERT(archive.exists("persist2.txt"));

		auto stream1 = archive.open("persist1.txt");
		ASSERT(stream1);
		std::string read1(static_cast<size_t>(stream1->size()), '\0');
		stream1->read(&read1[0], static_cast<size_t>(stream1->size()));
		ASSERT_EQ(read1, data1);

		auto stream2 = archive.open("persist2.txt");
		ASSERT(stream2);
		std::string read2(static_cast<size_t>(stream2->size()), '\0');
		stream2->read(&read2[0], static_cast<size_t>(stream2->size()));
		ASSERT_EQ(read2, data2);

		archive.unload();
	}

	removeTestFile(pkgPath);

	std::cout << "  PASS: persistence" << std::endl;
	testsPassed++;
}

// -----------------------------------------------------------------------
// Test: Replace existing file
// -----------------------------------------------------------------------
TEST(replaceFile)
{
	const std::string pkgPath = "/tmp/test_replace.package";
	const std::string oldData = "Old content";
	const std::string newData = "New content that is longer";

	Ogre::PackageArchive archive(pkgPath, "Package");
	archive.load();

	archive.addFile("replace.txt", oldData.data(), oldData.size());
	archive.addFile("replace.txt", newData.data(), newData.size());

	// Should only have one file with the new content
	ASSERT_EQ(archive.list(true, false)->size(), 1u);

	auto stream = archive.open("replace.txt");
	ASSERT(stream);
	ASSERT_EQ(stream->size(), newData.size());

	std::string readData(static_cast<size_t>(stream->size()), '\0');
	stream->read(&readData[0], static_cast<size_t>(stream->size()));
	ASSERT_EQ(readData, newData);

	archive.unload();
	removeTestFile(pkgPath);

	std::cout << "  PASS: replaceFile" << std::endl;
	testsPassed++;
}

// -----------------------------------------------------------------------
// Test: Error handling - open non-existent file
// -----------------------------------------------------------------------
TEST(openNonExistent)
{
	const std::string pkgPath = "/tmp/test_noexist.package";

	Ogre::PackageArchive archive(pkgPath, "Package");
	archive.load();

	ASSERT_THROW(archive.open("nonexistent.txt"),
		     Ogre::Exception);

	archive.unload();
	removeTestFile(pkgPath);

	std::cout << "  PASS: openNonExistent" << std::endl;
	testsPassed++;
}

// -----------------------------------------------------------------------
// Test: Error handling - remove non-existent file
// -----------------------------------------------------------------------
TEST(removeNonExistent)
{
	const std::string pkgPath = "/tmp/test_remove_noexist.package";

	Ogre::PackageArchive archive(pkgPath, "Package");
	archive.load();

	ASSERT_THROW(archive.remove("nonexistent.txt"),
		     Ogre::Exception);

	archive.unload();
	removeTestFile(pkgPath);

	std::cout << "  PASS: removeNonExistent" << std::endl;
	testsPassed++;
}

// -----------------------------------------------------------------------
// Test: Binary data integrity
// -----------------------------------------------------------------------
TEST(binaryData)
{
	const std::string pkgPath = "/tmp/test_binary.package";

	// Create binary data with null bytes and all byte values
	std::vector<char> binaryData(256);
	for (int i = 0; i < 256; i++)
		binaryData[i] = static_cast<char>(i);

	Ogre::PackageArchive archive(pkgPath, "Package");
	archive.load();

	archive.addFile("binary.bin", binaryData.data(), binaryData.size());

	auto stream = archive.open("binary.bin");
	ASSERT(stream);
	ASSERT_EQ(stream->size(), binaryData.size());

	std::vector<char> readData(static_cast<size_t>(stream->size()));
	stream->read(readData.data(), stream->size());

	// Verify all bytes
	for (size_t i = 0; i < binaryData.size(); i++) {
		ASSERT_EQ(static_cast<unsigned char>(readData[i]),
			  static_cast<unsigned char>(binaryData[i]));
	}

	archive.unload();
	removeTestFile(pkgPath);

	std::cout << "  PASS: binaryData" << std::endl;
	testsPassed++;
}

// -----------------------------------------------------------------------
// Test: Multiple add/remove cycles
// -----------------------------------------------------------------------
TEST(multipleCycles)
{
	const std::string pkgPath = "/tmp/test_cycles.package";

	Ogre::PackageArchive archive(pkgPath, "Package");
	archive.load();

	// Add files
	archive.addFile("a.txt", "a", 1);
	archive.addFile("b.txt", "b", 1);
	archive.addFile("c.txt", "c", 1);
	ASSERT_EQ(archive.list(true, false)->size(), 3u);

	// Remove one
	archive.remove("b.txt");
	ASSERT_EQ(archive.list(true, false)->size(), 2u);

	// Add another
	archive.addFile("d.txt", "d", 1);
	ASSERT_EQ(archive.list(true, false)->size(), 3u);

	// Remove another
	archive.remove("a.txt");
	ASSERT_EQ(archive.list(true, false)->size(), 2u);

	// Verify remaining
	ASSERT(archive.exists("c.txt"));
	ASSERT(archive.exists("d.txt"));
	ASSERT(!archive.exists("a.txt"));
	ASSERT(!archive.exists("b.txt"));

	archive.unload();
	removeTestFile(pkgPath);

	std::cout << "  PASS: multipleCycles" << std::endl;
	testsPassed++;
}

// -----------------------------------------------------------------------
// Test: Large file
// -----------------------------------------------------------------------
TEST(largeFile)
{
	const std::string pkgPath = "/tmp/test_large.package";

	// Create 1MB of data
	const size_t largeSize = 1024 * 1024;
	std::vector<char> largeData(largeSize);
	for (size_t i = 0; i < largeSize; i++)
		largeData[i] = static_cast<char>(i & 0xFF);

	Ogre::PackageArchive archive(pkgPath, "Package");
	archive.load();

	archive.addFile("large.bin", largeData.data(), largeData.size());

	auto stream = archive.open("large.bin");
	ASSERT(stream);
	ASSERT_EQ(stream->size(), largeSize);

	archive.unload();
	removeTestFile(pkgPath);

	std::cout << "  PASS: largeFile" << std::endl;
	testsPassed++;
}

// -----------------------------------------------------------------------
// Test: getModifiedTime
// -----------------------------------------------------------------------
TEST(modifiedTime)
{
	const std::string pkgPath = "/tmp/test_mtime.package";

	Ogre::PackageArchive archive(pkgPath, "Package");
	archive.load();

	archive.addFile("timestamped.txt", "data", 4);

	time_t mtime = archive.getModifiedTime("timestamped.txt");
	ASSERT(mtime > 0);

	// Non-existent file should return 0 or the package file's mtime
	time_t missingMtime = archive.getModifiedTime("missing.txt");
	ASSERT(missingMtime > 0);

	archive.unload();
	removeTestFile(pkgPath);

	std::cout << "  PASS: modifiedTime" << std::endl;
	testsPassed++;
}

// -----------------------------------------------------------------------
// Test: Factory creation
// -----------------------------------------------------------------------
TEST(factory)
{
	Ogre::PackageArchiveFactory factory;

	const std::string &type = factory.getType();
	ASSERT_EQ(type, "Package");

	Ogre::Archive *archive =
		factory.createInstance("/tmp/test_factory.package", false);
	ASSERT(archive);
	ASSERT_EQ(archive->getType(), "Package");

	delete archive;
	removeTestFile("/tmp/test_factory.package");

	std::cout << "  PASS: factory" << std::endl;
	testsPassed++;
}

// -----------------------------------------------------------------------
// Main
// -----------------------------------------------------------------------
int main()
{
	std::cout << "Package Archive Tests" << std::endl;
	std::cout << "=====================" << std::endl;

	// Run all tests (registered via static constructors)

	std::cout << std::endl;
	std::cout << "Results: " << (testsPassed + testsFailed) << " total, "
		  << testsPassed << " passed, " << testsFailed << " failed"
		  << std::endl;

	return testsFailed > 0 ? 1 : 0;
}
