// Copyright 2023 Northern.tech AS
//
//    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 <chrono>
#include <fstream>
#include <thread>

#include <gmock/gmock.h>
#include <gtest/gtest.h>

#include <api/client.hpp>
#include <common/common.hpp>
#include <client_shared/conf.hpp>
#include <common/events.hpp>
#include <common/http.hpp>
#include <common/io.hpp>
#include <common/json.hpp>
#include <common/log.hpp>
#include <common/path.hpp>
#include <common/testing.hpp>

#include <mender-update/deployments.hpp>

using namespace std;
using mender::nullopt;
using mender::optional;

namespace api = mender::api;
namespace common = mender::common;
namespace conf = mender::client_shared::conf;
namespace context = mender::update::context;
namespace deps = mender::update::deployments;
namespace error = mender::common::error;
namespace events = mender::common::events;
namespace expected = mender::common::expected;
namespace http = mender::common::http;
namespace io = mender::common::io;
namespace json = mender::common::json;
namespace mlog = mender::common::log;
namespace mtesting = mender::common::testing;
namespace path = mender::common::path;

#define TEST_SERVER "http://127.0.0.1:8001"

using TestEventLoop = mtesting::TestEventLoop;

class NoAuthHTTPClient : public api::Client {
public:
	NoAuthHTTPClient(const http::ClientConfig &config, events::EventLoop &event_loop) :
		http_client_ {config, event_loop} {};

	void AsyncCall(
		api::APIRequestPtr req,
		http::ResponseHandler header_handler,
		http::ResponseHandler body_handler) override {
		auto ex_req = req->WithAuthData({TEST_SERVER, ""});
		if (!ex_req) {
			// Not reached in tests (TEST_SERVER is non-empty); kept so the mock honors
			// api::Client's contract of always delivering the outcome via the handler.
			header_handler(expected::unexpected(ex_req.error()));
			return;
		}
		http_client_.AsyncCall(ex_req.value(), header_handler, body_handler);
	}

private:
	http::Client http_client_;
};

class DeploymentsTests : public testing::Test {
protected:
	mtesting::TemporaryDirectory test_state_dir;

	shared_ptr<http::IncomingResponse> CreateIncomingResponse(
		http::ClientInterface &client, shared_ptr<bool> cancelled, optional<string> retry_after) {
		auto resp =
			shared_ptr<http::IncomingResponse>(new http::IncomingResponse(client, cancelled));
		resp->status_code_ = http::StatusTooManyRequests;
		resp->status_message_ = "Too Many Requests";
		if (retry_after.has_value()) {
			resp->headers_.insert({"Retry-After", retry_after.value()});
		}
		return resp;
	}

	void DeploymentClientHeaderHandler(
		deps::CheckUpdatesAPIResponseHandler api_handler,
		http::ExpectedIncomingResponsePtr exp_resp) {
		deps::DeploymentClient().HeaderHandler(
			make_shared<vector<uint8_t>>(), api_handler, exp_resp);
	}

	void DeploymentClientPushStatusHeaderHandler(
		deps::StatusAPIResponseHandler api_handler, http::ExpectedIncomingResponsePtr exp_resp) {
		deps::DeploymentClient().PushStatusHeaderHandler(
			make_shared<vector<uint8_t>>(), api_handler, exp_resp);
	}

	void DeploymentClientPushLogsHeaderHandler(
		deps::LogsAPIResponseHandler api_handler, http::ExpectedIncomingResponsePtr exp_resp) {
		deps::DeploymentClient().PushLogsHeaderHandler(
			make_shared<vector<uint8_t>>(), api_handler, exp_resp);
	}

	int32_t JsonLogMessagesReaderGetMaxLogSize(deps::JsonLogMessagesReader &reader) {
		return reader.maximum_log_size_;
	}
};

TEST_F(DeploymentsTests, TestV2APIWithNextDeployment) {
	conf::MenderConfig cfg;
	cfg.paths.SetDataStore(test_state_dir.Path());

	context::MenderContext ctx(cfg);
	auto err = ctx.Initialize();
	ASSERT_EQ(err, error::NoError);

	auto &db = ctx.GetMenderStoreDB();
	err = db.Write("artifact-name", common::ByteVectorFromString("artifact-name value"));
	ASSERT_EQ(err, error::NoError);
	err = db.Write("artifact-group", common::ByteVectorFromString("artifact-group value"));
	ASSERT_EQ(err, error::NoError);

	const string input_provides_data_str = R"({
  "something_else": "something_else value"
})";
	err = db.Write("artifact-provides", common::ByteVectorFromString(input_provides_data_str));
	ASSERT_EQ(err, error::NoError);

	ofstream os(cfg.paths.GetDataStore() + "/device_type");
	ASSERT_TRUE(os);
	os << "device_type=Some device type" << endl;
	os.close();

	const string expected_request_data =
		R"({"device_provides":{"device_type":"Some device type","something_else":"something_else value","artifact_group":"artifact-group value","artifact_name":"artifact-name value"}})";
	const string response_data = R"({
  "some": "data here"
})";

	TestEventLoop loop;

	http::ServerConfig server_config;
	http::Server server(server_config, loop);

	http::ClientConfig client_config;
	NoAuthHTTPClient client {client_config, loop};

	vector<uint8_t> received_body;
	server.AsyncServeUrl(
		TEST_SERVER,
		[&received_body, &expected_request_data](http::ExpectedIncomingRequestPtr exp_req) {
			ASSERT_TRUE(exp_req) << exp_req.error().String();
			auto req = exp_req.value();

			auto content_length = req->GetHeader("Content-Length");
			ASSERT_TRUE(content_length);
			EXPECT_EQ(content_length.value(), to_string(expected_request_data.size()));
			auto ex_len = common::StringToLongLong(content_length.value());
			ASSERT_TRUE(ex_len);

			auto body_writer = make_shared<io::ByteWriter>(received_body);
			received_body.resize(ex_len.value());
			req->SetBodyWriter(body_writer);
		},
		[&received_body, &expected_request_data, &response_data](
			http::ExpectedIncomingRequestPtr exp_req) {
			ASSERT_TRUE(exp_req) << exp_req.error().String();

			auto req = exp_req.value();
			EXPECT_EQ(req->GetPath(), "/api/devices/v2/deployments/device/deployments/next");
			EXPECT_EQ(req->GetMethod(), http::Method::POST);
			EXPECT_EQ(common::StringFromByteVector(received_body), expected_request_data);

			auto result = req->MakeResponse();
			ASSERT_TRUE(result);
			auto resp = result.value();

			resp->SetHeader("Content-Length", to_string(response_data.size()));
			resp->SetBodyReader(make_shared<io::StringReader>(response_data));
			resp->SetStatusCodeAndMessage(200, "Success");
			resp->AsyncReply([](error::Error err) { ASSERT_EQ(error::NoError, err); });
		});

	bool handler_called = false;
	err = deps::DeploymentClient().CheckNewDeployments(
		ctx, client, [&response_data, &handler_called, &loop](deps::CheckUpdatesAPIResponse resp) {
			handler_called = true;
			ASSERT_TRUE(resp);

			auto o_js = resp.value();
			ASSERT_NE(o_js, nullopt);

			EXPECT_EQ(o_js->Dump(), response_data);
			loop.Stop();
		});
	EXPECT_EQ(err, error::NoError);

	loop.Run();
	EXPECT_TRUE(handler_called);
}

TEST_F(DeploymentsTests, TestV2APIWithNoNextDeployment) {
	conf::MenderConfig cfg;
	cfg.paths.SetDataStore(test_state_dir.Path());

	context::MenderContext ctx(cfg);
	auto err = ctx.Initialize();
	ASSERT_EQ(err, error::NoError);

	auto &db = ctx.GetMenderStoreDB();
	err = db.Write("artifact-name", common::ByteVectorFromString("artifact-name value"));
	ASSERT_EQ(err, error::NoError);

	ofstream os(cfg.paths.GetDataStore() + "/device_type");
	ASSERT_TRUE(os);
	os << "device_type=Some device type" << endl;
	os.close();

	const string expected_request_data =
		R"({"device_provides":{"device_type":"Some device type","artifact_name":"artifact-name value"}})";
	const string response_data = "";

	TestEventLoop loop;

	http::ServerConfig server_config;
	http::Server server(server_config, loop);

	http::ClientConfig client_config;
	NoAuthHTTPClient client {client_config, loop};

	vector<uint8_t> received_body;
	server.AsyncServeUrl(
		TEST_SERVER,
		[&received_body, &expected_request_data](http::ExpectedIncomingRequestPtr exp_req) {
			ASSERT_TRUE(exp_req) << exp_req.error().String();
			auto req = exp_req.value();

			auto content_length = req->GetHeader("Content-Length");
			ASSERT_TRUE(content_length);
			EXPECT_EQ(content_length.value(), to_string(expected_request_data.size()));
			auto ex_len = common::StringToLongLong(content_length.value());
			ASSERT_TRUE(ex_len);

			auto body_writer = make_shared<io::ByteWriter>(received_body);
			received_body.resize(ex_len.value());
			req->SetBodyWriter(body_writer);
		},
		[&received_body, &expected_request_data, &response_data](
			http::ExpectedIncomingRequestPtr exp_req) {
			ASSERT_TRUE(exp_req) << exp_req.error().String();

			auto req = exp_req.value();
			EXPECT_EQ(req->GetPath(), "/api/devices/v2/deployments/device/deployments/next");
			EXPECT_EQ(req->GetMethod(), http::Method::POST);
			EXPECT_EQ(common::StringFromByteVector(received_body), expected_request_data);

			auto result = req->MakeResponse();
			ASSERT_TRUE(result);
			auto resp = result.value();

			resp->SetHeader("Content-Length", to_string(response_data.size()));
			resp->SetBodyReader(make_shared<io::StringReader>(response_data));
			resp->SetStatusCodeAndMessage(http::StatusNoContent, "No content");
			resp->AsyncReply([](error::Error err) { ASSERT_EQ(error::NoError, err); });
		});

	bool handler_called = false;
	err = deps::DeploymentClient().CheckNewDeployments(
		ctx, client, [&handler_called, &loop](deps::CheckUpdatesAPIResponse resp) {
			handler_called = true;
			ASSERT_TRUE(resp);

			auto o_js = resp.value();
			EXPECT_EQ(o_js, nullopt);

			loop.Stop();
		});
	EXPECT_EQ(err, error::NoError);

	loop.Run();
	EXPECT_TRUE(handler_called);
}

TEST_F(DeploymentsTests, TestV2APIError) {
	conf::MenderConfig cfg;
	cfg.paths.SetDataStore(test_state_dir.Path());

	context::MenderContext ctx(cfg);
	auto err = ctx.Initialize();
	ASSERT_EQ(err, error::NoError);

	auto &db = ctx.GetMenderStoreDB();
	err = db.Write("artifact-name", common::ByteVectorFromString("artifact-name value"));
	ASSERT_EQ(err, error::NoError);

	ofstream os(cfg.paths.GetDataStore() + "/device_type");
	ASSERT_TRUE(os);
	os << "device_type=Some device type" << endl;
	os.close();

	const string expected_request_data =
		R"({"device_provides":{"device_type":"Some device type","artifact_name":"artifact-name value"}})";
	const string response_data = R"({"error": "JWT token expired", "response-id": "some id here"})";

	TestEventLoop loop;

	http::ServerConfig server_config;
	http::Server server(server_config, loop);

	http::ClientConfig client_config;
	NoAuthHTTPClient client {client_config, loop};

	vector<uint8_t> received_body;
	server.AsyncServeUrl(
		TEST_SERVER,
		[&received_body, &expected_request_data](http::ExpectedIncomingRequestPtr exp_req) {
			ASSERT_TRUE(exp_req) << exp_req.error().String();
			auto req = exp_req.value();

			auto content_length = req->GetHeader("Content-Length");
			ASSERT_TRUE(content_length);
			EXPECT_EQ(content_length.value(), to_string(expected_request_data.size()));
			auto ex_len = common::StringToLongLong(content_length.value());
			ASSERT_TRUE(ex_len);

			auto body_writer = make_shared<io::ByteWriter>(received_body);
			received_body.resize(ex_len.value());
			req->SetBodyWriter(body_writer);
		},
		[&received_body, &expected_request_data, &response_data](
			http::ExpectedIncomingRequestPtr exp_req) {
			ASSERT_TRUE(exp_req) << exp_req.error().String();

			auto req = exp_req.value();
			EXPECT_EQ(req->GetPath(), "/api/devices/v2/deployments/device/deployments/next");
			EXPECT_EQ(req->GetMethod(), http::Method::POST);
			EXPECT_EQ(common::StringFromByteVector(received_body), expected_request_data);

			auto result = req->MakeResponse();
			ASSERT_TRUE(result);
			auto resp = result.value();

			resp->SetHeader("Content-Length", to_string(response_data.size()));
			resp->SetBodyReader(make_shared<io::StringReader>(response_data));
			resp->SetStatusCodeAndMessage(403, "Forbidden");
			resp->AsyncReply([](error::Error err) { ASSERT_EQ(error::NoError, err); });
		});

	bool handler_called = false;
	err = deps::DeploymentClient().CheckNewDeployments(
		ctx, client, [&handler_called, &loop](deps::CheckUpdatesAPIResponse resp) {
			handler_called = true;
			ASSERT_FALSE(resp);

			EXPECT_THAT(resp.error().error.message, testing::HasSubstr("Got unexpected response"));
			EXPECT_THAT(resp.error().error.message, testing::HasSubstr("403"));
			EXPECT_THAT(resp.error().error.message, testing::HasSubstr("JWT token expired"));

			loop.Stop();
		});
	EXPECT_EQ(err, error::NoError);

	loop.Run();
	EXPECT_TRUE(handler_called);
}

TEST_F(DeploymentsTests, TestV1APIFallbackWithNextDeployment) {
	conf::MenderConfig cfg;
	cfg.paths.SetDataStore(test_state_dir.Path());

	context::MenderContext ctx(cfg);
	auto err = ctx.Initialize();
	ASSERT_EQ(err, error::NoError);

	auto &db = ctx.GetMenderStoreDB();
	err = db.Write("artifact-name", common::ByteVectorFromString("artifact-name value"));
	ASSERT_EQ(err, error::NoError);

	ofstream os(cfg.paths.GetDataStore() + "/device_type");
	ASSERT_TRUE(os);
	os << "device_type=Some device type" << endl;
	os.close();

	const string expected_request_data =
		R"({"device_provides":{"device_type":"Some device type","artifact_name":"artifact-name value"}})";
	const string response_data = R"({
  "some": "data here"
})";

	TestEventLoop loop;

	http::ServerConfig server_config;
	http::Server server(server_config, loop);

	http::ClientConfig client_config;
	NoAuthHTTPClient client {client_config, loop};

	vector<uint8_t> received_body;
	bool v2_requested = false;
	server.AsyncServeUrl(
		TEST_SERVER,
		[&received_body, &expected_request_data, &v2_requested](
			http::ExpectedIncomingRequestPtr exp_req) {
			ASSERT_TRUE(exp_req) << exp_req.error().String();
			auto req = exp_req.value();

			if (!v2_requested) {
				auto content_length = req->GetHeader("Content-Length");
				ASSERT_TRUE(content_length);
				EXPECT_EQ(content_length.value(), to_string(expected_request_data.size()));
				auto ex_len = common::StringToLongLong(content_length.value());
				ASSERT_TRUE(ex_len);

				auto body_writer = make_shared<io::ByteWriter>(received_body);
				received_body.resize(ex_len.value());
				req->SetBodyWriter(body_writer);
			} else {
				auto content_length = req->GetHeader("Content-Length");
				EXPECT_FALSE(content_length);
				received_body.clear();
			}
		},
		[&received_body, &expected_request_data, &response_data, &v2_requested](
			http::ExpectedIncomingRequestPtr exp_req) {
			ASSERT_TRUE(exp_req) << exp_req.error().String();

			if (!v2_requested) {
				auto req = exp_req.value();
				EXPECT_EQ(req->GetPath(), "/api/devices/v2/deployments/device/deployments/next");
				EXPECT_EQ(req->GetMethod(), http::Method::POST);
				EXPECT_EQ(common::StringFromByteVector(received_body), expected_request_data);

				auto result = req->MakeResponse();
				ASSERT_TRUE(result);
				auto resp = result.value();

				resp->SetHeader("Content-Length", "0");
				resp->SetBodyReader(make_shared<io::StringReader>(""));
				resp->SetStatusCodeAndMessage(404, "Not found");
				resp->AsyncReply([](error::Error err) { ASSERT_EQ(error::NoError, err); });
				v2_requested = true;
			} else {
				auto req = exp_req.value();
				EXPECT_EQ(
					req->GetPath(),
					"/api/devices/v1/deployments/device/deployments/next?artifact_name=artifact-name%20value&device_type=Some%20device%20type");
				EXPECT_EQ(req->GetMethod(), http::Method::GET);

				auto result = req->MakeResponse();
				ASSERT_TRUE(result);
				auto resp = result.value();

				resp->SetHeader("Content-Length", to_string(response_data.size()));
				resp->SetBodyReader(make_shared<io::StringReader>(response_data));
				resp->SetStatusCodeAndMessage(http::StatusOK, "Success");
				resp->AsyncReply([](error::Error err) { ASSERT_EQ(error::NoError, err); });
			}
		});

	bool handler_called = false;
	err = deps::DeploymentClient().CheckNewDeployments(
		ctx, client, [&response_data, &handler_called, &loop](deps::CheckUpdatesAPIResponse resp) {
			handler_called = true;
			ASSERT_TRUE(resp);

			auto o_js = resp.value();
			ASSERT_NE(o_js, nullopt);

			EXPECT_EQ(o_js->Dump(), response_data);
			loop.Stop();
		});
	EXPECT_EQ(err, error::NoError);

	loop.Run();
	EXPECT_TRUE(handler_called);
}

TEST_F(DeploymentsTests, TestV1APIFallbackWithNoNextDeployment) {
	conf::MenderConfig cfg;
	cfg.paths.SetDataStore(test_state_dir.Path());

	context::MenderContext ctx(cfg);
	auto err = ctx.Initialize();
	ASSERT_EQ(err, error::NoError);

	auto &db = ctx.GetMenderStoreDB();
	err = db.Write("artifact-name", common::ByteVectorFromString("artifact-name value"));
	ASSERT_EQ(err, error::NoError);

	ofstream os(cfg.paths.GetDataStore() + "/device_type");
	ASSERT_TRUE(os);
	os << "device_type=Some device type" << endl;
	os.close();

	const string expected_request_data =
		R"({"device_provides":{"device_type":"Some device type","artifact_name":"artifact-name value"}})";
	const string response_data = "";

	TestEventLoop loop(chrono::seconds(3600));

	http::ServerConfig server_config;
	http::Server server(server_config, loop);

	http::ClientConfig client_config;
	NoAuthHTTPClient client {client_config, loop};

	vector<uint8_t> received_body;
	bool v2_requested = false;
	server.AsyncServeUrl(
		TEST_SERVER,
		[&received_body, &expected_request_data, &v2_requested](
			http::ExpectedIncomingRequestPtr exp_req) {
			ASSERT_TRUE(exp_req) << exp_req.error().String();
			auto req = exp_req.value();

			if (!v2_requested) {
				auto content_length = req->GetHeader("Content-Length");
				ASSERT_TRUE(content_length);
				EXPECT_EQ(content_length.value(), to_string(expected_request_data.size()));
				auto ex_len = common::StringToLongLong(content_length.value());
				ASSERT_TRUE(ex_len);

				auto body_writer = make_shared<io::ByteWriter>(received_body);
				received_body.resize(ex_len.value());
				req->SetBodyWriter(body_writer);
			} else {
				auto content_length = req->GetHeader("Content-Length");
				EXPECT_FALSE(content_length);
				received_body.clear();
			}
		},
		[&received_body, &expected_request_data, &response_data, &v2_requested](
			http::ExpectedIncomingRequestPtr exp_req) {
			ASSERT_TRUE(exp_req) << exp_req.error().String();

			if (!v2_requested) {
				auto req = exp_req.value();
				EXPECT_EQ(req->GetPath(), "/api/devices/v2/deployments/device/deployments/next");
				EXPECT_EQ(req->GetMethod(), http::Method::POST);
				EXPECT_EQ(common::StringFromByteVector(received_body), expected_request_data);

				auto result = req->MakeResponse();
				ASSERT_TRUE(result);
				auto resp = result.value();

				resp->SetHeader("Content-Length", "0");
				resp->SetBodyReader(make_shared<io::StringReader>(""));
				resp->SetStatusCodeAndMessage(404, "Not found");
				resp->AsyncReply([](error::Error err) { ASSERT_EQ(error::NoError, err); });
				v2_requested = true;
			} else {
				auto req = exp_req.value();
				EXPECT_EQ(
					req->GetPath(),
					"/api/devices/v1/deployments/device/deployments/next?artifact_name=artifact-name%20value&device_type=Some%20device%20type");
				EXPECT_EQ(req->GetMethod(), http::Method::GET);

				auto result = req->MakeResponse();
				ASSERT_TRUE(result);
				auto resp = result.value();

				resp->SetHeader("Content-Length", to_string(response_data.size()));
				resp->SetBodyReader(make_shared<io::StringReader>(response_data));
				resp->SetStatusCodeAndMessage(http::StatusNoContent, "No content");
				resp->AsyncReply([](error::Error err) { ASSERT_EQ(error::NoError, err); });
			}
		});

	bool handler_called = false;
	err = deps::DeploymentClient().CheckNewDeployments(
		ctx, client, [&handler_called, &loop](deps::CheckUpdatesAPIResponse resp) {
			handler_called = true;
			ASSERT_TRUE(resp);

			auto o_js = resp.value();
			EXPECT_EQ(o_js, nullopt);

			loop.Stop();
		});
	EXPECT_EQ(err, error::NoError);

	loop.Run();
	EXPECT_TRUE(handler_called);
}

TEST_F(DeploymentsTests, TestV1APIFallbackWithError) {
	conf::MenderConfig cfg;
	cfg.paths.SetDataStore(test_state_dir.Path());

	context::MenderContext ctx(cfg);
	auto err = ctx.Initialize();
	ASSERT_EQ(err, error::NoError);

	auto &db = ctx.GetMenderStoreDB();
	err = db.Write("artifact-name", common::ByteVectorFromString("artifact-name value"));
	ASSERT_EQ(err, error::NoError);

	ofstream os(cfg.paths.GetDataStore() + "/device_type");
	ASSERT_TRUE(os);
	os << "device_type=Some device type" << endl;
	os.close();

	const string expected_request_data =
		R"({"device_provides":{"device_type":"Some device type","artifact_name":"artifact-name value"}})";
	const string response_data = "";

	TestEventLoop loop(chrono::seconds(3600));

	http::ServerConfig server_config;
	http::Server server(server_config, loop);

	http::ClientConfig client_config;
	NoAuthHTTPClient client {client_config, loop};

	vector<uint8_t> received_body;
	bool v2_requested = false;
	server.AsyncServeUrl(
		TEST_SERVER,
		[&received_body, &expected_request_data, &v2_requested](
			http::ExpectedIncomingRequestPtr exp_req) {
			ASSERT_TRUE(exp_req) << exp_req.error().String();
			auto req = exp_req.value();

			if (!v2_requested) {
				auto content_length = req->GetHeader("Content-Length");
				ASSERT_TRUE(content_length);
				EXPECT_EQ(content_length.value(), to_string(expected_request_data.size()));
				auto ex_len = common::StringToLongLong(content_length.value());
				ASSERT_TRUE(ex_len);

				auto body_writer = make_shared<io::ByteWriter>(received_body);
				received_body.resize(ex_len.value());
				req->SetBodyWriter(body_writer);
			} else {
				auto content_length = req->GetHeader("Content-Length");
				EXPECT_FALSE(content_length);
				received_body.clear();
			}
		},
		[&received_body, &expected_request_data, &response_data, &v2_requested](
			http::ExpectedIncomingRequestPtr exp_req) {
			ASSERT_TRUE(exp_req) << exp_req.error().String();

			if (!v2_requested) {
				auto req = exp_req.value();
				EXPECT_EQ(req->GetPath(), "/api/devices/v2/deployments/device/deployments/next");
				EXPECT_EQ(req->GetMethod(), http::Method::POST);
				EXPECT_EQ(common::StringFromByteVector(received_body), expected_request_data);

				auto result = req->MakeResponse();
				ASSERT_TRUE(result);
				auto resp = result.value();

				resp->SetHeader("Content-Length", "0");
				resp->SetBodyReader(make_shared<io::StringReader>(""));
				resp->SetStatusCodeAndMessage(404, "Not found");
				resp->AsyncReply([](error::Error err) { ASSERT_EQ(error::NoError, err); });
				v2_requested = true;
			} else {
				auto req = exp_req.value();
				EXPECT_EQ(
					req->GetPath(),
					"/api/devices/v1/deployments/device/deployments/next?artifact_name=artifact-name%20value&device_type=Some%20device%20type");
				EXPECT_EQ(req->GetMethod(), http::Method::GET);

				auto result = req->MakeResponse();
				ASSERT_TRUE(result);
				auto resp = result.value();

				resp->SetHeader("Content-Length", to_string(response_data.size()));
				resp->SetBodyReader(make_shared<io::StringReader>(response_data));
				resp->SetStatusCodeAndMessage(403, "Forbidden");
				resp->AsyncReply([](error::Error err) { ASSERT_EQ(error::NoError, err); });
			}
		});

	bool handler_called = false;
	err = deps::DeploymentClient().CheckNewDeployments(
		ctx, client, [&handler_called, &loop](deps::CheckUpdatesAPIResponse resp) {
			handler_called = true;

			ASSERT_FALSE(resp);

			EXPECT_THAT(resp.error().error.message, testing::HasSubstr("Got unexpected response"));
			EXPECT_THAT(resp.error().error.message, testing::HasSubstr("403"));
			EXPECT_THAT(resp.error().error.message, testing::HasSubstr("Forbidden"));

			loop.Stop();
		});
	EXPECT_EQ(err, error::NoError);

	loop.Run();
	EXPECT_TRUE(handler_called);
}

TEST_F(DeploymentsTests, PushStatusTest) {
	TestEventLoop loop;

	http::ServerConfig server_config;
	http::Server server(server_config, loop);

	http::ClientConfig client_config;
	NoAuthHTTPClient client {client_config, loop};

	auto status {deps::DeploymentStatus::Rebooting};
	string deployment_id = "2";
	string substatus = "Rebooting now";
	string expected_request_data = R"({"status":"rebooting","substate":")" + substatus + "\"}";

	const string response_data = "";

	vector<uint8_t> received_body;
	server.AsyncServeUrl(
		TEST_SERVER,
		[&received_body, &expected_request_data](http::ExpectedIncomingRequestPtr exp_req) {
			ASSERT_TRUE(exp_req) << exp_req.error().String();
			auto req = exp_req.value();

			auto content_length = req->GetHeader("Content-Length");
			ASSERT_TRUE(content_length);
			EXPECT_EQ(content_length.value(), to_string(expected_request_data.size()));
			auto ex_len = common::StringToLongLong(content_length.value());
			ASSERT_TRUE(ex_len);

			auto body_writer = make_shared<io::ByteWriter>(received_body);
			received_body.resize(ex_len.value());
			req->SetBodyWriter(body_writer);
		},
		[&received_body, &expected_request_data, &response_data, deployment_id](
			http::ExpectedIncomingRequestPtr exp_req) {
			ASSERT_TRUE(exp_req) << exp_req.error().String();

			auto req = exp_req.value();
			EXPECT_EQ(
				req->GetPath(),
				"/api/devices/v1/deployments/device/deployments/" + deployment_id + "/status");
			EXPECT_EQ(req->GetMethod(), http::Method::PUT);
			EXPECT_EQ(common::StringFromByteVector(received_body), expected_request_data);

			auto result = req->MakeResponse();
			ASSERT_TRUE(result);
			auto resp = result.value();

			resp->SetHeader("Content-Length", to_string(response_data.size()));
			resp->SetBodyReader(make_shared<io::StringReader>(response_data));
			resp->SetStatusCodeAndMessage(204, "No content");
			resp->AsyncReply([](error::Error err) { ASSERT_EQ(error::NoError, err); });
		});

	bool handler_called = false;
	auto err = deps::DeploymentClient().PushStatus(
		deployment_id,
		status,
		substatus,
		client,
		[&handler_called, &loop](deps::StatusAPIResponse resp) {
			handler_called = true;
			EXPECT_EQ(resp.error, error::NoError);
			loop.Stop();
		});
	EXPECT_EQ(err, error::NoError);

	loop.Run();
	EXPECT_TRUE(handler_called);
}

TEST_F(DeploymentsTests, PushStatusNoSubstatusTest) {
	TestEventLoop loop;

	http::ServerConfig server_config;
	http::Server server(server_config, loop);

	http::ClientConfig client_config;
	NoAuthHTTPClient client {client_config, loop};

	auto status {deps::DeploymentStatus::AlreadyInstalled};
	string deployment_id = "2";
	string substatus = "";
	string expected_request_data = R"({"status":"already-installed"})";

	const string response_data = "";

	vector<uint8_t> received_body;
	server.AsyncServeUrl(
		TEST_SERVER,
		[&received_body, &expected_request_data](http::ExpectedIncomingRequestPtr exp_req) {
			ASSERT_TRUE(exp_req) << exp_req.error().String();
			auto req = exp_req.value();

			auto content_length = req->GetHeader("Content-Length");
			ASSERT_TRUE(content_length);
			EXPECT_EQ(content_length.value(), to_string(expected_request_data.size()));
			auto ex_len = common::StringToLongLong(content_length.value());
			ASSERT_TRUE(ex_len);

			auto body_writer = make_shared<io::ByteWriter>(received_body);
			received_body.resize(ex_len.value());
			req->SetBodyWriter(body_writer);
		},
		[&received_body, &expected_request_data, &response_data, deployment_id](
			http::ExpectedIncomingRequestPtr exp_req) {
			ASSERT_TRUE(exp_req) << exp_req.error().String();

			auto req = exp_req.value();
			EXPECT_EQ(
				req->GetPath(),
				"/api/devices/v1/deployments/device/deployments/" + deployment_id + "/status");
			EXPECT_EQ(req->GetMethod(), http::Method::PUT);
			EXPECT_EQ(common::StringFromByteVector(received_body), expected_request_data);

			auto result = req->MakeResponse();
			ASSERT_TRUE(result);
			auto resp = result.value();

			resp->SetHeader("Content-Length", to_string(response_data.size()));
			resp->SetBodyReader(make_shared<io::StringReader>(response_data));
			resp->SetStatusCodeAndMessage(204, "No content");
			resp->AsyncReply([](error::Error err) { ASSERT_EQ(error::NoError, err); });
		});

	bool handler_called = false;
	auto err = deps::DeploymentClient().PushStatus(
		deployment_id,
		status,
		substatus,
		client,
		[&handler_called, &loop](deps::StatusAPIResponse resp) {
			handler_called = true;
			EXPECT_EQ(resp.error, error::NoError);
			loop.Stop();
		});
	EXPECT_EQ(err, error::NoError);

	loop.Run();
	EXPECT_TRUE(handler_called);
}

TEST_F(DeploymentsTests, PushStatusFailureTest) {
	TestEventLoop loop;

	http::ServerConfig server_config;
	http::Server server(server_config, loop);

	http::ClientConfig client_config;
	NoAuthHTTPClient client {client_config, loop};

	auto status {deps::DeploymentStatus::Installing};
	string deployment_id = "2";
	string substatus = "Installing now";
	string expected_request_data = R"({"status":"installing","substate":")" + substatus + "\"}";

	const string response_data = R"({"error": "Access denied", "response-id": "some id here"})";

	bool aborted_failure = false;
	bool request_body_too_large = false;

	vector<uint8_t> received_body;
	server.AsyncServeUrl(
		TEST_SERVER,
		[&received_body, &expected_request_data](http::ExpectedIncomingRequestPtr exp_req) {
			ASSERT_TRUE(exp_req) << exp_req.error().String();
			auto req = exp_req.value();

			auto content_length = req->GetHeader("Content-Length");
			ASSERT_TRUE(content_length);
			EXPECT_EQ(content_length.value(), to_string(expected_request_data.size()));
			auto ex_len = common::StringToLongLong(content_length.value());
			ASSERT_TRUE(ex_len);

			auto body_writer = make_shared<io::ByteWriter>(received_body);
			received_body.resize(ex_len.value());
			req->SetBodyWriter(body_writer);
		},
		[&received_body,
		 &expected_request_data,
		 &response_data,
		 deployment_id,
		 &aborted_failure,
		 &request_body_too_large](http::ExpectedIncomingRequestPtr exp_req) {
			ASSERT_TRUE(exp_req) << exp_req.error().String();

			auto req = exp_req.value();
			EXPECT_EQ(
				req->GetPath(),
				"/api/devices/v1/deployments/device/deployments/" + deployment_id + "/status");
			EXPECT_EQ(req->GetMethod(), http::Method::PUT);
			EXPECT_EQ(common::StringFromByteVector(received_body), expected_request_data);

			auto result = req->MakeResponse();
			ASSERT_TRUE(result);
			auto resp = result.value();

			resp->SetHeader("Content-Length", to_string(response_data.size()));
			resp->SetBodyReader(make_shared<io::StringReader>(response_data));
			if (request_body_too_large) {
				resp->SetStatusCodeAndMessage(413, "Request Body Too Large");
			} else if (aborted_failure) {
				resp->SetStatusCodeAndMessage(409, "Conflict");
			} else {
				resp->SetStatusCodeAndMessage(403, "Forbidden");
			}
			resp->AsyncReply([](error::Error err) { ASSERT_EQ(error::NoError, err); });
		});

	bool handler_called = false;
	auto err = deps::DeploymentClient().PushStatus(
		deployment_id,
		status,
		substatus,
		client,
		[&handler_called, &loop](deps::StatusAPIResponse resp) {
			handler_called = true;
			EXPECT_NE(resp.error, error::NoError);
			EXPECT_THAT(resp.error.message, testing::HasSubstr("Got unexpected response"));
			EXPECT_THAT(resp.error.message, testing::HasSubstr("403"));
			EXPECT_THAT(resp.error.message, testing::HasSubstr("Access denied"));
			EXPECT_NE(resp.error.code, deps::MakeError(deps::DeploymentAbortedError, "").code);
			loop.Stop();
		});
	EXPECT_EQ(err, error::NoError);

	loop.Run();
	EXPECT_TRUE(handler_called);

	// Redo with 409 Conflict response (deployment aborted).
	handler_called = false;
	aborted_failure = true;

	err = deps::DeploymentClient().PushStatus(
		deployment_id,
		status,
		substatus,
		client,
		[&handler_called, &loop](deps::StatusAPIResponse resp) {
			handler_called = true;
			EXPECT_NE(resp.error, error::NoError);
			EXPECT_EQ(resp.error.code, deps::MakeError(deps::DeploymentAbortedError, "").code);
			loop.Stop();
		});
	EXPECT_EQ(err, error::NoError);

	loop.Run();
	EXPECT_TRUE(handler_called);
}

TEST_F(DeploymentsTests, JsonLogMessageReaderTest) {
	const string messages =
		R"({"timestamp": "2016-03-11T13:03:17.063493443Z", "level": "INFO", "message": "OK"}
{"timestamp": "2020-03-11T13:03:17.063493443Z", "level": "WARNING", "message": "Warnings appeared"}
{"timestamp": "2021-03-11T13:03:17.063493443Z", "level": "DEBUG", "message": "Just some noise"}
)";
	const string test_log_file_path = test_state_dir.Path() + "/test.log";
	ofstream os {test_log_file_path};
	auto err = io::WriteStringIntoOfstream(os, messages);
	ASSERT_EQ(err, error::NoError);
	os.close();

	string header = R"({"messages":[)";
	string closing = "]}";
	string expected_data =
		R"({"messages":[{"timestamp": "2016-03-11T13:03:17.063493443Z", "level": "INFO", "message": "OK"},{"timestamp": "2020-03-11T13:03:17.063493443Z", "level": "WARNING", "message": "Warnings appeared"},{"timestamp": "2021-03-11T13:03:17.063493443Z", "level": "DEBUG", "message": "Just some noise"}]})";

	deps::JsonLogMessagesReader logs_reader {test_log_file_path};
	EXPECT_EQ(logs_reader.SanitizeLogs(), error::NoError);

	// the reader takes size of the data without a trailing newline
	auto expected_total_size = header.size() + messages.size() - 1 + closing.size();
	EXPECT_EQ(logs_reader.TotalDataSize(), expected_total_size);

	stringstream ss;
	vector<uint8_t> buf(1024);
	size_t n_read = 0;
	do {
		auto ex_n_read = logs_reader.Read(buf.begin(), buf.end());
		ASSERT_TRUE(ex_n_read);
		n_read = ex_n_read.value();
		EXPECT_LE(n_read, buf.size());
		for (auto it = buf.begin(); it < buf.begin() + n_read; it++) {
			ss << static_cast<char>(*it);
		}
	} while (n_read > 0);
	EXPECT_EQ(ss.str(), expected_data);
}

TEST_F(DeploymentsTests, JsonLogMessageReaderSmallBufferTest) {
	const string messages =
		R"({"timestamp": "2016-03-11T13:03:17.063493443Z", "level": "INFO", "message": "OK"}
{"timestamp": "2020-03-11T13:03:17.063493443Z", "level": "WARNING", "message": "Warnings appeared"}
{"timestamp": "2021-03-11T13:03:17.063493443Z", "level": "DEBUG", "message": "Just some noise"}
)";
	const string test_log_file_path = test_state_dir.Path() + "/test.log";
	ofstream os {test_log_file_path};
	auto err = io::WriteStringIntoOfstream(os, messages);
	ASSERT_EQ(err, error::NoError);
	os.close();

	string header = R"({"messages":[)";
	string closing = "]}";
	string expected_data =
		R"({"messages":[{"timestamp": "2016-03-11T13:03:17.063493443Z", "level": "INFO", "message": "OK"},{"timestamp": "2020-03-11T13:03:17.063493443Z", "level": "WARNING", "message": "Warnings appeared"},{"timestamp": "2021-03-11T13:03:17.063493443Z", "level": "DEBUG", "message": "Just some noise"}]})";

	deps::JsonLogMessagesReader logs_reader {test_log_file_path};
	EXPECT_EQ(logs_reader.SanitizeLogs(), error::NoError);

	// the reader takes size of the data without a trailing newline
	auto expected_total_size = header.size() + messages.size() - 1 + closing.size();
	EXPECT_EQ(logs_reader.TotalDataSize(), expected_total_size);

	stringstream ss;
	vector<uint8_t> buf(16);
	size_t n_read = 0;
	do {
		auto ex_n_read = logs_reader.Read(buf.begin(), buf.end());
		ASSERT_TRUE(ex_n_read);
		n_read = ex_n_read.value();
		EXPECT_LE(n_read, buf.size());
		for (auto it = buf.begin(); it < buf.begin() + n_read; it++) {
			ss << static_cast<char>(*it);
		}
	} while (n_read > 0);
	EXPECT_EQ(ss.str(), expected_data);
}

TEST_F(DeploymentsTests, JsonLogMessageReaderSmallEvenBufferTest) {
	const string messages =
		R"({"timestamp": "2016-03-11T13:03:17.063493443Z", "level": "INFO", "message": "OK"}
{"timestamp": "2020-03-11T13:03:17.063493443Z", "level": "WARNING", "message": "Warnings appeared"}
{"timestamp": "2021-03-11T13:03:17.063493443Z", "level": "DEBUG", "message": "Just some noise"}
)";
	const string test_log_file_path = test_state_dir.Path() + "/test.log";
	ofstream os {test_log_file_path};
	auto err = io::WriteStringIntoOfstream(os, messages);
	ASSERT_EQ(err, error::NoError);
	os.close();

	string header = R"({"messages":[)";
	string closing = "]}";
	string expected_data =
		R"({"messages":[{"timestamp": "2016-03-11T13:03:17.063493443Z", "level": "INFO", "message": "OK"},{"timestamp": "2020-03-11T13:03:17.063493443Z", "level": "WARNING", "message": "Warnings appeared"},{"timestamp": "2021-03-11T13:03:17.063493443Z", "level": "DEBUG", "message": "Just some noise"}]})";

	deps::JsonLogMessagesReader logs_reader {test_log_file_path};
	EXPECT_EQ(logs_reader.SanitizeLogs(), error::NoError);

	// the reader takes size of the data without a trailing newline
	auto expected_total_size = header.size() + messages.size() - 1 + closing.size();
	EXPECT_EQ(logs_reader.TotalDataSize(), expected_total_size);

	stringstream ss;
	vector<uint8_t> buf(7);
	size_t n_read = 0;
	do {
		auto ex_n_read = logs_reader.Read(buf.begin(), buf.end());
		ASSERT_TRUE(ex_n_read);
		n_read = ex_n_read.value();
		EXPECT_LE(n_read, buf.size());
		for (auto it = buf.begin(); it < buf.begin() + n_read; it++) {
			ss << static_cast<char>(*it);
		}
	} while (n_read > 0);
	EXPECT_EQ(ss.str(), expected_data);
}

TEST_F(DeploymentsTests, JsonLogMessageReaderRewindTest) {
	const string messages =
		R"({"timestamp": "2016-03-11T13:03:17.063493443Z", "level": "INFO", "message": "OK"}
{"timestamp": "2020-03-11T13:03:17.063493443Z", "level": "WARNING", "message": "Warnings appeared"}
{"timestamp": "2021-03-11T13:03:17.063493443Z", "level": "DEBUG", "message": "Just some noise"}
)";
	const string test_log_file_path = test_state_dir.Path() + "/test.log";
	ofstream os {test_log_file_path};
	auto err = io::WriteStringIntoOfstream(os, messages);
	ASSERT_EQ(err, error::NoError);
	os.close();

	string header = R"({"messages":[)";
	string closing = "]}";
	string expected_data =
		R"({"messages":[{"timestamp": "2016-03-11T13:03:17.063493443Z", "level": "INFO", "message": "OK"},{"timestamp": "2020-03-11T13:03:17.063493443Z", "level": "WARNING", "message": "Warnings appeared"},{"timestamp": "2021-03-11T13:03:17.063493443Z", "level": "DEBUG", "message": "Just some noise"}]})";

	auto file_reader = make_shared<io::FileReader>(test_log_file_path);
	deps::JsonLogMessagesReader logs_reader {test_log_file_path};
	EXPECT_EQ(logs_reader.SanitizeLogs(), error::NoError);

	// the reader takes size of the data without a trailing newline
	auto expected_total_size = header.size() + messages.size() - 1 + closing.size();
	EXPECT_EQ(logs_reader.TotalDataSize(), expected_total_size);

	stringstream ss;
	vector<uint8_t> buf(1024);
	size_t n_read = 0;
	do {
		auto ex_n_read = logs_reader.Read(buf.begin(), buf.end());
		ASSERT_TRUE(ex_n_read);
		n_read = ex_n_read.value();
		EXPECT_LE(n_read, buf.size());
		for (auto it = buf.begin(); it < buf.begin() + n_read; it++) {
			ss << static_cast<char>(*it);
		}
	} while (n_read > 0);
	EXPECT_EQ(ss.str(), expected_data);

	stringstream ss2;
	logs_reader.Rewind();
	do {
		auto ex_n_read = logs_reader.Read(buf.begin(), buf.end());
		ASSERT_TRUE(ex_n_read);
		n_read = ex_n_read.value();
		EXPECT_LE(n_read, buf.size());
		for (auto it = buf.begin(); it < buf.begin() + n_read; it++) {
			ss2 << static_cast<char>(*it);
		}
	} while (n_read > 0);
	EXPECT_EQ(ss2.str(), expected_data);
}

TEST_F(DeploymentsTests, JsonLogMessageReaderRewindIgnoresAppendedDataTest) {
	// Regression test: while a logs upload is in flight, the deployment's own
	// logging keeps appending to the original log file. The body produced after
	// Rewind() must stay identical to the snapshot that SanitizeLogs() computed
	// the Content-Length (TotalDataSize()) from. Otherwise the streamed body and
	// the declared Content-Length diverge and the server rejects the truncated
	// body with "unexpected EOF".
	const string messages =
		R"({"timestamp": "2016-03-11T13:03:17.063493443Z", "level": "INFO", "message": "OK"}
{"timestamp": "2020-03-11T13:03:17.063493443Z", "level": "WARNING", "message": "Warnings appeared"}
{"timestamp": "2021-03-11T13:03:17.063493443Z", "level": "DEBUG", "message": "Just some noise"}
)";
	const string test_log_file_path = test_state_dir.Path() + "/test.log";
	ofstream os {test_log_file_path};
	auto err = io::WriteStringIntoOfstream(os, messages);
	ASSERT_EQ(err, error::NoError);
	os.close();

	string header = R"({"messages":[)";
	string closing = "]}";
	string expected_data =
		R"({"messages":[{"timestamp": "2016-03-11T13:03:17.063493443Z", "level": "INFO", "message": "OK"},{"timestamp": "2020-03-11T13:03:17.063493443Z", "level": "WARNING", "message": "Warnings appeared"},{"timestamp": "2021-03-11T13:03:17.063493443Z", "level": "DEBUG", "message": "Just some noise"}]})";

	deps::JsonLogMessagesReader logs_reader {test_log_file_path};
	EXPECT_EQ(logs_reader.SanitizeLogs(), error::NoError);

	// the reader takes size of the data without a trailing newline
	auto expected_total_size = header.size() + messages.size() - 1 + closing.size();
	EXPECT_EQ(logs_reader.TotalDataSize(), expected_total_size);

	// Simulate the original log file growing after the snapshot and the
	// Content-Length have been computed, but before the body is streamed.
	ofstream append_os {test_log_file_path, std::ios::app};
	err = io::WriteStringIntoOfstream(
		append_os,
		R"({"timestamp": "2022-03-11T13:03:17.063493443Z", "level": "INFO", "message": "Appended after snapshot"})"
		"\n");
	ASSERT_EQ(err, error::NoError);
	append_os.close();

	// The advertised size must not change because of the append.
	EXPECT_EQ(logs_reader.TotalDataSize(), expected_total_size);

	ASSERT_EQ(logs_reader.Rewind(), error::NoError);

	stringstream ss;
	vector<uint8_t> buf(1024);
	size_t n_read = 0;
	size_t total_read = 0;
	do {
		auto ex_n_read = logs_reader.Read(buf.begin(), buf.end());
		ASSERT_TRUE(ex_n_read);
		n_read = ex_n_read.value();
		EXPECT_LE(n_read, buf.size());
		for (auto it = buf.begin(); it < buf.begin() + n_read; it++) {
			ss << static_cast<char>(*it);
		}
		total_read += n_read;
	} while (n_read > 0);

	// The streamed body must match the snapshot exactly: the appended entry is
	// not included, and the total number of bytes equals the advertised size.
	EXPECT_EQ(ss.str(), expected_data);
	EXPECT_EQ(total_read, static_cast<size_t>(expected_total_size));
}

TEST_F(DeploymentsTests, JsonLogMessageReaderRewindTruncatedLogsKeepNewestTest) {
	// Logs larger than maximum_log_size_ are truncated to keep the newest
	// entries. The reader starts at an offset into the sanitized snapshot, so
	// Rewind() must return to that offset rather than the start of the file.
	// Otherwise the upload would stream the oldest (dropped) entries instead of
	// the newest ones it advertised.
	const string test_log_file_path = test_state_dir.Path() + "/test.log";
	ofstream os {test_log_file_path};
	os << R"({"timestamp": "2016-03-11T13:03:17.063493443Z", "level": "INFO", "message": "FIRSTLINE_MARKER"})"
	   << "\n";
	// Enough filler to exceed maximum_log_size_ (~1MB) and trigger truncation.
	for (int i = 0; i < 15000; i++) {
		os << R"({"timestamp": "2020-03-11T13:03:17.063493443Z", "level": "INFO", "message": "filler entry padded out to add bytes xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"})"
		   << "\n";
	}
	os << R"({"timestamp": "2021-03-11T13:03:17.063493443Z", "level": "INFO", "message": "LASTLINE_MARKER"})"
	   << "\n";
	os.close();

	deps::JsonLogMessagesReader logs_reader {test_log_file_path};
	ASSERT_EQ(logs_reader.SanitizeLogs(), error::NoError);
	ASSERT_EQ(logs_reader.Rewind(), error::NoError);

	stringstream ss;
	vector<uint8_t> buf(4096);
	size_t n_read = 0;
	size_t total_read = 0;
	do {
		auto ex_n_read = logs_reader.Read(buf.begin(), buf.end());
		ASSERT_TRUE(ex_n_read);
		n_read = ex_n_read.value();
		for (auto it = buf.begin(); it < buf.begin() + n_read; it++) {
			ss << static_cast<char>(*it);
		}
		total_read += n_read;
	} while (n_read > 0);

	const string body = ss.str();
	// Truncation keeps the newest entries and drops the oldest.
	EXPECT_NE(body.find("LASTLINE_MARKER"), string::npos);
	EXPECT_EQ(body.find("FIRSTLINE_MARKER"), string::npos);
	// The body must match the advertised Content-Length exactly.
	EXPECT_EQ(total_read, static_cast<size_t>(logs_reader.TotalDataSize()));
}

TEST_F(DeploymentsTests, JsonLogMessageReaderMalformedJsonTest) {
	const string messages =
		R"({"timestamp": "2016-03-11T13:03:17.063493443Z", "level": "INFO", "message": "OK"}
{"timestamp": "2020-03-11T13:03:17.063493443Z", "level": "WARNING", "message": "Warnings appear
ed"}
{"timestamp": "2021-03-11T13:03:17.063493443Z", "level": "DEBUG", "message": "Just some noise"}
)";
	const string test_log_file_path = test_state_dir.Path() + "/test.log";
	ofstream os {test_log_file_path};
	auto err = io::WriteStringIntoOfstream(os, messages);
	ASSERT_EQ(err, error::NoError);
	os.close();

	// Corrupted line should be replaced with timestamp from previous valid line (2016-03-11...)
	string expected_data =
		R"delim({"messages":[{"timestamp": "2016-03-11T13:03:17.063493443Z", "level": "ERROR", "message": "(THE ORIGINAL LOGS CONTAINED INVALID ENTRIES)"},{"timestamp": "2016-03-11T13:03:17.063493443Z", "level": "INFO", "message": "OK"},{"timestamp": "2016-03-11T13:03:17.063493443Z", "level": "ERROR", "message": "(CORRUPTED LOG DATA)"},{"timestamp": "2016-03-11T13:03:17.063493443Z", "level": "ERROR", "message": "(CORRUPTED LOG DATA)"},{"timestamp": "2021-03-11T13:03:17.063493443Z", "level": "DEBUG", "message": "Just some noise"}]})delim";

	deps::JsonLogMessagesReader logs_reader {test_log_file_path};
	EXPECT_EQ(logs_reader.SanitizeLogs(), error::NoError);
	EXPECT_EQ(logs_reader.TotalDataSize(), expected_data.length());

	stringstream ss;
	vector<uint8_t> buf(1024);
	size_t n_read = 0;
	do {
		auto ex_n_read = logs_reader.Read(buf.begin(), buf.end());
		ASSERT_TRUE(ex_n_read);
		n_read = ex_n_read.value();
		for (auto it = buf.begin(); it < buf.begin() + n_read; it++) {
			ss << static_cast<char>(*it);
		}
	} while (n_read > 0);
	EXPECT_EQ(ss.str(), expected_data);
}

TEST_F(DeploymentsTests, JsonLogMessageReaderMalformedJsonShortFirstTstampTest) {
	const string messages =
		R"({"timestamp": "2016-03-11T13:03:17.063493Z", "level": "INFO", "message": "OK"}
{"timestamp": "2020-03-11T13:03:17.063493443Z", "level": "WARNING", "message": "Warnings appear
ed"}
{"timestamp": "2021-03-11T13:03:17.063493443Z", "level": "DEBUG", "message": "Just some noise"}
)";
	const string test_log_file_path = test_state_dir.Path() + "/test.log";
	ofstream os {test_log_file_path};
	auto err = io::WriteStringIntoOfstream(os, messages);
	ASSERT_EQ(err, error::NoError);
	os.close();

	// Corrupted line should be replaced with timestamp from previous valid line
	// (2016-03-11...), the first extra message should use the first timestamp
	// in a valid format, even when the timestamp is shorter than expected due
	// to lower time resolution.
	string expected_data =
		R"delim({"messages":[{"timestamp": "2016-03-11T13:03:17.063493Z"   , "level": "ERROR", "message": "(THE ORIGINAL LOGS CONTAINED INVALID ENTRIES)"},{"timestamp": "2016-03-11T13:03:17.063493Z", "level": "INFO", "message": "OK"},{"timestamp": "2016-03-11T13:03:17.063493Z", "level": "ERROR", "message": "(CORRUPTED LOG DATA)"},{"timestamp": "2016-03-11T13:03:17.063493Z", "level": "ERROR", "message": "(CORRUPTED LOG DATA)"},{"timestamp": "2021-03-11T13:03:17.063493443Z", "level": "DEBUG", "message": "Just some noise"}]})delim";

	deps::JsonLogMessagesReader logs_reader {test_log_file_path};
	EXPECT_EQ(logs_reader.SanitizeLogs(), error::NoError);
	EXPECT_EQ(logs_reader.TotalDataSize(), expected_data.length());

	stringstream ss;
	vector<uint8_t> buf(1024);
	size_t n_read = 0;
	do {
		auto ex_n_read = logs_reader.Read(buf.begin(), buf.end());
		ASSERT_TRUE(ex_n_read);
		n_read = ex_n_read.value();
		for (auto it = buf.begin(); it < buf.begin() + n_read; it++) {
			ss << static_cast<char>(*it);
		}
	} while (n_read > 0);
	EXPECT_EQ(ss.str(), expected_data);
}

TEST_F(DeploymentsTests, JsonLogMessageReaderMalformedJsonLongFirstTstampTest) {
	const string messages =
		R"({"timestamp": "2016-03-11T13:03:17.06349344355Z", "level": "INFO", "message": "OK"}
{"timestamp": "2020-03-11T13:03:17.063493443Z", "level": "WARNING", "message": "Warnings appear
ed"}
{"timestamp": "2021-03-11T13:03:17.063493443Z", "level": "DEBUG", "message": "Just some noise"}
)";
	const string test_log_file_path = test_state_dir.Path() + "/test.log";
	ofstream os {test_log_file_path};
	auto err = io::WriteStringIntoOfstream(os, messages);
	ASSERT_EQ(err, error::NoError);
	os.close();

	// Corrupted line should be replaced with timestamp from previous valid line
	// (2016-03-11...), the first extra message should use the first timestamp
	// in a valid format, even when the timestamp is longer than expected due to
	// lower time resolution (the other lines can use the full timestamps, they
	// don't rely on a template that needs to be filled-in).
	string expected_data =
		R"delim({"messages":[{"timestamp": "2016-03-11T13:03:17.063493443Z", "level": "ERROR", "message": "(THE ORIGINAL LOGS CONTAINED INVALID ENTRIES)"},{"timestamp": "2016-03-11T13:03:17.06349344355Z", "level": "INFO", "message": "OK"},{"timestamp": "2016-03-11T13:03:17.06349344355Z", "level": "ERROR", "message": "(CORRUPTED LOG DATA)"},{"timestamp": "2016-03-11T13:03:17.06349344355Z", "level": "ERROR", "message": "(CORRUPTED LOG DATA)"},{"timestamp": "2021-03-11T13:03:17.063493443Z", "level": "DEBUG", "message": "Just some noise"}]})delim";

	deps::JsonLogMessagesReader logs_reader {test_log_file_path};
	EXPECT_EQ(logs_reader.SanitizeLogs(), error::NoError);
	EXPECT_EQ(logs_reader.TotalDataSize(), expected_data.length());

	stringstream ss;
	vector<uint8_t> buf(1024);
	size_t n_read = 0;
	do {
		auto ex_n_read = logs_reader.Read(buf.begin(), buf.end());
		ASSERT_TRUE(ex_n_read);
		n_read = ex_n_read.value();
		for (auto it = buf.begin(); it < buf.begin() + n_read; it++) {
			ss << static_cast<char>(*it);
		}
	} while (n_read > 0);
	EXPECT_EQ(ss.str(), expected_data);
}

TEST_F(DeploymentsTests, JsonLogMessageReaderBinaryDataTest) {
	// Create file with binary data directly
	const string test_log_file_path = test_state_dir.Path() + "/test.log";
	ofstream os {test_log_file_path, std::ios::binary};
	os << R"({"timestamp": "2016-03-11T13:03:17.063493443Z", "level": "INFO", "message": "OK"})"
	   << "\n";
	os.write("\x00\x01\xFF\xFE corrupted binary data here", 32);
	os << "\n";
	os << R"({"timestamp": "2021-03-11T13:03:17.063493443Z", "level": "DEBUG", "message": "After corruption"})"
	   << "\n";
	os.close();

	// Binary line replaced with timestamp from previous valid line
	string expected_data =
		R"delim({"messages":[{"timestamp": "2016-03-11T13:03:17.063493443Z", "level": "ERROR", "message": "(THE ORIGINAL LOGS CONTAINED INVALID ENTRIES)"},{"timestamp": "2016-03-11T13:03:17.063493443Z", "level": "INFO", "message": "OK"},{"timestamp": "2016-03-11T13:03:17.063493443Z", "level": "ERROR", "message": "(CORRUPTED LOG DATA)"},{"timestamp": "2021-03-11T13:03:17.063493443Z", "level": "DEBUG", "message": "After corruption"}]})delim";

	deps::JsonLogMessagesReader logs_reader {test_log_file_path};
	EXPECT_EQ(logs_reader.SanitizeLogs(), error::NoError);
	EXPECT_EQ(logs_reader.TotalDataSize(), expected_data.length());

	stringstream ss;
	vector<uint8_t> buf(1024);
	size_t n_read = 0;
	do {
		auto ex_n_read = logs_reader.Read(buf.begin(), buf.end());
		ASSERT_TRUE(ex_n_read);
		n_read = ex_n_read.value();
		for (auto it = buf.begin(); it < buf.begin() + n_read; it++) {
			ss << static_cast<char>(*it);
		}
	} while (n_read > 0);
	EXPECT_EQ(ss.str(), expected_data);
}

TEST_F(DeploymentsTests, JsonLogMessageReaderIncompleteLineTest) {
	const string messages =
		R"({"timestamp": "2016-03-11T13:03:17.063493443Z", "level": "INFO", "message": "OK"}
{"timestamp": "2020-03-11T13:03:17.063493443Z", "level": "WARNING", "message": "Warnings appeared"}
{"timestamp": "2021-03-11T13:03:17.063493443Z", "level": "DEBUG", "message": "Incom)";
	const string test_log_file_path = test_state_dir.Path() + "/test.log";
	ofstream os {test_log_file_path};
	auto err = io::WriteStringIntoOfstream(os, messages);
	ASSERT_EQ(err, error::NoError);
	os.close();

	// Incomplete line replaced with timestamp from previous valid line (2020-03-11...)
	string expected_data =
		R"delim({"messages":[{"timestamp": "2016-03-11T13:03:17.063493443Z", "level": "ERROR", "message": "(THE ORIGINAL LOGS CONTAINED INVALID ENTRIES)"},{"timestamp": "2016-03-11T13:03:17.063493443Z", "level": "INFO", "message": "OK"},{"timestamp": "2020-03-11T13:03:17.063493443Z", "level": "WARNING", "message": "Warnings appeared"},{"timestamp": "2020-03-11T13:03:17.063493443Z", "level": "ERROR", "message": "(CORRUPTED LOG DATA)"}]})delim";

	deps::JsonLogMessagesReader logs_reader {test_log_file_path};
	EXPECT_EQ(logs_reader.SanitizeLogs(), error::NoError);
	EXPECT_EQ(logs_reader.TotalDataSize(), expected_data.length());

	stringstream ss;
	vector<uint8_t> buf(1024);
	size_t n_read = 0;
	do {
		auto ex_n_read = logs_reader.Read(buf.begin(), buf.end());
		ASSERT_TRUE(ex_n_read);
		n_read = ex_n_read.value();
		for (auto it = buf.begin(); it < buf.begin() + n_read; it++) {
			ss << static_cast<char>(*it);
		}
	} while (n_read > 0);
	EXPECT_EQ(ss.str(), expected_data);
}

TEST_F(DeploymentsTests, JsonLogMessageReaderEmptyLinesTest) {
	const string messages =
		R"({"timestamp": "2016-03-11T13:03:17.063493443Z", "level": "INFO", "message": "OK"}

  {"timestamp": "2020-03-11T13:03:17.063493443Z", "level": "WARNING", "message": "After blank"}
                    
	{"timestamp": "2021-03-11T13:03:17.063493443Z", "level": "DEBUG", "message": "After whitespace"}
)";
	const string test_log_file_path = test_state_dir.Path() + "/test.log";
	ofstream os {test_log_file_path};
	auto err = io::WriteStringIntoOfstream(os, messages);
	ASSERT_EQ(err, error::NoError);
	os.close();

	// Empty lines are OK, whitespace lines are bad
	// However the whitespace before a valid line should be kept as-is
	string expected_data =
		R"delim({"messages":[{"timestamp": "2016-03-11T13:03:17.063493443Z", "level": "ERROR", "message": "(THE ORIGINAL LOGS CONTAINED INVALID ENTRIES)"},{"timestamp": "2016-03-11T13:03:17.063493443Z", "level": "INFO", "message": "OK"},  {"timestamp": "2020-03-11T13:03:17.063493443Z", "level": "WARNING", "message": "After blank"},{"timestamp": "2020-03-11T13:03:17.063493443Z", "level": "ERROR", "message": "(CORRUPTED LOG DATA)"},	{"timestamp": "2021-03-11T13:03:17.063493443Z", "level": "DEBUG", "message": "After whitespace"}]})delim";

	deps::JsonLogMessagesReader logs_reader {test_log_file_path};
	EXPECT_EQ(logs_reader.SanitizeLogs(), error::NoError);
	EXPECT_EQ(logs_reader.TotalDataSize(), expected_data.length());

	stringstream ss;
	vector<uint8_t> buf(1024);
	size_t n_read = 0;
	do {
		auto ex_n_read = logs_reader.Read(buf.begin(), buf.end());
		ASSERT_TRUE(ex_n_read);
		n_read = ex_n_read.value();
		for (auto it = buf.begin(); it < buf.begin() + n_read; it++) {
			ss << static_cast<char>(*it);
		}
	} while (n_read > 0);
	EXPECT_EQ(ss.str(), expected_data);
}

TEST_F(DeploymentsTests, JsonLogMessageReaderRecoveryTest) {
	const string messages =
		R"({"timestamp": "2016-03-11T13:03:17.063493443Z", "level": "INFO", "message": "OK"}
this is not json at all
{"timestamp": "2020-03-11T13:03:17.063493443Z", "level": "WARNING", "message": "After corruption"}
{corrupt}
{"timestamp": "2021-03-11T13:03:17.063493443Z", "level": "DEBUG", "message": "After more corruption"}
)";
	const string test_log_file_path = test_state_dir.Path() + "/test.log";
	ofstream os {test_log_file_path};
	auto err = io::WriteStringIntoOfstream(os, messages);
	ASSERT_EQ(err, error::NoError);
	os.close();

	// Each corrupted line replaced with timestamp from previous valid line
	string expected_data =
		R"delim({"messages":[{"timestamp": "2016-03-11T13:03:17.063493443Z", "level": "ERROR", "message": "(THE ORIGINAL LOGS CONTAINED INVALID ENTRIES)"},{"timestamp": "2016-03-11T13:03:17.063493443Z", "level": "INFO", "message": "OK"},{"timestamp": "2016-03-11T13:03:17.063493443Z", "level": "ERROR", "message": "(CORRUPTED LOG DATA)"},{"timestamp": "2020-03-11T13:03:17.063493443Z", "level": "WARNING", "message": "After corruption"},{"timestamp": "2020-03-11T13:03:17.063493443Z", "level": "ERROR", "message": "(CORRUPTED LOG DATA)"},{"timestamp": "2021-03-11T13:03:17.063493443Z", "level": "DEBUG", "message": "After more corruption"}]})delim";

	deps::JsonLogMessagesReader logs_reader {test_log_file_path};
	EXPECT_EQ(logs_reader.SanitizeLogs(), error::NoError);
	EXPECT_EQ(logs_reader.TotalDataSize(), expected_data.length());

	stringstream ss;
	vector<uint8_t> buf(1024);
	size_t n_read = 0;
	do {
		auto ex_n_read = logs_reader.Read(buf.begin(), buf.end());
		ASSERT_TRUE(ex_n_read);
		n_read = ex_n_read.value();
		for (auto it = buf.begin(); it < buf.begin() + n_read; it++) {
			ss << static_cast<char>(*it);
		}
	} while (n_read > 0);
	EXPECT_EQ(ss.str(), expected_data);
}

TEST_F(DeploymentsTests, JsonLogMessageReaderFirstLineCorruptTest) {
	const string messages =
		R"(this is not json
{"timestamp": "2020-03-11T13:03:17.063493443Z", "level": "WARNING", "message": "After corruption"}
{"timestamp": "2021-03-11T13:03:17.063493443Z", "level": "DEBUG", "message": "Valid"}
)";
	const string test_log_file_path = test_state_dir.Path() + "/test.log";
	ofstream os {test_log_file_path};
	auto err = io::WriteStringIntoOfstream(os, messages);
	ASSERT_EQ(err, error::NoError);
	os.close();

	// First line corrupted - use a default timestamp (e.g., epoch or current)
	// Let's assume it uses epoch: "1970-01-01T00:00:00.000000000Z"
	string expected_data =
		R"delim({"messages":[{"timestamp": "1970-01-01T00:00:00.000000000Z", "level": "ERROR", "message": "(THE ORIGINAL LOGS CONTAINED INVALID ENTRIES)"},{"timestamp": "1970-01-01T00:00:00.000000000Z", "level": "ERROR", "message": "(CORRUPTED LOG DATA)"},{"timestamp": "2020-03-11T13:03:17.063493443Z", "level": "WARNING", "message": "After corruption"},{"timestamp": "2021-03-11T13:03:17.063493443Z", "level": "DEBUG", "message": "Valid"}]})delim";

	deps::JsonLogMessagesReader logs_reader {test_log_file_path};
	EXPECT_EQ(logs_reader.SanitizeLogs(), error::NoError);
	EXPECT_EQ(logs_reader.TotalDataSize(), expected_data.length());

	stringstream ss;
	vector<uint8_t> buf(1024);
	size_t n_read = 0;
	do {
		auto ex_n_read = logs_reader.Read(buf.begin(), buf.end());
		ASSERT_TRUE(ex_n_read);
		n_read = ex_n_read.value();
		for (auto it = buf.begin(); it < buf.begin() + n_read; it++) {
			ss << static_cast<char>(*it);
		}
	} while (n_read > 0);
	EXPECT_EQ(ss.str(), expected_data);
}

TEST_F(DeploymentsTests, JsonLogMessageReaderSmallBufferCorruptTest) {
	const string messages =
		R"({"timestamp": "2016-03-11T13:03:17.063493443Z", "level": "INFO", "message": "OK"}
not valid json
{"timestamp": "2021-03-11T13:03:17.063493443Z", "level": "DEBUG", "message": "Valid"}
)";
	const string test_log_file_path = test_state_dir.Path() + "/test.log";
	ofstream os {test_log_file_path};
	auto err = io::WriteStringIntoOfstream(os, messages);
	ASSERT_EQ(err, error::NoError);
	os.close();

	string expected_data =
		R"delim({"messages":[{"timestamp": "2016-03-11T13:03:17.063493443Z", "level": "ERROR", "message": "(THE ORIGINAL LOGS CONTAINED INVALID ENTRIES)"},{"timestamp": "2016-03-11T13:03:17.063493443Z", "level": "INFO", "message": "OK"},{"timestamp": "2016-03-11T13:03:17.063493443Z", "level": "ERROR", "message": "(CORRUPTED LOG DATA)"},{"timestamp": "2021-03-11T13:03:17.063493443Z", "level": "DEBUG", "message": "Valid"}]})delim";

	deps::JsonLogMessagesReader logs_reader {test_log_file_path};
	EXPECT_EQ(logs_reader.SanitizeLogs(), error::NoError);
	EXPECT_EQ(logs_reader.TotalDataSize(), expected_data.length());

	stringstream ss;
	vector<uint8_t> buf(16); // Small buffer
	size_t n_read = 0;
	do {
		auto ex_n_read = logs_reader.Read(buf.begin(), buf.end());
		ASSERT_TRUE(ex_n_read);
		n_read = ex_n_read.value();
		EXPECT_LE(n_read, buf.size());
		for (auto it = buf.begin(); it < buf.begin() + n_read; it++) {
			ss << static_cast<char>(*it);
		}
	} while (n_read > 0);
	EXPECT_EQ(ss.str(), expected_data);
}

TEST_F(DeploymentsTests, JsonLogMessageReaderLargeLogsTest) {
	const string messages =
		R"({"timestamp": "2016-03-11T13:03:17.063493443Z", "level": "INFO", "message": "OK"}
{"timestamp": "2020-03-11T13:03:17.063493443Z", "level": "WARNING", "message": "Warnings appeared"}
{"timestamp": "2021-03-11T13:03:17.063493443Z", "level": "DEBUG", "message": "Just some noise"}
)";
	const string test_log_file_path = test_state_dir.Path() + "/test.log";
	ofstream os {test_log_file_path};
	auto err = io::WriteStringIntoOfstream(os, messages);
	ASSERT_EQ(err, error::NoError);
	for (int i = 0; i < 20000; i++) {
		os << R"({"timestamp": "2016-03-11T13:05:17.063493443Z", "level": "INFO", "message": "excessive log data"})"
		   << endl;
	}
	os.close();

	string expected_data_start =
		R"d({"messages":[{"timestamp": "2016-03-11T13:03:17.063493443Z", "level": "WARNING", "message": "(THE ORIGINAL LOGS WERE TOO BIG, THIS LOG IS TRUNCATED. The full log can be found on the device)"},{"timestamp": "2016-03-11T13:05:17.063493443Z", "level": "INFO", "message": "excessive log data"})d";
	string expected_data_end =
		R"d({"timestamp": "2016-03-11T13:05:17.063493443Z", "level": "INFO", "message": "excessive log data"}]})d";

	deps::JsonLogMessagesReader logs_reader {test_log_file_path};
	EXPECT_EQ(logs_reader.SanitizeLogs(), error::NoError);
	auto total_size = logs_reader.TotalDataSize();
	EXPECT_LT(total_size, JsonLogMessagesReaderGetMaxLogSize(logs_reader));

	stringstream ss;
	vector<uint8_t> buf(1024);
	size_t n_read = 0;
	do {
		auto ex_n_read = logs_reader.Read(buf.begin(), buf.end());
		ASSERT_TRUE(ex_n_read);
		n_read = ex_n_read.value();
		for (auto it = buf.begin(); it < buf.begin() + n_read; it++) {
			ss << static_cast<char>(*it);
		}
	} while (n_read > 0);
	EXPECT_EQ(ss.str().substr(0, expected_data_start.size()), expected_data_start);
	EXPECT_EQ(ss.str().substr(ss.str().size() - expected_data_end.size()), expected_data_end);
	EXPECT_EQ(ss.str().size(), total_size);
}

TEST_F(DeploymentsTests, PushLogsTest) {
	TestEventLoop loop;

	http::ServerConfig server_config;
	http::Server server(server_config, loop);

	http::ClientConfig client_config;
	NoAuthHTTPClient client {client_config, loop};

	const string messages =
		R"({"timestamp": "2016-03-11T13:03:17.063493443Z", "level": "INFO", "message": "OK"}
{"timestamp": "2020-03-11T13:03:17.063493443Z", "level": "WARNING", "message": "Warnings appeared"}
{"timestamp": "2021-03-11T13:03:17.063493443Z", "level": "DEBUG", "message": "Just some noise"}
)";
	const string test_log_file_path = test_state_dir.Path() + "/test.log";
	ofstream os {test_log_file_path};
	auto err = io::WriteStringIntoOfstream(os, messages);
	ASSERT_EQ(err, error::NoError);
	os.close();

	string deployment_id = "2";
	string expected_request_data =
		R"({"messages":[{"timestamp": "2016-03-11T13:03:17.063493443Z", "level": "INFO", "message": "OK"},{"timestamp": "2020-03-11T13:03:17.063493443Z", "level": "WARNING", "message": "Warnings appeared"},{"timestamp": "2021-03-11T13:03:17.063493443Z", "level": "DEBUG", "message": "Just some noise"}]})";

	const string response_data = "";

	vector<uint8_t> received_body;
	server.AsyncServeUrl(
		TEST_SERVER,
		[&received_body, &expected_request_data](http::ExpectedIncomingRequestPtr exp_req) {
			ASSERT_TRUE(exp_req) << exp_req.error().String();
			auto req = exp_req.value();

			auto content_length = req->GetHeader("Content-Length");
			ASSERT_TRUE(content_length);
			EXPECT_EQ(content_length.value(), to_string(expected_request_data.size()));
			auto ex_len = common::StringToLongLong(content_length.value());
			ASSERT_TRUE(ex_len);

			auto body_writer = make_shared<io::ByteWriter>(received_body);
			received_body.resize(ex_len.value());
			req->SetBodyWriter(body_writer);
		},
		[&received_body, &expected_request_data, &response_data, deployment_id](
			http::ExpectedIncomingRequestPtr exp_req) {
			ASSERT_TRUE(exp_req) << exp_req.error().String();

			auto req = exp_req.value();
			EXPECT_EQ(
				req->GetPath(),
				"/api/devices/v1/deployments/device/deployments/" + deployment_id + "/log");
			EXPECT_EQ(req->GetMethod(), http::Method::PUT);
			EXPECT_EQ(common::StringFromByteVector(received_body), expected_request_data);

			auto result = req->MakeResponse();
			ASSERT_TRUE(result);
			auto resp = result.value();

			resp->SetHeader("Content-Length", to_string(response_data.size()));
			resp->SetBodyReader(make_shared<io::StringReader>(response_data));
			resp->SetStatusCodeAndMessage(204, "No content");
			resp->AsyncReply([](error::Error err) { ASSERT_EQ(error::NoError, err); });
		});

	bool handler_called = false;
	err = deps::DeploymentClient().PushLogs(
		deployment_id,
		test_log_file_path,
		client,
		[&handler_called, &loop](deps::StatusAPIResponse resp) {
			handler_called = true;
			EXPECT_EQ(resp.error, error::NoError);
			loop.Stop();
		});
	EXPECT_EQ(err, error::NoError);

	loop.Run();
	EXPECT_TRUE(handler_called);
}

TEST_F(DeploymentsTests, PushLogsOneMessageTest) {
	TestEventLoop loop;

	http::ServerConfig server_config;
	http::Server server(server_config, loop);

	http::ClientConfig client_config;
	NoAuthHTTPClient client {client_config, loop};

	const string messages =
		R"({"timestamp": "2021-03-11T13:03:17.063493443Z", "level": "DEBUG", "message": "Just some noise"}
)";
	const string test_log_file_path = test_state_dir.Path() + "/test.log";
	ofstream os {test_log_file_path};
	auto err = io::WriteStringIntoOfstream(os, messages);
	ASSERT_EQ(err, error::NoError);
	os.close();

	string deployment_id = "2";
	string expected_request_data =
		R"({"messages":[{"timestamp": "2021-03-11T13:03:17.063493443Z", "level": "DEBUG", "message": "Just some noise"}]})";

	const string response_data = "";

	vector<uint8_t> received_body;
	server.AsyncServeUrl(
		TEST_SERVER,
		[&received_body, &expected_request_data](http::ExpectedIncomingRequestPtr exp_req) {
			ASSERT_TRUE(exp_req) << exp_req.error().String();
			auto req = exp_req.value();

			auto content_length = req->GetHeader("Content-Length");
			ASSERT_TRUE(content_length);
			EXPECT_EQ(content_length.value(), to_string(expected_request_data.size()));
			auto ex_len = common::StringToLongLong(content_length.value());
			ASSERT_TRUE(ex_len);

			auto body_writer = make_shared<io::ByteWriter>(received_body);
			received_body.resize(ex_len.value());
			req->SetBodyWriter(body_writer);
		},
		[&received_body, &expected_request_data, &response_data, deployment_id](
			http::ExpectedIncomingRequestPtr exp_req) {
			ASSERT_TRUE(exp_req) << exp_req.error().String();

			auto req = exp_req.value();
			EXPECT_EQ(
				req->GetPath(),
				"/api/devices/v1/deployments/device/deployments/" + deployment_id + "/log");
			EXPECT_EQ(req->GetMethod(), http::Method::PUT);
			EXPECT_EQ(common::StringFromByteVector(received_body), expected_request_data);

			auto result = req->MakeResponse();
			ASSERT_TRUE(result);
			auto resp = result.value();

			resp->SetHeader("Content-Length", to_string(response_data.size()));
			resp->SetBodyReader(make_shared<io::StringReader>(response_data));
			resp->SetStatusCodeAndMessage(204, "No content");
			resp->AsyncReply([](error::Error err) { ASSERT_EQ(error::NoError, err); });
		});

	bool handler_called = false;
	err = deps::DeploymentClient().PushLogs(
		deployment_id,
		test_log_file_path,
		client,
		[&handler_called, &loop](deps::StatusAPIResponse resp) {
			handler_called = true;
			EXPECT_EQ(resp.error, error::NoError);
			loop.Stop();
		});
	EXPECT_EQ(err, error::NoError);

	loop.Run();
	EXPECT_TRUE(handler_called);
}

TEST_F(DeploymentsTests, PushLogsFailureTest) {
	TestEventLoop loop;

	http::ServerConfig server_config;
	http::Server server(server_config, loop);

	http::ClientConfig client_config;
	const string messages =
		R"({"timestamp": "2021-03-11T13:03:17.063493443Z", "level": "DEBUG", "message": "Just some noise"}
)";
	const string test_log_file_path = test_state_dir.Path() + "/test.log";
	ofstream os {test_log_file_path};
	auto err = io::WriteStringIntoOfstream(os, messages);
	ASSERT_EQ(err, error::NoError);
	os.close();

	string deployment_id = "2";
	string expected_request_data =
		R"({"messages":[{"timestamp": "2021-03-11T13:03:17.063493443Z", "level": "DEBUG", "message": "Just some noise"}]})";

	const string response_data = R"({"error": "Access denied", "response-id": "some id here"})";

	bool request_body_too_large = false;

	vector<uint8_t> received_body;
	server.AsyncServeUrl(
		TEST_SERVER,
		[&received_body, &expected_request_data](http::ExpectedIncomingRequestPtr exp_req) {
			ASSERT_TRUE(exp_req) << exp_req.error().String();
			auto req = exp_req.value();

			auto content_length = req->GetHeader("Content-Length");
			ASSERT_TRUE(content_length);
			EXPECT_EQ(content_length.value(), to_string(expected_request_data.size()));
			auto ex_len = common::StringToLongLong(content_length.value());
			ASSERT_TRUE(ex_len);

			auto body_writer = make_shared<io::ByteWriter>(received_body);
			received_body.resize(ex_len.value());
			req->SetBodyWriter(body_writer);
		},
		[&received_body,
		 &expected_request_data,
		 &response_data,
		 deployment_id,
		 &request_body_too_large](http::ExpectedIncomingRequestPtr exp_req) {
			ASSERT_TRUE(exp_req) << exp_req.error().String();

			auto req = exp_req.value();
			EXPECT_EQ(
				req->GetPath(),
				"/api/devices/v1/deployments/device/deployments/" + deployment_id + "/log");
			EXPECT_EQ(req->GetMethod(), http::Method::PUT);
			EXPECT_EQ(common::StringFromByteVector(received_body), expected_request_data);

			auto result = req->MakeResponse();
			ASSERT_TRUE(result);
			auto resp = result.value();

			resp->SetHeader("Content-Length", to_string(response_data.size()));
			resp->SetBodyReader(make_shared<io::StringReader>(response_data));
			if (request_body_too_large) {
				resp->SetStatusCodeAndMessage(413, "Request Body Too Large");
			} else {
				resp->SetStatusCodeAndMessage(403, "Forbidden");
			}
			resp->AsyncReply([](error::Error err) { ASSERT_EQ(error::NoError, err); });
		});

	// Each attempt uses its own client scope so the request, and the reader
	// holding the sanitized snapshot, is torn down before the next attempt. This
	// mirrors the daemon, which releases the previous request when it issues the
	// next one between retries.
	bool handler_called = false;
	{
		NoAuthHTTPClient client {client_config, loop};
		err = deps::DeploymentClient().PushLogs(
			deployment_id,
			test_log_file_path,
			client,
			[&handler_called, &loop](deps::StatusAPIResponse resp) {
				handler_called = true;
				EXPECT_NE(resp.error, error::NoError);
				EXPECT_THAT(resp.error.message, testing::HasSubstr("Got unexpected response"));
				EXPECT_THAT(resp.error.message, testing::HasSubstr("403"));
				EXPECT_THAT(resp.error.message, testing::HasSubstr("Access denied"));
				loop.Stop();
			});
		EXPECT_EQ(err, error::NoError);

		loop.Run();
	}
	EXPECT_TRUE(handler_called);

	// Redo with 413 Request Body Too Large
	handler_called = false;
	request_body_too_large = true;

	{
		NoAuthHTTPClient client {client_config, loop};
		err = deps::DeploymentClient().PushLogs(
			deployment_id,
			test_log_file_path,
			client,
			[&handler_called, &loop](deps::StatusAPIResponse resp) {
				handler_called = true;
				EXPECT_NE(resp.error, error::NoError);
				EXPECT_EQ(
					resp.error.code, deps::MakeError(deps::RequestBodyTooLargeError, "").code);
				EXPECT_THAT(
					resp.error.message, testing::HasSubstr("Could not send logs to server"));
				loop.Stop();
			});
		EXPECT_EQ(err, error::NoError);

		loop.Run();
	}
	EXPECT_TRUE(handler_called);
}

TEST_F(DeploymentsTests, DeploymentLogTest) {
	deps::DeploymentLog dlog {test_state_dir.Path(), "1"};
	dlog.BeginLogging();
	mlog::Info("Testing info deployment logging");
	mlog::Error("Testing error deployment logging");
	dlog.FinishLogging();
	mlog::Warning("Shouldn't appear in the deployment log");

	ifstream is {path::Join(test_state_dir.Path(), "deployments.0000.1.log")};
	string line;
	size_t line_idx = 0;
	for (; getline(is, line); line_idx++) {
		EXPECT_THAT(line, Not(testing::HasSubstr("Shouldn't appear")));

		auto ex_j = json::Load(line);
		ASSERT_TRUE(ex_j);
		auto j = ex_j.value();
		EXPECT_TRUE(j.IsObject());

		auto ex_ts = j.Get("timestamp");
		ASSERT_TRUE(ex_ts);
		EXPECT_TRUE(ex_ts.value().IsString());

		auto ex_level = j.Get("level").and_then(json::ToString);
		ASSERT_TRUE(ex_level);
		EXPECT_THAT(
			ex_level.value(),
			testing::Conditional(line_idx == 0, testing::Eq("info"), testing::Eq("error")));

		auto ex_msg = j.Get("message").and_then(json::ToString);
		ASSERT_TRUE(ex_msg);
		EXPECT_THAT(
			ex_msg.value(),
			testing::Conditional(
				line_idx == 0,
				testing::Eq("Testing info deployment logging"),
				testing::Eq("Testing error deployment logging")));
	}
}

TEST_F(DeploymentsTests, DeploymentLogScopedTest) {
	{
		deps::DeploymentLog dlog {test_state_dir.Path(), "1"};
		dlog.BeginLogging();
		mlog::Info("Testing info deployment logging");
		mlog::Error("Testing error deployment logging");
	}
	mlog::Warning("Shouldn't appear in the deployment log");

	ifstream is {path::Join(test_state_dir.Path(), "deployments.0000.1.log")};
	string line;
	size_t line_idx = 0;
	for (; getline(is, line); line_idx++) {
		EXPECT_THAT(line, Not(testing::HasSubstr("Shouldn't appear")));

		auto ex_j = json::Load(line);
		ASSERT_TRUE(ex_j);
		auto j = ex_j.value();
		EXPECT_TRUE(j.IsObject());

		auto ex_ts = j.Get("timestamp");
		ASSERT_TRUE(ex_ts);
		EXPECT_TRUE(ex_ts.value().IsString());

		auto ex_level = j.Get("level").and_then(json::ToString);
		ASSERT_TRUE(ex_level);
		EXPECT_THAT(
			ex_level.value(),
			testing::Conditional(line_idx == 0, testing::Eq("info"), testing::Eq("error")));

		auto ex_msg = j.Get("message").and_then(json::ToString);
		ASSERT_TRUE(ex_msg);
		EXPECT_THAT(
			ex_msg.value(),
			testing::Conditional(
				line_idx == 0,
				testing::Eq("Testing info deployment logging"),
				testing::Eq("Testing error deployment logging")));
	}
}

TEST_F(DeploymentsTests, DeploymentLogFileAppendTest) {
	{
		deps::DeploymentLog dlog {test_state_dir.Path(), "1"};
		dlog.BeginLogging();
		mlog::Info("Testing info deployment logging");
	}
	{
		// should just append to the same file
		deps::DeploymentLog dlog {test_state_dir.Path(), "1"};
		dlog.BeginLogging();
		mlog::Error("Testing error deployment logging");
	}

	ifstream is {path::Join(test_state_dir.Path(), "deployments.0000.1.log")};
	string line;
	size_t line_idx = 0;
	for (; getline(is, line); line_idx++) {
		EXPECT_THAT(line, Not(testing::HasSubstr("Shouldn't appear")));

		auto ex_j = json::Load(line);
		ASSERT_TRUE(ex_j);
		auto j = ex_j.value();
		EXPECT_TRUE(j.IsObject());

		auto ex_ts = j.Get("timestamp");
		ASSERT_TRUE(ex_ts);
		EXPECT_TRUE(ex_ts.value().IsString());

		auto ex_level = j.Get("level").and_then(json::ToString);
		ASSERT_TRUE(ex_level);
		EXPECT_THAT(
			ex_level.value(),
			testing::Conditional(line_idx == 0, testing::Eq("info"), testing::Eq("error")));

		auto ex_msg = j.Get("message").and_then(json::ToString);
		ASSERT_TRUE(ex_msg);
		EXPECT_THAT(
			ex_msg.value(),
			testing::Conditional(
				line_idx == 0,
				testing::Eq("Testing info deployment logging"),
				testing::Eq("Testing error deployment logging")));
	}
}

string GetFileContent(const string &path) {
	ifstream is {path};
	if (!is) {
		return "";
	}
	// taken from https://stackoverflow.com/questions/2912520/read-file-contents-into-a-string-in-c
	string content {istreambuf_iterator<char>(is), istreambuf_iterator<char>()};

	return content;
}

TEST_F(DeploymentsTests, DeploymentLogRenameAndCleanPreviousLogsTest) {
	ofstream os;
	for (int i : {0, 1, 2, 3, 4}) {
		string file_name = "deployments.000" + to_string(i) + ".1" + to_string(i) + ".log";
		os.open(path::Join(test_state_dir.Path(), file_name));
		os << "Test content " + to_string(i) + " here\n";
		os.close();
	}
	os.open(path::Join(test_state_dir.Path(), "deployments.log"));
	os << "Test content in malformed file name\n";
	os.close();

	os.open(path::Join(test_state_dir.Path(), "deployments.00000.1.log"));
	os << "Test content in malformed file name 1\n";
	os.close();

	os.open(path::Join(test_state_dir.Path(), "deployments.000.2.log"));
	os << "Test content in malformed file name 2\n";
	os.close();

	os.open(path::Join(test_state_dir.Path(), "deployments.3.log"));
	os << "Test content in malformed file name 3\n";
	os.close();

	deps::DeploymentLog dlog {test_state_dir.Path(), "21"};
	dlog.BeginLogging();
	mlog::Info("Testing info deployment logging");
	mlog::Error("Testing error deployment logging");
	dlog.FinishLogging();
	mlog::Warning("Shouldn't appear in the deployment log");

	ifstream is {path::Join(test_state_dir.Path(), "deployments.0000.21.log")};
	string line;
	size_t line_idx = 0;
	for (; getline(is, line); line_idx++) {
		EXPECT_THAT(line, Not(testing::HasSubstr("Shouldn't appear")));

		auto ex_j = json::Load(line);
		ASSERT_TRUE(ex_j);
		auto j = ex_j.value();
		EXPECT_TRUE(j.IsObject());

		auto ex_ts = j.Get("timestamp");
		ASSERT_TRUE(ex_ts);
		EXPECT_TRUE(ex_ts.value().IsString());

		auto ex_level = j.Get("level").and_then(json::ToString);
		ASSERT_TRUE(ex_level);
		EXPECT_THAT(
			ex_level.value(),
			testing::Conditional(line_idx == 0, testing::Eq("info"), testing::Eq("error")));

		auto ex_msg = j.Get("message").and_then(json::ToString);
		ASSERT_TRUE(ex_msg);
		EXPECT_THAT(
			ex_msg.value(),
			testing::Conditional(
				line_idx == 0,
				testing::Eq("Testing info deployment logging"),
				testing::Eq("Testing error deployment logging")));
	}

	for (int i : {0, 1, 2, 3}) {
		string file_name = "deployments.000" + to_string(i + 1) + ".1" + to_string(i) + ".log";
		EXPECT_EQ(
			GetFileContent(path::Join(test_state_dir.Path(), file_name)),
			"Test content " + to_string(i) + " here\n");
	}
	// the past log with the highest index shouldn't exist anymore under any name
	EXPECT_EQ(GetFileContent(path::Join(test_state_dir.Path(), "deployments.0004.14.log")), "");
	EXPECT_EQ(GetFileContent(path::Join(test_state_dir.Path(), "deployments.0005.14.log")), "");

	// malformed log files intact
	EXPECT_EQ(
		GetFileContent(path::Join(test_state_dir.Path(), "deployments.log")),
		"Test content in malformed file name\n");
	EXPECT_EQ(
		GetFileContent(path::Join(test_state_dir.Path(), "deployments.00000.1.log")),
		"Test content in malformed file name 1\n");
	EXPECT_EQ(
		GetFileContent(path::Join(test_state_dir.Path(), "deployments.000.2.log")),
		"Test content in malformed file name 2\n");
	EXPECT_EQ(
		GetFileContent(path::Join(test_state_dir.Path(), "deployments.3.log")),
		"Test content in malformed file name 3\n");
}

TEST_F(DeploymentsTests, TestTooManyRequestsWithRetryAfterHeader_HeaderHandler) {
	TestEventLoop loop;
	http::ClientConfig client_config;
	http::Client client {client_config, loop};
	auto cancelled = make_shared<bool>(false);

	vector<string> retry_after_cases {"100", "Fri, 31 Dec 1999 23:59:59 GMT"};

	for (auto retry_after : retry_after_cases) {
		auto resp = CreateIncomingResponse(client, cancelled, retry_after);
		http::ExpectedIncomingResponsePtr exp_resp = resp;
		bool handler_called {false};

		auto api_handler = [&handler_called, retry_after](deps::CheckUpdatesAPIResponse resp) {
			handler_called = true;
			ASSERT_FALSE(resp.has_value());
			EXPECT_EQ(resp.error().http_code, http::StatusTooManyRequests);
			ASSERT_TRUE(resp.error().http_headers.has_value());
			auto retry_from_header = resp.error().http_headers.value().find("Retry-After");
			EXPECT_NE(retry_from_header, resp.error().http_headers.value().end());
			EXPECT_EQ(retry_from_header->second, retry_after);
		};

		DeploymentClientHeaderHandler(api_handler, exp_resp);
		EXPECT_TRUE(handler_called);
	}
}

TEST_F(DeploymentsTests, TestTooManyRequestsWithoutRetryAfterHeader_HeaderHandler) {
	TestEventLoop loop;
	http::ClientConfig client_config;
	http::Client client {client_config, loop};
	auto cancelled = make_shared<bool>(false);

	auto resp = CreateIncomingResponse(client, cancelled, nullopt);
	http::ExpectedIncomingResponsePtr exp_resp = resp;
	bool handler_called {false};

	auto api_handler = [&handler_called](deps::CheckUpdatesAPIResponse resp) {
		handler_called = true;
		ASSERT_FALSE(resp.has_value());
		EXPECT_EQ(resp.error().http_code, http::StatusTooManyRequests);
		EXPECT_TRUE(resp.error().http_headers.has_value());
		auto retry_from_header = resp.error().http_headers.value().find("Retry-After");
		EXPECT_EQ(retry_from_header, resp.error().http_headers.value().end());
	};

	DeploymentClientHeaderHandler(api_handler, exp_resp);
	EXPECT_TRUE(handler_called);
}

TEST_F(DeploymentsTests, TestTooManyRequestsWithRetryAfterHeader_PushStatusHeaderHandler) {
	TestEventLoop loop;
	http::ClientConfig client_config;
	http::Client client {client_config, loop};
	auto cancelled = make_shared<bool>(false);

	vector<string> retry_after_cases {"100", "Fri, 31 Dec 1999 23:59:59 GMT"};

	for (auto retry_after : retry_after_cases) {
		auto resp = CreateIncomingResponse(client, cancelled, retry_after);
		http::ExpectedIncomingResponsePtr exp_resp = resp;
		bool handler_called {false};

		auto api_handler = [&handler_called, retry_after](deps::StatusAPIResponse resp) {
			handler_called = true;
			EXPECT_EQ(resp.http_code, http::StatusTooManyRequests);
			ASSERT_TRUE(resp.http_headers.has_value());
			auto retry_from_header = resp.http_headers.value().find("Retry-After");
			EXPECT_NE(retry_from_header, resp.http_headers.value().end());
			EXPECT_EQ(retry_from_header->second, retry_after);
		};

		DeploymentClientPushStatusHeaderHandler(api_handler, exp_resp);
		EXPECT_TRUE(handler_called);
	}
}

TEST_F(DeploymentsTests, TestTooManyRequestsWithoutRetryAfterHeader_PushStatusHeaderHandler) {
	TestEventLoop loop;
	http::ClientConfig client_config;
	http::Client client {client_config, loop};
	auto cancelled = make_shared<bool>(false);

	auto resp = CreateIncomingResponse(client, cancelled, nullopt);
	http::ExpectedIncomingResponsePtr exp_resp = resp;
	bool handler_called {false};

	auto api_handler = [&handler_called](deps::StatusAPIResponse resp) {
		handler_called = true;
		EXPECT_EQ(resp.http_code, http::StatusTooManyRequests);
		EXPECT_TRUE(resp.http_headers.has_value());
		auto retry_from_header = resp.http_headers.value().find("Retry-After");
		EXPECT_EQ(retry_from_header, resp.http_headers.value().end());
	};

	DeploymentClientPushStatusHeaderHandler(api_handler, exp_resp);
	EXPECT_TRUE(handler_called);
}

TEST_F(DeploymentsTests, TestTooManyRequestsWithRetryAfterHeader_PushLogsHeaderHandler) {
	TestEventLoop loop;
	http::ClientConfig client_config;
	http::Client client {client_config, loop};
	auto cancelled = make_shared<bool>(false);

	vector<string> retry_after_cases {"100", "Fri, 31 Dec 1999 23:59:59 GMT"};

	for (auto retry_after : retry_after_cases) {
		auto resp = CreateIncomingResponse(client, cancelled, retry_after);
		http::ExpectedIncomingResponsePtr exp_resp = resp;
		bool handler_called {false};

		auto api_handler = [&handler_called, retry_after](deps::LogsAPIResponse resp) {
			handler_called = true;
			EXPECT_EQ(resp.http_code, http::StatusTooManyRequests);
			ASSERT_TRUE(resp.http_headers.has_value());
			auto retry_from_header = resp.http_headers.value().find("Retry-After");
			EXPECT_NE(retry_from_header, resp.http_headers.value().end());
			EXPECT_EQ(retry_from_header->second, retry_after);
		};

		DeploymentClientPushLogsHeaderHandler(api_handler, exp_resp);
		EXPECT_TRUE(handler_called);
	}
}

TEST_F(DeploymentsTests, TestTooManyRequestsWithoutRetryAfterHeader_PushLogsHeaderHandler) {
	TestEventLoop loop;
	http::ClientConfig client_config;
	http::Client client {client_config, loop};
	auto cancelled = make_shared<bool>(false);

	auto resp = CreateIncomingResponse(client, cancelled, nullopt);
	http::ExpectedIncomingResponsePtr exp_resp = resp;
	bool handler_called {false};

	auto api_handler = [&handler_called](deps::LogsAPIResponse resp) {
		handler_called = true;
		EXPECT_EQ(resp.http_code, http::StatusTooManyRequests);
		EXPECT_TRUE(resp.http_headers.has_value());
		auto retry_from_header = resp.http_headers.value().find("Retry-After");
		EXPECT_EQ(retry_from_header, resp.http_headers.value().end());
	};

	DeploymentClientPushLogsHeaderHandler(api_handler, exp_resp);
	EXPECT_TRUE(handler_called);
}
