// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

mod common;
use crate::common::*;

use serde_json::json;

use glean_core::metrics::*;
use glean_core::storage::StorageManager;
use glean_core::{test_get_num_recorded_errors, ErrorType};
use glean_core::{CommonMetricData, Lifetime};

#[test]
fn can_create_dual_labeled_counter_metric() {
    let (glean, _t) = new_glean(None);
    let dual_labeled_counter = DualLabeledCounterMetric::new(
        CommonMetricData {
            name: "dual_labeled_counter".into(),
            category: "telemetry".into(),
            send_in_pings: vec!["store1".into()],
            disabled: false,
            lifetime: Lifetime::Ping,
            ..Default::default()
        },
        Some(vec!["key1".into()]),
        Some(vec!["category1".into()]),
    );

    let metric = dual_labeled_counter.get("key1", "category1");
    metric.add_sync(&glean, 1);

    let snapshot = StorageManager
        .snapshot_as_json(glean.storage(), "store1", true)
        .unwrap();

    assert_eq!(
        json!({
            "dual_labeled_counter": {
                "telemetry.dual_labeled_counter": {
                    "key1": {
                        "category1": 1
                    }
                }
            }
        }),
        snapshot
    );
}

#[test]
fn can_use_multiple_labels() {
    let (glean, _t) = new_glean(None);
    let dual_labeled_counter = DualLabeledCounterMetric::new(
        CommonMetricData {
            name: "dual_labeled_counter".into(),
            category: "telemetry".into(),
            send_in_pings: vec!["store1".into()],
            disabled: false,
            lifetime: Lifetime::Ping,
            ..Default::default()
        },
        None,
        None,
    );

    let metric = dual_labeled_counter.get("key1", "category1");
    metric.add_sync(&glean, 1);

    let metric = dual_labeled_counter.get("key1", "category2");
    metric.add_sync(&glean, 2);

    let metric = dual_labeled_counter.get("key2", "category1");
    metric.add_sync(&glean, 3);

    let metric = dual_labeled_counter.get("key2", "category3");
    metric.add_sync(&glean, 4);

    let snapshot = StorageManager
        .snapshot_as_json(glean.storage(), "store1", true)
        .unwrap();

    assert_eq!(
        json!({
            "dual_labeled_counter": {
                "telemetry.dual_labeled_counter": {
                    "key1": {
                        "category1": 1,
                        "category2": 2
                    },
                    "key2": {
                        "category1": 3,
                        "category3": 4
                    }
                }
            }
        }),
        snapshot
    );
}

#[test]
fn can_record_error_for_submetric() {
    let (glean, _t) = new_glean(None);
    let dual_labeled_counter = DualLabeledCounterMetric::new(
        CommonMetricData {
            name: "dual_labeled_counter".into(),
            category: "telemetry".into(),
            send_in_pings: vec!["store1".into()],
            disabled: false,
            lifetime: Lifetime::Ping,
            ..Default::default()
        },
        Some(vec!["key1".into()]),
        Some(vec!["category1".into()]),
    );

    let metric = dual_labeled_counter.get("key1", "category1");
    metric.add_sync(&glean, -1);

    // Make sure that the errors have been recorded
    assert_eq!(
        Ok(1),
        test_get_num_recorded_errors(&glean, metric.meta(), ErrorType::InvalidValue)
    );
}

#[test]
fn labels_are_checked_against_static_list() {
    let (glean, _t) = new_glean(None);
    let dual_labeled_counter = DualLabeledCounterMetric::new(
        CommonMetricData {
            name: "dual_labeled_counter".into(),
            category: "telemetry".into(),
            send_in_pings: vec!["store1".into()],
            disabled: false,
            lifetime: Lifetime::Ping,
            ..Default::default()
        },
        Some(vec!["key1".into(), "key2".into()]),
        Some(vec!["category1".into(), "category2".into()]),
    );

    let metric = dual_labeled_counter.get("key1", "category1");
    metric.add_sync(&glean, 1);

    let metric = dual_labeled_counter.get("key1", "category2");
    metric.add_sync(&glean, 2);

    let metric = dual_labeled_counter.get("key2", "category1");
    metric.add_sync(&glean, 3);

    let metric = dual_labeled_counter.get("key2", "category2");
    metric.add_sync(&glean, 4);

    // All non-registed labels get mapped to the `other` label
    let metric = dual_labeled_counter.get("key3", "category1");
    metric.add_sync(&glean, 5);
    let metric = dual_labeled_counter.get("key1", "category3");
    metric.add_sync(&glean, 6);
    let metric = dual_labeled_counter.get("key3", "category3");
    metric.add_sync(&glean, 7);

    let snapshot = StorageManager
        .snapshot_as_json(glean.storage(), "store1", true)
        .unwrap();

    assert_eq!(
        json!({
            "dual_labeled_counter": {
                "telemetry.dual_labeled_counter": {
                    "key1": {
                        "category1": 1,
                        "category2": 2,
                        "__other__": 6
                    },
                    "key2": {
                        "category1": 3,
                        "category2": 4
                    },
                    "__other__": {
                        "category1": 5,
                        "__other__": 7
                    }
                }
            }
        }),
        snapshot
    );
}

#[test]
fn dynamic_labels_too_long() {
    let (glean, _t) = new_glean(None);
    let dual_labeled_counter = DualLabeledCounterMetric::new(
        CommonMetricData {
            name: "dual_labeled_counter".into(),
            category: "telemetry".into(),
            send_in_pings: vec!["store1".into()],
            disabled: false,
            lifetime: Lifetime::Ping,
            ..Default::default()
        },
        None,
        None,
    );

    let metric = dual_labeled_counter.get("1".repeat(112), "2".repeat(112));
    metric.add_sync(&glean, 1);

    let snapshot = StorageManager
        .snapshot_as_json(glean.storage(), "store1", true)
        .unwrap();

    // We end up with 2x InvalidLabel errors, one each for key and category which are both too long.
    assert_eq!(
        json!({
            "dual_labeled_counter": {
                "telemetry.dual_labeled_counter": {
                    "__other__": {
                        "__other__": 1
                    }
                }
            },
            "labeled_counter": {
                "glean.error.invalid_label": { "telemetry.dual_labeled_counter": 2 }
            }
        }),
        snapshot
    );
}

#[test]
fn dynamic_labels_regex_allowed() {
    let (glean, _t) = new_glean(None);
    let dual_labeled_counter = DualLabeledCounterMetric::new(
        CommonMetricData {
            name: "dual_labeled_counter".into(),
            category: "telemetry".into(),
            send_in_pings: vec!["store1".into()],
            disabled: false,
            lifetime: Lifetime::Ping,
            ..Default::default()
        },
        None,
        None,
    );

    let labels_validating = vec![
        ("key.is.fine", "cat.is.fine"),
        ("key_is_fine_too", "cat_is_fine_too"),
        ("key.is_still_fine", "cat.is_still_fine"),
        ("keyisfine", "catisfine"),
        ("_.key-is_fine", "_.cat-is_fine"),
        ("key.is-fine", "cat.is-fine"),
        ("key-is-fine", "cat-is-fine"),
    ];

    for (key, category) in &labels_validating {
        dual_labeled_counter.get(key, category).add_sync(&glean, 1);
    }

    let snapshot = StorageManager
        .snapshot_as_json(glean.storage(), "store1", true)
        .unwrap();

    assert_eq!(
        json!({
            "dual_labeled_counter": {
                "telemetry.dual_labeled_counter": {
                    "key.is.fine": {
                        "cat.is.fine": 1
                    },
                    "key_is_fine_too": {
                        "cat_is_fine_too": 1
                    },
                    "key.is_still_fine": {
                        "cat.is_still_fine": 1
                    },
                    "keyisfine": {
                        "catisfine": 1
                    },
                    "_.key-is_fine": {
                        "_.cat-is_fine": 1
                    },
                    "key.is-fine": {
                        "cat.is-fine": 1
                    },
                    "key-is-fine": {
                        "cat-is-fine": 1
                    }
                }
            }
        }),
        snapshot
    );
}

#[test]
fn seen_labels_get_reloaded_from_disk() {
    let (mut tempdir, _) = tempdir();

    let (glean, dir) = new_glean(Some(tempdir));
    tempdir = dir;

    let dual_labeled_counter = DualLabeledCounterMetric::new(
        CommonMetricData {
            name: "dual_labeled_counter".into(),
            category: "telemetry".into(),
            send_in_pings: vec!["store1".into()],
            disabled: false,
            lifetime: Lifetime::Ping,
            ..Default::default()
        },
        None,
        None,
    );

    // Store some data into labeled metrics
    {
        // Set the maximum number of labels
        for i in 1..=16 {
            let key = format!("key{i}");
            let category = format!("category{i}");
            dual_labeled_counter.get(key, category).add_sync(&glean, i);
        }

        let snapshot = StorageManager
            .snapshot_as_json(glean.storage(), "store1", false)
            .unwrap();

        // Check that the data is there
        for i in 1..=16 {
            let key = format!("key{i}");
            let category = format!("category{i}");
            assert_eq!(
                i,
                snapshot["dual_labeled_counter"]["telemetry.dual_labeled_counter"][&key][&category]
            );
        }

        drop(glean);
    }

    // Force a reload
    {
        let (glean, _t) = new_glean(Some(tempdir));

        // Try to store another label
        dual_labeled_counter
            .get("new_key", "new_category")
            .add_sync(&glean, 40);

        let snapshot = StorageManager
            .snapshot_as_json(glean.storage(), "store1", false)
            .unwrap();

        // Check that the old data is still there
        for i in 1..=16 {
            let key = format!("key{i}");
            let category = format!("category{i}");
            assert_eq!(
                i,
                snapshot["dual_labeled_counter"]["telemetry.dual_labeled_counter"][&key][&category]
            );
        }

        // The new label lands in the __other__ bucket, due to too many labels
        assert_eq!(
            40,
            snapshot["dual_labeled_counter"]["telemetry.dual_labeled_counter"]["__other__"]
                ["__other__"]
        );
    }
}

#[test]
fn caching_metrics_with_dynamic_labels() {
    let (glean, _t) = new_glean(None);
    let dual_labeled_counter = DualLabeledCounterMetric::new(
        CommonMetricData {
            name: "dual_labeled_counter".into(),
            category: "telemetry".into(),
            send_in_pings: vec!["store1".into()],
            disabled: false,
            lifetime: Lifetime::Ping,
            ..Default::default()
        },
        None,
        None,
    );

    // Create multiple metric instances and cache them for later use.
    let metrics = (1..=20)
        .map(|i| {
            let key = format!("key{i}");
            let category = format!("category{i}");
            dual_labeled_counter.get(key, category)
        })
        .collect::<Vec<_>>();

    // Only now use them.
    for metric in metrics {
        metric.add_sync(&glean, 1);
    }

    // The maximum number of labels we store is 16.
    // So we should have put 4 metrics in the __other__ bucket.
    let other = dual_labeled_counter.get("__other__", "__other__");
    assert_eq!(Some(4), other.get_value(&glean, Some("store1")));
}

#[test]
fn metrics_with_static_keys_and_dynamic_categories() {
    let (glean, _t) = new_glean(None);
    let dual_labeled_counter = DualLabeledCounterMetric::new(
        CommonMetricData {
            name: "dual_labeled_counter".into(),
            category: "telemetry".into(),
            send_in_pings: vec!["store1".into()],
            disabled: false,
            lifetime: Lifetime::Ping,
            ..Default::default()
        },
        Some(vec![
            std::borrow::Cow::Borrowed("key1"),
            std::borrow::Cow::Borrowed("key2"),
            std::borrow::Cow::Borrowed("key3"),
            std::borrow::Cow::Borrowed("key4"),
        ]),
        None,
    );

    // Create multiple metric instances and cache them for later use.
    // We also purposefully create a single key that should fail static
    // validation and be replaced with `__other__`
    let mut metrics: Vec<std::sync::Arc<CounterMetric>> = Vec::new();
    for i in 1..=5 {
        let key = format!("key{i}");
        for j in 1..=20 {
            let category = format!("category{j}");
            metrics.push(dual_labeled_counter.get(&key, &category));
        }
    }

    // Only now use them.
    for metric in metrics {
        metric.add_sync(&glean, 1);
    }

    // The maximum number of categories we store is 16.
    // So we should have put 4 metrics in the __other__ bucket for every key.
    for i in 1..=5 {
        let other = dual_labeled_counter.get(format!("key{i}"), "__other__".into());
        let recorded_value = other.get_value(&glean, Some("store1"));
        assert_eq!(
            Some(4),
            recorded_value,
            "Other value should be 4, found {recorded_value:?} for key{i}"
        );
    }

    // The above would have asked for "key5", but since that doesn't exist as a static key it should have fallen into
    // `__other__` as a key, the way `get()` works the above assert wouldn't fail but the actual key would have been
    // renamed. Let's check for that explicitly, just to verify.
    let other = dual_labeled_counter.get("__other__".to_string(), "__other__".to_string());
    let recorded_value = other.get_value(&glean, Some("store1"));
    assert_eq!(
        Some(4),
        recorded_value,
        "Other value should be 4, found {recorded_value:?}"
    );
}

#[test]
fn metrics_with_dynamic_keys_and_static_categories() {
    let (glean, _t) = new_glean(None);
    let dual_labeled_counter = DualLabeledCounterMetric::new(
        CommonMetricData {
            name: "dual_labeled_counter".into(),
            category: "telemetry".into(),
            send_in_pings: vec!["store1".into()],
            disabled: false,
            lifetime: Lifetime::Ping,
            ..Default::default()
        },
        None,
        Some(vec![
            std::borrow::Cow::Borrowed("category1"),
            std::borrow::Cow::Borrowed("category2"),
            std::borrow::Cow::Borrowed("category3"),
            std::borrow::Cow::Borrowed("category4"),
        ]),
    );

    // Create multiple metric instances and cache them for later use.
    // We also purposefully create a single key that should fail static
    // validation and be replaced with `__other__`
    let mut metrics: Vec<std::sync::Arc<CounterMetric>> = Vec::new();
    for i in 1..=20 {
        let key = format!("key{i}");
        for j in 1..=5 {
            let category = format!("category{j}");
            metrics.push(dual_labeled_counter.get(&key, &category));
        }
    }

    // Only now use them.
    for metric in metrics {
        metric.add_sync(&glean, 1);
    }

    for i in 1..=16 {
        for j in 1..=4 {
            let metric = dual_labeled_counter.get(format!("key{i}"), format!("category{j}"));
            let recorded_value = metric.get_value(&glean, Some("store1"));
            // The maximum number of keys we store is 16.
            // The first 16 keys are valid, but we also used an invalid static category ("category5"), so each of the
            // valid keys should have 1 count in the `_other_` category too, we will check that explicitly later in this test.
            assert_eq!(
                Some(1),
                recorded_value,
                "Value should be 1, found {recorded_value:?} for key{i}/category{j}"
            );
        }
        // Now we test for the invalid static category ("category5") that we used above went into the `__other__` category.
        let metric = dual_labeled_counter.get(format!("key{i}"), "__other__".to_string());
        let recorded_value = metric.get_value(&glean, Some("store1"));
        assert_eq!(
            Some(1),
            recorded_value,
            "Value should be 1, found {recorded_value:?} for key{i}/__other__"
        );
    }

    // Now lets check that key17 through key20 landed in the `__other__` key, and have counts in the proper categories. Each
    // category should have 4 counts since we had 4 keys that resulted in `__other__`
    for i in 1..=4 {
        let metric = dual_labeled_counter.get("__other__".into(), format!("category{i}"));
        let recorded_value = metric.get_value(&glean, Some("store1"));
        // The maximum number of keys we store is 16.
        // The first 16 keys are valid, but we also used an invalid static category ("category5"), so each of the
        // valid keys should have 1 count in the `_other_` category too, we will check that explicitly later in this test.
        assert_eq!(
            Some(4),
            recorded_value,
            "Value should be 4, found {recorded_value:?} for __other__/category{i}"
        );
    }

    // Finally, 4 keys each should have had one category that was invalid so we should have 4 counts in `__other__`/`__other__`
    let metric = dual_labeled_counter.get("__other__", "__other__");
    let recorded_value = metric.get_value(&glean, Some("store1"));
    assert_eq!(
        Some(4),
        recorded_value,
        "Value should be 4, found {recorded_value:?} for __other__/__other__"
    );
}

#[test]
fn caching_metrics_with_dynamic_labels_across_pings() {
    let (glean, _t) = new_glean(None);
    let dual_labeled_counter = DualLabeledCounterMetric::new(
        CommonMetricData {
            name: "dual_labeled_counter".into(),
            category: "telemetry".into(),
            send_in_pings: vec!["store1".into()],
            disabled: false,
            lifetime: Lifetime::Ping,
            ..Default::default()
        },
        None,
        None,
    );

    // Create multiple metric instances and cache them for later use.
    let metrics = (1..=20)
        .map(|i| {
            let key = format!("key{i}");
            let category = format!("category{i}");
            dual_labeled_counter.get(key, category)
        })
        .collect::<Vec<_>>();

    // Only now use them.
    for metric in &metrics {
        metric.add_sync(&glean, 1);
    }

    // The maximum number of keys/categories we store is 16.
    // So we should have put 4 metrics in the __other__ bucket.
    let other = dual_labeled_counter.get("__other__", "__other__");
    assert_eq!(Some(4), other.get_value(&glean, Some("store1")));

    // Snapshot (so we can inspect the JSON)
    // and clear out storage (the same way submitting a ping would)
    let snapshot = StorageManager
        .snapshot_as_json(glean.storage(), "store1", true)
        .unwrap();

    // We didn't send the 20th key/category
    assert_eq!(
        json!(null),
        snapshot["dual_labeled_counter"]["telemetry.dual_labeled_counter"]["key20"]["category20"]
    );

    // We now set the ones that ended up in `__other__` before.
    // Note: indexing is zero-based,
    // but we later check the names, so let's offset it by 1.
    metrics[16].add_sync(&glean, 17);
    metrics[17].add_sync(&glean, 18);
    metrics[18].add_sync(&glean, 19);
    metrics[19].add_sync(&glean, 20);

    assert_eq!(Some(17), metrics[16].get_value(&glean, Some("store1")));
    assert_eq!(Some(18), metrics[17].get_value(&glean, Some("store1")));
    assert_eq!(Some(19), metrics[18].get_value(&glean, Some("store1")));
    assert_eq!(Some(20), metrics[19].get_value(&glean, Some("store1")));
    assert_eq!(None, other.get_value(&glean, Some("store1")));

    let snapshot = StorageManager
        .snapshot_as_json(glean.storage(), "store1", true)
        .unwrap();

    let cached_labels = &snapshot["dual_labeled_counter"]["telemetry.dual_labeled_counter"];
    assert_eq!(json!(17), cached_labels["key17"]["category17"]);
    assert_eq!(json!(18), cached_labels["key18"]["category18"]);
    assert_eq!(json!(19), cached_labels["key19"]["category19"]);
    assert_eq!(json!(20), cached_labels["key20"]["category20"]);
    assert_eq!(json!(null), cached_labels["__other__"]["__other__"]);
}

#[test]
fn labels_containing_a_record_separator_record_an_error() {
    let (glean, _t) = new_glean(None);
    let dual_labeled_counter = DualLabeledCounterMetric::new(
        CommonMetricData {
            name: "dual_labeled_counter".into(),
            category: "telemetry".into(),
            send_in_pings: vec!["store1".into()],
            disabled: false,
            lifetime: Lifetime::Ping,
            ..Default::default()
        },
        None,
        None,
    );

    let metric = dual_labeled_counter.get("record\x1eseparator", "category1");
    metric.add_sync(&glean, 1);

    let snapshot = StorageManager
        .snapshot_as_json(glean.storage(), "store1", true)
        .unwrap();

    // We end up with 1x InvalidLabel error because the key contains an ASCII record separator, but both
    // labels end up as other because the use of the record separator prevents us from knowing which one
    // contained it (or both) at this point in exection.
    assert_eq!(
        json!({
            "dual_labeled_counter": {
                "telemetry.dual_labeled_counter": {
                    "__other__": {
                        "__other__": 1
                    }
                }
            },
            "labeled_counter": {
                "glean.error.invalid_label": { "telemetry.dual_labeled_counter": 1 }
            }
        }),
        snapshot
    );
}
