// 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 <mender-update/context.hpp>

#include <cerrno>
#include <fstream>
#include <unordered_map>

#include <artifact/artifact.hpp>
#include <common/common.hpp>
#include <common/device_tier.hpp>
#include <client_shared/conf.hpp>
#include <common/key_value_database_lmdb.hpp>
#include <common/json.hpp>
#include <common/path.hpp>
#include <common/testing.hpp>

#include <gtest/gtest.h>

namespace common = mender::common;
namespace conf = mender::client_shared::conf;
namespace device_tier = mender::common::device_tier;
namespace error = mender::common::error;
namespace json = mender::common::json;
namespace kv_db = mender::common::key_value_database;
namespace path = mender::common::path;

namespace artifact = mender::artifact;

namespace context = mender::update::context;

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

class ContextTests : public testing::Test {
protected:
	TemporaryDirectory test_state_dir;
};

TEST_F(ContextTests, DeviceTypeFileConfig) {
	conf::MenderConfig cfg;
	cfg.paths.SetDataStore(test_state_dir.Path());

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

	// No device_type available.
	auto exp_device_type = ctx.GetDeviceType();
	ASSERT_FALSE(exp_device_type);
	EXPECT_EQ(exp_device_type.error().code, make_error_condition(errc::no_such_file_or_directory));

	string my_device_type_file = path::Join(test_state_dir.Path(), "my_device_type");
	string device_type_file = path::Join(test_state_dir.Path(), "device_type");

	{
		ofstream f(my_device_type_file);
		f << "device_type=custom_device_type\n";
		ASSERT_TRUE(f.good());
	}

	// Available through config.
	cfg.device_type_file = my_device_type_file;
	exp_device_type = ctx.GetDeviceType();
	ASSERT_TRUE(exp_device_type) << exp_device_type.error().String();
	EXPECT_EQ(*exp_device_type, "custom_device_type");

	{
		ofstream f(device_type_file);
		f << "device_type=default_device_type\n";
		ASSERT_TRUE(f.good());
	}

	// Shadowing default version.
	cfg.device_type_file = my_device_type_file;
	exp_device_type = ctx.GetDeviceType();
	ASSERT_TRUE(exp_device_type) << exp_device_type.error().String();
	EXPECT_EQ(*exp_device_type, "custom_device_type");

	// Default version.
	cfg.device_type_file = "";
	exp_device_type = ctx.GetDeviceType();
	ASSERT_TRUE(exp_device_type) << exp_device_type.error().String();
	EXPECT_EQ(*exp_device_type, "default_device_type");

	// Missing custom version.
	cfg.device_type_file = my_device_type_file;
	ASSERT_EQ(path::FileDelete(my_device_type_file), error::NoError);
	exp_device_type = ctx.GetDeviceType();
	ASSERT_FALSE(exp_device_type);
	EXPECT_EQ(exp_device_type.error().code, make_error_condition(errc::no_such_file_or_directory));
}

TEST_F(ContextTests, LoadProvidesValid) {
	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();
	const string input_provides_data_str = R"({
  "something_else": "something_else value"
})";
	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);
	err = db.Write("artifact-provides", common::ByteVectorFromString(input_provides_data_str));
	ASSERT_EQ(err, error::NoError);

	auto ex_provides_data = ctx.LoadProvides();
	ASSERT_TRUE(ex_provides_data);

	auto provides_data = ex_provides_data.value();
	EXPECT_EQ(provides_data.size(), 3);
	EXPECT_EQ(provides_data["artifact_name"], "artifact-name value");
	EXPECT_EQ(provides_data["artifact_group"], "artifact-group value");
	EXPECT_EQ(provides_data["something_else"], "something_else value");
}

TEST_F(ContextTests, LoadProvidesEmpty) {
	conf::MenderConfig cfg;
	cfg.paths.SetDataStore(test_state_dir.Path());

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

	auto ex_provides_data = ctx.LoadProvides();
	ASSERT_TRUE(ex_provides_data);

	auto provides_data = ex_provides_data.value();
	EXPECT_EQ(provides_data.size(), 0);
}

TEST_F(ContextTests, LoadProvidesInvalidJSON) {
	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();
	const string input_provides_data_str = R"({
  "something_else": "something_else" invalid
})";
	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);
	err = db.Write("artifact-provides", common::ByteVectorFromString(input_provides_data_str));
	ASSERT_EQ(err, error::NoError);

	auto ex_provides_data = ctx.LoadProvides();
	ASSERT_FALSE(ex_provides_data);
	EXPECT_EQ(
		ex_provides_data.error().code, json::MakeError(json::JsonErrorCode::ParseError, "").code);
}

TEST_F(ContextTests, LoadProvidesInvalidData) {
	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();
	const string input_provides_data_str = R"({
  "something_else_array": ["something_else_array value"]
})";
	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);
	err = db.Write("artifact-provides", common::ByteVectorFromString(input_provides_data_str));
	ASSERT_EQ(err, error::NoError);

	auto ex_provides_data = ctx.LoadProvides();
	ASSERT_FALSE(ex_provides_data);
	EXPECT_EQ(
		ex_provides_data.error().code, json::MakeError(json::JsonErrorCode::TypeError, "").code);
}

TEST_F(ContextTests, LoadProvidesClosedDB) {
#ifndef NDEBUG
	GTEST_SKIP() << "requires assert() to be a no-op";
#else
	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();
	const string input_provides_data_str = R"({
  "something_else": "something_else value"
})";
	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);
	err = db.Write("artifact-provides", common::ByteVectorFromString(input_provides_data_str));
	ASSERT_EQ(err, error::NoError);

	auto &lmdb = dynamic_cast<kv_db::KeyValueDatabaseLmdb &>(db);
	lmdb.Close();

	auto ex_provides_data = ctx.LoadProvides();
	ASSERT_FALSE(ex_provides_data);
	EXPECT_EQ(ex_provides_data.error().code, error::MakeError(error::ProgrammingError, "").code);
#endif // NDEBUG
}

TEST_F(ContextTests, CommitArtifactDataValid) {
	conf::MenderConfig cfg;
	cfg.paths.SetDataStore(test_state_dir.Path());

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

	context::ProvidesData data;
	string artifact_name = "artifact_name value";
	string artifact_group = "artifact_group value";
	data["something_extra"] = "something_extra value";
	data["something_extra2"] = "something_extra2 value";

	err = ctx.CommitArtifactData(
		artifact_name,
		artifact_group,
		data,
		optional<context::ClearsProvidesData>(),
		[](kv_db::Transaction &txn) { return error::NoError; });
	ASSERT_EQ(err, error::NoError);

	auto &db = ctx.GetMenderStoreDB();
	auto ex_data = db.Read("artifact-name");
	ASSERT_TRUE(ex_data);
	EXPECT_EQ(common::StringFromByteVector(ex_data.value()), "artifact_name value");

	ex_data = db.Read("artifact-group");
	ASSERT_TRUE(ex_data);
	EXPECT_EQ(common::StringFromByteVector(ex_data.value()), "artifact_group value");

	ex_data = db.Read("artifact-provides");
	ASSERT_TRUE(ex_data);
	EXPECT_EQ(
		common::StringFromByteVector(ex_data.value()),
		R"({"something_extra2":"something_extra2 value","something_extra":"something_extra value"})");
}

TEST_F(ContextTests, CommitArtifactDataEscaped) {
	conf::MenderConfig cfg;
	cfg.paths.SetDataStore(test_state_dir.Path());

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

	context::ProvidesData data;
	string artifact_name = "artifact_name value";
	string artifact_group = "artifact_group value";
	data["something_extra"] = "something_extra\nvalue";
	data["something_extra2"] = "something_extra2\tvalue";

	err = ctx.CommitArtifactData(
		artifact_name,
		artifact_group,
		data,
		optional<context::ClearsProvidesData>(),
		[](kv_db::Transaction &txn) { return error::NoError; });
	ASSERT_EQ(err, error::NoError);

	auto &db = ctx.GetMenderStoreDB();
	auto ex_data = db.Read("artifact-name");
	ASSERT_TRUE(ex_data);
	EXPECT_EQ(common::StringFromByteVector(ex_data.value()), "artifact_name value");

	ex_data = db.Read("artifact-group");
	ASSERT_TRUE(ex_data);
	EXPECT_EQ(common::StringFromByteVector(ex_data.value()), "artifact_group value");

	ex_data = db.Read("artifact-provides");
	ASSERT_TRUE(ex_data);
	EXPECT_EQ(
		common::StringFromByteVector(ex_data.value()),
		R"({"something_extra2":"something_extra2\tvalue","something_extra":"something_extra\nvalue"})");
}

TEST_F(ContextTests, CommitLegacyArtifact) {
	// Legacy artifacts come without Provides and Clears Provides data

	conf::MenderConfig cfg;
	cfg.paths.SetDataStore(test_state_dir.Path());

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

	string artifact_name = "artifact_name value";
	string artifact_group = "artifact_group value";

	err = ctx.CommitArtifactData(
		artifact_name,
		artifact_group,
		optional<context::ProvidesData>(),
		optional<context::ClearsProvidesData>(),
		[](kv_db::Transaction &txn) { return error::NoError; });
	ASSERT_EQ(err, error::NoError);

	auto &db = ctx.GetMenderStoreDB();
	auto ex_data = db.Read("artifact-name");
	ASSERT_TRUE(ex_data);
	EXPECT_EQ(common::StringFromByteVector(ex_data.value()), "artifact_name value");

	ex_data = db.Read("artifact-group");
	ASSERT_TRUE(ex_data);
	EXPECT_EQ(common::StringFromByteVector(ex_data.value()), "artifact_group value");

	ex_data = db.Read("artifact-provides");
	ASSERT_FALSE(ex_data);
}

TEST_F(ContextTests, CommitArtifactWithClearsProvides) {
	// Legacy artifacts come without Provides and Clears Provides data

	conf::MenderConfig cfg;
	cfg.paths.SetDataStore(test_state_dir.Path());

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

	context::ProvidesData data;
	string artifact_name = "artifact_name value";
	string artifact_group = "artifact_group value";
	data["something_extra"] = "something_extra value";
	data["something_extra2"] = "something_extra2 value";
	data["something_different"] = "something_different value";

	// Initialize.

	err = ctx.CommitArtifactData(
		artifact_name,
		artifact_group,
		data,
		optional<context::ClearsProvidesData>(),
		[](kv_db::Transaction &txn) { return error::NoError; });
	ASSERT_EQ(err, error::NoError);

	auto &db = ctx.GetMenderStoreDB();
	auto ex_data = db.Read("artifact-name");
	ASSERT_TRUE(ex_data);
	EXPECT_EQ(common::StringFromByteVector(ex_data.value()), "artifact_name value");

	ex_data = db.Read("artifact-group");
	ASSERT_TRUE(ex_data);
	EXPECT_EQ(common::StringFromByteVector(ex_data.value()), "artifact_group value");

	ex_data = db.Read("artifact-provides");
	ASSERT_TRUE(ex_data);
	EXPECT_EQ(
		common::StringFromByteVector(ex_data.value()),
		R"({"something_different":"something_different value","something_extra2":"something_extra2 value","something_extra":"something_extra value"})");

	// Use clears_provides to get rid of a wildcard value.

	context::ClearsProvidesData clears_provides;
	clears_provides.push_back("something_extra*");

	err = ctx.CommitArtifactData(
		artifact_name,
		string {},
		optional<context::ProvidesData>(),
		clears_provides,
		[](kv_db::Transaction &txn) { return error::NoError; });
	ASSERT_EQ(err, error::NoError);

	ex_data = db.Read("artifact-name");
	ASSERT_TRUE(ex_data);
	EXPECT_EQ(common::StringFromByteVector(ex_data.value()), "artifact_name value");

	ex_data = db.Read("artifact-group");
	ASSERT_TRUE(ex_data);
	EXPECT_EQ(common::StringFromByteVector(ex_data.value()), "artifact_group value");

	ex_data = db.Read("artifact-provides");
	ASSERT_TRUE(ex_data);
	EXPECT_EQ(
		common::StringFromByteVector(ex_data.value()),
		R"({"something_different":"something_different value"})");

	// Use clears_provides to get rid of artifact_group.

	clears_provides.push_back("artifact_group");

	err = ctx.CommitArtifactData(
		artifact_name,
		string {},
		optional<context::ProvidesData>(),
		clears_provides,
		[](kv_db::Transaction &txn) { return error::NoError; });
	ASSERT_EQ(err, error::NoError);

	ex_data = db.Read("artifact-name");
	ASSERT_TRUE(ex_data);
	EXPECT_EQ(common::StringFromByteVector(ex_data.value()), "artifact_name value");

	ex_data = db.Read("artifact-group");
	ASSERT_FALSE(ex_data);

	ex_data = db.Read("artifact-provides");
	ASSERT_TRUE(ex_data);
	EXPECT_EQ(
		common::StringFromByteVector(ex_data.value()),
		R"({"something_different":"something_different value"})");

	// Use clears_provides at the same time as new provides values..

	data.clear();
	data["something_extra"] = "something_extra value";
	clears_provides.push_back("something_different");
	clears_provides.push_back("something_extra");

	err = ctx.CommitArtifactData(
		artifact_name, string {}, data, clears_provides, [](kv_db::Transaction &txn) {
			return error::NoError;
		});
	ASSERT_EQ(err, error::NoError);

	ex_data = db.Read("artifact-name");
	ASSERT_TRUE(ex_data);
	EXPECT_EQ(common::StringFromByteVector(ex_data.value()), "artifact_name value");

	ex_data = db.Read("artifact-group");
	ASSERT_FALSE(ex_data);

	ex_data = db.Read("artifact-provides");
	ASSERT_TRUE(ex_data);
	EXPECT_EQ(
		common::StringFromByteVector(ex_data.value()),
		R"({"something_extra":"something_extra value"})");
}

TEST_F(ContextTests, GetDeviceTypeValid) {
	conf::MenderConfig cfg;
	cfg.paths.SetDataStore(test_state_dir.Path());

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

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

	auto ex_s = ctx.GetDeviceType();
	ASSERT_TRUE(ex_s);
	EXPECT_EQ(ex_s.value(), "Some device type");

	os.open(cfg.paths.GetDataStore() + "/device_type");
	ASSERT_TRUE(os);
	os << "device_type=Device type no endl";
	os.close();

	ex_s = ctx.GetDeviceType();
	ASSERT_TRUE(ex_s);
	EXPECT_EQ(ex_s.value(), "Device type no endl");
}

TEST_F(ContextTests, GetDeviceTypeNoexist) {
	conf::MenderConfig cfg;
	cfg.paths.SetDataStore(test_state_dir.Path());

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

	auto ex_s = ctx.GetDeviceType();
	ASSERT_FALSE(ex_s);
	EXPECT_TRUE(ex_s.error().IsErrno(ENOENT));
}

TEST_F(ContextTests, GetDeviceTypeEmpty) {
	conf::MenderConfig cfg;
	cfg.paths.SetDataStore(test_state_dir.Path());

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

	string dtype_fpath = cfg.paths.GetDataStore() + "/device_type";
	ofstream os(dtype_fpath);
	ASSERT_TRUE(os);
	os.close();

	auto ex_s = ctx.GetDeviceType();
	ASSERT_FALSE(ex_s);
	EXPECT_EQ(ex_s.error().code, context::MakeError(context::ParseError, "").code);
}

TEST_F(ContextTests, GetDeviceTypeInvalid) {
	conf::MenderConfig cfg;
	cfg.paths.SetDataStore(test_state_dir.Path());

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

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

	auto ex_s = ctx.GetDeviceType();
	ASSERT_FALSE(ex_s);
	EXPECT_EQ(ex_s.error().code, context::MakeError(context::ParseError, "").code);

	os.open(dtype_fpath);
	ASSERT_TRUE(os);
	os << "device_type Some device type" << endl;
	os.close();

	ex_s = ctx.GetDeviceType();
	ASSERT_FALSE(ex_s);
	EXPECT_EQ(ex_s.error().code, context::MakeError(context::ParseError, "").code);
}

TEST_F(ContextTests, GetDeviceTypeTrailingData) {
	conf::MenderConfig cfg;
	cfg.paths.SetDataStore(test_state_dir.Path());

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

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

	auto ex_s = ctx.GetDeviceType();
	ASSERT_FALSE(ex_s);
	EXPECT_EQ(ex_s.error().code, context::MakeError(context::ValueError, "").code);

	os.open(cfg.paths.GetDataStore() + "/device_type");
	ASSERT_TRUE(os);
	os << "device_type=Some device type" << endl;
	os << endl << "some debris here after a blank line" << endl;
	os.close();

	ex_s = ctx.GetDeviceType();
	ASSERT_FALSE(ex_s);
	EXPECT_EQ(ex_s.error().code, context::MakeError(context::ValueError, "").code);
}

TEST(ContextArtifactTests, ArtifactMatchesContextTest) {
	context::ProvidesData provides = {
		{"artifact_name", "artifact_name"}, {"artifact_group", "artifact_group"}};
	string device_type = "device_type";

	artifact::HeaderView hdr;

	// MATCHES
	hdr.header_info.depends = {{"device_type"}, nullopt, nullopt};
	auto ex_match = context::ArtifactMatchesContext(provides, device_type, hdr);
	ASSERT_TRUE(ex_match);
	EXPECT_TRUE(ex_match.value());

	hdr.header_info.depends = {
		{"device_type"},
		optional<vector<string>>({"artifact_name"}),
		optional<vector<string>>({"artifact_group"})};

	ex_match = context::ArtifactMatchesContext(provides, device_type, hdr);
	ASSERT_TRUE(ex_match);
	EXPECT_TRUE(ex_match.value());

	hdr.header_info.depends = {
		{"device_type1", "device_type"},
		optional<vector<string>>({"artifact_name1", "artifact_name"}),
		optional<vector<string>>({"artifact_group1", "artifact_group"})};

	ex_match = context::ArtifactMatchesContext(provides, device_type, hdr);
	ASSERT_TRUE(ex_match);
	EXPECT_TRUE(ex_match.value());

	hdr.header_info.depends = {
		{"device_type1", "device_type"},
		optional<vector<string>>({"artifact_name1", "artifact_name"}),
		nullopt};

	ex_match = context::ArtifactMatchesContext(provides, device_type, hdr);
	ASSERT_TRUE(ex_match);
	EXPECT_TRUE(ex_match.value());

	hdr.header_info.depends = {
		{"device_type1", "device_type"},
		nullopt,
		optional<vector<string>>({"artifact_group1", "artifact_group"})};

	ex_match = context::ArtifactMatchesContext(provides, device_type, hdr);
	ASSERT_TRUE(ex_match);
	EXPECT_TRUE(ex_match.value());

	// Mismatches in device type or artifact name/group
	hdr.header_info.depends = {
		{"device_type"},
		optional<vector<string>>({"artifact_name_other"}),
		optional<vector<string>>({"artifact_group"})};

	ex_match = context::ArtifactMatchesContext(provides, device_type, hdr);
	ASSERT_TRUE(ex_match);
	EXPECT_FALSE(ex_match.value());

	hdr.header_info.depends = {
		{"device_type"},
		optional<vector<string>>({"artifact_name"}),
		optional<vector<string>>({"artifact_group_other"})};

	ex_match = context::ArtifactMatchesContext(provides, device_type, hdr);
	ASSERT_TRUE(ex_match);
	EXPECT_FALSE(ex_match.value());

	hdr.header_info.depends = {
		{"device_type_other"},
		optional<vector<string>>({"artifact_name"}),
		optional<vector<string>>({"artifact_group"})};

	ex_match = context::ArtifactMatchesContext(provides, device_type, hdr);
	ASSERT_TRUE(ex_match);
	EXPECT_FALSE(ex_match.value());

	// Missing artifact_group
	provides = {{"artifact_name", "artifact_name"}};
	hdr.header_info.depends = {
		{"device_type"},
		optional<vector<string>>({"artifact_name"}),
		optional<vector<string>>({"artifact_group"})};

	ex_match = context::ArtifactMatchesContext(provides, device_type, hdr);
	ASSERT_TRUE(ex_match);
	EXPECT_FALSE(ex_match.value());

	// Match incl. extra provides/depends
	provides = {
		{"artifact_name", "artifact_name"},
		{"artifact_group", "artifact_group"},
		{"something_else", "something_else"}};
	hdr.header_info.depends = {
		{"device_type"},
		optional<vector<string>>({"artifact_name"}),
		optional<vector<string>>({"artifact_group"})};
	hdr.type_info.artifact_depends = optional<unordered_map<string, string>>(
		unordered_map<string, string> {{"something_else", "something_else"}});

	ex_match = context::ArtifactMatchesContext(provides, device_type, hdr);
	ASSERT_TRUE(ex_match);
	EXPECT_TRUE(ex_match.value());

	// Extra provides/depends mismatch
	hdr.type_info.artifact_depends = optional<unordered_map<string, string>>(
		unordered_map<string, string> {{"something_else", "other_something_else"}});

	ex_match = context::ArtifactMatchesContext(provides, device_type, hdr);
	ASSERT_TRUE(ex_match);
	EXPECT_FALSE(ex_match.value());

	// Missing extra provides/depends
	hdr.type_info.artifact_depends = optional<unordered_map<string, string>>(
		unordered_map<string, string> {{"other_something_else", "something_else"}});

	ex_match = context::ArtifactMatchesContext(provides, device_type, hdr);
	ASSERT_TRUE(ex_match);
	EXPECT_FALSE(ex_match.value());
}

struct TestWildCard {
	std::string to_match;
	std::string pattern;
	bool expected;
};

TEST_F(ContextTests, CheckClearsProvidesTest) {
	std::vector<TestWildCard> test_wildcard = {
		{"abc_123_def_456_ghi", "abc_123_def_456_ghi", true},
		{"abc_123_def_456_ghi", "abc_123_def_456_g", false},
		{"abc_123_def_456_ghi", "abc*def*ghi", true},
		{"abc_123_def_456_ghi", "abc*123*def*ghi", true},
		{"abc_123_def_456_ghi", "*def*456*", true},
		{"abc_123_def_456_ghi", "abc*789*ghi", false},
		{"hello_world", "hello*world", true},
		{"hello_world", "hello*worlds", false},
		{"a_b_c", "a*_*b*", true},
		{"a_b_c", "a*c*b", false},
		{"abc", "*a*bc*", true},
		{"abcabcabc", "a*c*a*c*", true},
		{"test_key_1", "test_*", true},
		{"test_key_1", "test_", false},
		{"best_test_key_1", "test_*", false},
		{"test_key_1", "", false},
		{"", "", true},
		{"abc", "*", true},
		{"", "abc", false},
		{"", "abc*", false},
		{"", "*", true},
		{"abc", "xyz", false},
		{"abc", "abc*", true},
		{"abc", "*abc", true},
		{"abc", "a*xyz", false},
		{"test_string", "test_*test_*", false},
		{"test_string", "test_*string*", true},
		{"test_test_key", "***", true}};


	for (const auto &test : test_wildcard) {
		bool result = context::CheckClearsMatch(test.to_match, test.pattern);
		ASSERT_EQ(result, test.expected) << "Expected '" << test.to_match << "', '" << test.pattern
										 << "' to return " << test.expected;
	}
}


TEST_F(ContextTests, ClearsProvides) {
	context::ProvidesData provides = {
		{"test_key_1", "type"},
		{"test_key_2", "type"},
		{"test_key_test_1", "type"},
		{"test_key_test_2", "type"},
	};
	context::ProvidesData new_provides;
	context::ProvidesData unmodified_provides = provides;
	context::ClearsProvidesData clears_provides;

	clears_provides.push_back("");
	auto ex_matched = context::FilterProvides(new_provides, clears_provides, provides);
	ASSERT_EQ(provides.size(), 4);

	clears_provides.push_back("*");
	ex_matched = context::FilterProvides(new_provides, clears_provides, provides);
	ASSERT_EQ(provides.size(), 0);

	provides = unmodified_provides;
	clears_provides.clear();
	clears_provides.push_back("test_key*");
	ex_matched = context::FilterProvides(new_provides, clears_provides, provides);
	ASSERT_EQ(provides.size(), 0);

	provides = unmodified_provides;
	clears_provides.clear();
	clears_provides.push_back("test_key_test*");
	ex_matched = context::FilterProvides(new_provides, clears_provides, provides);
	ASSERT_EQ(provides.size(), 2);

	provides = unmodified_provides;
	clears_provides.clear();
	clears_provides.push_back("key*");
	ex_matched = context::FilterProvides(new_provides, clears_provides, provides);
	ASSERT_EQ(provides.size(), 4);

	provides = unmodified_provides;
	clears_provides.clear();
	clears_provides.push_back("test_key_test_1");
	ex_matched = context::FilterProvides(new_provides, clears_provides, provides);
	ASSERT_EQ(provides.size(), 3);

	provides = unmodified_provides;
	clears_provides.clear();
	clears_provides.push_back("test_key_");
	ex_matched = context::FilterProvides(new_provides, clears_provides, provides);
	ASSERT_EQ(provides.size(), 4);

	provides = unmodified_provides;
	clears_provides.clear();
	clears_provides.push_back("*");
	provides = {{"artifact_name", "name1"}};
	new_provides = {{"artifact_name", "name1"}};
	ex_matched = context::FilterProvides(new_provides, clears_provides, provides);
	ASSERT_EQ(provides.size(), 1);
}

TEST(ContextArtifactTests, ArtifactMatchesContextErrorsTest) {
#ifndef NDEBUG
	GTEST_SKIP() << "requires assert() to be a no-op";
#else
	context::ProvidesData provides = {
		{"artifact_name", "artifact_name"}, {"artifact_group", "artifact_group"}};
	string device_type = "device_type";

	artifact::HeaderView hdr;

	// Missing device type
	hdr.header_info.depends = {{}, nullopt, nullopt};

	auto ex_match = context::ArtifactMatchesContext(provides, device_type, hdr);
	ASSERT_FALSE(ex_match);

	// Empty artifact names vector
	hdr.header_info.depends = {
		{"device_type"}, optional<vector<string>>(vector<string>(0)), nullopt};

	ex_match = context::ArtifactMatchesContext(provides, device_type, hdr);
	ASSERT_FALSE(ex_match);

	// Empty artifact groups vector
	hdr.header_info.depends = {
		{"device_type"}, nullopt, optional<vector<string>>(vector<string>(0))};

	ex_match = context::ArtifactMatchesContext(provides, device_type, hdr);
	ASSERT_FALSE(ex_match);
#endif
}

#ifdef MENDER_USE_YAML_CPP
TEST_F(ContextTests, GetSystemType) {
	conf::MenderConfig cfg;
	cfg.paths.SetDataStore(test_state_dir.Path());

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

	string topology_dir = path::Join(test_state_dir.Path(), "mender-orchestrator");
	ASSERT_EQ(path::CreateDirectory(topology_dir), error::NoError);
	string topology_file = path::Join(topology_dir, "topology.yaml");

	setenv("MENDER_ORCHESTRATOR_TOPOLOGY_DIR", topology_dir.c_str(), 1);

	{
		ofstream f(topology_file);
		f << "system_type: test_system_type\n";
		ASSERT_TRUE(f.good());
	}

	auto ex_system_type = ctx.GetSystemType();
	ASSERT_TRUE(ex_system_type) << ex_system_type.error().String();
	EXPECT_EQ(*ex_system_type, "test_system_type");

	unsetenv("MENDER_ORCHESTRATOR_TOPOLOGY_DIR");
}
#endif

TEST_F(ContextTests, GetCompatibleTypeDevice) {
	conf::MenderConfig cfg;
	cfg.paths.SetDataStore(test_state_dir.Path());

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

	string device_type_file = path::Join(test_state_dir.Path(), "device_type");
	{
		ofstream f(device_type_file);
		f << "device_type=regular_device_type\n";
		ASSERT_TRUE(f.good());
	}

	// Test deployment polling (empty payload type)
	auto ex_compatible_type = ctx.GetCompatibleType();
	ASSERT_TRUE(ex_compatible_type) << ex_compatible_type.error().String();
	EXPECT_EQ(*ex_compatible_type, "regular_device_type");

	// Test regular artifact processing
	ex_compatible_type = ctx.GetCompatibleType("some-regular-payload");
	ASSERT_TRUE(ex_compatible_type) << ex_compatible_type.error().String();
	EXPECT_EQ(*ex_compatible_type, "regular_device_type");

	// Test manifest processing - should still use device_type for regular devices
	ex_compatible_type = ctx.GetCompatibleType("mender-orchestrator-manifest");
	ASSERT_TRUE(ex_compatible_type) << ex_compatible_type.error().String();
	EXPECT_EQ(*ex_compatible_type, "regular_device_type");
}

#ifdef MENDER_USE_YAML_CPP
TEST_F(ContextTests, GetCompatibleTypeSystem) {
	conf::MenderConfig cfg;
	cfg.paths.SetDataStore(test_state_dir.Path());
	cfg.device_tier = device_tier::kSystem;

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

	string topology_dir = path::Join(test_state_dir.Path(), "mender-orchestrator");
	ASSERT_EQ(path::CreateDirectory(topology_dir), error::NoError);
	string topology_file = path::Join(topology_dir, "topology.yaml");

	setenv("MENDER_ORCHESTRATOR_TOPOLOGY_DIR", topology_dir.c_str(), 1);

	{
		ofstream f(topology_file);
		f << "system_type: system_device_type\n";
		ASSERT_TRUE(f.good());
	}

	string device_type_file = path::Join(test_state_dir.Path(), "device_type");
	{
		ofstream f(device_type_file);
		f << "device_type=regular_device_type\n";
		ASSERT_TRUE(f.good());
	}

	// Test deployment polling (empty payload type) - should use system_type
	auto ex_compatible_type = ctx.GetCompatibleType();
	ASSERT_TRUE(ex_compatible_type) << ex_compatible_type.error().String();
	EXPECT_EQ(*ex_compatible_type, "system_device_type");

	// Test manifest processing - should use system_type
	ex_compatible_type = ctx.GetCompatibleType("mender-orchestrator-manifest");
	ASSERT_TRUE(ex_compatible_type) << ex_compatible_type.error().String();
	EXPECT_EQ(*ex_compatible_type, "system_device_type");

	// Test regular artifact processing - should use device_type
	ex_compatible_type = ctx.GetCompatibleType("some-regular-payload");
	ASSERT_TRUE(ex_compatible_type) << ex_compatible_type.error().String();
	EXPECT_EQ(*ex_compatible_type, "regular_device_type");

	unsetenv("MENDER_ORCHESTRATOR_TOPOLOGY_DIR");
}

TEST_F(ContextTests, GetCompatibleTypeSystemMissingTopology) {
	conf::MenderConfig cfg;
	cfg.paths.SetDataStore(test_state_dir.Path());
	cfg.device_tier = device_tier::kSystem;

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

	string topology_dir = path::Join(test_state_dir.Path(), "mender-orchestrator");
	setenv("MENDER_ORCHESTRATOR_TOPOLOGY_DIR", topology_dir.c_str(), 1);

	// Missing topology file but device_tier is "system" - should fail with ParseError
	auto ex_compatible_type = ctx.GetCompatibleType();
	ASSERT_FALSE(ex_compatible_type);
	EXPECT_EQ(ex_compatible_type.error().code, context::MakeError(context::ParseError, "").code);

	unsetenv("MENDER_ORCHESTRATOR_TOPOLOGY_DIR");
}
#else
TEST_F(ContextTests, GetCompatibleTypeSystemNoYaml) {
	conf::MenderConfig cfg;
	cfg.paths.SetDataStore(test_state_dir.Path());
	cfg.device_tier = device_tier::kSystem;

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

	// No YAML support but device_tier is "system" - should fail
	auto ex_compatible_type = ctx.GetCompatibleType();
	ASSERT_FALSE(ex_compatible_type);
	EXPECT_EQ(ex_compatible_type.error().code, context::MakeError(context::ValueError, "").code);
}
#endif
