/***************************************************************
 *
 * Copyright (C) 2024, Pelican Project, Morgridge Institute for Research
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you
 * may not use this file except in compliance with the License.  You may
 * obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 ***************************************************************/

#include "../src/S3Commands.hh"
#include "../src/S3FileSystem.hh"
#include "../src/stl_string_utils.hh"
#include "s3_tests_common.hh"

#include <XrdOuc/XrdOucEnv.hh>
#include <XrdSys/XrdSysError.hh>
#include <gtest/gtest.h>

class TestAmazonRequest : public AmazonRequest {
  public:
	XrdSysLogger log{};
	XrdSysError err{&log, "TestS3CommandsLog"};

	TestAmazonRequest(const std::string &url, const std::string &akf,
					  const std::string &skf, const std::string &bucket,
					  const std::string &object, const std::string &path,
					  int sigVersion)
		: AmazonRequest(url, akf, skf, bucket, object, path, sigVersion, err) {}

	// For getting access to otherwise-protected members
	std::string getHostUrl() const { return hostUrl; }
};

TEST(TestS3URLGeneration, Test1) {
	const std::string serviceUrl = "https://s3-service.com:443";
	const std::string b = "test-bucket";
	const std::string o = "test-object";

	// Test path-style URL generation
	TestAmazonRequest pathReq{serviceUrl, "akf", "skf", b, o, "path", 4};
	std::string generatedHostUrl = pathReq.getHostUrl();
	ASSERT_EQ(generatedHostUrl,
			  "https://s3-service.com:443/test-bucket/test-object")
		<< "generatedURL: " << generatedHostUrl;

	// Test virtual-style URL generation
	TestAmazonRequest virtReq{serviceUrl, "akf", "skf", b, o, "virtual", 4};
	generatedHostUrl = virtReq.getHostUrl();
	ASSERT_EQ(generatedHostUrl,
			  "https://test-bucket.s3-service.com:443/test-object");

	// Test path-style with empty bucket (which we use for exporting an entire
	// endpoint)
	TestAmazonRequest pathReqNoBucket{serviceUrl, "akf",  "skf", "",
									  o,		  "path", 4};
	generatedHostUrl = pathReqNoBucket.getHostUrl();
	ASSERT_EQ(generatedHostUrl, "https://s3-service.com:443/test-object");
}

class FileSystemS3VirtualBucket : public FileSystemFixtureBase {
  protected:
	virtual std::string GetConfig() override {
		return R"(
s3.begin
s3.path_name        /test
s3.bucket_name      genome-browser
s3.service_name     s3.amazonaws.com
s3.region           us-east-1
s3.service_url      https://s3.us-east-1.amazonaws.com
s3.url_style        virtual
s3.end
)";
	}
};

class FileSystemS3VirtualNoBucket : public FileSystemFixtureBase {
  protected:
	virtual std::string GetConfig() override {
		return R"(
s3.begin
s3.path_name        /test
s3.service_name     s3.amazonaws.com
s3.region           us-east-1
s3.service_url      https://s3.us-east-1.amazonaws.com
s3.url_style        virtual
s3.end
)";
	}
};

class FileSystemS3PathBucket : public FileSystemFixtureBase {
  protected:
	virtual std::string GetConfig() override {
		return R"(
s3.begin
s3.path_name        /test
s3.service_name     s3.amazonaws.com
s3.region           us-east-1
s3.bucket_name      genome-browser
s3.service_url      https://s3.us-east-1.amazonaws.com
s3.url_style        path
s3.end
)";
	}
};

class FileSystemS3PathNoBucket : public FileSystemFixtureBase {
  protected:
	virtual std::string GetConfig() override {
		return R"(
s3.begin
s3.path_name        /test
s3.service_name     s3.amazonaws.com
s3.region           us-east-1
s3.service_url      https://s3.us-east-1.amazonaws.com
s3.url_style        path
s3.end
)";
	}
};

// Regression test for when the service_url ends in a `/`
class FileSystemS3PathBucketSlash : public FileSystemFixtureBase {
  protected:
	virtual std::string GetConfig() override {
		return R"(
s3.begin
s3.path_name        /test
s3.service_name     s3.amazonaws.com
s3.region           us-east-1
s3.bucket_name      genome-browser
s3.service_url      https://s3.us-east-1.amazonaws.com/
s3.url_style        path
s3.end
)";
	}
};

void TestDirectoryContents(S3FileSystem &fs, const std::string &dirname) {
	std::unique_ptr<XrdOssDF> dir(fs.newDir());
	ASSERT_TRUE(dir);

	XrdOucEnv env;
	auto rv = dir->Opendir(dirname.c_str(), env);
	ASSERT_EQ(rv, 0);

	struct stat buf;
	ASSERT_EQ(dir->StatRet(&buf), 0);

	std::vector<char> name;
	name.resize(255);

	rv = dir->Readdir(&name[0], 255);
	ASSERT_EQ(rv, 0);
	ASSERT_EQ(std::string(&name[0]), "cellbrowser.json.bak");
	ASSERT_EQ(buf.st_mode & S_IFREG,
			  static_cast<decltype(buf.st_mode)>(S_IFREG));
	ASSERT_EQ(buf.st_size, 672);

	rv = dir->Readdir(&name[0], 255);
	ASSERT_EQ(rv, 0);
	ASSERT_EQ(std::string(&name[0]), "dataset.json");
	ASSERT_EQ(buf.st_mode & S_IFREG,
			  static_cast<decltype(buf.st_mode)>(S_IFREG));
	ASSERT_EQ(buf.st_size, 1847);

	rv = dir->Readdir(&name[0], 255);
	ASSERT_EQ(rv, 0);
	ASSERT_EQ(std::string(&name[0]), "desc.json");
	ASSERT_EQ(buf.st_mode & S_IFREG,
			  static_cast<decltype(buf.st_mode)>(S_IFREG));
	ASSERT_EQ(buf.st_size, 1091);

	rv = dir->Readdir(&name[0], 255);
	ASSERT_EQ(rv, 0);
	ASSERT_EQ(std::string(&name[0]), "all");
	ASSERT_EQ(buf.st_mode & S_IFDIR,
			  static_cast<decltype(buf.st_mode)>(S_IFDIR));

	rv = dir->Readdir(&name[0], 255);
	ASSERT_EQ(rv, 0);
	ASSERT_EQ(std::string(&name[0]), "by-organ");
	ASSERT_EQ(buf.st_mode & S_IFDIR,
			  static_cast<decltype(buf.st_mode)>(S_IFDIR));

	rv = dir->Readdir(&name[0], 255);
	ASSERT_EQ(rv, 0);
	ASSERT_EQ(std::string(&name[0]), "func-compart");
	ASSERT_EQ(buf.st_mode & S_IFDIR,
			  static_cast<decltype(buf.st_mode)>(S_IFDIR));

	rv = dir->Readdir(&name[0], 255);
	ASSERT_EQ(rv, 0);
	ASSERT_EQ(std::string(&name[0]), "");

	ASSERT_EQ(dir->Close(), 0);
}

TEST_F(FileSystemS3VirtualBucket, Create) {
	EXPECT_NO_THROW(
		{ S3FileSystem fs(m_log.get(), m_configfn.c_str(), nullptr); });
}

TEST_F(FileSystemS3VirtualBucket, Stat) {
	S3FileSystem fs(m_log.get(), m_configfn.c_str(), nullptr);
	struct stat buff;
	auto rv = fs.Stat("/test/cells/tabula-sapiens/cellbrowser.json.bak", &buff);
	ASSERT_EQ(rv, 0) << "Failed to stat AWS bucket (" << strerror(-rv) << ")";
}

TEST_F(FileSystemS3VirtualBucket, List) {
	S3FileSystem fs(m_log.get(), m_configfn.c_str(), nullptr);
	TestDirectoryContents(fs, "/test/cells/tabula-sapiens");
}

TEST_F(FileSystemS3VirtualNoBucket, Stat) {
	S3FileSystem fs(m_log.get(), m_configfn.c_str(), nullptr);
	struct stat buff;
	auto rv = fs.Stat(
		"/test/genome-browser/cells/tabula-sapiens/cellbrowser.json.bak",
		&buff);
	ASSERT_EQ(rv, 0) << "Failed to stat AWS bucket (" << strerror(-rv) << ")";
}

TEST_F(FileSystemS3VirtualNoBucket, List) {
	S3FileSystem fs(m_log.get(), m_configfn.c_str(), nullptr);
	TestDirectoryContents(fs, "/test/genome-browser/cells/tabula-sapiens");
}

TEST_F(FileSystemS3PathBucket, Stat) {
	S3FileSystem fs(m_log.get(), m_configfn.c_str(), nullptr);
	struct stat buff;
	auto rv = fs.Stat("/test/cells/tabula-sapiens/cellbrowser.json.bak", &buff);
	ASSERT_EQ(rv, 0) << "Failed to stat AWS bucket (" << strerror(-rv) << ")";
}

TEST_F(FileSystemS3PathBucket, List) {
	S3FileSystem fs(m_log.get(), m_configfn.c_str(), nullptr);
	TestDirectoryContents(fs, "/test/cells/tabula-sapiens");
}

TEST_F(FileSystemS3PathNoBucket, Stat) {
	S3FileSystem fs(m_log.get(), m_configfn.c_str(), nullptr);
	struct stat buff;
	auto rv = fs.Stat(
		"/test/genome-browser/cells/tabula-sapiens/cellbrowser.json.bak",
		&buff);
	ASSERT_EQ(rv, 0) << "Failed to stat AWS bucket (" << strerror(-rv) << ")";
}

TEST_F(FileSystemS3PathNoBucket, List) {
	S3FileSystem fs(m_log.get(), m_configfn.c_str(), nullptr);
	TestDirectoryContents(fs, "/test/genome-browser/cells/tabula-sapiens/");
}

TEST_F(FileSystemS3PathBucketSlash, Stat) {
	S3FileSystem fs(m_log.get(), m_configfn.c_str(), nullptr);
	struct stat buff;
	auto rv = fs.Stat("/test/cells/tabula-sapiens/cellbrowser.json.bak", &buff);
	ASSERT_EQ(rv, 0) << "Failed to stat AWS bucket (" << strerror(-rv) << ")";
}

TEST_F(FileSystemS3PathBucketSlash, List) {
	S3FileSystem fs(m_log.get(), m_configfn.c_str(), nullptr);
	TestDirectoryContents(fs, "/test/cells/tabula-sapiens");
}

// ---------------------------------------------------------------------------
// urlquote / urlunquote round-trip and correctness tests
// ---------------------------------------------------------------------------

TEST(UrlEncoding, QuoteProducesUppercaseHex) {
	// Space should become %20, not %32 (which was the old decimal bug)
	auto result = urlquote("hello world");
	EXPECT_EQ(result, "hello%20world");
}

TEST(UrlEncoding, QuotePreservesUnreservedChars) {
	// Letters, digits, and _.-~/ should pass through unchanged
	std::string unreserved = "abcXYZ019_.-~/";
	EXPECT_EQ(urlquote(unreserved), unreserved);
}

TEST(UrlEncoding, QuoteEncodesSpecialChars) {
	EXPECT_EQ(urlquote("a&b<c>d"), "a%26b%3Cc%3Ed");
}

TEST(UrlEncoding, UnquoteDecodesPercents) {
	EXPECT_EQ(urlunquote("hello%20world"), "hello world");
	EXPECT_EQ(urlunquote("foo%2Fbar"), "foo/bar");
	EXPECT_EQ(urlunquote("%26%3C%3E"), "&<>");
}

TEST(UrlEncoding, UnquoteLowerAndUpperHex) {
	EXPECT_EQ(urlunquote("%2f"), "/");
	EXPECT_EQ(urlunquote("%2F"), "/");
}

TEST(UrlEncoding, UnquotePassesThroughInvalidSequences) {
	// Incomplete percent sequence at end of string
	EXPECT_EQ(urlunquote("abc%2"), "abc%2");
	// Invalid hex chars
	EXPECT_EQ(urlunquote("abc%GG"), "abc%GG");
	// Bare percent
	EXPECT_EQ(urlunquote("100%"), "100%");
}

TEST(UrlEncoding, RoundTrip) {
	std::string original = "path/to/file with spaces & ampersands.txt";
	EXPECT_EQ(urlunquote(urlquote(original)), original);
}

// ---------------------------------------------------------------------------
// ParseListBucketResult tests
// ---------------------------------------------------------------------------

TEST(S3ListParsing, BasicKeysAndPrefixes) {
	std::string xml = R"(<?xml version="1.0" encoding="UTF-8"?>
<ListBucketResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
  <IsTruncated>false</IsTruncated>
  <Contents>
    <Key>testfolder/file1.txt</Key>
    <Size>42</Size>
  </Contents>
  <Contents>
    <Key>testfolder/file2.txt</Key>
    <Size>100</Size>
  </Contents>
  <CommonPrefixes>
    <Prefix>testfolder/subdir/</Prefix>
  </CommonPrefixes>
</ListBucketResult>)";

	std::vector<S3ObjectInfo> objs;
	std::vector<std::string> prefixes;
	std::string ct, err;
	ASSERT_TRUE(
		AmazonS3List::ParseListBucketResult(xml, objs, prefixes, ct, err))
		<< err;
	ASSERT_EQ(objs.size(), 2);
	EXPECT_EQ(objs[0].m_key, "testfolder/file1.txt");
	EXPECT_EQ(objs[0].m_size, 42);
	EXPECT_EQ(objs[1].m_key, "testfolder/file2.txt");
	EXPECT_EQ(objs[1].m_size, 100);
	ASSERT_EQ(prefixes.size(), 1);
	EXPECT_EQ(prefixes[0], "testfolder/subdir/");
	EXPECT_TRUE(ct.empty());
}

TEST(S3ListParsing, DecodesUrlEncodedKeysAndPrefixes) {
	// Simulates encoding-type=url response from S3 where keys/prefixes
	// contain percent-encoded characters
	std::string xml = R"(<?xml version="1.0" encoding="UTF-8"?>
<ListBucketResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
  <IsTruncated>false</IsTruncated>
  <Contents>
    <Key>testfolder%2Ffile%201.txt</Key>
    <Size>42</Size>
  </Contents>
  <CommonPrefixes>
    <Prefix>testfolder%2Fsubdir%2F</Prefix>
  </CommonPrefixes>
</ListBucketResult>)";

	std::vector<S3ObjectInfo> objs;
	std::vector<std::string> prefixes;
	std::string ct, err;
	ASSERT_TRUE(
		AmazonS3List::ParseListBucketResult(xml, objs, prefixes, ct, err))
		<< err;
	ASSERT_EQ(objs.size(), 1);
	EXPECT_EQ(objs[0].m_key, "testfolder/file 1.txt");
	ASSERT_EQ(prefixes.size(), 1);
	EXPECT_EQ(prefixes[0], "testfolder/subdir/");
}

TEST(S3ListParsing, ContinuationToken) {
	std::string xml = R"(<?xml version="1.0" encoding="UTF-8"?>
<ListBucketResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
  <IsTruncated>true</IsTruncated>
  <NextContinuationToken>abc123</NextContinuationToken>
  <Contents>
    <Key>foo/bar.txt</Key>
    <Size>10</Size>
  </Contents>
</ListBucketResult>)";

	std::vector<S3ObjectInfo> objs;
	std::vector<std::string> prefixes;
	std::string ct, err;
	ASSERT_TRUE(
		AmazonS3List::ParseListBucketResult(xml, objs, prefixes, ct, err))
		<< err;
	EXPECT_EQ(ct, "abc123");
}

TEST(S3ListParsing, DecodesUrlEncodedContinuationToken) {
	std::string xml = R"(<?xml version="1.0" encoding="UTF-8"?>
<ListBucketResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
  <IsTruncated>true</IsTruncated>
  <NextContinuationToken>path%2Fto%2Fkey</NextContinuationToken>
  <Contents>
    <Key>foo/bar.txt</Key>
    <Size>10</Size>
  </Contents>
</ListBucketResult>)";

	std::vector<S3ObjectInfo> objs;
	std::vector<std::string> prefixes;
	std::string ct, err;
	ASSERT_TRUE(
		AmazonS3List::ParseListBucketResult(xml, objs, prefixes, ct, err))
		<< err;
	EXPECT_EQ(ct, "path/to/key");
}

TEST(S3ListParsing, ContinuationTokenClearedWhenNotTruncated) {
	std::string xml = R"(<?xml version="1.0" encoding="UTF-8"?>
<ListBucketResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
  <IsTruncated>false</IsTruncated>
  <NextContinuationToken>leftover</NextContinuationToken>
  <Contents>
    <Key>obj</Key>
    <Size>1</Size>
  </Contents>
</ListBucketResult>)";

	std::vector<S3ObjectInfo> objs;
	std::vector<std::string> prefixes;
	std::string ct, err;
	ASSERT_TRUE(
		AmazonS3List::ParseListBucketResult(xml, objs, prefixes, ct, err))
		<< err;
	EXPECT_TRUE(ct.empty());
}

TEST(S3ListParsing, InvalidXml) {
	std::string xml = "this is not xml at all";
	std::vector<S3ObjectInfo> objs;
	std::vector<std::string> prefixes;
	std::string ct, err;
	EXPECT_FALSE(
		AmazonS3List::ParseListBucketResult(xml, objs, prefixes, ct, err));
	EXPECT_FALSE(err.empty());
}

TEST(S3ListParsing, WrongRootElement) {
	std::string xml = R"(<?xml version="1.0"?>
<Error><Code>NoSuchBucket</Code></Error>)";
	std::vector<S3ObjectInfo> objs;
	std::vector<std::string> prefixes;
	std::string ct, err;
	EXPECT_FALSE(
		AmazonS3List::ParseListBucketResult(xml, objs, prefixes, ct, err));
	EXPECT_NE(err.find("ListBucketResult"), std::string::npos);
}

int main(int argc, char **argv) {
	auto logger = new XrdSysLogger(2, 0);
	auto log = new XrdSysError(logger, "curl_");
	AmazonRequest::Init(*log);
	::testing::InitGoogleTest(&argc, argv);
	return RUN_ALL_TESTS();
}
