pvoutput-client 0.0.3

Client for the PVOutput.org API
Documentation
use jiff::civil::{Date, Time};
use proptest::property_test;

use crate::{
	parser::{parse_add_output_error, parse_add_status_error, parse_datetime_error, parse_post_system_error},
	types::error::{AddOutputError, AddStatusError, DateTimeError, ExtendedDataField, PostSystemError},
};

#[test]
fn missing_data() {
	// `Invalid data format [xyz]` is not the same as `Invalid data format []`!
	assert_eq!(
		parse_add_status_error("Invalid data format []"),
		Some(AddStatusError::NoData),
		"parse_add_status_error should return AddStatusError::NoData for an empty data format"
	);

	assert_eq!(
		parse_add_status_error("Invalid data format [xyz]"),
		Some(AddStatusError::InvalidDataFormat(String::from("xyz"))),
		"parse_add_status_error should return AddStatusError::InvalidDataFormat(\"xyz\")"
	);
}

#[test]
fn date_time_edge_cases() {
	assert_eq!(
		parse_datetime_error("Invalid date: [test]"),
		Some(DateTimeError::InvalidDate(String::from("[test]")))
	);
	assert_eq!(parse_datetime_error("Invalid Date [test]"), Some(DateTimeError::InvalidDate(String::from("test"))));
	assert_eq!(
		parse_datetime_error("Invalid time: [test]"),
		Some(DateTimeError::InvalidTime(String::from("[test]")))
	);
	assert_eq!(parse_datetime_error("Invalid Time [test]"), Some(DateTimeError::InvalidTime(String::from("test"))));
}

#[property_test]
#[test]
fn invalid_date(#[strategy = ".+"] date: String) {
	let error = DateTimeError::InvalidDate(date.clone());
	assert_eq!(parse_datetime_error(&format!("Date [{date}] invalid")), Some(error.clone()));
	assert_eq!(parse_datetime_error(&format!("Invalid Date [{date}]")), Some(error.clone()));
	assert_eq!(parse_datetime_error(&format!("Date is invalid [{date}]")), Some(error.clone()));
	assert_eq!(parse_datetime_error(&format!("Invalid date: {date}")), Some(error));
}

#[property_test]
#[test]
fn invalid_time(#[strategy = ".+"] time: String) {
	let error = DateTimeError::InvalidTime(time.clone());
	assert_eq!(parse_datetime_error(&format!("Invalid Time [{time}]")), Some(error.clone()));
	assert_eq!(parse_datetime_error(&format!("Time is invalid [{time}]")), Some(error.clone()));
	assert_eq!(parse_datetime_error(&format!("Invalid time: {time}")), Some(error));
}

#[property_test]
#[test]
fn future_date(#[strategy = 0..=9999i16] year: i16, #[strategy = 1..=12i8] month: i8, #[strategy = 1..=28i8] day: i8) {
	let date = Date::new(year, month, day).unwrap();
	let error = DateTimeError::DateTooNew(date);
	let date_str = format!("{year:04}{month:02}{day:02}");

	assert_eq!(parse_datetime_error(&format!("Date [{date_str}] too new")), Some(error.clone()));
	assert_eq!(parse_datetime_error(&format!("Invalid future date [{date_str}]")), Some(error.clone()));
	assert_eq!(parse_datetime_error(&format!("Date is in the future [{date_str}]")), Some(error));
}

#[property_test]
#[test]
fn date_too_old(
	#[strategy = 0..=9999i16] year: i16,
	#[strategy = 1..=12i8] month: i8,
	#[strategy = 1..=28i8] day: i8,
	number: u64,
) {
	let date = Date::new(year, month, day).unwrap();
	let error = DateTimeError::DateTooOld(date);
	let date_str = format!("{year:04}{month:02}{day:02}");

	assert_eq!(parse_datetime_error(&format!("Date [{date_str}] too old")), Some(error.clone()));
	assert_eq!(parse_datetime_error(&format!("Date is older than {number} days [{date_str}]")), Some(error));
}

#[property_test]
#[test]
fn missing_temperature(
	which: bool,
	#[strategy = 0..=9999i16] year: i16,
	#[strategy = 1..=12i8] month: i8,
	#[strategy = 1..=28i8] day: i8,
) {
	let date = Date::new(year, month, day).unwrap();
	let (which, error) = if which {
		("Max", AddOutputError::MinTemperatureWithoutMax(date))
	} else {
		("Min", AddOutputError::MaxTemperatureWithoutMin(date))
	};

	assert_eq!(
		parse_add_output_error(&format!("{which} temp missing on [{year:04}{month:02}{day:02}]")),
		Some(error)
	);
}

#[property_test]
#[test]
fn production_too_high(which: bool, value: u32, system_size: u32) {
	let (which, error) = if which {
		(
			"Power",
			AddStatusError::ExcessivePowerGeneration {
				power: value,
				system_size,
			},
		)
	} else {
		(
			"Energy",
			AddStatusError::ExcessiveEnergyGeneration {
				energy: value,
				system_size,
			},
		)
	};

	assert_eq!(
		parse_add_status_error(&format!("{which} value [{value}] too high for system size [{system_size}]")),
		Some(error)
	);
}

#[property_test]
#[test]
fn energy_too_high(energy: u32, #[strategy = 1..=23i8] hour: i8, #[strategy = 0..=59i8] minute: i8) {
	assert_eq!(
		parse_add_status_error(&format!("Energy value [{energy}] too high for [{hour:02}:{minute:02}]")),
		Some(AddStatusError::EnergyTooHighForTime(energy, Time::new(hour, minute, 0, 0).unwrap()))
	);
}

#[property_test]
#[test]
fn invalid_data_format(#[strategy = ".+"] data: String) {
	assert_eq!(
		parse_add_status_error(&format!("Invalid data format [{data}]")),
		Some(AddStatusError::InvalidDataFormat(data))
	);
}

#[test]
fn generation_too_high() {
	let error = include_str!("../../../cases/errors/generation_too_high.txt").trim();

	assert_eq!(
		parse_add_output_error(error.trim_start_matches("Bad request 400: ")),
		Some(AddOutputError::GenerationTooHigh {
			generation: 10_000_000_000.0,
			system_size: 6200,
			date: Date::constant(2025, 9, 22)
		})
	);
}

#[test]
fn invalid_colour() {
	let error = include_str!("../../../cases/errors/invalid_extended_colour.txt").trim();
	assert_eq!(
		parse_post_system_error(error.trim_start_matches("Bad Request 400: ")),
		Some(PostSystemError::InvalidExtendedDataField(7, ExtendedDataField::Colour))
	);
}