// agent C1: host test of native/src/core/GameCacheCore.cpp (the part of the
// client's cache manager that decides what is good and what is not).
//
//   clang++ -std=c++17 -O2 -I native/src/core api/tests/gamecache_test.cpp \
//           native/src/core/GameCacheCore.cpp -o gamecache_test
//   gamecache_test api/cache_manifest.json C:/cachevarf [sample.zip]
//
// Checks: SHA-256 / CRC-32 test vectors, the manifest parses, every Nth file
// of the real cache hashes to exactly what the manifest (made by the Python
// generator with hashlib / zlib) says, the local index round-trips, path
// safety, URL building, and the zip directory reader on a real per-file zip
// from S3.

#include "GameCacheCore.h"

#include <stdio.h>
#include <string.h>
#include <string>

using namespace GameCacheCore;

static int gFail = 0;
#define CHECK(cond) do { if(!(cond)){ printf("FAIL %s:%d  %s\n", __FILE__, __LINE__, #cond); gFail++; } } while(0)

int main(int argc, char **argv)
{
	// --- vectors
	{
		Sha256 s;
		s.Update("abc", 3);
		CHECK(s.FinalHex() == "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad");
		Sha256 e;
		CHECK(e.FinalHex() == "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
		std::string million(1000000, 'a');
		Sha256 m;
		m.Update(million.data(), 333333);
		m.Update(million.data() + 333333, million.size() - 333333);
		CHECK(m.FinalHex() == "cdc76e5c9914fb9281a1c7e284d73e67f1809a48a497200e046d39ccc7112cd0");
		CHECK(Crc32Update(0, "123456789", 9) == 0xCBF43926u);
		uint32_t c = Crc32Update(0, "1234", 4);
		c = Crc32Update(c, "56789", 5);
		CHECK(c == 0xCBF43926u);
		uint32_t v;
		CHECK(ParseCrc32Hex("cbf43926", v) && v == 0xCBF43926u);
		CHECK(!ParseCrc32Hex("xyz", v));
	}
	// --- JSON
	{
		JsonValue j;
		std::string err;
		const char *t = "{\"a\":[1,-2,3.5e2,true,false,null],\"s\":\"\\u0416\\n\\\"q\\\"\",\"big\":4618098599}";
		CHECK(JsonParse(t, strlen(t), j, &err));
		CHECK(j.Get("a") && j.Get("a")->arr.size() == 6);
		CHECK(j.Get("a")->arr[1].AsInt() == -2);
		CHECK(j.Get("a")->arr[2].num == 350.0);
		CHECK(j.GetString("s") == "\xD0\x96\n\"q\"");
		CHECK(j.GetInt("big") == 4618098599ll);
		const char *bad = "{\"a\":[1,2}";
		CHECK(!JsonParse(bad, strlen(bad), j, &err));
		std::string out;
		JsonAppendString(out, "a\"b\\c\n\x01");
		CHECK(out == "\"a\\\"b\\\\c\\n\\u0001\"");
	}
	// --- paths / urls
	{
		CHECK(IsSafeRelativePath("data/timecyc.dat"));
		CHECK(!IsSafeRelativePath("../etc/passwd"));
		CHECK(!IsSafeRelativePath("data/../../x"));
		CHECK(!IsSafeRelativePath("/abs"));
		CHECK(!IsSafeRelativePath("a\\b"));
		CHECK(!IsSafeRelativePath("a//b"));
		CHECK(!IsSafeRelativePath("C:/x"));
		CHECK(UrlJoin("https://h/zip/", "a b/c+d.txd") == "https://h/zip/a%20b/c%2Bd.txd");
	}

	if(argc < 3){
		printf("(no manifest/cache given: only the built-in checks ran)\n");
		printf(gFail ? "FAILED %d\n" : "OK\n", gFail);
		return gFail ? 1 : 0;
	}

	// --- the real manifest
	std::string text;
	CHECK(ReadWholeFile(argv[1], text));
	Manifest m;
	std::string err;
	bool parsed = ParseManifest(text.data(), text.size(), m, &err);
	CHECK(parsed);
	if(!parsed){ printf("manifest: %s\n", err.c_str()); return 1; }
	printf("manifest v%lld: %d files, %lld bytes, layout %d\n", (long long)m.version, (int)m.files.size(),
	       (long long)m.totalSize, (int)m.layout);
	CHECK(m.files.size() > 1000);

	const ManifestEntry *timecyc = nullptr;
	for(size_t i = 0; i < m.files.size(); i++)
		if(m.files[i].path == "data/timecyc.dat") timecyc = &m.files[i];
	CHECK(timecyc != nullptr);
	if(timecyc){
		DownloadSource s = ResolveSource(m, *timecyc);
		CHECK(s.kind == SOURCE_FILEZIP);
		CHECK(s.url == "https://s3mobile.firsts3.ru/gamecache/zip/data/timecyc.dat.zip");
		CHECK(s.zipEntry == "timecyc.dat");
		CHECK(s.expectedBytes == timecyc->zipSize);
	}

	// every 7th file + all under 64 KB: hash with the client's code
	std::string root = argv[2];
	int hashed = 0;
	long long bytes = 0;
	for(size_t i = 0; i < m.files.size(); i++){
		const ManifestEntry &e = m.files[i];
		if(e.mutableFile) continue;
		if(i % 7 != 0 && e.size > 65536) continue;
		int64_t size = 0;
		std::string sha;
		uint32_t crc = 0;
		bool ok = HashFile(root + "/" + e.path, size, sha, crc, nullptr, nullptr);
		CHECK(ok);
		if(!ok) continue;
		if(size != e.size || sha != e.sha256 || !e.hasCrc || crc != e.crc32){
			printf("MISMATCH %s\n", e.path.c_str());
			gFail++;
		}
		hashed++;
		bytes += size;
	}
	printf("hashed %d files (%lld bytes) - all match the manifest\n", hashed, bytes);

	// --- index round trip
	{
		LocalIndex a, b;
		IndexRecord r;
		r.size = 51432; r.mtime = 1740000000; r.sha256 = timecyc ? timecyc->sha256 : "x"; r.crc32 = 0xfc9eba88;
		a.files["data/timecyc.dat"] = r;
		r.size = 5; a.files["Text/russian.gxt"] = r;
		std::string p = std::string(argv[1]) + ".indextest";
		CHECK(a.Save(p));
		CHECK(b.Load(p));
		CHECK(b.files.size() == 2);
		CHECK(b.files["data/timecyc.dat"].crc32 == 0xfc9eba88u);
		CHECK(b.files["data/timecyc.dat"].mtime == 1740000000);
		CHECK(b.files["data/timecyc.dat"].sha256 == r.sha256);
		remove(p.c_str());
	}

	// --- a real per-file zip from S3
	if(argc >= 4 && timecyc){
		ZipEntryInfo zi;
		bool ok = ZipFindEntry(argv[3], "timecyc.dat", zi, &err);
		CHECK(ok);
		if(ok){
			CHECK(zi.method == 8);
			CHECK(zi.uncompressedSize == timecyc->size);
			CHECK(zi.crc32 == timecyc->crc32);
			CHECK(zi.dataOffset == 30 + (int64_t)strlen("timecyc.dat") || zi.dataOffset > 30);
			printf("zip: %s method %d csize %lld usize %lld data@%lld\n", zi.name.c_str(), zi.method,
			       (long long)zi.compressedSize, (long long)zi.uncompressedSize, (long long)zi.dataOffset);
		}else
			printf("zip: %s\n", err.c_str());
		// case-insensitive fallback + single-entry fallback
		CHECK(ZipFindEntry(argv[3], "TIMECYC.DAT", zi, &err));
		CHECK(ZipFindEntry(argv[3], "other.name", zi, &err));
	}

	printf(gFail ? "FAILED %d\n" : "OK\n", gFail);
	return gFail ? 1 : 0;
}
