

//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
//_/_/
//_/_/  This file is part of CMSDK version 2.1.1.260726 (26 July 2026)
//_/_/  
//_/_/  developed by
//_/_/  
//_/_/  Communicative Machines Ltd. 
//_/_/    http://cmlabs.com/
//_/_/  Copyright (c) 2026
//_/_/  
//_/_/  in collaboration with 
//_/_/  
//_/_/  Icelandic Institute for Intelligent Machines ses
//_/_/    Menntavegur 1
//_/_/    101 Reykjavik
//_/_/    ICELAND
//_/_/    http://www.iiim.is/
//_/_/
//_/_/  and is released under the following
//_/_/  
//_/_/  EXTENDED BSD License
//_/_/
//_/_/  Redistribution and use in source and binary forms, with or without
//_/_/  modification, is permitted provided that the following conditions 
//_/_/  are met:
//_/_/
//_/_/  - Redistributions of source code must retain the above copyright 
//_/_/    and collaboration notice, this list of conditions and the following 
//_/_/    disclaimer.
//_/_/
//_/_/  - Redistributions in binary form must reproduce the above copyright 
//_/_/    notice, this list of conditions and the following disclaimer in the 
//_/_/    documentation and/or other materials provided with the distribution.
//_/_/
//_/_/  - Neither the name of its copyright holders nor the names of its 
//_/_/    contributors may be used to endorse or promote products derived from 
//_/_/    this software without specific prior written permission.
//_/_/
//_/_/  - CADIA Clause: The license granted in and to the software under this 
//_/_/    agreement is a limited-use license. The software may not be used in 
//_/_/    furtherance of: (i) intentionally causing bodily injury or severe 
//_/_/    emotional harm to any person; (ii) invading the personal privacy or 
//_/_/    violating the human rights of any person; or (iii) committing or 
//_/_/    preparing for any act of war. 
//_/_/
//_/_/  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 
//_/_/  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 
//_/_/  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 
//_/_/  PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER 
//_/_/  OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 
//_/_/  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 
//_/_/  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
//_/_/  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 
//_/_/  LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 
//_/_/  NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 
//_/_/  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//_/_/
//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
#include "Observations.h"
#include "PsyTime.h"
#include "UnitTestFramework.h"

namespace cmlabs {


bool ObservationTest() {
	GenericObservation<ObsLocation> loc;
	GenericObservation<ObsDimension> dim;
	return true;
}


uint32 GetObsTypeFromRawData(const char* data, uint32 size) {
	if ((size <= sizeof(ObsHeader)) || (((ObsHeader*)data)->cid != OBSERVATIONID))
		return 0;
	return ((ObsHeader*)data)->type;
}


int64 ExtractIntFromMessage(const char* name, DataMessage* msg, std::map<std::string, std::string>& mappings, bool &ok) {
	uint32 contentType;
	ok = true;
	std::map<std::string, std::string>::iterator i = mappings.find(name);
	if (i != mappings.end()) {
		if ((contentType = msg->getContentType(i->second.c_str())) == INTID)
			return msg->getInt(i->second.c_str(), ok);
		else if (contentType)
			return msg->getAsInt(i->second.c_str());
	}
	else {
		if ((contentType = msg->getContentType(name)) == INTID)
			return msg->getInt(name);
		else if (contentType)
			return msg->getAsInt(name);
	}
	ok = false;
	return 0;
}

float64 ExtractFloatFromMessage(const char* name, DataMessage* msg, std::map<std::string, std::string>& mappings, bool &ok) {
	uint32 contentType;
	ok = true;
	std::map<std::string, std::string>::iterator i = mappings.find(name);
	if (i != mappings.end()) {
		if ((contentType = msg->getContentType(i->second.c_str())) == DOUBLEID)
			return msg->getFloat(i->second.c_str(), ok);
		else if (contentType)
			return msg->getAsFloat(i->second.c_str());
	}
	else {
		if ((contentType = msg->getContentType(name)) == DOUBLEID)
			return msg->getFloat(name);
		else if (contentType)
			return msg->getAsFloat(name);
	}
	ok = false;
	return 0;
}

uint64 ExtractTimeFromMessage(const char* name, DataMessage* msg, std::map<std::string, std::string>& mappings, bool &ok) {
	uint32 contentType;
	ok = true;
	std::map<std::string, std::string>::iterator i = mappings.find(name);
	if (i != mappings.end()) {
		if ((contentType = msg->getContentType(i->second.c_str())) == TIMEID)
			return msg->getTime(i->second.c_str(), ok);
		//else if (contentType)
		//	return msg->getAsTime(i->second.c_str());
	}
	else {
		if ((contentType = msg->getContentType(name)) == TIMEID)
			return msg->getInt(name);
		//else if (contentType)
		//	return msg->getAsTime(name);
	}
	ok = false;
	return 0;
}

std::string ExtractStringFromMessage(const char* name, DataMessage* msg, std::map<std::string, std::string>& mappings, bool &ok) {
	uint32 contentType;
	ok = true;
	std::map<std::string, std::string>::iterator i = mappings.find(name);
	if (i != mappings.end()) {
		if ((contentType = msg->getContentType(i->second.c_str())) == CONSTCHARID)
			return msg->getString(i->second.c_str(), ok);
		else if (contentType)
			return msg->getAsString(i->second.c_str());
	}
	else {
		if ((contentType = msg->getContentType(name)) == CONSTCHARID)
			return msg->getString(name);
		else if (contentType)
			return msg->getAsString(name);
	}
	ok = false;
	return "";
}


bool Observations_UnitTest() {

	// 1. Construction: default-constructed observation must carry the right
	//    object id, declared size and a type matching the payload struct.
	unittest::progress(5, "construction");
	{
		GenericObservation<ObsLocation> loc;
		if (loc.header.cid != OBSERVATIONID) {
			unittest::fail("Observations test: default ctor cid != OBSERVATIONID\n");
			return false;
		}
		if (loc.header.type != OBSID_LOC) {
			unittest::fail("Observations test: ObsLocation type != OBSID_LOC\n");
			return false;
		}
		if (loc.header.size != sizeof(ObsHeader)) {
			unittest::fail("Observations test: header.size != sizeof(ObsHeader)\n");
			return false;
		}

		GenericObservation<ObsInteger> i;
		if (i.header.type != OBSID_INT) {
			unittest::fail("Observations test: ObsInteger type != OBSID_INT\n");
			return false;
		}
		GenericObservation<ObsFloat> f;
		if (f.header.type != OBSID_FLOAT) {
			unittest::fail("Observations test: ObsFloat type != OBSID_FLOAT\n");
			return false;
		}
		if (GetObsTypeFromRawData((const char*)NULL, 0) != 0) {
			unittest::fail("Observations test: GetObsTypeFromRawData(NULL) != 0\n");
			return false;
		}
	}

	// 2. extractDataFromMessage: pull observation payload + header metadata
	//    out of a DataMessage. ObsLocation reads x/y/z/... and the header
	//    reads ObjectID/Confidence/Consensus/SystemID/created-time.
	unittest::progress(25, "extract from message");
	{
		DataMessage msg(CTRL_TEST, 0);
		msg.setFloat("x", 1.5);
		msg.setFloat("y", 2.5);
		msg.setFloat("z", 3.5);
		msg.setFloat("yaw", 0.25);
		msg.setInt("mapid", 7);
		msg.setInt("ObjectID", 4242);
		msg.setFloat("Confidence", 0.9);
		msg.setFloat("Consensus", 0.8);
		msg.setSystemID(99);
		const uint64 created = 1234567890ULL;
		msg.setCreatedTime(created);

		std::map<std::string, std::string> mappings; // identity mapping

		GenericObservation<ObsLocation> loc;
		if (!loc.extractDataFromMessage(&msg, mappings)) {
			unittest::fail("Observations test: extractDataFromMessage failed\n");
			return false;
		}
		if (loc.data.x < 1.49 || loc.data.x > 1.51 ||
			loc.data.y < 2.49 || loc.data.y > 2.51 ||
			loc.data.z < 3.49 || loc.data.z > 3.51) {
			unittest::fail("Observations test: extracted location x/y/z mismatch\n");
			return false;
		}
		if (loc.data.mapID != 7) {
			unittest::fail("Observations test: extracted mapID mismatch\n");
			return false;
		}
		if (loc.header.objectID != 4242) {
			unittest::fail("Observations test: extracted objectID mismatch\n");
			return false;
		}
		if (loc.header.creatorID != 99) {
			unittest::fail("Observations test: extracted creatorID (systemID) mismatch\n");
			return false;
		}
		if (loc.header.timestamp != created) {
			unittest::fail("Observations test: extracted timestamp mismatch\n");
			return false;
		}
		if (loc.header.confidence < 0.89 || loc.header.confidence > 0.91) {
			unittest::fail("Observations test: extracted confidence mismatch\n");
			return false;
		}

		// A message missing the required payload keys must fail extraction.
		DataMessage bad(CTRL_TEST, 0);
		GenericObservation<ObsLocation> badLoc;
		if (badLoc.extractDataFromMessage(&bad, mappings)) {
			unittest::fail("Observations test: extract from empty message should fail\n");
			return false;
		}
	}

	// 3. Mapping indirection: extractor should follow a name->key mapping so
	//    a differently-named message field still populates the observation.
	unittest::progress(40, "extract via mappings");
	{
		DataMessage msg(CTRL_TEST, 0);
		msg.setInt("reading", -55);
		std::map<std::string, std::string> mappings;
		mappings["val"] = "reading";

		GenericObservation<ObsInteger> obs;
		if (!obs.extractDataFromMessage(&msg, mappings)) {
			unittest::fail("Observations test: mapped extract failed\n");
			return false;
		}
		if (obs.data.val != -55) {
			unittest::fail("Observations test: mapped value mismatch\n");
			return false;
		}
	}

	// 4. isBetween range test. NOTE: per the struct implementation, the method
	//    returns TRUE when the value is OUTSIDE [a,b] and FALSE when inside.
	//    We assert against the real implemented behaviour, not the name.
	unittest::progress(55, "range / isBetween");
	{
		GenericObservation<ObsInteger> lo, hi, mid, out;
		lo.data.val = 10;
		hi.data.val = 20;
		mid.data.val = 15;
		out.data.val = 100;
		if (mid.isBetween(&lo, &hi)) {
			unittest::fail("Observations test: in-range value reported outside\n");
			return false;
		}
		if (!out.isBetween(&lo, &hi)) {
			unittest::fail("Observations test: out-of-range value reported inside\n");
			return false;
		}
	}

	// 5. writeToMessage / readFromMessage round-trip by id. The observation
	//    payload must survive a serialise-into / read-back-out of a message.
	unittest::progress(70, "write / read round-trip");
	{
		DataMessage msg(CTRL_TEST, 0);
		GenericObservation<ObsFloat> obs;
		obs.data.val = 42.25;
		obs.header.objectID = 555;
		const uint64 id = 1001;
		if (!obs.writeToMessage(&msg, id)) {
			unittest::fail("Observations test: writeToMessage failed\n");
			return false;
		}
		// Writing the same id twice must be refused.
		if (obs.writeToMessage(&msg, id)) {
			unittest::fail("Observations test: duplicate writeToMessage should fail\n");
			return false;
		}

		GenericObservation<ObsFloat> back;
		if (!back.readFromMessage(&msg, id)) {
			unittest::fail("Observations test: readFromMessage failed\n");
			return false;
		}
		if (back.data.val < 42.24 || back.data.val > 42.26) {
			unittest::fail("Observations test: round-trip value mismatch\n");
			return false;
		}
		if (back.header.objectID != 555) {
			unittest::fail("Observations test: round-trip objectID mismatch\n");
			return false;
		}
		// readFromMessage with a non-existent id must fail.
		GenericObservation<ObsFloat> none;
		if (none.readFromMessage(&msg, 7777)) {
			unittest::fail("Observations test: readFromMessage of missing id should fail\n");
			return false;
		}
	}

	// 6. ReadAllFromMessage aggregate: write several observations into one
	//    message under sequential ids, read them all back as a list.
	unittest::progress(85, "ReadAllFromMessage aggregate");
	{
		DataMessage msg(CTRL_TEST, 0);
		const uint32 n = 5;
		for (uint32 k = 1; k <= n; k++) {
			GenericObservation<ObsInteger> obs;
			obs.data.val = (int64)(k * 10);
			if (!obs.writeToMessage(&msg, k)) {
				unittest::fail("Observations test: writeToMessage in loop failed\n");
				return false;
			}
		}
		std::list<GenericObservation<ObsInteger>*> all =
			GenericObservation<ObsInteger>::ReadAllFromMessage(&msg);
		if (all.size() != n) {
			unittest::fail("Observations test: ReadAllFromMessage count mismatch\n");
			std::list<GenericObservation<ObsInteger>*>::iterator ci = all.begin();
			while (ci != all.end()) { delete *ci; ci++; }
			return false;
		}
		int64 sum = 0;
		std::list<GenericObservation<ObsInteger>*>::iterator i = all.begin(), e = all.end();
		while (i != e) {
			sum += (*i)->data.val;
			// print() / toJSON() must produce non-empty output.
			if ((*i)->print().empty() || (*i)->toJSON().empty()) {
				unittest::fail("Observations test: print/toJSON empty\n");
				std::list<GenericObservation<ObsInteger>*>::iterator ci = all.begin();
				while (ci != all.end()) { delete *ci; ci++; }
				return false;
			}
			delete *i;
			i++;
		}
		if (sum != 10 + 20 + 30 + 40 + 50) {
			unittest::fail("Observations test: ReadAllFromMessage value sum mismatch\n");
			return false;
		}
		unittest::metric("observations_roundtrip", (double)n, "obs", true);
	}

	unittest::progress(100, "done");
	return true;
}

void Register_Observations_Tests() {
	UnitTestRunner::instance().registerTest("observations", Observations_UnitTest,
		"Observation construct/extract/range/serialise round-trip", "data");
}

} // namespace cmlabs
