pvoutput-client 0.0.1

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

use crate::{
	parser::error_regex,
	types::error::{DataError, ExtendedDataField},
};

#[test]
fn missing_data() {
	// the regex handles `Invalid data format [xyz]`, but explicitly *doesn't* handle the special case where there's
	// nothing inside the square brackets.
	assert_eq!(
		error_regex("Invalid data format []"),
		None,
		"error_regex should return None for an empty data format"
	);
}

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

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

#[property_test]
#[test]
fn invalid_time(#[strategy = ".+"] time: String) {
	let error = DataError::InvalidTime(time.clone());
	assert_eq!(error_regex(&format!("Invalid Time [{time}]")), Some(error.clone()));
	assert_eq!(error_regex(&format!("Time is invalid [{time}]")), Some(error.clone()));
	assert_eq!(error_regex(&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 = DataError::DateTooNew(date);
	let date_str = format!("{year:04}{month:02}{day:02}");

	assert_eq!(error_regex(&format!("Date [{date_str}] too new")), Some(error.clone()));
	assert_eq!(error_regex(&format!("Invalid future date [{date_str}]")), Some(error.clone()));
	assert_eq!(error_regex(&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 = DataError::DateTooOld(date);
	let date_str = format!("{year:04}{month:02}{day:02}");

	assert_eq!(error_regex(&format!("Date [{date_str}] too old")), Some(error.clone()));
	assert_eq!(error_regex(&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", DataError::MinTemperatureWithoutMax(date))
	} else {
		("Min", DataError::MaxTemperatureWithoutMin(date))
	};

	assert_eq!(error_regex(&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",
			DataError::ExcessivePowerGeneration {
				power: value,
				system_size,
			},
		)
	} else {
		(
			"Energy",
			DataError::ExcessiveEnergyGeneration {
				energy: value,
				system_size,
			},
		)
	};

	assert_eq!(
		error_regex(&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!(
		error_regex(&format!("Energy value [{energy}] too high for [{hour:02}:{minute:02}]")),
		Some(DataError::EnergyTooHighForTime(energy, Time::new(hour, minute, 0, 0).unwrap()))
	);
}

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

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

	assert_eq!(
		error_regex(error.trim_start_matches("Bad request 400: ")),
		Some(DataError::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!(
		error_regex(error.trim_start_matches("Bad Request 400: ")),
		Some(DataError::InvalidExtendedDataField(7, ExtendedDataField::Colour))
	);
}