#![cfg(test)]
#![cfg(feature = "std")]
#![expect(
clippy::shadow_reuse,
reason = "Intentional shadowing with rstest async fixture"
)]
use amber_api::{Amber, models};
use anyhow::{Result, anyhow};
use jiff::ToSpan as _;
use pretty_assertions::assert_eq;
use rstest::{fixture, rstest};
#[fixture]
fn amber_client() -> Amber {
Amber::default()
}
#[fixture]
fn seven_days_ago() -> jiff::civil::Date {
jiff::Zoned::now()
.round(jiff::Unit::Day)
.expect("Failed to get today's date")
.date()
.saturating_sub(7.days())
}
#[rstest]
#[tokio::test]
async fn current_renewables_default(amber_client: Amber) -> Result<()> {
let renewables = amber_client
.current_renewables()
.state(models::State::Vic)
.call()
.await?;
assert_eq!(renewables.len(), 1);
let entry = renewables
.first()
.ok_or_else(|| anyhow!("Expect at least one entry"))?;
assert!(entry.is_current_renewable());
let base = entry.as_base_renewable();
assert!(base.start_time < base.end_time);
assert_eq!(base.duration, 30);
Ok(())
}
#[rstest]
#[tokio::test]
async fn current_renewables_optional(amber_client: Amber) -> Result<()> {
let renewables = amber_client
.current_renewables()
.state(models::State::Vic)
.previous(6)
.next(3)
.resolution(models::Resolution::FiveMinute)
.call()
.await?;
assert_eq!(renewables.len(), 10);
let actual_count = renewables
.iter()
.filter(|e| e.is_actual_renewable())
.count();
let current_count = renewables
.iter()
.filter(|e| e.is_current_renewable())
.count();
let forecast_count = renewables
.iter()
.filter(|e| e.is_forecast_renewable())
.count();
assert_eq!(actual_count, 6);
assert_eq!(current_count, 1);
assert_eq!(forecast_count, 3);
for entry in renewables {
let base = entry.as_base_renewable();
assert!(base.start_time < base.end_time);
assert_eq!(base.duration, 5);
}
Ok(())
}
#[rstest]
#[tokio::test]
async fn sites_retrieval(amber_client: Amber) -> Result<()> {
let sites = amber_client.sites().await?;
assert!(!sites.is_empty(), "Expected non-empty sites list");
let site = sites
.first()
.ok_or_else(|| anyhow!("Expected at least one site"))?;
assert!(!site.id.is_empty(), "Site ID should not be empty");
assert!(!site.nmi.is_empty(), "Site NMI should not be empty");
assert!(!site.network.is_empty(), "Site network should not be empty");
assert!(
site.interval_length == 5 || site.interval_length == 30,
"Site interval length should be 5 or 30"
);
let display_string = format!("{site}");
assert!(display_string.contains(&site.id));
assert!(display_string.contains(&site.nmi));
assert!(display_string.contains(&site.network));
Ok(())
}
#[fixture]
async fn site_id(amber_client: Amber) -> String {
amber_client
.sites()
.await
.expect("Failed to obtain sites")
.into_iter()
.next()
.map(|site| site.id)
.expect("Expected at least one site")
}
#[rstest]
#[tokio::test]
async fn prices_default(amber_client: Amber, #[future] site_id: String) -> Result<()> {
let site_id = site_id.await;
let intervals = amber_client.prices().site_id(&site_id).call().await?;
assert!(!intervals.is_empty(), "Expected non-empty prices list");
let interval = intervals
.first()
.ok_or_else(|| anyhow!("Expected at least one price"))?;
assert!(interval.is_actual_interval());
let base_interval = interval.as_base_interval().expect("Expected base interval");
assert_eq!(base_interval.duration, 5);
Ok(())
}
#[rstest]
#[tokio::test]
async fn prices_optional(
amber_client: Amber,
#[future] site_id: String,
seven_days_ago: jiff::civil::Date,
) -> Result<()> {
let site_id = site_id.await;
let intervals = amber_client
.prices()
.site_id(&site_id)
.start_date(seven_days_ago)
.end_date(seven_days_ago)
.resolution(models::Resolution::ThirtyMinute)
.call()
.await?;
assert!(!intervals.is_empty(), "Expected non-empty prices list");
assert_eq!(intervals.len(), 96, "Expected 96 intervals");
assert_eq!(
intervals
.iter()
.filter(|i| i
.as_base_interval()
.is_some_and(|b| b.channel_type == models::ChannelType::General))
.count(),
48
);
assert_eq!(
intervals
.iter()
.filter(|i| i
.as_base_interval()
.is_some_and(|b| b.channel_type == models::ChannelType::FeedIn))
.count(),
48
);
Ok(())
}
#[rstest]
#[tokio::test]
async fn current_prices_default(amber_client: Amber, #[future] site_id: String) -> Result<()> {
let site_id = site_id.await;
let intervals = amber_client
.current_prices()
.site_id(&site_id)
.call()
.await?;
assert!(
!intervals.is_empty(),
"Expected non-empty current prices list"
);
let interval = intervals
.first()
.ok_or_else(|| anyhow!("Expected at least one current price"))?;
assert!(interval.is_current_interval());
let base_interval = interval.as_base_interval().expect("Expected base interval");
assert!(
base_interval.duration == 5 || base_interval.duration == 30,
"Expected duration to be 5 or 30 minutes"
);
Ok(())
}
#[rstest]
#[tokio::test]
async fn current_prices_optional(amber_client: Amber, #[future] site_id: String) -> Result<()> {
let site_id = site_id.await;
let intervals = amber_client
.current_prices()
.site_id(&site_id)
.previous(6)
.next(3)
.resolution(models::Resolution::ThirtyMinute)
.call()
.await?;
assert!(
!intervals.is_empty(),
"Expected non-empty current prices list"
);
let actual_count = intervals.iter().filter(|i| i.is_actual_interval()).count();
let current_count = intervals.iter().filter(|i| i.is_current_interval()).count();
let forecast_count = intervals
.iter()
.filter(|i| i.is_forecast_interval())
.count();
assert_eq!(actual_count, 12, "Expected at most 12 actual intervals");
assert_eq!(current_count, 2, "Expected exactly 2 current interval");
assert_eq!(forecast_count, 6, "Expected at most 6 forecast intervals");
for interval in &intervals {
if let Some(base) = interval.as_base_interval() {
assert_eq!(base.duration, 30, "Expected 30-minute resolution");
}
}
Ok(())
}
#[rstest]
#[tokio::test]
async fn usage_default(
amber_client: Amber,
#[future] site_id: String,
seven_days_ago: jiff::civil::Date,
) -> Result<()> {
let site_id = site_id.await;
let usage_data = amber_client
.usage()
.site_id(&site_id)
.start_date(seven_days_ago)
.end_date(seven_days_ago)
.call()
.await?;
assert!(!usage_data.is_empty(), "Expected non-empty usage data list");
let usage = usage_data
.first()
.ok_or_else(|| anyhow!("Expected at least one usage entry"))?;
assert!(
!usage.channel_identifier.is_empty(),
"Channel identifier should not be empty"
);
assert!(usage.kwh >= 0.0_f64, "kWh should be non-negative");
assert_eq!(
usage.base.date, seven_days_ago,
"Date should match requested date"
);
Ok(())
}
#[rstest]
#[tokio::test]
async fn usage_multi_day(
amber_client: Amber,
#[future] site_id: String,
seven_days_ago: jiff::civil::Date,
) -> Result<()> {
let site_id = site_id.await;
let usage_data = amber_client
.usage()
.site_id(&site_id)
.start_date(seven_days_ago)
.end_date(seven_days_ago)
.call()
.await?;
assert!(!usage_data.is_empty(), "Expected non-empty usage data list");
let earliest_date = usage_data
.iter()
.map(|u| u.base.date)
.min()
.ok_or_else(|| anyhow!("Expected at least one usage entry"))?;
let latest_date = usage_data
.iter()
.map(|u| u.base.date)
.max()
.ok_or_else(|| anyhow!("Expected at least one usage entry"))?;
assert!(
earliest_date >= seven_days_ago,
"Earliest usage date should be >= start date"
);
assert!(
latest_date <= seven_days_ago,
"Latest usage date should be <= end date"
);
for usage in &usage_data {
assert!(
!usage.channel_identifier.is_empty(),
"Channel identifier should not be empty"
);
assert!(usage.kwh >= 0.0_f64, "kWh should be non-negative");
assert!(usage.cost.is_finite(), "Cost should be a finite number");
assert!(
usage.base.start_time < usage.base.end_time,
"Start time should be before end time"
);
}
Ok(())
}