//! Tests for geometries of SVG primitives
//!
//! These use the *.svg and *.svg.geom files in the tests/fixtures/primitive_geometries directory.
//!
//! Each .svg.geom is a JSON file formatted like this:
//!
//! ```json
//! {
//!     "#element_id": {
//!         "ink_rect": {
//!             "x": 5.0,
//!             "y": 15.0,
//!             "width": 40.0,
//!             "height": 50.0
//!         },
//!         "logical_rect": {
//!             "x": 10.0,
//!             "y": 20.0,
//!             "width": 30.0,
//!             "height": 40.0
//!         }
//!     }
//! }
//! ```
//!
//! Any number of element_ids may appear in the file.  For each of those, the `test()` function will
//! call `CairoRenderer::get_layer_geometry()` and compare its result against the provided rectangles.

use anyhow::{Context, Result};
use cairo;
use rsvg::{CairoRenderer, LengthUnit, Loader, Rect};
use serde::Deserialize;
use std::collections::BTreeMap;
use std::fs;

// Copy of cairo::Rectangle
//
// Somehow I can't make serde's "remote" work here, in combination with the BTreeMap below...
#[derive(Copy, Clone, Deserialize, Debug, PartialEq)]
struct Rectangle {
    x: f64,
    y: f64,
    width: f64,
    height: f64,
}

impl From<Rectangle> for Rect {
    fn from(r: Rectangle) -> Rect {
        Rect {
            x0: r.x,
            y0: r.y,
            x1: r.x + r.width,
            y1: r.y + r.height,
        }
    }
}

#[derive(Copy, Clone, Deserialize, Debug, PartialEq)]
struct ElementGeometry {
    ink_rect: Option<Rectangle>,
    logical_rect: Option<Rectangle>,
}

#[derive(Deserialize)]
struct Geometries(BTreeMap<String, ElementGeometry>);

fn read_geometries(path: &str) -> Result<Geometries> {
    let contents = fs::read_to_string(path).context(format!("could not read {:?}", path))?;
    Ok(serde_json::from_str(&contents).context(format!("could not parse JSON from {:?}", path))?)
}

// We create a struct with the id and geometry so that
// assert_eq!() in the tests will print out the element name for failures.
//
// Here we use rsvg::Rect as that one has an approx_eq() method.
#[derive(Debug, PartialEq)]
struct Element {
    id: String,
    ink_rect: Option<Rect>,
    logical_rect: Option<Rect>,
}

impl Element {
    fn from_element_geometry(id: &str, geom: &ElementGeometry) -> Element {
        Element {
            id: String::from(id),
            ink_rect: geom.ink_rect.map(Into::into),
            logical_rect: geom.logical_rect.map(Into::into),
        }
    }

    fn from_rectangles(
        id: &str,
        ink_rect: cairo::Rectangle,
        logical_rect: cairo::Rectangle,
    ) -> Element {
        Element {
            id: String::from(id),
            ink_rect: Some(ink_rect.into()),
            logical_rect: Some(logical_rect.into()),
        }
    }
}

fn test(svg_filename: &str, geometries_filename: &str) {
    let geometries = read_geometries(geometries_filename).expect("reading geometries JSON");

    let handle = Loader::new()
        .read_path(svg_filename)
        .expect("reading geometries SVG");
    let renderer = CairoRenderer::new(&handle);
    let dimensions = renderer.intrinsic_dimensions();
    let (svg_width, svg_height) = renderer
        .intrinsic_size_in_pixels()
        .expect("intrinsic size in pixels");

    assert!(matches!(dimensions.width.unit, LengthUnit::Px));
    assert!(matches!(dimensions.height.unit, LengthUnit::Px));
    assert_eq!(dimensions.width.length, svg_width);
    assert_eq!(dimensions.height.length, svg_height);

    for (id, expected) in geometries.0.iter() {
        let expected = Element::from_element_geometry(id, expected);

        let viewport = cairo::Rectangle::new(0.0, 0.0, svg_width, svg_height);

        let (ink_rect, logical_rect) = renderer
            .geometry_for_layer(Some(id), &viewport)
            .expect(&format!("getting geometry for {}", id));

        let computed = Element::from_rectangles(id, ink_rect, logical_rect);

        assert_eq!(expected, computed);
    }
}

#[test]
fn rect() {
    test(
        "tests/fixtures/primitive_geometries/rect.svg",
        "tests/fixtures/primitive_geometries/rect.svg.geom",
    );
}

#[test]
fn rect_stroke() {
    test(
        "tests/fixtures/primitive_geometries/rect_stroke.svg",
        "tests/fixtures/primitive_geometries/rect_stroke.svg.geom",
    );
}

#[test]
fn rect_stroke_unfilled() {
    test(
        "tests/fixtures/primitive_geometries/rect_stroke_unfilled.svg",
        "tests/fixtures/primitive_geometries/rect_stroke_unfilled.svg.geom",
    );
}

#[test]
fn rect_isolate() {
    test(
        "tests/fixtures/primitive_geometries/rect_isolate.svg",
        "tests/fixtures/primitive_geometries/rect_isolate.svg.geom",
    );
}
