#include <TestSupport.h>
#include <Core/SpawningKit/Handshake/Prepare.h>
#include <FileTools/FileManip.h>
#include <SystemTools/UserDatabase.h>

using namespace std;
using namespace Passenger;
using namespace Passenger::SpawningKit;

namespace tut {
	struct SpawnEnvSetupperTest: public TestBase {
		WrapperRegistry::Registry wrapperRegistry;
		SpawningKit::Context::Schema schema;
		SpawningKit::Context context;
		SpawningKit::Config config;
		boost::shared_ptr<HandshakeSession> session;

		SpawnEnvSetupperTest()
			: context(schema)
		{
			wrapperRegistry.finalize();
			context.resourceLocator = resourceLocator;
			context.wrapperRegistry = &wrapperRegistry;
			context.integrationMode = "standalone";
			context.spawnDir = getSystemTempDir();
			context.finalize();

			config.startCommand = "true";
			config.appGroupName = "appgroup";
			config.appRoot = "tmp.wsgi";
			config.startupFile = "tmp.wsgi/passenger_wsgi.py";
			config.appType = "wsgi";
			config.spawnMethod = "direct";
			config.bindAddress = "127.0.0.1";
			config.user = lookupSystemUsernameByUid(getuid());
			config.group = lookupSystemGroupnameByGid(getgid());
			config.internStrings();
		}

		void init(JourneyType type, const Json::Value &extraArgs = Json::Value()) {
			vector<StaticString> errors;
			ensure("Config is valid", config.validate(errors));
			session = boost::make_shared<HandshakeSession>(context, config, type);

			session->journey.setStepInProgress(SPAWNING_KIT_PREPARATION);
			HandshakePrepare(*session, extraArgs).execute();

			session->journey.setStepInProgress(SPAWNING_KIT_HANDSHAKE_PERFORM);
			session->journey.setStepInProgress(SUBPROCESS_BEFORE_FIRST_EXEC);
		}

		bool execute(const StaticString &mode, bool quiet = false) {
			string command = escapeShell(resourceLocator->findSupportBinary(AGENT_EXE))
				+ " spawn-env-setupper "
				+ escapeShell(session->workDir->getPath())
				+ " " + mode;
			if (quiet) {
				command.append(" >/dev/null 2>/dev/null");
			}
			return runShellCommand(command) == 0;
		}
	};

	DEFINE_TEST_GROUP(SpawnEnvSetupperTest);


	/***** Dumping information *****/

	TEST_METHOD(1) {
		set_test_name("It sets the SUBPROCESS_BEFORE_FIRST_EXEC to the PERFORMED state");

		TempDirCopy dir("stub/wsgi", "tmp.wsgi");
		init(SPAWN_DIRECTLY);
		ensure("SpawnEnvSetupper succeeds", execute("--before"));

		ensure_equals(
			unsafeReadFile(session->workDir->getPath()
				+ "/response/steps/subprocess_before_first_exec/state"),
			"STEP_PERFORMED");
	}

	TEST_METHOD(2) {
		set_test_name("It dumps environment variables into the work dir");

		TempDirCopy dir("stub/wsgi", "tmp.wsgi");
		init(SPAWN_DIRECTLY);
		ensure("SpawnEnvSetupper succeeds", execute("--before"));

		string envvars = unsafeReadFile(session->workDir->getPath()
			+ "/envdump/envvars");
		ensure(containsSubstring(envvars, "PATH="));
	}

	TEST_METHOD(3) {
		set_test_name("It dumps user info into the work dir");

		TempDirCopy dir("stub/wsgi", "tmp.wsgi");
		init(SPAWN_DIRECTLY);
		ensure("SpawnEnvSetupper succeeds", execute("--before"));

		string envvars = unsafeReadFile(session->workDir->getPath()
			+ "/envdump/user_info");
		ensure(containsSubstring(envvars, "uid="));
	}

	TEST_METHOD(4) {
		set_test_name("It dumps ulimits info into the work dir");

		TempDirCopy dir("stub/wsgi", "tmp.wsgi");
		init(SPAWN_DIRECTLY);
		ensure("SpawnEnvSetupper succeeds", execute("--before"));

		string envvars = unsafeReadFile(session->workDir->getPath()
			+ "/envdump/ulimits");
		ensure(containsSubstring(envvars, "open files")
			|| containsSubstring(envvars, "nofiles"));
	}

	TEST_METHOD(5) {
		set_test_name("It sets default environment variables such as PASSENGER_APP_ENV");

		TempDirCopy dir("stub/wsgi", "tmp.wsgi");
		init(SPAWN_DIRECTLY);
		ensure("SpawnEnvSetupper succeeds", execute("--before"));

		string envvars = unsafeReadFile(session->workDir->getPath()
			+ "/envdump/envvars");
		ensure(containsSubstring(envvars, "PASSENGER_APP_ENV="));
	}


	/***** Command execution and environment modification *****/

	TEST_METHOD(10) {
		set_test_name("It runs the start command inside the app root");

		TempDirCopy dir("stub/wsgi", "tmp.wsgi");
		config.startCommand = "touch foo";
		init(SPAWN_DIRECTLY);
		ensure("SpawnEnvSetupper succeeds", execute("--before"));

		ensure("Start command succeeds", fileExists("tmp.wsgi/foo"));
	}

	TEST_METHOD(11) {
		set_test_name("It sets the environment variables specified in the config");

		TempDirCopy dir("stub/wsgi", "tmp.wsgi");
		config.environmentVariables.insert("MY_VAR", "value");
		config.startCommand = "echo 'import os, json; print(json.dumps(dict(os.environ)))' | python > env.json";
		init(SPAWN_DIRECTLY);
		ensure("SpawnEnvSetupper succeeds", execute("--before"));

		Json::Value doc;
		ensure("Load environment JSON dump",
			Json::Reader().parse(unsafeReadFile("tmp.wsgi/env.json"), doc));
		ensure_equals(doc["MY_VAR"].asString(), "value");
	}

	TEST_METHOD(12) {
		set_test_name("It switches to the corresponding user and group, if possible");

		if (geteuid() != 0) {
			return;
		}

		string user = testConfig["normal_user_1"].asString();
		string group = testConfig["normal_group_1"].asString();

		TempDirCopy dir("stub/wsgi", "tmp.wsgi");
		runShellCommand("chown -R " + user + ":" + group + " tmp.wsgi");
		config.user = user;
		config.group = group;
		config.startCommand = "sh -c 'id -un > user.txt && id -gn > group.txt'";
		init(SPAWN_DIRECTLY);
		ensure("SpawnEnvSetupper succeeds", execute("--before"));

		ensure_equals(strip(unsafeReadFile("tmp.wsgi/user.txt")), user);
		ensure_equals(strip(unsafeReadFile("tmp.wsgi/group.txt")), group);
	}

	TEST_METHOD(13) {
		set_test_name("It sets ulimits to the corresponding settings, if possible");

		if (geteuid() != 0) {
			return;
		}

		string user = testConfig["normal_user_1"].asString();
		string group = testConfig["normal_group_1"].asString();

		TempDirCopy dir("stub/wsgi", "tmp.wsgi");
		runShellCommand("chown -R " + user + ":" + group + " tmp.wsgi");
		config.fileDescriptorUlimit = 128;
		config.startCommand = "sh -c 'ulimit -n > openfiles.txt'";
		init(SPAWN_DIRECTLY);
		ensure("SpawnEnvSetupper succeeds", execute("--before"));

		ensure_equals(strip(unsafeReadFile("tmp.wsgi/openfiles.txt")), "128");
	}


	/***** Step state recording *****/

	TEST_METHOD(20) {
		set_test_name("It sets the SUBPROCESS_SPAWN_ENV_SETUPPER_BEFORE_SHELL"
			" and SUBPROCESS_SPAWN_ENV_SETUPPER_AFTER_SHELL steps to the PERFORMED state");

		TempDirCopy dir("stub/wsgi", "tmp.wsgi");
		init(SPAWN_DIRECTLY);
		ensure("SpawnEnvSetupper succeeds", execute("--before"));

		ensure_equals(
			unsafeReadFile(session->workDir->getPath()
				+ "/response/steps/subprocess_spawn_env_setupper_before_shell/state"),
			"STEP_PERFORMED");
		ensure_equals(
			unsafeReadFile(session->workDir->getPath()
				+ "/response/steps/subprocess_spawn_env_setupper_after_shell/state"),
			"STEP_PERFORMED");
	}

	TEST_METHOD(21) {
		set_test_name("If loadShellEnvvars is true, it sets SUBPROCESS_OS_SHELL"
			" step to the PERFORMED state");

		// This test is known to fail erroneously if all of
		// the following conditions apply:
		// - You are running this test with root privileges.
		// - The root user's shell is not supported by
		//   the shouldLoadShellEnvvars() function in SpawnEnvSetupperMain.cpp.

		TempDirCopy dir("stub/wsgi", "tmp.wsgi");
		config.loadShellEnvvars = true;
		init(SPAWN_DIRECTLY);
		ensure("SpawnEnvSetupper succeeds", execute("--before"));

		ensure_equals(
			unsafeReadFile(session->workDir->getPath()
				+ "/response/steps/subprocess_os_shell/state"),
			"STEP_PERFORMED");
	}

	TEST_METHOD(22) {
		set_test_name("If loadShellEnvvars is false, it keeps the SUBPROCESS_OS_SHELL"
			" step in the NOT_STARTED state");

		TempDirCopy dir("stub/wsgi", "tmp.wsgi");
		config.loadShellEnvvars = false;
		init(SPAWN_DIRECTLY);
		ensure("SpawnEnvSetupper succeeds", execute("--before"));

		ensure(!fileExists(session->workDir->getPath()
			+ "/response/steps/subprocess_os_shell"));
	}

	TEST_METHOD(23) {
		set_test_name("If startsUsingWrapper is true,"
			" and the start command can be executed,"
			" then it sets the SUBPROCESS_EXEC_WRAPPER step to the IN_PROGRESS state");

		TempDirCopy dir("stub/wsgi", "tmp.wsgi");
		config.startsUsingWrapper = true;
		init(SPAWN_DIRECTLY);
		ensure("SpawnEnvSetupper succeeds", execute("--before"));

		ensure_equals(
			unsafeReadFile(session->workDir->getPath()
				+ "/response/steps/subprocess_exec_wrapper/state"),
			"STEP_IN_PROGRESS");
	}

	TEST_METHOD(24) {
		set_test_name("If startsUsingWrapper is true,"
			" and the start command cannot be executed,"
			" then it sets the SUBPROCESS_EXEC_WRAPPER step to the ERRORED state");

		TempDirCopy dir("stub/wsgi", "tmp.wsgi");
		config.startsUsingWrapper = true;
		Json::Value extraArgs;
		extraArgs["_bin_sh_path"] = "/non-existant-command";
		init(SPAWN_DIRECTLY, extraArgs);
		ensure("SpawnEnvSetupper fails", !execute("--before", true));

		ensure_equals(
			unsafeReadFile(session->workDir->getPath()
				+ "/response/steps/subprocess_exec_wrapper/state"),
			"STEP_ERRORED");
	}

	TEST_METHOD(25) {
		set_test_name("If startsUsingWrapper is false,"
			" and the start command can be executed,"
			" then it sets the SUBPROCESS_APP_LOAD_OR_EXEC step to the IN_PROGRESS state");

		TempDirCopy dir("stub/wsgi", "tmp.wsgi");
		config.startsUsingWrapper = false;
		init(SPAWN_DIRECTLY);
		ensure("SpawnEnvSetupper succeeds", execute("--before"));

		ensure_equals(
			unsafeReadFile(session->workDir->getPath()
				+ "/response/steps/subprocess_app_load_or_exec/state"),
			"STEP_IN_PROGRESS");
	}

	TEST_METHOD(26) {
		set_test_name("If startsUsingWrapper is false,"
			" and the start command cannot be executed,"
			" then it sets the SUBPROCESS_APP_LOAD_OR_EXEC step to the ERRORED state");

		TempDirCopy dir("stub/wsgi", "tmp.wsgi");
		config.startsUsingWrapper = false;
		Json::Value extraArgs;
		extraArgs["_bin_sh_path"] = "/non-existant-command";
		init(SPAWN_DIRECTLY, extraArgs);
		ensure("SpawnEnvSetupper fails", !execute("--before", true));

		ensure_equals(
			unsafeReadFile(session->workDir->getPath()
				+ "/response/steps/subprocess_app_load_or_exec/state"),
			"STEP_ERRORED");
	}


	/***** Miscellaneous *****/

	TEST_METHOD(30) {
		set_test_name("If the user does not have a access to one of the app root's"
			" parent directories, or the app root itself, then it provides a clear"
			" error message explaining this (level 1)");

		if (geteuid() == 0) {
			return;
		}

		runShellCommand("mkdir -p tmp.check/a/b/c");
		TempDirCopy dir("stub/wsgi", "tmp.check/a/b/c/d");
		TempDir dir2("tmp.check");
		runShellCommand("chmod 000 tmp.check/a/b/c/d");
		runShellCommand("chmod 600 tmp.check/a/b/c");
		runShellCommand("chmod 600 tmp.check/a");

		char buffer[PATH_MAX];
		string cwd = getcwd(buffer, sizeof(buffer));

		if (defaultLogLevel == (LoggingKit::Level) DEFAULT_LOG_LEVEL) {
			// If the user did not customize the test's log level,
			// then we'll want to tone down the noise.
			LoggingKit::setLevel(LoggingKit::CRIT);
		}

		config.appRoot = "tmp.check/a/b/c/d";
		init(SPAWN_DIRECTLY);
		ensure("SpawnEnvSetupper fails", !execute("--before", true));

		ensure(containsSubstring(
			unsafeReadFile(session->workDir->getPath()
				+ "/response/error/summary"),
			"Directory '" + cwd + "/tmp.check/a' is inaccessible"));

		#if 0
		try {
			spawner->spawn(options);
			fail("SpawnException expected");
		} catch (const SpawnException &e) {
			ensure("(1)", containsSubstring(e.getErrorPage(),
				"the parent directory '" + cwd + "/tmp.check/a' has wrong permissions"));
		}

		runShellCommand("chmod 700 tmp.check/a");
		try {
			spawner->spawn(options);
			fail("SpawnException expected");
		} catch (const SpawnException &e) {
			ensure("(2)", containsSubstring(e.getErrorPage(),
				"the parent directory '" + cwd + "/tmp.check/a/b/c' has wrong permissions"));
		}

		runShellCommand("chmod 700 tmp.check/a/b/c");
		try {
			spawner->spawn(options);
			fail("SpawnException expected");
		} catch (const SpawnException &e) {
			ensure("(3)", containsSubstring(e.getErrorPage(),
				"However this directory is not accessible because it has wrong permissions."));
		}

		runShellCommand("chmod 700 tmp.check/a/b/c/d");
		spawner->spawn(options); // Should not throw.
		#endif
	}

	TEST_METHOD(31) {
		set_test_name("If the user does not have a access to one of the app root's"
			" parent directories, or the app root itself, then it provides a clear"
			" error message explaining this (level 2)");

		if (geteuid() == 0) {
			return;
		}

		runShellCommand("mkdir -p tmp.check/a/b/c");
		TempDirCopy dir("stub/wsgi", "tmp.check/a/b/c/d");
		TempDir dir2("tmp.check");
		runShellCommand("chmod 000 tmp.check/a/b/c/d");
		runShellCommand("chmod 600 tmp.check/a/b/c");
		runShellCommand("chmod 700 tmp.check/a");

		char buffer[PATH_MAX];
		string cwd = getcwd(buffer, sizeof(buffer));

		if (defaultLogLevel == (LoggingKit::Level) DEFAULT_LOG_LEVEL) {
			// If the user did not customize the test's log level,
			// then we'll want to tone down the noise.
			LoggingKit::setLevel(LoggingKit::CRIT);
		}

		config.appRoot = "tmp.check/a/b/c/d";
		init(SPAWN_DIRECTLY);
		ensure("SpawnEnvSetupper fails", !execute("--before", true));

		ensure(containsSubstring(
			unsafeReadFile(session->workDir->getPath()
				+ "/response/error/summary"),
			"Directory '" + cwd + "/tmp.check/a/b/c' is inaccessible"));
	}

	TEST_METHOD(32) {
		set_test_name("If the user does not have a access to one of the app root's"
			" parent directories, or the app root itself, then it provides a clear"
			" error message explaining this (level 3)");

		if (geteuid() == 0) {
			return;
		}

		runShellCommand("mkdir -p tmp.check/a/b/c");
		TempDirCopy dir("stub/wsgi", "tmp.check/a/b/c/d");
		TempDir dir2("tmp.check");
		runShellCommand("chmod 700 tmp.check/a/b/c/d");
		runShellCommand("chmod 600 tmp.check/a/b/c");
		runShellCommand("chmod 700 tmp.check/a");

		char buffer[PATH_MAX];
		string cwd = getcwd(buffer, sizeof(buffer));

		if (defaultLogLevel == (LoggingKit::Level) DEFAULT_LOG_LEVEL) {
			// If the user did not customize the test's log level,
			// then we'll want to tone down the noise.
			LoggingKit::setLevel(LoggingKit::CRIT);
		}

		config.appRoot = "tmp.check/a/b/c/d";
		init(SPAWN_DIRECTLY);
		ensure("SpawnEnvSetupper fails", !execute("--before", true));

		ensure(containsSubstring(
			unsafeReadFile(session->workDir->getPath()
				+ "/response/error/summary"),
			"Directory '" + cwd + "/tmp.check/a/b/c' is inaccessible"));
	}
}
