use std::collections::BTreeMap;

use crate::{cdr, json::FromJson as _, price, test, warning, Version};

/// 13 elements raise warnings in the [`CDR_WITH_WARNINGS`] below.
const ELEM_TOTAL: usize = 13;

/// Each element produces one warning.
const WARN_TOTAL: usize = 13;

/// The 13 warnings are the same two warning types repeated;
/// `excessive_precision` and `field_invalid_value(CONGESTION_TIME)`.
const WARN_TYPES_TOTAL: usize = 2;

/// This CDR has 13 warnings where the warnings fall into two types of warning;
/// `excessive_precision` and `field_invalid_value(CONGESTION_TIME)`.
const CDR_WITH_WARNINGS: &str = r#"{
    "id": "12345",
    "start_date_time": "2015-06-29T17:00:09Z",
    "stop_date_time": "2015-06-29T23:00:32Z",
    "currency": "EUR",
    "charging_periods": [
        {
            "start_date_time": "2015-06-29T17:00:09Z",
           	"dimensions": [{
                "type": "TIME",
                "volume": 0.50003
           	}]
        },
        {
            "start_date_time": "2015-06-29T17:30:09Z",
           	"dimensions": [{
                "type": "TIME",
                "volume": 0.50003
           	}]
        },
        {
            "start_date_time": "2015-06-29T18:00:09Z",
           	"dimensions": [{
                "type": "CONGESTION_TIME",
                "volume": 0.50003
           	}]
        },
        {
            "start_date_time": "2015-06-29T18:30:09Z",
           	"dimensions": [{
                "type": "CONGESTION_TIME",
                "volume": 0.50003
           	}]
        },
        {
            "start_date_time": "2015-06-29T19:00:09Z",
           	"dimensions": [{
                "type": "CONGESTION_TIME",
                "volume": 0.50003
           	}]
        },
        {
            "start_date_time": "2015-06-29T19:30:09Z",
           	"dimensions": [{
                "type": "CONGESTION_TIME",
                "volume": 0.50003
           	}]
        }
    ],
    "total_cost": 4.001234,
    "total_energy": 15.342021,
    "total_time": 6.03045
}"#;

#[test]
fn should_produce_expected_warnings() {
    let warnings = parse_cdr(CDR_WITH_WARNINGS);

    let warnings = warnings.path_id_map();
    let expect_warnings = BTreeMap::from([
        (
            "$.charging_periods[0].dimensions[0].volume",
            vec![id("excessive_precision")],
        ),
        (
            "$.charging_periods[1].dimensions[0].volume",
            vec![id("excessive_precision")],
        ),
        (
            "$.charging_periods[2].dimensions[0].type",
            vec![id("field_invalid_value(CONGESTION_TIME)")],
        ),
        (
            "$.charging_periods[2].dimensions[0].volume",
            vec![id("excessive_precision")],
        ),
        (
            "$.charging_periods[3].dimensions[0].type",
            vec![id("field_invalid_value(CONGESTION_TIME)")],
        ),
        (
            "$.charging_periods[3].dimensions[0].volume",
            vec![id("excessive_precision")],
        ),
        (
            "$.charging_periods[4].dimensions[0].type",
            vec![id("field_invalid_value(CONGESTION_TIME)")],
        ),
        (
            "$.charging_periods[4].dimensions[0].volume",
            vec![id("excessive_precision")],
        ),
        (
            "$.charging_periods[5].dimensions[0].type",
            vec![id("field_invalid_value(CONGESTION_TIME)")],
        ),
        (
            "$.charging_periods[5].dimensions[0].volume",
            vec![id("excessive_precision")],
        ),
        ("$.total_cost", vec![id("excessive_precision")]),
        ("$.total_energy", vec![id("excessive_precision")]),
        ("$.total_time", vec![id("excessive_precision")]),
    ]);
    assert_eq!(warnings, expect_warnings, "{warnings:#?}");
}

#[test]
fn should_allow_all() {
    let warnings = parse_cdr(CDR_WITH_WARNINGS);
    let outcome = warnings.id_path_map(warning::Limit::None);
    let warning::IdPathMap {
        warnings,
        total_warnings,
        total_elements,
        elements_filtered,
        warning_distinct_types_filtered,
    } = outcome;

    assert_eq!(total_warnings, WARN_TOTAL);
    assert_eq!(total_elements, ELEM_TOTAL);
    assert_eq!(elements_filtered, 0, "No elements are filtered");
    assert_eq!(
        warning_distinct_types_filtered, 0,
        "No warnings are filtered"
    );

    let expect_warnings = BTreeMap::from([
        (
            id("excessive_precision"),
            vec![
                "$.charging_periods[0].dimensions[0].volume",
                "$.charging_periods[1].dimensions[0].volume",
                "$.charging_periods[2].dimensions[0].volume",
                "$.charging_periods[3].dimensions[0].volume",
                "$.charging_periods[4].dimensions[0].volume",
                "$.charging_periods[5].dimensions[0].volume",
                "$.total_cost",
                "$.total_energy",
                "$.total_time",
            ],
        ),
        (
            id("field_invalid_value(CONGESTION_TIME)"),
            vec![
                "$.charging_periods[2].dimensions[0].type",
                "$.charging_periods[3].dimensions[0].type",
                "$.charging_periods[4].dimensions[0].type",
                "$.charging_periods[5].dimensions[0].type",
            ],
        ),
    ]);
    assert_eq!(warnings, expect_warnings, "{warnings:#?}");
}

#[test]
fn should_limit_warning_ids_only() {
    const WARN_LIMIT: usize = 1;

    let warnings = parse_cdr(CDR_WITH_WARNINGS);

    let outcome = warnings.id_path_map(warning::Limit::WarningTypes(WARN_LIMIT));
    let warning::IdPathMap {
        warnings,
        total_warnings,
        total_elements,
        elements_filtered,
        warning_distinct_types_filtered,
    } = outcome;

    assert_eq!(total_warnings, WARN_TOTAL);
    assert_eq!(total_elements, ELEM_TOTAL);
    assert_eq!(
        elements_filtered, 4,
        "The second warning has 4 element paths that are filtered"
    );
    assert_eq!(
        warning_distinct_types_filtered, 1,
        "There are two warning types raised; one is allowed"
    );

    let expect_warnings = BTreeMap::from([(
        id("excessive_precision"),
        vec![
            "$.charging_periods[0].dimensions[0].volume",
            "$.charging_periods[1].dimensions[0].volume",
            "$.charging_periods[2].dimensions[0].volume",
            "$.charging_periods[3].dimensions[0].volume",
            "$.charging_periods[4].dimensions[0].volume",
            "$.charging_periods[5].dimensions[0].volume",
            "$.total_cost",
            "$.total_energy",
            "$.total_time",
        ],
    )]);
    assert_eq!(warnings, expect_warnings, "{warnings:#?}");
}

#[test]
fn should_limit_zero_warning_ids_only() {
    const WARN_LIMIT: usize = 0;

    let warnings = parse_cdr(CDR_WITH_WARNINGS);

    let outcome = warnings.id_path_map(warning::Limit::WarningTypes(WARN_LIMIT));
    let warning::IdPathMap {
        warnings,
        total_warnings,
        total_elements,
        elements_filtered,
        warning_distinct_types_filtered,
    } = outcome;

    assert_eq!(total_warnings, WARN_TOTAL);
    assert_eq!(total_elements, ELEM_TOTAL);
    assert_eq!(
        warning_distinct_types_filtered, WARN_TYPES_TOTAL,
        "All warning types are filtered"
    );
    assert_eq!(
        elements_filtered, ELEM_TOTAL,
        "All warning types are filtered, therefore all elements are filtered"
    );

    let expect_warnings = BTreeMap::new();
    assert_eq!(warnings, expect_warnings, "{warnings:#?}");
}

#[test]
fn should_limit_element_paths() {
    let warnings = parse_cdr(CDR_WITH_WARNINGS);

    let outcome = warnings.id_path_map(warning::Limit::ElemPathsPerId(3));
    let warning::IdPathMap {
        warnings,
        total_warnings,
        total_elements,
        elements_filtered,
        warning_distinct_types_filtered,
    } = outcome;

    assert_eq!(total_warnings, WARN_TOTAL);
    assert_eq!(total_elements, ELEM_TOTAL);
    assert_eq!(elements_filtered, 7, "The first warning type has 6 elements filtered and the second warning type has 4 elements filtered.");
    assert_eq!(
        warning_distinct_types_filtered, 0,
        "Only element paths are filtered; all warning types pass-through"
    );

    let expect_warnings = BTreeMap::from([
        (
            id("excessive_precision"),
            vec![
                "$.charging_periods[0].dimensions[0].volume",
                "$.charging_periods[1].dimensions[0].volume",
                "$.charging_periods[2].dimensions[0].volume",
            ],
        ),
        (
            id("field_invalid_value(CONGESTION_TIME)"),
            vec![
                "$.charging_periods[2].dimensions[0].type",
                "$.charging_periods[3].dimensions[0].type",
                "$.charging_periods[4].dimensions[0].type",
            ],
        ),
    ]);
    assert_eq!(warnings, expect_warnings, "{warnings:#?}");
}

#[test]
fn should_limit_zero_element_paths() {
    let warnings = parse_cdr(CDR_WITH_WARNINGS);

    let outcome = warnings.id_path_map(warning::Limit::ElemPathsPerId(0));
    let warning::IdPathMap {
        warnings,
        total_warnings,
        total_elements,
        elements_filtered,
        warning_distinct_types_filtered,
    } = outcome;

    assert_eq!(total_warnings, WARN_TOTAL);
    assert_eq!(total_elements, ELEM_TOTAL);
    assert_eq!(
        elements_filtered, ELEM_TOTAL,
        "All element paths are filtered"
    );
    assert_eq!(
        warning_distinct_types_filtered, 0,
        "Only element paths are filtered"
    );

    let expect_warnings = BTreeMap::from([
        (id("excessive_precision"), vec![]),
        (id("field_invalid_value(CONGESTION_TIME)"), vec![]),
    ]);
    assert_eq!(warnings, expect_warnings, "{warnings:#?}");
}

#[test]
fn should_limit_ids_and_paths() {
    let warnings = parse_cdr(CDR_WITH_WARNINGS);

    let outcome = warnings.id_path_map(warning::Limit::All {
        max_warning_types: 1,
        max_elem_paths_per_warning_id: 3,
    });
    let warning::IdPathMap {
        warnings,
        total_warnings,
        total_elements,
        elements_filtered,
        warning_distinct_types_filtered,
    } = outcome;

    assert_eq!(total_warnings, WARN_TOTAL);
    assert_eq!(total_elements, ELEM_TOTAL);
    assert_eq!(
        elements_filtered, 10,
        "The first warning type has 6 elements filtered and the second warning type\
        is filtered due to only one type being allowed"
    );
    assert_eq!(
        warning_distinct_types_filtered, 1,
        "Only one warning type is allowed so the second is filtered"
    );

    let expect_warnings = BTreeMap::from([(
        id("excessive_precision"),
        vec![
            "$.charging_periods[0].dimensions[0].volume",
            "$.charging_periods[1].dimensions[0].volume",
            "$.charging_periods[2].dimensions[0].volume",
        ],
    )]);
    assert_eq!(warnings, expect_warnings, "{warnings:#?}");
}

#[test]
fn should_limit_zero_ids_and_paths() {
    let warnings = parse_cdr(CDR_WITH_WARNINGS);

    let outcome = warnings.id_path_map(warning::Limit::All {
        max_warning_types: 0,
        max_elem_paths_per_warning_id: 3,
    });
    let warning::IdPathMap {
        warnings,
        total_warnings,
        total_elements,
        elements_filtered,
        warning_distinct_types_filtered,
    } = outcome;

    assert_eq!(total_warnings, WARN_TOTAL);
    assert_eq!(total_elements, ELEM_TOTAL);
    assert_eq!(
        warning_distinct_types_filtered, WARN_TYPES_TOTAL,
        "All warning types should be filtered"
    );
    assert_eq!(
        elements_filtered, ELEM_TOTAL,
        "All elements are filtered as zero warning types are allowed"
    );

    let expect_warnings = BTreeMap::new();
    assert_eq!(warnings, expect_warnings, "{warnings:#?}");
}

#[test]
fn should_limit_ids_and_zero_paths() {
    let warnings = parse_cdr(CDR_WITH_WARNINGS);

    let outcome = warnings.id_path_map(warning::Limit::All {
        max_warning_types: 1,
        max_elem_paths_per_warning_id: 0,
    });
    let warning::IdPathMap {
        warnings,
        total_warnings,
        total_elements,
        elements_filtered,
        warning_distinct_types_filtered,
    } = outcome;

    assert_eq!(total_warnings, WARN_TOTAL);
    assert_eq!(total_elements, ELEM_TOTAL);
    assert_eq!(elements_filtered, ELEM_TOTAL);
    assert_eq!(warning_distinct_types_filtered, 1);

    let expect_warnings = BTreeMap::from([(id("excessive_precision"), vec![])]);
    assert_eq!(warnings, expect_warnings, "{warnings:#?}");
}

#[test]
fn should_limit_zero_ids_and_zero_paths() {
    let warnings = parse_cdr(CDR_WITH_WARNINGS);

    let outcome = warnings.id_path_map(warning::Limit::All {
        max_warning_types: 0,
        max_elem_paths_per_warning_id: 0,
    });
    let warning::IdPathMap {
        warnings,
        total_warnings,
        total_elements,
        elements_filtered,
        warning_distinct_types_filtered,
    } = outcome;

    assert_eq!(total_warnings, WARN_TOTAL);
    assert_eq!(total_elements, ELEM_TOTAL);
    assert_eq!(elements_filtered, ELEM_TOTAL);
    assert_eq!(
        warning_distinct_types_filtered, WARN_TYPES_TOTAL,
        "All warning types should be filtered"
    );

    let expect_warnings = BTreeMap::new();
    assert_eq!(warnings, expect_warnings, "{warnings:#?}");
}

#[test]
fn should_allow_all_msgs_path_maps() {
    let warnings = parse_cdr(CDR_WITH_WARNINGS);
    let outcome = warnings.msg_path_map(warning::Limit::None);
    let warning::MsgPathMap {
        warnings,
        total_warnings,
        total_elements,
        elements_filtered,
        warning_distinct_types_filtered,
    } = outcome;

    assert_eq!(total_warnings, WARN_TOTAL);
    assert_eq!(total_elements, ELEM_TOTAL);
    assert_eq!(elements_filtered, 0, "No elements are filtered");
    assert_eq!(
        warning_distinct_types_filtered, 0,
        "No warnings are filtered"
    );

    let expect_warnings = BTreeMap::from([
        (
            "excessive_precision".to_owned(),
            vec![
                "$.charging_periods[0].dimensions[0].volume",
                "$.charging_periods[1].dimensions[0].volume",
                "$.charging_periods[2].dimensions[0].volume",
                "$.charging_periods[3].dimensions[0].volume",
                "$.charging_periods[4].dimensions[0].volume",
                "$.charging_periods[5].dimensions[0].volume",
                "$.total_cost",
                "$.total_energy",
                "$.total_time",
            ],
        ),
        (
            "field_invalid_value(CONGESTION_TIME)".to_owned(),
            vec![
                "$.charging_periods[2].dimensions[0].type",
                "$.charging_periods[3].dimensions[0].type",
                "$.charging_periods[4].dimensions[0].type",
                "$.charging_periods[5].dimensions[0].type",
            ],
        ),
    ]);
    assert_eq!(warnings, expect_warnings, "{warnings:#?}");
}

/// Parse a v211 CDR and return the warnings raised.
#[track_caller]
fn parse_cdr(json: &str) -> warning::Set<price::Warning> {
    let cdr = cdr::build(crate::json::parse_object(json).unwrap(), Version::V211);
    test::assert_no_unexpected_fields(&cdr);

    let (_cdr, warnings) = price::v211::cdr::WithTariffs::from_json(cdr.as_element())
        .unwrap()
        .into_parts();

    warnings
}

/// Create a `warning::Id` from a `str`.
const fn id(s: &'static str) -> warning::Id {
    warning::Id::from_static(s)
}
