use std::collections::BTreeMap;

use aqc_file_engine_core::{
    ConfigScalar, ForbiddenGlobRequirements, ListRequirements, Provenance, ScalarAssertion,
};
use aqc_rustfmt_toml_engine::{
    RustfmtIgnorePathGlob, RustfmtListSetting, RustfmtScalarSetting, RustfmtTomlRequirements,
};
use globset as _;
use toml_edit as _;

type IgnoreGlobCases<'a> = Vec<(&'a str, &'a str)>;

#[test]
fn merge_keeps_equal_scalar_requirements() {
    let (resolved, conflicts) = RustfmtTomlRequirements::merge(vec![
        (
            Provenance {
                policy: "left".to_owned(),
            },
            req(ScalarAssertion::Equals(
                ConfigScalar::Str("2024".to_owned()),
                "left".to_owned(),
            )),
        ),
        (
            Provenance {
                policy: "right".to_owned(),
            },
            req(ScalarAssertion::Equals(
                ConfigScalar::Str("2024".to_owned()),
                "right".to_owned(),
            )),
        ),
    ]);

    assert!(conflicts.is_empty(), "equal settings must merge cleanly");
    assert!(
        resolved
            .scalar_settings
            .contains_key(&RustfmtScalarSetting::Edition),
        "merged edition requirement must be retained"
    );
}

#[test]
fn merge_reports_conflicting_scalar_requirements() {
    let (_resolved, conflicts) = RustfmtTomlRequirements::merge(vec![
        (
            Provenance {
                policy: "left".to_owned(),
            },
            req(ScalarAssertion::Equals(
                ConfigScalar::Str("2021".to_owned()),
                "left".to_owned(),
            )),
        ),
        (
            Provenance {
                policy: "right".to_owned(),
            },
            req(ScalarAssertion::Equals(
                ConfigScalar::Str("2024".to_owned()),
                "right".to_owned(),
            )),
        ),
    ]);

    assert_eq!(
        conflicts.len(),
        1,
        "conflicting edition must produce one conflict"
    );
    let conflict = conflicts
        .first()
        .expect("one scalar conflict should be present");
    assert_eq!(conflict.key, "edition", "conflict key must be file key");
}

#[test]
fn rustfmt_requirements_use_core_scalar_assertions() {
    let (resolved, conflicts) = RustfmtTomlRequirements::merge(vec![(
        Provenance {
            policy: "policy".to_owned(),
        },
        req(ScalarAssertion::Equals(
            ConfigScalar::Str("2024".to_owned()),
            "edition".to_owned(),
        )),
    )]);

    assert!(conflicts.is_empty());
    assert!(
        resolved
            .scalar_settings
            .contains_key(&RustfmtScalarSetting::Edition)
    );
}

#[test]
fn rustfmt_rejects_scalar_operations_outside_setting_type() {
    let (resolved, conflicts) = RustfmtTomlRequirements::merge(vec![(
        Provenance {
            policy: "policy".to_owned(),
        },
        RustfmtTomlRequirements {
            scalar_settings: BTreeMap::from([
                (
                    RustfmtScalarSetting::HardTabs,
                    ScalarAssertion::OneOf(
                        std::collections::BTreeSet::from([ConfigScalar::Bool(true)]),
                        "bool oneof".to_owned(),
                    ),
                ),
                (
                    RustfmtScalarSetting::MaxWidth,
                    ScalarAssertion::Equals(
                        ConfigScalar::Str("100".to_owned()),
                        "wrong type".to_owned(),
                    ),
                ),
                (
                    RustfmtScalarSetting::Edition,
                    ScalarAssertion::AtLeast(
                        ConfigScalar::Str("2021".to_owned()),
                        "ordered".to_owned(),
                    ),
                ),
            ]),
            ..RustfmtTomlRequirements::default()
        },
    )]);

    assert!(resolved.scalar_settings.is_empty());
    assert_eq!(
        conflicts
            .iter()
            .filter(|conflict| conflict.reason == "scalar-operation-unsupported")
            .count(),
        3
    );
}

#[test]
fn rustfmt_scalar_setting_kind_validation_covers_all_kinds() {
    let (resolved, conflicts) = RustfmtTomlRequirements::merge(vec![(
        Provenance {
            policy: "policy".to_owned(),
        },
        RustfmtTomlRequirements {
            scalar_settings: BTreeMap::from([
                (
                    RustfmtScalarSetting::HardTabs,
                    ScalarAssertion::Equals(
                        ConfigScalar::Str("true".to_owned()),
                        "wrong bool".to_owned(),
                    ),
                ),
                (
                    RustfmtScalarSetting::MaxWidth,
                    ScalarAssertion::OneOf(
                        std::collections::BTreeSet::from([ConfigScalar::Str("100".to_owned())]),
                        "wrong int oneof".to_owned(),
                    ),
                ),
                (
                    RustfmtScalarSetting::Edition,
                    ScalarAssertion::Equals(ConfigScalar::Int(2024), "wrong text".to_owned()),
                ),
                (
                    RustfmtScalarSetting::StyleEdition,
                    ScalarAssertion::OneOf(
                        std::collections::BTreeSet::from([ConfigScalar::Int(2024)]),
                        "wrong text oneof".to_owned(),
                    ),
                ),
                (
                    RustfmtScalarSetting::TabSpaces,
                    ScalarAssertion::Range(
                        ConfigScalar::Int(2),
                        ConfigScalar::Int(4),
                        "range".to_owned(),
                    ),
                ),
                (
                    RustfmtScalarSetting::UseTryShorthand,
                    ScalarAssertion::Present("bool present".to_owned()),
                ),
                (
                    RustfmtScalarSetting::FnCallWidth,
                    ScalarAssertion::Absent("int absent".to_owned()),
                ),
                (
                    RustfmtScalarSetting::Version,
                    ScalarAssertion::Present("text present".to_owned()),
                ),
            ]),
            ..RustfmtTomlRequirements::default()
        },
    )]);

    assert!(
        resolved
            .scalar_settings
            .contains_key(&RustfmtScalarSetting::UseTryShorthand)
    );
    assert!(
        resolved
            .scalar_settings
            .contains_key(&RustfmtScalarSetting::FnCallWidth)
    );
    assert!(
        resolved
            .scalar_settings
            .contains_key(&RustfmtScalarSetting::Version)
    );
    assert_eq!(
        conflicts
            .iter()
            .filter(|conflict| conflict.reason == "scalar-operation-unsupported")
            .count(),
        5
    );
}

#[test]
fn forbidden_ignore_path_globs_dedupe_attribution() {
    let (resolved, conflicts) = RustfmtTomlRequirements::merge(vec![
        (
            Provenance {
                policy: "left".to_owned(),
            },
            ignore_globs(vec![("target/**", "left")]),
        ),
        (
            Provenance {
                policy: "right".to_owned(),
            },
            ignore_globs(vec![("target/**", "right")]),
        ),
    ]);

    assert!(conflicts.is_empty(), "same glob should merge cleanly");
    assert_eq!(
        resolved
            .forbidden_ignore_path_globs
            .globs
            .get("target/**")
            .expect("merged forbidden glob should exist")
            .collected
            .len(),
        2,
        "same glob should retain both contributors"
    );
}

#[test]
fn forbidden_ignore_path_glob_conflicts_with_required_ignore_path() {
    let (resolved, conflicts) = RustfmtTomlRequirements::merge(vec![
        (
            Provenance {
                policy: "required".to_owned(),
            },
            RustfmtTomlRequirements {
                list_settings: BTreeMap::from([(
                    RustfmtListSetting::Ignore,
                    ListRequirements {
                        contains: BTreeMap::from([(
                            "target/generated".to_owned(),
                            "must ignore generated".to_owned(),
                        )]),
                        ..ListRequirements::default()
                    },
                )]),
                ..RustfmtTomlRequirements::default()
            },
        ),
        (
            Provenance {
                policy: "forbid".to_owned(),
            },
            ignore_globs(vec![("target/**", "do not ignore target")]),
        ),
    ]);

    assert!(
        conflicts
            .iter()
            .any(|conflict| conflict.reason == "ignore-path-glob-forbids-required-path"),
        "required ignore entry matching glob should conflict"
    );
    assert!(
        resolved
            .ignore_glob_conflicts
            .path_globs
            .contains("target/**"),
        "conflicting glob should be blocked during reconcile"
    );
}

fn req(assertion: ScalarAssertion<ConfigScalar>) -> RustfmtTomlRequirements {
    RustfmtTomlRequirements {
        scalar_settings: BTreeMap::from([(RustfmtScalarSetting::Edition, assertion)]),
        ..RustfmtTomlRequirements::default()
    }
}

fn ignore_globs(globs: IgnoreGlobCases<'_>) -> RustfmtTomlRequirements {
    RustfmtTomlRequirements {
        forbidden_ignore_path_globs: ForbiddenGlobRequirements {
            globs: globs
                .into_iter()
                .map(|(glob, msg)| {
                    (
                        RustfmtIgnorePathGlob {
                            glob: glob.to_owned(),
                        },
                        msg.to_owned(),
                    )
                })
                .collect(),
        },
        ..RustfmtTomlRequirements::default()
    }
}
