use fp_checker_core::{
    check_code, load_config_from_path, CheckerConfig, ExternalPurityCategory,
    ImplementationStyleCategory, PurityConfig, PuritySeverityConfig, RuleId, Severity, StyleConfig,
    StyleSeverityConfig, UnknownCallBehavior,
};
use std::collections::BTreeSet;
use std::path::PathBuf;

fn fixture_path(relative: &str) -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .join("../../tests/fixtures")
        .join(relative)
}

#[test]
fn check_code_reports_ignore_and_core_rules() {
    let source = r#"
STATE = []

@fp_ignore
def ignored():
    print("x")
    return STATE

def active(items=[]):
    print("x")
    return STATE
"#;
    let report = check_code(source, "memory.py", &CheckerConfig::default());

    assert_eq!(report.summary.checked_functions, 1);
    assert_eq!(report.summary.ignored_functions, 1);

    let rule_ids = report
        .files
        .iter()
        .flat_map(|file| {
            file.diagnostics
                .iter()
                .map(|diagnostic| diagnostic.rule_id.clone())
        })
        .collect::<BTreeSet<_>>();

    assert!(rule_ids.contains(&RuleId::MutableDefaultArgument));
    assert!(rule_ids.contains(&RuleId::DirectIo));
    assert!(rule_ids.contains(&RuleId::MutableGlobalState));
}

#[test]
fn check_code_reports_parse_error_as_diagnostic() {
    let report = check_code(
        "def broken(:\n    return 1\n",
        "broken.py",
        &CheckerConfig::default(),
    );
    assert_eq!(report.summary.diagnostics, 1);
    assert_eq!(report.files[0].diagnostics[0].rule_id, RuleId::ParseError);
}

#[test]
fn check_code_normalizes_crlf_offsets_to_match_lf_reports() {
    let lf_source = "STATE = []\n\ndef f(items=[]):\n    return STATE\n";
    let crlf_source = lf_source.replace('\n', "\r\n");

    let lf_report = check_code(lf_source, "memory.py", &CheckerConfig::default());
    let crlf_report = check_code(&crlf_source, "memory.py", &CheckerConfig::default());

    let lf_diagnostics = &lf_report.files[0].diagnostics;
    let crlf_diagnostics = &crlf_report.files[0].diagnostics;

    assert_eq!(lf_diagnostics.len(), crlf_diagnostics.len());

    for (lf_diagnostic, crlf_diagnostic) in lf_diagnostics.iter().zip(crlf_diagnostics) {
        assert_eq!(lf_diagnostic.rule_id, crlf_diagnostic.rule_id);
        assert_eq!(lf_diagnostic.location.start, crlf_diagnostic.location.start);
        assert_eq!(lf_diagnostic.location.end, crlf_diagnostic.location.end);
    }
}

#[test]
fn load_config_from_pyproject_reads_tool_section() {
    let config = load_config_from_path(fixture_path("sample_project/pyproject.toml")).unwrap();
    assert_eq!(config.ignore_decorator_name, "fp_ignore");
    assert!(config.is_rule_enabled(&RuleId::MutableGlobalState));
    assert!(!config.is_rule_enabled(&RuleId::DirectIo));
}

#[test]
fn function_assessment_separates_purity_from_reassignment() {
    let source = r"
def sum_positive(xs):
    total = 0
    for x in xs:
        if x > 0:
            total = total + x
    return total
";
    let report = check_code(source, "memory.py", &CheckerConfig::default());
    let assessment = &report.files[0].function_assessments[0];
    let purity = assessment.external_purity.as_ref().unwrap();
    let style = assessment.implementation_style.as_ref().unwrap();

    assert_eq!(purity.primary, ExternalPurityCategory::Pure);
    assert!(style
        .categories
        .contains(&ImplementationStyleCategory::HasReassignment));
    assert_eq!(style.primary, ImplementationStyleCategory::HasReassignment);
}

#[test]
fn function_assessment_reports_impure_io_without_reassignment() {
    let source = "def announce(x):\n    print(x)\n    return x\n";
    let report = check_code(source, "memory.py", &CheckerConfig::default());
    let assessment = &report.files[0].function_assessments[0];
    let purity = assessment.external_purity.as_ref().unwrap();
    let style = assessment.implementation_style.as_ref().unwrap();

    assert_eq!(purity.primary, ExternalPurityCategory::ImpureDirectIo);
    assert_eq!(style.primary, ImplementationStyleCategory::NoReassignment);
}

#[test]
fn local_mutation_stays_pure_but_is_reported_in_style_assessment() {
    let source = r"
def build(xs):
    out = []
    for x in xs:
        out.append(x * 2)
    return out
";
    let report = check_code(source, "memory.py", &CheckerConfig::default());
    let assessment = &report.files[0].function_assessments[0];
    let purity = assessment.external_purity.as_ref().unwrap();
    let style = assessment.implementation_style.as_ref().unwrap();

    assert_eq!(purity.primary, ExternalPurityCategory::Pure);
    assert!(style
        .categories
        .contains(&ImplementationStyleCategory::HasMutation));
}

#[test]
fn unknown_call_behavior_can_mark_functions_as_impure() {
    let config = CheckerConfig {
        purity: PurityConfig {
            treat_unknown_calls_as: UnknownCallBehavior::WarnUnknown,
            ..PurityConfig::default()
        },
        ..CheckerConfig::default()
    };
    let report = check_code("def f(x):\n    return helper(x)\n", "memory.py", &config);
    let assessment = &report.files[0].function_assessments[0];

    assert_eq!(
        assessment.external_purity.as_ref().unwrap().primary,
        ExternalPurityCategory::ImpureUnknownCall
    );
}

#[test]
fn nonlocal_cumulative_assignment_is_tracked_as_reassignment() {
    let source = r"
def outer():
    count = 0
    def inner():
        nonlocal count
        count = count + 1
        return count
    return inner
";
    let report = check_code(source, "memory.py", &CheckerConfig::default());
    let assessment = report.files[0]
        .function_assessments
        .iter()
        .find(|assessment| assessment.qualified_name == "outer.inner")
        .unwrap();
    let purity = assessment.external_purity.as_ref().unwrap();
    let style = assessment.implementation_style.as_ref().unwrap();

    assert_eq!(
        purity.primary,
        ExternalPurityCategory::ImpureWritesExternalState
    );
    assert!(style
        .categories
        .contains(&ImplementationStyleCategory::HasReassignment));
    assert_eq!(style.metrics.cumulative_reassignment_count, 1);
}

#[test]
fn closure_reads_are_treated_as_external_state() {
    let source = r"
def outer():
    seed = []
    def inner():
        return seed
    return inner
";
    let report = check_code(source, "memory.py", &CheckerConfig::default());
    let assessment = report.files[0]
        .function_assessments
        .iter()
        .find(|assessment| assessment.qualified_name == "outer.inner")
        .unwrap();

    assert_eq!(
        assessment.external_purity.as_ref().unwrap().primary,
        ExternalPurityCategory::ImpureReadsExternalState
    );
}

#[test]
fn ambient_dependency_calls_are_nondeterministic() {
    let source = "import os\n\ndef current_user():\n    return os.getenv(\"USER\")\n";
    let report = check_code(source, "memory.py", &CheckerConfig::default());

    assert_eq!(
        report.files[0].function_assessments[0]
            .external_purity
            .as_ref()
            .unwrap()
            .primary,
        ExternalPurityCategory::ImpureNondeterministic
    );
}

#[test]
fn assessment_severity_participates_in_fail_on() {
    let config = CheckerConfig {
        enabled_rules: Some(BTreeSet::new()),
        fail_on: Severity::Error,
        ..CheckerConfig::default()
    };
    let report = check_code(
        "def announce(x):\n    print(x)\n    return x\n",
        "memory.py",
        &config,
    );

    assert_eq!(report.summary.diagnostics, 0);
    assert!(report.should_fail(Severity::Error));
}

#[test]
fn lowered_purity_severity_can_prevent_fail_on() {
    let config = CheckerConfig {
        enabled_rules: Some(BTreeSet::new()),
        fail_on: Severity::Error,
        purity: PurityConfig {
            severity: PuritySeverityConfig {
                impure_direct_io: Severity::Warning,
                ..PuritySeverityConfig::default()
            },
            ..PurityConfig::default()
        },
        ..CheckerConfig::default()
    };
    let report = check_code(
        "def announce(x):\n    print(x)\n    return x\n",
        "memory.py",
        &config,
    );

    assert_eq!(
        report.files[0].function_assessments[0]
            .external_purity
            .as_ref()
            .unwrap()
            .severity,
        Severity::Warning
    );
    assert!(!report.should_fail(Severity::Error));
}

#[test]
fn raised_style_severity_can_trigger_fail_on() {
    let config = CheckerConfig {
        enabled_rules: Some(BTreeSet::new()),
        fail_on: Severity::Error,
        style: StyleConfig {
            severity: StyleSeverityConfig {
                has_reassignment: Severity::Error,
                ..StyleSeverityConfig::default()
            },
            ..StyleConfig::default()
        },
        ..CheckerConfig::default()
    };
    let report = check_code(
        "def sum_positive(xs):\n    total = 0\n    for x in xs:\n        total = total + x\n    return total\n",
        "memory.py",
        &config,
    );

    assert_eq!(
        report.files[0].function_assessments[0]
            .implementation_style
            .as_ref()
            .unwrap()
            .severity,
        Severity::Error
    );
    assert!(report.should_fail(Severity::Error));
}
