//! Loader for `.canvas` (JSON Canvas 1.0) files.

use std::fs;
use std::path::{Path, PathBuf};

use thiserror::Error;

use super::model::Canvas;

#[derive(Debug, Error)]
pub enum ParseError {
    #[error("could not read {path}: {source}")]
    Io {
        path: PathBuf,
        #[source]
        source: std::io::Error,
    },
    #[error("invalid JSON Canvas in {path} at line {line}, column {column}: {message}")]
    Json {
        path: PathBuf,
        line: usize,
        column: usize,
        message: String,
    },
}

/// Read and parse a JSON Canvas file from disk.
pub fn load(path: &Path) -> Result<Canvas, ParseError> {
    let bytes = fs::read(path).map_err(|e| ParseError::Io {
        path: path.to_path_buf(),
        source: e,
    })?;
    parse_bytes(path, &bytes)
}

/// Parse JSON Canvas bytes. `path` is used for error reporting only.
pub fn parse_bytes(path: &Path, bytes: &[u8]) -> Result<Canvas, ParseError> {
    serde_json::from_slice::<Canvas>(bytes).map_err(|e| ParseError::Json {
        path: path.to_path_buf(),
        line: e.line(),
        column: e.column(),
        message: e.to_string(),
    })
}

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

    #[test]
    fn malformed_json_returns_position() {
        let err = parse_bytes(Path::new("x.canvas"), b"{ not json").unwrap_err();
        match err {
            ParseError::Json { line, .. } => assert!(line >= 1),
            _ => panic!("expected json error"),
        }
    }

    #[test]
    fn missing_field_message_mentions_field() {
        let bytes =
            br#"{"nodes":[{"id":"x","type":"text","x":0,"y":0,"width":1,"height":1}],"edges":[]}"#;
        let err = parse_bytes(Path::new("x.canvas"), bytes).unwrap_err();
        let msg = format!("{err}");
        assert!(msg.contains("text") || msg.contains("missing"), "msg={msg}");
    }
}
