/*
 * Copyright 2024, Intel Corporation
 *
 * This is part of PowerTOP
 *
 * This program file is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation; version 2 of the License.
 */

#include <iostream>
#include <fstream>
#include <sstream>
#include <algorithm>
#include <format>
#include <cstdlib>
#include <unistd.h>
#include "test_framework.h"

test_framework_manager& test_framework_manager::get() {
	static test_framework_manager instance;
	return instance;
}

#ifdef ENABLE_TEST_FRAMEWORK
static bool pttest_debug_env()
{
	const char *v = std::getenv("PTTEST_DEBUG");
	return v && v[0] == '1';
}

test_framework_manager::test_framework_manager()
    : recording(false), replaying(false), debug_mode(pttest_debug_env()) {}

test_framework_manager::~test_framework_manager() {
	if (recording) {
		save();
	}
}

void test_framework_manager::set_record(const std::string& filename) {
	recording = true;
	record_filename = filename;
}

void test_framework_manager::set_replay(const std::string& filename) {
	replaying = true;
	replay_filename = filename;
	load();
}

void test_framework_manager::record_read(const std::string& path, const std::string& content) {
	if (!recording) return;
	recorded_reads.push_back({path, content});
}

void test_framework_manager::record_read_fail(const std::string& path) {
	if (!recording) return;
	recorded_reads.push_back({path, "__POWERTOP_FILE_NOT_FOUND__"});
}

std::string test_framework_manager::replay_read(const std::string& path) {
	if (!replaying) return "";
	if (read_sequences[path].empty()) {
		throw test_exception("TEST FAIL: No more recorded content for read: " + path);
	}
	std::string content = read_sequences[path].front();
	read_sequences[path].pop_front();
	if (content == "__POWERTOP_FILE_NOT_FOUND__") {
		if (debug_mode)
			std::cerr << "[pttest] read: " << path << " -> NOT_FOUND\n";
		return "";
	}
	if (debug_mode) {
		std::string preview = content.substr(0, 60);
		if (content.size() > 60) preview += "...";
		std::cerr << "[pttest] read: " << path << " -> \"" << preview << "\"\n";
	}
	return content;
}

void test_framework_manager::record_write(const std::string& path, const std::string& content) {
	if (!recording) return;
	recorded_writes.push_back({path, content});
	write_log.push_back({path, content});
}

void test_framework_manager::replay_write(const std::string& path, const std::string& content) {
	if (!replaying) return;
	write_log.push_back({path, content});
	if (debug_mode)
		std::cerr << "[pttest] write: " << path << " = \"" << content << "\"\n";
	if (write_sequences.count(path) == 0) {
		/* No W records provided for this path — captured in write_log for
		   test-side assertion; no validation error since this is opt-in. */
		return;
	}
	if (write_sequences[path].empty()) {
		std::cerr << "TEST FAIL: Extra write to: " << path << " (W queue exhausted)" << std::endl;
		return;
	}
	std::string expected = write_sequences[path].front();
	write_sequences[path].pop_front();
	if (expected != content) {
		std::cerr << "TEST FAIL: Write mismatch for " << path << std::endl;
		std::cerr << "  Expected: " << expected << std::endl;
		std::cerr << "  Actual:   " << content << std::endl;
	}
}

const std::vector<std::pair<std::string, std::string>>&
test_framework_manager::get_write_log() const {
	return write_log;
}

void test_framework_manager::record_msr(int cpu, uint64_t offset, uint64_t value) {
	if (!recording) return;
	recorded_msrs.push_back({cpu, offset, value});
}

int test_framework_manager::replay_msr(int cpu, uint64_t offset, uint64_t *value) {
	if (!replaying) return -1;
	auto key = std::make_pair(cpu, offset);
	if (msr_sequences[key].empty()) {
		throw test_exception(std::format("TEST FAIL: No more recorded content for MSR read: cpu {} offset 0x{:x}", cpu, offset));
	}
	*value = msr_sequences[key].front();
	msr_sequences[key].pop_front();
	if (debug_mode)
		std::cerr << std::format("[pttest] msr: cpu={} offset=0x{:x} -> 0x{:x}\n", cpu, offset, *value);
	return 0;
}

void test_framework_manager::record_readlink(const std::string& path, const std::string& target) {
	if (!recording) return;
	recorded_links.push_back({path, target});
}

std::string test_framework_manager::replay_readlink(const std::string& path) {
	if (!replaying) return "";
	if (link_sequences[path].empty()) {
		throw test_exception("TEST FAIL: No more recorded content for readlink: " + path);
	}
	std::string target = link_sequences[path].front();
	link_sequences[path].pop_front();
	if (debug_mode)
		std::cerr << "[pttest] readlink: " << path << " -> \"" << target << "\"\n";
	return target;
}

void test_framework_manager::record_dir(const std::string& path, const std::vector<std::string>& entries) {
	if (!recording) return;
	std::string joined;
	for (size_t i = 0; i < entries.size(); ++i) {
		if (i > 0) joined += '\n';
		joined += entries[i];
	}
	recorded_dirs.push_back({path, joined});
}

void test_framework_manager::record_dir_fail(const std::string& path) {
	if (!recording) return;
	recorded_dirs.push_back({path, ""});
}

std::vector<std::string> test_framework_manager::replay_dir(const std::string& path) {
	if (!replaying) return {};
	if (dir_sequences[path].empty()) {
		throw test_exception("TEST FAIL: No recorded dir listing for: " + path);
	}
	std::string content = dir_sequences[path].front();
	dir_sequences[path].pop_front();
	if (content.empty()) {
		if (debug_mode)
			std::cerr << "[pttest] dir: " << path << " -> []\n";
		return {};
	}
	std::vector<std::string> result;
	std::istringstream ss(content);
	std::string line;
	while (std::getline(ss, line)) {
		if (!line.empty())
			result.push_back(line);
	}
	if (debug_mode) {
		std::cerr << "[pttest] dir: " << path << " -> [";
		for (size_t i = 0; i < result.size(); ++i) {
			if (i > 0) std::cerr << ", ";
			std::cerr << result[i];
		}
		std::cerr << "]\n";
	}
	return result;
}

void test_framework_manager::record_time(struct timeval tv) {
	if (!recording) return;
	recorded_times.push_back(tv);
}

void test_framework_manager::record_access(const std::string& path, int mode, int result) {
	if (!recording) return;
	recorded_accesses.emplace_back(path, mode, result);
}

static std::string access_mode_str(int mode) {
	if (mode == F_OK) return "F_OK";
	std::string s;
	if (mode & R_OK) s += "R_OK|";
	if (mode & W_OK) s += "W_OK|";
	if (mode & X_OK) s += "X_OK|";
	if (!s.empty()) s.pop_back();
	return s;
}

int test_framework_manager::replay_access(const std::string& path, int mode) {
	if (!replaying) return -1;
	auto key = std::make_pair(path, mode);
	if (access_sequences[key].empty()) {
		throw test_exception("TEST FAIL: No recorded access() for: " + path
			+ " (mode=" + access_mode_str(mode) + "/" + std::to_string(mode) + ")");
	}
	int result = access_sequences[key].front();
	access_sequences[key].pop_front();
	if (debug_mode)
		std::cerr << "[pttest] access: " << path
		          << " mode=" << access_mode_str(mode)
		          << " -> " << (result == 0 ? "ok" : "fail") << "\n";
	return result;
}

void test_framework_manager::replay_time(struct timeval *tv) {
	if (!replaying) return;
	if (time_sequences.empty()) {
		throw test_exception("TEST FAIL: No more recorded content for time read");
	}
	*tv = time_sequences.front();
	time_sequences.pop_front();
	if (debug_mode)
		std::cerr << std::format("[pttest] time: {}.{:06}\n", tv->tv_sec, tv->tv_usec);
}

void test_framework_manager::reset() {
	recording = false;
	replaying = false;
	record_filename.clear();
	replay_filename.clear();
	read_sequences.clear();
	recorded_reads.clear();
	write_sequences.clear();
	recorded_writes.clear();
	msr_sequences.clear();
	recorded_msrs.clear();
	time_sequences.clear();
	recorded_times.clear();
	link_sequences.clear();
	recorded_links.clear();
	dir_sequences.clear();
	recorded_dirs.clear();
	access_sequences.clear();
	recorded_accesses.clear();
	write_log.clear();
}

void test_framework_manager::save() {
	std::ofstream file(record_filename);
	if (!file) {
		std::cerr << "Failed to open record file: " << record_filename << std::endl;
		return;
	}

	for (const auto& p : recorded_reads) {
		if (p.second == "__POWERTOP_FILE_NOT_FOUND__")
			file << "N " << p.first << std::endl;
		else
			file << "R " << p.first << " " << base64_encode(p.second) << std::endl;
	}
	for (const auto& p : recorded_writes) {
		file << "W " << p.first << " " << base64_encode(p.second) << std::endl;
	}
	for (const auto& m : recorded_msrs) {
		file << "M " << std::get<0>(m) << " " << std::hex << std::get<1>(m) << " " << std::get<2>(m) << std::dec << std::endl;
	}
	for (const auto& t : recorded_times) {
		file << "T " << t.tv_sec << " " << t.tv_usec << std::endl;
	}
	/* L base64(target) path — empty base64 means readlink failed */
	for (const auto& p : recorded_links) {
		file << "L " << base64_encode(p.second) << " " << p.first << std::endl;
	}
	/* D path base64(entries) — entries are newline-joined sorted names;
	 * empty base64 means directory not found or empty */
	for (const auto& p : recorded_dirs) {
		file << "D " << p.first << " " << base64_encode(p.second) << std::endl;
	}
	/* A path mode result — access() interception; result is 0 or -1 */
	for (const auto& a : recorded_accesses) {
		file << "A " << std::get<0>(a) << " " << std::get<1>(a)
		     << " " << std::get<2>(a) << std::endl;
	}
}

void test_framework_manager::load() {
	std::ifstream file(replay_filename);
	if (!file) {
		std::cerr << "Failed to open replay file: " << replay_filename << std::endl;
		return;
	}

	std::string line;
	while (getline(file, line)) {
		if (line.empty()) continue;

		size_t first_space = line.find(' ');
		if (first_space == std::string::npos) continue;

		char type = line[0];
		std::string rest = line.substr(first_space + 1);

		if (type == 'N') {
			read_sequences[rest].push_back("__POWERTOP_FILE_NOT_FOUND__");
			continue;
		}

		if (type == 'M') {
			std::stringstream msr_ss(rest);
			int cpu;
			uint64_t offset, value;
			msr_ss >> cpu >> std::hex >> offset >> value;
			msr_sequences[std::make_pair(cpu, offset)].push_back(value);
			continue;
		}

		if (type == 'T') {
			std::stringstream time_ss(rest);
			struct timeval tv;
			time_ss >> tv.tv_sec >> tv.tv_usec;
			time_sequences.push_back(tv);
			continue;
		}

		if (type == 'L') {
			/* format: base64(target) path — split on first space */
			size_t sp = rest.find(' ');
			std::string b64 = (sp != std::string::npos) ? rest.substr(0, sp) : "";
			std::string path = (sp != std::string::npos) ? rest.substr(sp + 1) : rest;
			link_sequences[path].push_back(base64_decode(b64));
			continue;
		}

		if (type == 'D') {
			/* b64 may be absent (empty directory) */
			size_t last_space = rest.rfind(' ');
			std::string path = (last_space != std::string::npos) ? rest.substr(0, last_space) : rest;
			std::string b64_content = (last_space != std::string::npos) ? rest.substr(last_space + 1) : "";
			dir_sequences[path].push_back(base64_decode(b64_content));
			continue;
		}

		if (type == 'A') {
			/* format: path mode result — e.g. "/sys/foo 4 0" */
			size_t last_space = rest.rfind(' ');
			if (last_space == std::string::npos) continue;
			size_t prev_space = rest.rfind(' ', last_space - 1);
			if (prev_space == std::string::npos) continue;
			std::string path = rest.substr(0, prev_space);
			int mode   = std::stoi(rest.substr(prev_space + 1, last_space - prev_space - 1));
			int result = std::stoi(rest.substr(last_space + 1));
			access_sequences[{path, mode}].push_back(result);
			continue;
		}

		size_t last_space = rest.rfind(' ');
		if (last_space == std::string::npos) continue;

		std::string path = rest.substr(0, last_space);
		std::string b64_content = rest.substr(last_space + 1);

		if (type == 'R') {
			read_sequences[path].push_back(base64_decode(b64_content));
		} else if (type == 'W') {
			write_sequences[path].push_back(base64_decode(b64_content));
		}
	}
}

static const std::string base64_chars =
             "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
             "abcdefghijklmnopqrstuvwxyz"
             "0123456789+/";

std::string test_framework_manager::base64_encode(const std::string& in) {
    std::string out;
    int val = 0, valb = -6;
    for (unsigned char c : in) {
        val = (val << 8) + c;
        valb += 8;
        while (valb >= 0) {
            out.push_back(base64_chars[(val >> valb) & 0x3F]);
            valb -= 6;
        }
    }
    if (valb > -6) out.push_back(base64_chars[((val << 8) >> (valb + 8)) & 0x3F]);
    while (out.size() % 4) out.push_back('=');
    return out;
}

std::string test_framework_manager::base64_decode(const std::string& in) {
    std::string out;
    std::vector<int> T(256, -1);
    for (int i = 0; i < 64; i++) T[base64_chars[i]] = i;

    int val = 0, valb = -8;
    for (unsigned char c : in) {
        if (T[c] == -1) break;
        val = (val << 6) + T[c];
        valb += 6;
        if (valb >= 0) {
            out.push_back(char((val >> valb) & 0xFF));
            valb -= 8;
        }
    }
    return out;
}
#else
test_framework_manager::test_framework_manager() {}
test_framework_manager::~test_framework_manager() {}
#endif
