use base64::Engine;
use base64::engine::general_purpose::STANDARD_NO_PAD;

use rpki::data_model::resources::as_resources::Asn;
use rpki::data_model::resources::ip_resources::{IPAddress, IPAddressPrefix};
use rpki::rtr::payload::{Aspa, Payload, RouteOrigin, RouterKey, Ski};
use rpki::slurm::file::{SlurmFile, SlurmVersion};

fn sample_spki() -> Vec<u8> {
    vec![
        0x30, 0x13, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01,
        0x05, 0x00, 0x03, 0x02, 0x00, 0x00,
    ]
}

fn sample_ski() -> [u8; 20] {
    [
        0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee,
        0xff, 0x10, 0x20, 0x30, 0x40,
    ]
}

fn sample_ski_b64() -> String {
    STANDARD_NO_PAD.encode(sample_ski())
}

fn log_slurm_input(name: &str, json: &str) {
    println!("[{name}] SLURM input:\n{json}");
}

fn log_slurm_ok(name: &str, slurm: &SlurmFile) {
    println!(
        "[{name}] parsed ok: version={}, prefix_filters={}, bgpsec_filters={}, aspa_filters={}, prefix_assertions={}, bgpsec_assertions={}, aspa_assertions={}",
        slurm.version().as_u32(),
        slurm.validation_output_filters().prefix_filters.len(),
        slurm.validation_output_filters().bgpsec_filters.len(),
        slurm.validation_output_filters().aspa_filters.len(),
        slurm.locally_added_assertions().prefix_assertions.len(),
        slurm.locally_added_assertions().bgpsec_assertions.len(),
        slurm.locally_added_assertions().aspa_assertions.len(),
    );
}

fn log_slurm_err(name: &str, err: &impl std::fmt::Display) {
    println!("[{name}] rejected: {err}");
}

fn log_payload_result(name: &str, payloads: &[Payload]) {
    println!("[{name}] payload result count={}", payloads.len());
    for payload in payloads {
        println!("[{name}] payload: {:?}", payload);
    }
}

fn assert_invalid_slurm(json: &str, expected: &str) {
    log_slurm_input("invalid_slurm", json);
    let err = SlurmFile::from_slice(json.as_bytes()).unwrap_err();
    log_slurm_err("invalid_slurm", &err);
    let err_text = err.to_string();
    assert!(
        err_text.contains(expected),
        "expected error containing '{expected}', got '{err_text}'"
    );
}

#[test]
// Parses a baseline RFC 8416 v1 SLURM file with prefix and BGPsec entries.
fn parses_rfc8416_v1_slurm() {
    let ski_b64 = sample_ski_b64();
    let router_public_key = STANDARD_NO_PAD.encode(sample_spki());
    let json = format!(
        r#"{{
            "slurmVersion": 1,
            "validationOutputFilters": {{
                "prefixFilters": [
                    {{ "prefix": "192.0.2.0/24", "asn": 64496, "comment": "drop roa" }}
                ],
                "bgpsecFilters": [
                    {{ "asn": 64497, "SKI": "{ski_b64}" }}
                ]
            }},
            "locallyAddedAssertions": {{
                "prefixAssertions": [
                    {{ "prefix": "198.51.100.0/24", "asn": 64500, "maxPrefixLength": 24 }}
                ],
                "bgpsecAssertions": [
                    {{ "asn": 64501, "SKI": "{ski_b64}", "routerPublicKey": "{router_public_key}" }}
                ]
            }}
        }}"#
    );

    log_slurm_input("parses_rfc8416_v1_slurm", &json);
    let slurm = SlurmFile::from_slice(json.as_bytes()).unwrap();
    log_slurm_ok("parses_rfc8416_v1_slurm", &slurm);

    assert_eq!(slurm.version(), SlurmVersion::V1);
    assert_eq!(slurm.validation_output_filters().prefix_filters.len(), 1);
    assert_eq!(slurm.validation_output_filters().bgpsec_filters.len(), 1);
    assert!(slurm.validation_output_filters().aspa_filters.is_empty());
    assert_eq!(slurm.locally_added_assertions().prefix_assertions.len(), 1);
    assert_eq!(slurm.locally_added_assertions().bgpsec_assertions.len(), 1);
    assert!(slurm.locally_added_assertions().aspa_assertions.is_empty());
}

#[test]
// Parses a v2 SLURM file carrying the ASPA extension members from the draft.
fn parses_v2_slurm_with_aspa_extensions() {
    let json = r#"{
        "slurmVersion": 2,
        "validationOutputFilters": {
            "prefixFilters": [],
            "bgpsecFilters": [],
            "aspaFilters": [
                { "customerAsn": 64496 }
            ]
        },
        "locallyAddedAssertions": {
            "prefixAssertions": [],
            "bgpsecAssertions": [],
            "aspaAssertions": [
                { "customerAsn": 64510, "providerAsns": [64511, 64512] }
            ]
        }
    }"#;

    log_slurm_input("parses_v2_slurm_with_aspa_extensions", json);
    let slurm = SlurmFile::from_slice(json.as_bytes()).unwrap();
    log_slurm_ok("parses_v2_slurm_with_aspa_extensions", &slurm);

    assert_eq!(slurm.version(), SlurmVersion::V2);
    assert_eq!(slurm.validation_output_filters().aspa_filters.len(), 1);
    assert_eq!(slurm.locally_added_assertions().aspa_assertions.len(), 1);
}

#[test]
// Rejects ASPA members in a v1 file because they are not part of RFC 8416 v1.
fn rejects_v1_file_with_aspa_members() {
    let json = r#"{
        "slurmVersion": 1,
        "validationOutputFilters": {
            "prefixFilters": [],
            "bgpsecFilters": [],
            "aspaFilters": []
        },
        "locallyAddedAssertions": {
            "prefixAssertions": [],
            "bgpsecAssertions": []
        }
    }"#;

    log_slurm_input("rejects_v1_file_with_aspa_members", json);
    let err = SlurmFile::from_slice(json.as_bytes()).unwrap_err();
    log_slurm_err("rejects_v1_file_with_aspa_members", &err);
    assert!(err.to_string().contains("unknown field"));
}

#[test]
// Rejects malformed v1 top-level objects and nested containers that violate RFC 8416 member rules.
fn rejects_invalid_v1_file_structure() {
    let cases = [
        (
            r#"{
                "validationOutputFilters": {
                    "prefixFilters": [],
                    "bgpsecFilters": []
                },
                "locallyAddedAssertions": {
                    "prefixAssertions": [],
                    "bgpsecAssertions": []
                }
            }"#,
            "missing field `slurmVersion`",
        ),
        (
            r#"{
                "slurmVersion": 3,
                "validationOutputFilters": {
                    "prefixFilters": [],
                    "bgpsecFilters": []
                },
                "locallyAddedAssertions": {
                    "prefixAssertions": [],
                    "bgpsecAssertions": []
                }
            }"#,
            "unsupported slurmVersion 3",
        ),
        (
            r#"{
                "slurmVersion": 1,
                "locallyAddedAssertions": {
                    "prefixAssertions": [],
                    "bgpsecAssertions": []
                }
            }"#,
            "missing field `validationOutputFilters`",
        ),
        (
            r#"{
                "slurmVersion": 1,
                "validationOutputFilters": {
                    "prefixFilters": [],
                    "bgpsecFilters": []
                }
            }"#,
            "missing field `locallyAddedAssertions`",
        ),
        (
            r#"{
                "slurmVersion": 1,
                "validationOutputFilters": {
                    "prefixFilters": [],
                    "bgpsecFilters": []
                },
                "locallyAddedAssertions": {
                    "prefixAssertions": [],
                    "bgpsecAssertions": []
                },
                "extra": true
            }"#,
            "unknown field `extra`",
        ),
        (
            r#"{
                "slurmVersion": 1,
                "validationOutputFilters": {
                    "bgpsecFilters": []
                },
                "locallyAddedAssertions": {
                    "prefixAssertions": [],
                    "bgpsecAssertions": []
                }
            }"#,
            "missing field `prefixFilters`",
        ),
        (
            r#"{
                "slurmVersion": 1,
                "validationOutputFilters": {
                    "prefixFilters": []
                },
                "locallyAddedAssertions": {
                    "prefixAssertions": [],
                    "bgpsecAssertions": []
                }
            }"#,
            "missing field `bgpsecFilters`",
        ),
        (
            r#"{
                "slurmVersion": 1,
                "validationOutputFilters": {
                    "prefixFilters": [],
                    "bgpsecFilters": [],
                    "aspaFilters": []
                },
                "locallyAddedAssertions": {
                    "prefixAssertions": [],
                    "bgpsecAssertions": []
                }
            }"#,
            "unknown field `aspaFilters`",
        ),
        (
            r#"{
                "slurmVersion": 1,
                "validationOutputFilters": {
                    "prefixFilters": [],
                    "bgpsecFilters": []
                },
                "locallyAddedAssertions": {
                    "bgpsecAssertions": []
                }
            }"#,
            "missing field `prefixAssertions`",
        ),
        (
            r#"{
                "slurmVersion": 1,
                "validationOutputFilters": {
                    "prefixFilters": [],
                    "bgpsecFilters": []
                },
                "locallyAddedAssertions": {
                    "prefixAssertions": []
                }
            }"#,
            "missing field `bgpsecAssertions`",
        ),
        (
            r#"{
                "slurmVersion": 1,
                "validationOutputFilters": {
                    "prefixFilters": [],
                    "bgpsecFilters": []
                },
                "locallyAddedAssertions": {
                    "prefixAssertions": [],
                    "bgpsecAssertions": [],
                    "aspaAssertions": []
                }
            }"#,
            "unknown field `aspaAssertions`",
        ),
    ];

    for (json, expected) in cases {
        assert_invalid_slurm(json, expected);
    }
}

#[test]
// Rejects malformed v1 member objects that contain unknown fields or omit mandatory members.
fn rejects_invalid_v1_member_structure() {
    let ski_b64 = sample_ski_b64();
    let router_public_key = STANDARD_NO_PAD.encode(sample_spki());

    let cases = vec![
        (
            r#"{
                "slurmVersion": 1,
                "validationOutputFilters": {
                    "prefixFilters": [
                        { "prefix": "192.0.2.0/24", "unexpected": true }
                    ],
                    "bgpsecFilters": []
                },
                "locallyAddedAssertions": {
                    "prefixAssertions": [],
                    "bgpsecAssertions": []
                }
            }"#
            .to_string(),
            "unknown field `unexpected`",
        ),
        (
            format!(
                r#"{{
                    "slurmVersion": 1,
                    "validationOutputFilters": {{
                        "prefixFilters": [],
                        "bgpsecFilters": [
                            {{ "asn": 64496, "SKI": "{ski_b64}", "unexpected": true }}
                        ]
                    }},
                    "locallyAddedAssertions": {{
                        "prefixAssertions": [],
                        "bgpsecAssertions": []
                    }}
                }}"#
            ),
            "unknown field `unexpected`",
        ),
        (
            r#"{
                "slurmVersion": 1,
                "validationOutputFilters": {
                    "prefixFilters": [],
                    "bgpsecFilters": []
                },
                "locallyAddedAssertions": {
                    "prefixAssertions": [
                        { "prefix": "198.51.100.0/24" }
                    ],
                    "bgpsecAssertions": []
                }
            }"#
            .to_string(),
            "missing field `asn`",
        ),
        (
            format!(
                r#"{{
                    "slurmVersion": 1,
                    "validationOutputFilters": {{
                        "prefixFilters": [],
                        "bgpsecFilters": []
                    }},
                    "locallyAddedAssertions": {{
                        "prefixAssertions": [],
                        "bgpsecAssertions": [
                            {{ "asn": 64501, "SKI": "{ski_b64}", "routerPublicKey": "{router_public_key}", "unexpected": true }}
                        ]
                    }}
                }}"#
            ),
            "unknown field `unexpected`",
        ),
    ];

    for (json, expected) in cases {
        assert_invalid_slurm(&json, expected);
    }
}

#[test]
// Rejects non-canonical prefixes and unsorted ASPA provider lists during validation.
fn rejects_non_canonical_prefixes_and_unsorted_aspa_providers() {
    let non_canonical = r#"{
        "slurmVersion": 1,
        "validationOutputFilters": {
            "prefixFilters": [
                { "prefix": "192.0.2.1/24" }
            ],
            "bgpsecFilters": []
        },
        "locallyAddedAssertions": {
            "prefixAssertions": [],
            "bgpsecAssertions": []
        }
    }"#;
    log_slurm_input(
        "rejects_non_canonical_prefixes_and_unsorted_aspa_providers.non_canonical",
        non_canonical,
    );
    let non_canonical_err = SlurmFile::from_slice(non_canonical.as_bytes()).unwrap_err();
    log_slurm_err(
        "rejects_non_canonical_prefixes_and_unsorted_aspa_providers.non_canonical",
        &non_canonical_err,
    );
    assert!(non_canonical_err.to_string().contains("not canonical"));

    let unsorted_aspa = r#"{
        "slurmVersion": 2,
        "validationOutputFilters": {
            "prefixFilters": [],
            "bgpsecFilters": [],
            "aspaFilters": []
        },
        "locallyAddedAssertions": {
            "prefixAssertions": [],
            "bgpsecAssertions": [],
            "aspaAssertions": [
                { "customerAsn": 64500, "providerAsns": [64502, 64501] }
            ]
        }
    }"#;
    log_slurm_input(
        "rejects_non_canonical_prefixes_and_unsorted_aspa_providers.unsorted_aspa",
        unsorted_aspa,
    );
    let aspa_err = SlurmFile::from_slice(unsorted_aspa.as_bytes()).unwrap_err();
    log_slurm_err(
        "rejects_non_canonical_prefixes_and_unsorted_aspa_providers.unsorted_aspa",
        &aspa_err,
    );
    assert!(aspa_err.to_string().contains("strictly increasing"));
}

#[test]
// Rejects malformed v2 top-level objects and nested containers that violate the ASPA SLURM draft.
fn rejects_invalid_v2_file_structure() {
    let cases = [
        (
            r#"{
                "slurmVersion": 2,
                "validationOutputFilters": {
                    "prefixFilters": [],
                    "bgpsecFilters": []
                },
                "locallyAddedAssertions": {
                    "prefixAssertions": [],
                    "bgpsecAssertions": [],
                    "aspaAssertions": []
                }
            }"#,
            "missing field `aspaFilters`",
        ),
        (
            r#"{
                "slurmVersion": 2,
                "validationOutputFilters": {
                    "prefixFilters": [],
                    "bgpsecFilters": [],
                    "aspaFilters": []
                },
                "locallyAddedAssertions": {
                    "prefixAssertions": [],
                    "bgpsecAssertions": []
                }
            }"#,
            "missing field `aspaAssertions`",
        ),
        (
            r#"{
                "slurmVersion": 2,
                "validationOutputFilters": {
                    "prefixFilters": [],
                    "bgpsecFilters": [],
                    "aspaFilters": [],
                    "extra": true
                },
                "locallyAddedAssertions": {
                    "prefixAssertions": [],
                    "bgpsecAssertions": [],
                    "aspaAssertions": []
                }
            }"#,
            "unknown field `extra`",
        ),
        (
            r#"{
                "slurmVersion": 2,
                "validationOutputFilters": {
                    "prefixFilters": [],
                    "bgpsecFilters": [],
                    "aspaFilters": []
                },
                "locallyAddedAssertions": {
                    "prefixAssertions": [],
                    "bgpsecAssertions": [],
                    "aspaAssertions": [],
                    "extra": true
                }
            }"#,
            "unknown field `extra`",
        ),
        (
            r#"{
                "slurmVersion": 2,
                "validationOutputFilters": {
                    "prefixFilters": [],
                    "bgpsecFilters": [],
                    "aspaFilters": []
                },
                "locallyAddedAssertions": {
                    "prefixAssertions": [],
                    "bgpsecAssertions": [],
                    "aspaAssertions": []
                },
                "extra": true
            }"#,
            "unknown field `extra`",
        ),
    ];

    for (json, expected) in cases {
        assert_invalid_slurm(json, expected);
    }
}

#[test]
// Rejects malformed v2 ASPA member objects that omit required fields or contain unknown fields.
fn rejects_invalid_v2_member_structure() {
    let cases = [
        (
            r#"{
                "slurmVersion": 2,
                "validationOutputFilters": {
                    "prefixFilters": [],
                    "bgpsecFilters": [],
                    "aspaFilters": [
                        { "comment": "missing customer" }
                    ]
                },
                "locallyAddedAssertions": {
                    "prefixAssertions": [],
                    "bgpsecAssertions": [],
                    "aspaAssertions": []
                }
            }"#,
            "missing field `customerAsn`",
        ),
        (
            r#"{
                "slurmVersion": 2,
                "validationOutputFilters": {
                    "prefixFilters": [],
                    "bgpsecFilters": [],
                    "aspaFilters": [
                        { "customerAsn": 64496, "unexpected": true }
                    ]
                },
                "locallyAddedAssertions": {
                    "prefixAssertions": [],
                    "bgpsecAssertions": [],
                    "aspaAssertions": []
                }
            }"#,
            "unknown field `unexpected`",
        ),
        (
            r#"{
                "slurmVersion": 2,
                "validationOutputFilters": {
                    "prefixFilters": [],
                    "bgpsecFilters": [],
                    "aspaFilters": []
                },
                "locallyAddedAssertions": {
                    "prefixAssertions": [],
                    "bgpsecAssertions": [],
                    "aspaAssertions": [
                        { "customerAsn": 64500 }
                    ]
                }
            }"#,
            "missing field `providerAsns`",
        ),
        (
            r#"{
                "slurmVersion": 2,
                "validationOutputFilters": {
                    "prefixFilters": [],
                    "bgpsecFilters": [],
                    "aspaFilters": []
                },
                "locallyAddedAssertions": {
                    "prefixAssertions": [],
                    "bgpsecAssertions": [],
                    "aspaAssertions": [
                        { "providerAsns": [64501] }
                    ]
                }
            }"#,
            "missing field `customerAsn`",
        ),
        (
            r#"{
                "slurmVersion": 2,
                "validationOutputFilters": {
                    "prefixFilters": [],
                    "bgpsecFilters": [],
                    "aspaFilters": []
                },
                "locallyAddedAssertions": {
                    "prefixAssertions": [],
                    "bgpsecAssertions": [],
                    "aspaAssertions": [
                        { "customerAsn": 64500, "providerAsns": [64501], "unexpected": true }
                    ]
                }
            }"#,
            "unknown field `unexpected`",
        ),
    ];

    for (json, expected) in cases {
        assert_invalid_slurm(json, expected);
    }
}

#[test]
// Applies filters first, then adds assertions, while removing duplicate payloads.
fn applies_filters_before_assertions_and_excludes_duplicates() {
    let ski = Ski::from_bytes(sample_ski());
    let spki = sample_spki();
    let spki_b64 = STANDARD_NO_PAD.encode(&spki);
    let ski_b64 = sample_ski_b64();
    let json = format!(
        r#"{{
            "slurmVersion": 2,
            "validationOutputFilters": {{
                "prefixFilters": [
                    {{ "prefix": "192.0.2.0/24", "asn": 64496 }}
                ],
                "bgpsecFilters": [
                    {{ "SKI": "{ski_b64}" }}
                ],
                "aspaFilters": [
                    {{ "customerAsn": 64496 }}
                ]
            }},
            "locallyAddedAssertions": {{
                "prefixAssertions": [
                    {{ "prefix": "198.51.100.0/24", "asn": 64500, "maxPrefixLength": 24 }},
                    {{ "prefix": "198.51.100.0/24", "asn": 64500, "maxPrefixLength": 24 }}
                ],
                "bgpsecAssertions": [
                    {{ "asn": 64501, "SKI": "{ski_b64}", "routerPublicKey": "{spki_b64}" }}
                ],
                "aspaAssertions": [
                    {{ "customerAsn": 64510, "providerAsns": [64511, 64512] }}
                ]
            }}
        }}"#
    );
    log_slurm_input("applies_filters_before_assertions_and_excludes_duplicates", &json);
    let slurm = SlurmFile::from_slice(json.as_bytes()).unwrap();
    log_slurm_ok(
        "applies_filters_before_assertions_and_excludes_duplicates",
        &slurm,
    );

    let input = vec![
        Payload::RouteOrigin(RouteOrigin::new(
            IPAddressPrefix::new(IPAddress::from_ipv4("192.0.2.0".parse().unwrap()), 24),
            24,
            Asn::from(64496u32),
        )),
        Payload::RouteOrigin(RouteOrigin::new(
            IPAddressPrefix::new(IPAddress::from_ipv4("203.0.113.0".parse().unwrap()), 24),
            24,
            Asn::from(64497u32),
        )),
        Payload::RouterKey(RouterKey::new(ski, Asn::from(64497u32), spki.clone())),
        Payload::Aspa(Aspa::new(Asn::from(64496u32), vec![Asn::from(64498u32)])),
    ];

    let output = slurm.apply(&input);
    log_payload_result(
        "applies_filters_before_assertions_and_excludes_duplicates",
        &output,
    );

    assert_eq!(output.len(), 4);
    assert!(output.iter().any(|payload| matches!(
        payload,
        Payload::RouteOrigin(route_origin)
            if route_origin.prefix().address() == IPAddress::from_ipv4("203.0.113.0".parse().unwrap())
    )));
    assert!(output.iter().any(|payload| matches!(
        payload,
        Payload::RouteOrigin(route_origin)
            if route_origin.prefix().address() == IPAddress::from_ipv4("198.51.100.0".parse().unwrap())
    )));
    assert!(output.iter().any(|payload| matches!(
        payload,
        Payload::RouterKey(router_key)
            if router_key.asn() == Asn::from(64501u32)
    )));
    assert!(output.iter().any(|payload| matches!(
        payload,
        Payload::Aspa(aspa)
            if aspa.customer_asn() == Asn::from(64510u32)
    )));
}

#[test]
// Rejects non-RFC hex SKI encoding and ASPA assertions that self-reference the customer ASN.
fn rejects_hex_encoded_ski_and_aspa_customer_in_providers() {
    let ski_hex = hex::encode(sample_ski());
    let router_public_key = STANDARD_NO_PAD.encode(sample_spki());
    let invalid_ski = format!(
        r#"{{
            "slurmVersion": 1,
            "validationOutputFilters": {{
                "prefixFilters": [],
                "bgpsecFilters": [
                    {{ "SKI": "{ski_hex}" }}
                ]
            }},
            "locallyAddedAssertions": {{
                "prefixAssertions": [],
                "bgpsecAssertions": [
                    {{ "asn": 64501, "SKI": "{ski_hex}", "routerPublicKey": "{router_public_key}" }}
                ]
            }}
        }}"#
    );
    log_slurm_input(
        "rejects_hex_encoded_ski_and_aspa_customer_in_providers.invalid_ski",
        &invalid_ski,
    );
    let ski_err = SlurmFile::from_slice(invalid_ski.as_bytes()).unwrap_err();
    log_slurm_err(
        "rejects_hex_encoded_ski_and_aspa_customer_in_providers.invalid_ski",
        &ski_err,
    );
    let ski_err_text = ski_err.to_string();
    assert!(
        ski_err_text.contains("invalid SKI base64")
            || ski_err_text.contains("SKI must be exactly 20 bytes")
    );

    let invalid_aspa = r#"{
        "slurmVersion": 2,
        "validationOutputFilters": {
            "prefixFilters": [],
            "bgpsecFilters": [],
            "aspaFilters": []
        },
        "locallyAddedAssertions": {
            "prefixAssertions": [],
            "bgpsecAssertions": [],
            "aspaAssertions": [
                { "customerAsn": 64500, "providerAsns": [64500, 64501] }
            ]
        }
    }"#;
    log_slurm_input(
        "rejects_hex_encoded_ski_and_aspa_customer_in_providers.invalid_aspa",
        invalid_aspa,
    );
    let aspa_err = SlurmFile::from_slice(invalid_aspa.as_bytes()).unwrap_err();
    log_slurm_err(
        "rejects_hex_encoded_ski_and_aspa_customer_in_providers.invalid_aspa",
        &aspa_err,
    );
    assert!(aspa_err
        .to_string()
        .contains("providerAsns must not contain customerAsn"));
}

#[test]
// Merges non-overlapping SLURM files and upgrades the merged policy version as needed.
fn merges_multiple_slurm_files_without_conflict() {
    let a = r#"{
        "slurmVersion": 1,
        "validationOutputFilters": {
            "prefixFilters": [],
            "bgpsecFilters": []
        },
        "locallyAddedAssertions": {
            "prefixAssertions": [
                { "prefix": "198.51.100.0/24", "asn": 64500, "maxPrefixLength": 24 }
            ],
            "bgpsecAssertions": []
        }
    }"#;

    let b = r#"{
        "slurmVersion": 2,
        "validationOutputFilters": {
            "prefixFilters": [],
            "bgpsecFilters": [],
            "aspaFilters": [
                { "customerAsn": 64510 }
            ]
        },
        "locallyAddedAssertions": {
            "prefixAssertions": [],
            "bgpsecAssertions": [],
            "aspaAssertions": []
        }
    }"#;

    log_slurm_input("merges_multiple_slurm_files_without_conflict.a", a);
    log_slurm_input("merges_multiple_slurm_files_without_conflict.b", b);

    let merged = SlurmFile::merge_named(vec![
        (
            "a.slurm".to_string(),
            SlurmFile::from_slice(a.as_bytes()).unwrap(),
        ),
        (
            "b.slurm".to_string(),
            SlurmFile::from_slice(b.as_bytes()).unwrap(),
        ),
    ])
    .unwrap();
    log_slurm_ok("merges_multiple_slurm_files_without_conflict.merged", &merged);

    assert_eq!(merged.version(), SlurmVersion::V2);
    assert_eq!(merged.locally_added_assertions().prefix_assertions.len(), 1);
    assert_eq!(merged.validation_output_filters().aspa_filters.len(), 1);
}

#[test]
// Rejects multiple SLURM files whose policy scopes overlap and would conflict when merged.
fn rejects_conflicting_multiple_slurm_files() {
    let a = r#"{
        "slurmVersion": 1,
        "validationOutputFilters": {
            "prefixFilters": [],
            "bgpsecFilters": []
        },
        "locallyAddedAssertions": {
            "prefixAssertions": [
                { "prefix": "10.0.0.0/8", "asn": 64500, "maxPrefixLength": 24 }
            ],
            "bgpsecAssertions": []
        }
    }"#;

    let b = r#"{
        "slurmVersion": 1,
        "validationOutputFilters": {
            "prefixFilters": [
                { "prefix": "10.0.0.0/16" }
            ],
            "bgpsecFilters": []
        },
        "locallyAddedAssertions": {
            "prefixAssertions": [],
            "bgpsecAssertions": []
        }
    }"#;

    log_slurm_input("rejects_conflicting_multiple_slurm_files.a", a);
    log_slurm_input("rejects_conflicting_multiple_slurm_files.b", b);

    let err = SlurmFile::merge_named(vec![
        (
            "a.slurm".to_string(),
            SlurmFile::from_slice(a.as_bytes()).unwrap(),
        ),
        (
            "b.slurm".to_string(),
            SlurmFile::from_slice(b.as_bytes()).unwrap(),
        ),
    ])
    .unwrap_err();
    log_slurm_err("rejects_conflicting_multiple_slurm_files", &err);

    assert!(err.to_string().contains("conflicting SLURM files"));
}
