#![cfg(feature = "lasso")]

use lasso::{Rodeo, RodeoReader};
use measured::{CounterVec, FixedCardinalityLabel, LabelGroup, MetricGroup, label::StaticLabelSet};
use prometheus_client::encoding::{EncodeLabelSet, EncodeLabelValue};

const LOOPS: usize = 2000;

#[test]
fn measured() {
    #[derive(MetricGroup)]
    #[metric(new(route: lasso::RodeoReader))]
    struct Metrics {
        /// help text
        #[metric(label_set = ErrorsSet{ kind: StaticLabelSet::new(), route })]
        http_request_errors_total: CounterVec<ErrorsSet>,
    }

    let metrics = Metrics::new(Rodeo::from_iter(routes()).into_reader());

    let mut encoder = measured::text::BufferedTextEncoder::new();

    for _ in 0..LOOPS {
        for &kind in errors() {
            for route in routes() {
                metrics.http_request_errors_total.inc(Error { kind, route });
            }
        }
    }

    metrics.collect_group_into(&mut encoder).unwrap();
    assert_eq!(
        encoder.finish(),
        r#"# HELP http_request_errors_total help text
# TYPE http_request_errors_total counter
http_request_errors_total{kind="user",route="/api/v1/users"} 2000
http_request_errors_total{kind="user",route="/api/v1/users/:id"} 2000
http_request_errors_total{kind="user",route="/api/v1/products"} 2000
http_request_errors_total{kind="user",route="/api/v1/products/:id"} 2000
http_request_errors_total{kind="user",route="/api/v1/products/:id/owner"} 2000
http_request_errors_total{kind="user",route="/api/v1/products/:id/purchase"} 2000
http_request_errors_total{kind="internal",route="/api/v1/users"} 2000
http_request_errors_total{kind="internal",route="/api/v1/users/:id"} 2000
http_request_errors_total{kind="internal",route="/api/v1/products"} 2000
http_request_errors_total{kind="internal",route="/api/v1/products/:id"} 2000
http_request_errors_total{kind="internal",route="/api/v1/products/:id/owner"} 2000
http_request_errors_total{kind="internal",route="/api/v1/products/:id/purchase"} 2000
http_request_errors_total{kind="network",route="/api/v1/users"} 2000
http_request_errors_total{kind="network",route="/api/v1/users/:id"} 2000
http_request_errors_total{kind="network",route="/api/v1/products"} 2000
http_request_errors_total{kind="network",route="/api/v1/products/:id"} 2000
http_request_errors_total{kind="network",route="/api/v1/products/:id/owner"} 2000
http_request_errors_total{kind="network",route="/api/v1/products/:id/purchase"} 2000
"#
    );
}

#[test]
fn prometheus() {
    let registry = prometheus::Registry::new();
    let counter_vec = prometheus::register_int_counter_vec_with_registry!(
        "http_request_errors_total",
        "help text",
        &["kind", "route"],
        registry
    )
    .unwrap();

    for _ in 0..LOOPS {
        for &kind in errors() {
            for route in routes() {
                counter_vec.with_label_values(&[kind.to_str(), route]).inc();
            }
        }
    }

    let s = prometheus::TextEncoder
        .encode_to_string(&registry.gather())
        .unwrap();

    assert_eq!(
        s,
        r#"# HELP http_request_errors_total help text
# TYPE http_request_errors_total counter
http_request_errors_total{kind="internal",route="/api/v1/products"} 2000
http_request_errors_total{kind="internal",route="/api/v1/products/:id"} 2000
http_request_errors_total{kind="internal",route="/api/v1/products/:id/owner"} 2000
http_request_errors_total{kind="internal",route="/api/v1/products/:id/purchase"} 2000
http_request_errors_total{kind="internal",route="/api/v1/users"} 2000
http_request_errors_total{kind="internal",route="/api/v1/users/:id"} 2000
http_request_errors_total{kind="network",route="/api/v1/products"} 2000
http_request_errors_total{kind="network",route="/api/v1/products/:id"} 2000
http_request_errors_total{kind="network",route="/api/v1/products/:id/owner"} 2000
http_request_errors_total{kind="network",route="/api/v1/products/:id/purchase"} 2000
http_request_errors_total{kind="network",route="/api/v1/users"} 2000
http_request_errors_total{kind="network",route="/api/v1/users/:id"} 2000
http_request_errors_total{kind="user",route="/api/v1/products"} 2000
http_request_errors_total{kind="user",route="/api/v1/products/:id"} 2000
http_request_errors_total{kind="user",route="/api/v1/products/:id/owner"} 2000
http_request_errors_total{kind="user",route="/api/v1/products/:id/purchase"} 2000
http_request_errors_total{kind="user",route="/api/v1/users"} 2000
http_request_errors_total{kind="user",route="/api/v1/users/:id"} 2000
"#
    );
}

#[test]
fn metrics() {
    let recorder = metrics_exporter_prometheus::PrometheusBuilder::new().build_recorder();

    metrics::with_local_recorder(&recorder, || {
        metrics::describe_counter!("http_request_errors_total", "help text");
    });

    metrics::with_local_recorder(&recorder, || {
        for _ in 0..LOOPS {
            for &kind in errors() {
                for route in routes() {
                    let labels = [("kind", kind.to_str()), ("route", route)];
                    metrics::counter!("http_request_errors_total", &labels).increment(1);
                }
            }
        }
    });

    // output is unstable
    let output = recorder.handle().render();
    let mut lines: Vec<&str> = output.lines().collect();
    lines.sort_unstable();
    let output = lines.join("\n");

    assert_eq!(
        output,
        r#"
# HELP http_request_errors_total help text
# TYPE http_request_errors_total counter
http_request_errors_total{kind="internal",route="/api/v1/products"} 2000
http_request_errors_total{kind="internal",route="/api/v1/products/:id"} 2000
http_request_errors_total{kind="internal",route="/api/v1/products/:id/owner"} 2000
http_request_errors_total{kind="internal",route="/api/v1/products/:id/purchase"} 2000
http_request_errors_total{kind="internal",route="/api/v1/users"} 2000
http_request_errors_total{kind="internal",route="/api/v1/users/:id"} 2000
http_request_errors_total{kind="network",route="/api/v1/products"} 2000
http_request_errors_total{kind="network",route="/api/v1/products/:id"} 2000
http_request_errors_total{kind="network",route="/api/v1/products/:id/owner"} 2000
http_request_errors_total{kind="network",route="/api/v1/products/:id/purchase"} 2000
http_request_errors_total{kind="network",route="/api/v1/users"} 2000
http_request_errors_total{kind="network",route="/api/v1/users/:id"} 2000
http_request_errors_total{kind="user",route="/api/v1/products"} 2000
http_request_errors_total{kind="user",route="/api/v1/products/:id"} 2000
http_request_errors_total{kind="user",route="/api/v1/products/:id/owner"} 2000
http_request_errors_total{kind="user",route="/api/v1/products/:id/purchase"} 2000
http_request_errors_total{kind="user",route="/api/v1/users"} 2000
http_request_errors_total{kind="user",route="/api/v1/users/:id"} 2000"#
    );
}

#[test]
fn prometheus_client() {
    use prometheus_client::encoding::text::encode;
    use prometheus_client::metrics::counter::Counter;
    use prometheus_client::metrics::family::Family;
    use prometheus_client::registry::Registry;

    let mut registry = Registry::default();

    let counter_vec = Family::<ErrorStatic, Counter>::default();

    // Register the metric family with the registry.
    registry.register(
        // With the metric name.
        "http_request_errors",
        // And the metric help text.
        "help text",
        counter_vec.clone(),
    );

    for _ in 0..LOOPS {
        for &kind in errors() {
            for route in routes() {
                counter_vec
                    .get_or_create(&ErrorStatic { kind, route })
                    .inc();
            }
        }
    }

    let mut output = String::new();
    encode(&mut output, &registry).unwrap();

    // output is unstable
    let mut lines: Vec<&str> = output.lines().collect();
    lines.sort_unstable();
    let output = lines.join("\n");

    assert_eq!(
        output,
        r#"# EOF
# HELP http_request_errors help text.
# TYPE http_request_errors counter
http_request_errors_total{kind="Internal",route="/api/v1/products"} 2000
http_request_errors_total{kind="Internal",route="/api/v1/products/:id"} 2000
http_request_errors_total{kind="Internal",route="/api/v1/products/:id/owner"} 2000
http_request_errors_total{kind="Internal",route="/api/v1/products/:id/purchase"} 2000
http_request_errors_total{kind="Internal",route="/api/v1/users"} 2000
http_request_errors_total{kind="Internal",route="/api/v1/users/:id"} 2000
http_request_errors_total{kind="Network",route="/api/v1/products"} 2000
http_request_errors_total{kind="Network",route="/api/v1/products/:id"} 2000
http_request_errors_total{kind="Network",route="/api/v1/products/:id/owner"} 2000
http_request_errors_total{kind="Network",route="/api/v1/products/:id/purchase"} 2000
http_request_errors_total{kind="Network",route="/api/v1/users"} 2000
http_request_errors_total{kind="Network",route="/api/v1/users/:id"} 2000
http_request_errors_total{kind="User",route="/api/v1/products"} 2000
http_request_errors_total{kind="User",route="/api/v1/products/:id"} 2000
http_request_errors_total{kind="User",route="/api/v1/products/:id/owner"} 2000
http_request_errors_total{kind="User",route="/api/v1/products/:id/purchase"} 2000
http_request_errors_total{kind="User",route="/api/v1/users"} 2000
http_request_errors_total{kind="User",route="/api/v1/users/:id"} 2000"#
    );
}

fn routes() -> &'static [&'static str] {
    &[
        "/api/v1/users",
        "/api/v1/users/:id",
        "/api/v1/products",
        "/api/v1/products/:id",
        "/api/v1/products/:id/owner",
        "/api/v1/products/:id/purchase",
    ]
}

fn errors() -> &'static [ErrorKind] {
    &[ErrorKind::User, ErrorKind::Internal, ErrorKind::Network]
}

#[derive(Clone, Copy, PartialEq, Debug, LabelGroup)]
#[label(set = ErrorsSet)]
struct Error<'a> {
    kind: ErrorKind,
    #[label(fixed_with = RodeoReader)]
    route: &'a str,
}

#[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelSet)]
struct ErrorStatic {
    kind: ErrorKind,
    route: &'static str,
}

#[derive(Clone, Copy, PartialEq, Debug, Hash, Eq, FixedCardinalityLabel, EncodeLabelValue)]
#[label(rename_all = "kebab-case")]
enum ErrorKind {
    User,
    Internal,
    Network,
}

impl ErrorKind {
    fn to_str(self) -> &'static str {
        match self {
            ErrorKind::User => "user",
            ErrorKind::Internal => "internal",
            ErrorKind::Network => "network",
        }
    }
}
