//! `system_monitor_disk` — disk usage (cached because mount scan isn't <1ms).

use async_trait::async_trait;
use sysinfo::Disks;

use crate::payload::{BarsData, Body, Payload, RatioData, TextData};
use crate::render::Shape;
use crate::samples;

use super::super::{FetchContext, FetchError, Fetcher, Safety};
use super::{disk_bars, disk_label, payload, primary_disk, ratio_of};

pub struct SystemMonitorDisk;

#[async_trait]
impl Fetcher for SystemMonitorDisk {
    fn name(&self) -> &str {
        "system_monitor_disk"
    }
    fn safety(&self) -> Safety {
        Safety::Safe
    }
    fn description(&self) -> &'static str {
        "Disk usage for the largest mounted volume. `Ratio` drives gauges with the used/total fraction; `Text` formats it as `\"45% of 500 GB\"`; `Bars` lists every mount with used bytes for chart_bar."
    }
    fn refresh_interval(&self) -> u64 {
        60 * 5
    }
    fn shapes(&self) -> &[Shape] {
        &[Shape::Ratio, Shape::Text, Shape::Bars]
    }
    fn sample_body(&self, shape: Shape) -> Option<Body> {
        Some(match shape {
            Shape::Ratio => samples::ratio(0.58, "disk"),
            Shape::Text => samples::text("58% of 400 GB"),
            Shape::Bars => samples::bars(&[("/", 42), ("/home", 110), ("/data", 200)]),
            _ => return None,
        })
    }
    async fn fetch(&self, ctx: &FetchContext) -> Result<Payload, FetchError> {
        let disks = Disks::new_with_refreshed_list();
        let payload = match ctx.shape.unwrap_or(Shape::Ratio) {
            Shape::Bars => payload(Body::Bars(BarsData {
                bars: disk_bars(&disks),
            })),
            Shape::Text => payload(Body::Text(TextData {
                value: primary_disk(&disks)
                    .map(|(t, a)| disk_label(t, a))
                    .unwrap_or_else(|| "no disks".into()),
            })),
            _ => primary_disk(&disks)
                .map(|(total, available)| {
                    let used = total.saturating_sub(available);
                    payload(Body::Ratio(RatioData {
                        value: ratio_of(used, total),
                        label: Some(disk_label(total, available)),
                        denominator: None,
                    }))
                })
                .unwrap_or_else(|| {
                    payload(Body::Ratio(RatioData {
                        value: 0.0,
                        label: Some("no disks".into()),
                        denominator: None,
                    }))
                }),
        };
        Ok(payload)
    }
}

#[cfg(test)]
mod tests {
    use super::super::test_helpers::ctx_with_shape;
    use super::*;

    #[test]
    fn sample_body_matches_documented_shapes() {
        assert!(matches!(
            SystemMonitorDisk.sample_body(Shape::Ratio),
            Some(Body::Ratio(ratio))
                if ratio.label.as_deref() == Some("disk") && ratio.value == 0.58
        ));
        assert!(matches!(
            SystemMonitorDisk.sample_body(Shape::Text),
            Some(Body::Text(text)) if text.value == "58% of 400 GB"
        ));
        assert!(matches!(
            SystemMonitorDisk.sample_body(Shape::Bars),
            Some(Body::Bars(bars))
                if bars.bars.len() == 3
                    && bars.bars[0].label == "/"
                    && bars.bars[2].value == 200
        ));
    }

    #[tokio::test]
    async fn defaults_to_ratio_or_text_fallback() {
        let ctx = ctx_with_shape(None);
        let p = SystemMonitorDisk.fetch(&ctx).await.unwrap();
        assert!(matches!(p.body, Body::Ratio(_) | Body::Text(_)));
    }

    #[tokio::test]
    async fn bars_shape_emits_bars_body() {
        let ctx = ctx_with_shape(Some(Shape::Bars));
        let p = SystemMonitorDisk.fetch(&ctx).await.unwrap();
        assert!(matches!(p.body, Body::Bars(_)));
    }

    /// `Shape::Text` exercises the `disk_label` / "no disks" fallback arm. The exact label
    /// depends on the host's mounted disks (and may legitimately be `"no disks"` in a sandbox);
    /// we just assert the dispatch produces a non-empty `Text` body.
    #[tokio::test]
    async fn text_shape_emits_text_body() {
        let ctx = ctx_with_shape(Some(Shape::Text));
        let p = SystemMonitorDisk.fetch(&ctx).await.unwrap();
        let Body::Text(t) = p.body else {
            panic!("expected Body::Text");
        };
        assert!(!t.value.is_empty());
    }
}
