use rustc_data_structures::fx::FxHashSet;
use rustc_middle::lint::LintExpectation;
use rustc_middle::query::Providers;
use rustc_middle::ty::TyCtxt;
use rustc_session::lint::builtin::UNFULFILLED_LINT_EXPECTATIONS;
use rustc_session::lint::{LintExpectationId, StableLintExpectationId};
use rustc_span::Symbol;

use crate::lints::{Expectation, ExpectationNote};

pub(crate) fn provide(providers: &mut Providers) {
    *providers = Providers { lint_expectations, check_expectations, ..*providers };
}

fn lint_expectations(tcx: TyCtxt<'_>, (): ()) -> Vec<(StableLintExpectationId, LintExpectation)> {
    let krate = tcx.hir_crate_items(());

    let mut expectations = Vec::new();

    for owner in krate.owners() {
        let lints = tcx.shallow_lint_levels_on(owner);
        expectations.extend_from_slice(&lints.expectations);
    }

    expectations
}

fn check_expectations(tcx: TyCtxt<'_>, tool_filter: Option<Symbol>) {
    let lint_expectations = tcx.lint_expectations(());
    let fulfilled_expectations = tcx.dcx().steal_fulfilled_expectation_ids();

    // Turn a `LintExpectationId` into a `(AttrId, lint_index)` pair.
    let canonicalize_id = |expect_id: &LintExpectationId| {
        let (attr_id, lint_index) = match *expect_id {
            LintExpectationId::Unstable(id) => (id.attr_id, id.lint_index),
            LintExpectationId::Stable(id) => {
                // We are an `eval_always` query, so looking at the attribute's `AttrId` is ok.
                (tcx.hir_attrs(id.hir_id)[id.attr_index as usize].id(), id.lint_index)
            }
        };
        (attr_id, lint_index)
    };

    let fulfilled_expectations: FxHashSet<_> =
        fulfilled_expectations.iter().map(canonicalize_id).collect();

    for (expect_id, expectation) in lint_expectations {
        let hir_id = expect_id.hir_id;
        let expect_id = canonicalize_id(&LintExpectationId::Stable(*expect_id));

        if !fulfilled_expectations.contains(&expect_id)
            && tool_filter.is_none_or(|filter| expectation.lint_tool == Some(filter))
        {
            let rationale = expectation.reason.map(|rationale| ExpectationNote { rationale });
            let note = expectation.is_unfulfilled_lint_expectations;
            tcx.emit_node_span_lint(
                UNFULFILLED_LINT_EXPECTATIONS,
                hir_id,
                expectation.emission_span,
                Expectation { rationale, note },
            );
        }
    }
}
