// Copyright Two Neutron Stars Incorporated and contributors
// SPDX-License-Identifier: BlueOak-1.0.0

//! Symbol resolution implementation.
//!
//! This module provides the core symbol resolution functionality, handling
//! name lookup through scope chains and namespace access. Resolution converts
//! identifier paths into concrete symbol references or appropriate error
//! states.
//!
//! # Resolution Process
//!
//! Symbol resolution follows these steps:
//! 1. **Path Parsing**: Break down qualified names into segments
//! 2. **Scope Chain Search**: Look up the first segment in the scope hierarchy
//! 3. **Namespace Resolution**: Resolve subsequent segments within namespace
//!    symbols
//! 4. **Error Handling**: Provide detailed feedback for resolution failures
//!
//! # Resolution Types
//!
//! - **Resolved**: Successfully found a concrete symbol
//! - **Phantom**: Placeholder for missing symbols (error recovery)
//! - **WrongKind**: Found symbol but incorrect type for usage
//! - **Failed**: Complete resolution failure with error details
//!
//! # Scope Chain Resolution
//!
//! Name lookup searches through the scope hierarchy:
//! 1. Start in the current scope
//! 2. Check parent scopes up to the global scope
//! 3. Return the first match found
//! 4. Fail if no symbol is found in any accessible scope
//!
//! # Namespace Resolution
//!
//! Namespace resolution supports qualified access patterns where symbols
//! contain other symbols in their namespace:
//! - Module namespaces: `std::collections::HashMap`
//! - Object namespaces: `object.field`
//! - Type namespaces: `String::from_utf8`
//! - Hierarchical access: `module.submodule.symbol`

use {
  crate::{
    core::{Ident, SymbolPath, SymbolVisibility, Value, Visibility},
    partitions::SymbolEntry,
  },
  std::hash::Hash,
};

/// Render an identifier as source text via the resolver, falling back to its
/// debug form when neither a known-ident entry nor a span yields text.
fn ident_text<I: Ident>(
  ident: &I,
  resolver: &dyn laburnum::SourceResolver,
) -> String {
  // Prefer the actual source text (the user's spelling); fall back to the
  // resolver's known-ident table for spanless keyword/builtin idents.
  ident
    .span()
    .and_then(|span| resolver.try_resolve_span(&span))
    .or_else(|| ident.ident().and_then(|i| resolver.resolve_ident(i)))
    // `I` has no Debug; placeholder when nothing resolves.
    .unwrap_or_else(|| "<unresolved>".to_string())
}

pub enum Resolution<V, I, P, S = SymbolVisibility>
where
  V: Value<I>,
  I: Ident + Hash,
  P: SymbolPath,
  S: Visibility,
{
  Resolved(SymbolEntry<V, I, P, S>),
  Deferred,
  Phantom(PhantomSymbol<V, I, P, S>),
  WrongKind {
    found: SymbolEntry<V, I, P, S>,
    expected_kinds: Vec<String>,
  },
  /// No definition exists for the given path.
  Unresolved,
  /// A definition was found, but its visibility does not admit the
  /// referencing site (see [`Visibility::is_visible_from`]). Carries the
  /// hidden target so callers can still offer go-to-definition and a
  /// precise "defined here, but not accessible" diagnostic.
  Inaccessible(SymbolEntry<V, I, P, S>),
}

impl<V, I, P, S> Clone for Resolution<V, I, P, S>
where
  V: Value<I>,
  I: Ident + Hash,
  P: SymbolPath,
  S: Visibility,
{
  fn clone(&self) -> Self {
    match self {
      | Resolution::Resolved(id) => Resolution::Resolved(id.clone()),
      | Resolution::Deferred => Resolution::Deferred,
      | Resolution::Phantom(p) => Resolution::Phantom(p.clone()),
      | Resolution::WrongKind {
        found,
        expected_kinds,
      } => Resolution::WrongKind {
        found: found.clone(),
        expected_kinds: expected_kinds.clone(),
      },
      | Resolution::Unresolved => Resolution::Unresolved,
      | Resolution::Inaccessible(id) => Resolution::Inaccessible(id.clone()),
    }
  }
}

impl<V, I, P, S> PartialEq for Resolution<V, I, P, S>
where
  V: Value<I>,
  I: Ident + Hash,
  P: SymbolPath,
  S: Visibility,
{
  fn eq(&self, other: &Self) -> bool {
    match (self, other) {
      | (Resolution::Resolved(a), Resolution::Resolved(b)) => a == b,
      | (Resolution::Deferred, Resolution::Deferred) => true,
      | (Resolution::Phantom(a), Resolution::Phantom(b)) => a == b,
      | (
        Resolution::WrongKind {
          found: f1,
          expected_kinds: e1,
        },
        Resolution::WrongKind {
          found: f2,
          expected_kinds: e2,
        },
      ) => f1 == f2 && e1 == e2,
      | (Resolution::Unresolved, Resolution::Unresolved) => true,
      | (Resolution::Inaccessible(a), Resolution::Inaccessible(b)) => a == b,
      | _ => false,
    }
  }
}

impl<V, I, P, S> Resolution<V, I, P, S>
where
  V: Value<I>,
  I: Ident + Hash,
  P: SymbolPath,
  S: Visibility,
{
  pub fn is_resolved(&self) -> bool {
    matches!(self, Resolution::Resolved(_))
  }

  pub fn is_failed(&self) -> bool {
    matches!(self, Resolution::Unresolved | Resolution::Inaccessible(_))
  }

  pub fn is_unresolved(&self) -> bool {
    matches!(self, Resolution::Unresolved)
  }

  pub fn is_inaccessible(&self) -> bool {
    matches!(self, Resolution::Inaccessible(_))
  }

  pub fn is_deferred(&self) -> bool {
    matches!(self, Resolution::Deferred)
  }

  pub fn is_phantom(&self) -> bool {
    matches!(self, Resolution::Phantom(_))
  }

  pub fn is_wrong_kind(&self) -> bool {
    matches!(self, Resolution::WrongKind { .. })
  }

  pub fn qualified_symbol_id(&self) -> Option<&SymbolEntry<V, I, P, S>> {
    match self {
      | Resolution::Resolved(qid) => Some(qid),
      | Resolution::WrongKind { found, .. } => Some(found),
      | _ => None,
    }
  }

  pub fn symbol_id(&self) -> Option<&SymbolEntry<V, I, P, S>> {
    self.qualified_symbol_id()
  }

  pub fn description(&self, resolver: &dyn laburnum::SourceResolver) -> String {
    match self {
      | Resolution::Resolved(qid) => {
        format!(
          "Resolved to symbol {}",
          laburnum::PartitionSortKey::resolve(&qid.path, resolver)
        )
      },
      | Resolution::Deferred => "Deferred to next pass".to_string(),
      | Resolution::Phantom(phantom) => {
        format!(
          "Expected {} '{}': {}",
          phantom.expected_kind,
          ident_text(&phantom.name, resolver),
          phantom.reason
        )
      },
      | Resolution::WrongKind { expected_kinds, .. } => {
        format!("Wrong type, expected: {}", expected_kinds.join(" or "))
      },
      | Resolution::Unresolved => "Unresolved: no such definition".to_string(),
      | Resolution::Inaccessible(_) => {
        "Inaccessible: definition is not visible from here".to_string()
      },
    }
  }

  pub fn into_result(
    self,
    resolver: &dyn laburnum::SourceResolver,
  ) -> Result<SymbolEntry<V, I, P, S>, String> {
    match self {
      | Resolution::Resolved(qid) => Ok(qid),
      | other => Err(other.description(resolver)),
    }
  }
}

pub struct PhantomSymbol<V, I, P, S = SymbolVisibility>
where
  V: Value<I>,
  I: Ident,
  P: SymbolPath,
  S: Visibility,
{
  pub expected_kind: String,
  pub name: I,
  pub reason: String,
  pub expected_in_scope: SymbolEntry<V, I, P, S>,
  pub hints: Vec<String>,
}

impl<V, I, P, S> Clone for PhantomSymbol<V, I, P, S>
where
  V: Value<I>,
  I: Ident,
  P: SymbolPath,
  S: Visibility,
{
  fn clone(&self) -> Self {
    Self {
      expected_kind: self.expected_kind.clone(),
      name: self.name.clone(),
      reason: self.reason.clone(),
      expected_in_scope: self.expected_in_scope.clone(),
      hints: self.hints.clone(),
    }
  }
}

impl<V, I, P, S> PartialEq for PhantomSymbol<V, I, P, S>
where
  V: Value<I>,
  I: Ident,
  P: SymbolPath,
  S: Visibility,
{
  fn eq(&self, other: &Self) -> bool {
    self.expected_kind == other.expected_kind
      && self.name == other.name
      && self.reason == other.reason
      && self.expected_in_scope == other.expected_in_scope
      && self.hints == other.hints
  }
}

impl<V, I, P, S> PhantomSymbol<V, I, P, S>
where
  V: Value<I>,
  I: Ident,
  P: SymbolPath,
  S: Visibility,
{
  pub fn new(
    expected_kind: String,
    name: I,
    reason: String,
    expected_in_scope: SymbolEntry<V, I, P, S>,
  ) -> Self {
    Self {
      expected_kind,
      name,
      reason,
      expected_in_scope,
      hints: Vec::new(),
    }
  }

  pub fn with_hint(mut self, hint: String) -> Self {
    self.hints.push(hint);
    self
  }

  pub fn with_hints(mut self, hints: Vec<String>) -> Self {
    self.hints.extend(hints);
    self
  }

  pub fn has_hints(&self) -> bool {
    !self.hints.is_empty()
  }

  pub fn display_name(&self, resolver: &dyn laburnum::SourceResolver) -> String {
    ident_text(&self.name, resolver)
  }
}

pub struct ResolutionStep<V, I, P, S = SymbolVisibility>
where
  V: Value<I>,
  I: Ident + Hash,
  P: SymbolPath,
  S: Visibility,
{
  pub segment: I,
  pub span: laburnum::Span,
  pub resolution: Resolution<V, I, P, S>,
  pub path_so_far: Vec<I>,
}

impl<V, I, P, S> Clone for ResolutionStep<V, I, P, S>
where
  V: Value<I>,
  I: Ident + Hash,
  P: SymbolPath,
  S: Visibility,
{
  fn clone(&self) -> Self {
    Self {
      segment: self.segment.clone(),
      span: self.span,
      resolution: self.resolution.clone(),
      path_so_far: self.path_so_far.clone(),
    }
  }
}

impl<V, I, P, S> ResolutionStep<V, I, P, S>
where
  V: Value<I>,
  I: Ident + Hash,
  P: SymbolPath,
  S: Visibility,
{
  pub fn new(
    segment: I,
    span: laburnum::Span,
    resolution: Resolution<V, I, P, S>,
    path_so_far: Vec<I>,
  ) -> Self {
    Self {
      segment,
      span,
      resolution,
      path_so_far,
    }
  }

  pub fn is_successful(&self) -> bool {
    self.resolution.is_resolved()
  }

  pub fn description(&self, resolver: &dyn laburnum::SourceResolver) -> String {
    format!(
      "Resolving '{}': {}",
      ident_text(&self.segment, resolver),
      self.resolution.description(resolver)
    )
  }
}

pub struct ResolutionResult<V, I, P, S = SymbolVisibility>
where
  V: Value<I>,
  I: Ident + Hash,
  P: SymbolPath,
  S: Visibility,
{
  pub steps: Vec<ResolutionStep<V, I, P, S>>,
  pub final_resolution: Resolution<V, I, P, S>,
}

impl<V, I, P, S> Clone for ResolutionResult<V, I, P, S>
where
  V: Value<I>,
  I: Ident + Hash,
  P: SymbolPath,
  S: Visibility,
{
  fn clone(&self) -> Self {
    Self {
      steps: self.steps.clone(),
      final_resolution: self.final_resolution.clone(),
    }
  }
}

impl<V, I, P, S> ResolutionResult<V, I, P, S>
where
  V: Value<I>,
  I: Ident + Hash,
  P: SymbolPath,
  S: Visibility,
{
  pub fn new(
    steps: Vec<ResolutionStep<V, I, P, S>>,
    final_resolution: Resolution<V, I, P, S>,
  ) -> Self {
    Self {
      steps,
      final_resolution,
    }
  }

  pub fn simple(resolution: Resolution<V, I, P, S>) -> Self {
    Self {
      steps: Vec::new(),
      final_resolution: resolution,
    }
  }

  pub fn is_successful(&self) -> bool {
    self.final_resolution.is_resolved()
  }

  pub fn step_count(&self) -> usize {
    self.steps.len()
  }

  pub fn is_qualified_path(&self) -> bool {
    self.step_count() > 1
  }

  pub fn symbol_id(&self) -> Option<&SymbolEntry<V, I, P, S>> {
    self.final_resolution.symbol_id()
  }

  pub fn report(&self, resolver: &dyn laburnum::SourceResolver) -> String {
    if self.steps.is_empty() {
      format!(
        "Simple resolution: {}",
        self.final_resolution.description(resolver)
      )
    } else {
      let mut report =
        format!("Multi-step resolution ({} steps):\n", self.steps.len());
      for (i, step) in self.steps.iter().enumerate() {
        report.push_str(&format!(
          "  {}: {}\n",
          i + 1,
          step.description(resolver)
        ));
      }
      report.push_str(&format!(
        "Final: {}",
        self.final_resolution.description(resolver)
      ));
      report
    }
  }
}

/// Trait for customizing symbol resolution behavior.
///
/// Read-only access to the symbol partitions, handed to
/// [`SymboliqueResolver::resolve`] so a resolver can look up candidate
/// definitions and read their visibility without holding a partition
/// reader itself.
///
/// symbolique provides a concrete implementation
/// ([`QueryResolveCtx`](crate::driver::QueryResolveCtx)) backed by a
/// `QueryClient`; languages normally use that rather than implementing this
/// trait. A resolver that carries its own symbol data can ignore the ctx.
pub trait ResolveCtx<V, I, P, S = SymbolVisibility>
where
  V: Value<I>,
  I: Ident + Hash,
  P: SymbolPath,
  S: Visibility,
{
  /// Resolve a definition path to its entry in the symbol table.
  fn lookup(&self, path: &P) -> Option<SymbolEntry<V, I, P, S>>;

  /// The visibility carried by a looked-up definition, or `None` if the
  /// entry's shape is not a definition.
  fn visibility_of(&self, entry: &SymbolEntry<V, I, P, S>) -> Option<S>;
}

/// A [`ResolveCtx`] that knows nothing — every lookup returns `None`.
///
/// Useful for resolvers that carry their own symbol data and for resolution
/// that does not consult the symbol table.
#[derive(Debug, Clone, Copy, Default)]
pub struct EmptyResolveCtx;

impl<V, I, P, S> ResolveCtx<V, I, P, S> for EmptyResolveCtx
where
  V: Value<I>,
  I: Ident + Hash,
  P: SymbolPath,
  S: Visibility,
{
  fn lookup(&self, _path: &P) -> Option<SymbolEntry<V, I, P, S>> {
    None
  }

  fn visibility_of(&self, _entry: &SymbolEntry<V, I, P, S>) -> Option<S> {
    None
  }
}

/// Implement this trait to provide language-specific resolution logic.
///
/// # Type Parameters
///
/// * `V` - Value type for literals
/// * `I` - Identifier type for names
/// * `P` - Path type for symbol paths (defaults to String)
pub trait SymboliqueResolver<
  V: Value<I>,
  I: Ident + Hash,
  P: SymbolPath = String,
  S: Visibility = SymbolVisibility,
>
{
  /// Resolve a symbol path to a target symbol.
  ///
  /// Given a path, this method resolves it starting from the given scope.
  /// The exact resolution strategy depends on the implementation.
  ///
  /// # Arguments
  ///
  /// * `path` - The path to resolve
  /// * `from_path` - The path of the scope resolution starts from. Pass this
  ///   to [`Visibility::is_visible_from`] when enforcing access control.
  ///   (A `SymbolEntry` carries no path — shapes are content-addressed and
  ///   pathless — so the referencing path must be supplied separately.)
  /// * `from_scope` - The scope occurrence resolution starts from (span +
  ///   handle to its shape), for scope-chain walking.
  ///
  /// # Returns
  ///
  /// * `Resolution::Resolved(entry.clone())` - Successfully resolved to a symbol
  /// * `Resolution::Deferred` - Should be processed in a later pass
  /// * `Resolution::Unresolved` - No definition exists for the path
  /// * `Resolution::Inaccessible(target)` - Found, but not visible from here
  fn resolve(
    &self,
    ctx: &dyn ResolveCtx<V, I, P, S>,
    path: &P,
    from_path: &P,
    from_scope: SymbolEntry<V, I, P, S>,
  ) -> Resolution<V, I, P, S>;
}

/// A default no-op resolver that always fails.
///
/// This is a placeholder implementation. Languages should provide their own
/// resolver that understands their path format.
#[derive(Debug, Clone, Copy, Default)]
pub struct DefaultResolver;

impl<V: Value<I>, I: Ident + Hash, P: SymbolPath, S: Visibility>
  SymboliqueResolver<V, I, P, S> for DefaultResolver
{
  fn resolve(
    &self,
    _ctx: &dyn ResolveCtx<V, I, P, S>,
    _path: &P,
    _from_path: &P,
    _from_scope: SymbolEntry<V, I, P, S>,
  ) -> Resolution<V, I, P, S> {
    Resolution::Unresolved
  }
}

#[cfg(test)]
mod tests {
  use super::*;
  use crate::test_helpers::*;

  // -- Resolution enum tests -----------------------------------------------

  #[test]
  fn resolved_is_resolved() {
    let mut cache = test_span_cache();
    let entry = dummy_symbol_entry(&mut cache);
    let resolution = Resolution::<DV, SI, TP>::Resolved(entry);
    assert!(resolution.is_resolved());
  }

  #[test]
  fn failed_is_failed() {
    let resolution = Resolution::<DV, SI, TP>::Unresolved;
    assert!(resolution.is_failed());
  }

  #[test]
  fn deferred_is_deferred() {
    let resolution = Resolution::<DV, SI, TP>::Deferred;
    assert!(resolution.is_deferred());
  }

  #[test]
  fn phantom_is_phantom() {
    let mut cache = test_span_cache();
    let entry = dummy_symbol_entry(&mut cache);
    let phantom = PhantomSymbol::<DV, SI, TP>::new(
      "function".to_string(),
      SI::new("foo"),
      "not found".to_string(),
      entry,
    );
    let resolution = Resolution::<DV, SI, TP>::Phantom(phantom);
    assert!(resolution.is_phantom());
  }

  #[test]
  fn wrong_kind_is_wrong_kind() {
    let mut cache = test_span_cache();
    let entry = dummy_symbol_entry(&mut cache);
    let resolution = Resolution::<DV, SI, TP>::WrongKind {
      found: entry,
      expected_kinds: vec!["function".to_string()],
    };
    assert!(resolution.is_wrong_kind());
  }

  #[test]
  fn resolved_symbol_id() {
    let mut cache = test_span_cache();
    let entry = dummy_symbol_entry(&mut cache);
    let resolution = Resolution::<DV, SI, TP>::Resolved(entry);
    assert!(resolution.symbol_id().is_some());
  }

  #[test]
  fn failed_symbol_id_none() {
    let resolution = Resolution::<DV, SI, TP>::Unresolved;
    assert!(resolution.symbol_id().is_none());
  }

  #[test]
  fn deferred_symbol_id_none() {
    let resolution = Resolution::<DV, SI, TP>::Deferred;
    assert!(resolution.symbol_id().is_none());
  }

  #[test]
  fn wrong_kind_symbol_id() {
    let mut cache = test_span_cache();
    let entry = dummy_symbol_entry(&mut cache);
    let resolution = Resolution::<DV, SI, TP>::WrongKind {
      found: entry,
      expected_kinds: vec!["function".to_string()],
    };
    assert!(resolution.symbol_id().is_some());
  }

  #[test]
  fn resolved_into_result_ok() {
    let mut cache = test_span_cache();
    let entry = dummy_symbol_entry(&mut cache);
    let resolution = Resolution::<DV, SI, TP>::Resolved(entry);
    let resolver = test_source_resolver();
    assert!(resolution.into_result(&resolver).is_ok());
  }

  #[test]
  fn failed_into_result_err() {
    let resolution = Resolution::<DV, SI, TP>::Unresolved;
    let resolver = test_source_resolver();
    assert!(resolution.into_result(&resolver).is_err());
  }

  #[test]
  fn resolved_description() {
    let mut cache = test_span_cache();
    let entry = dummy_symbol_entry(&mut cache);
    let resolution = Resolution::<DV, SI, TP>::Resolved(entry);
    let resolver = test_source_resolver();
    let desc = resolution.description(&resolver);
    assert!(desc.contains("Resolved"));
  }

  #[test]
  fn unresolved_description() {
    let resolution = Resolution::<DV, SI, TP>::Unresolved;
    let resolver = test_source_resolver();
    let desc = resolution.description(&resolver);
    assert!(desc.contains("Unresolved"));
  }

  #[test]
  fn deferred_description() {
    let resolution = Resolution::<DV, SI, TP>::Deferred;
    let resolver = test_source_resolver();
    let desc = resolution.description(&resolver);
    assert!(desc.contains("Deferred"));
  }

  // -- PhantomSymbol tests -------------------------------------------------

  #[test]
  fn phantom_new() {
    let mut cache = test_span_cache();
    let entry = dummy_symbol_entry(&mut cache);
    let phantom = PhantomSymbol::<DV, SI, TP>::new(
      "function".to_string(),
      SI::new("foo"),
      "not found".to_string(),
      entry,
    );
    assert_eq!(phantom.expected_kind, "function");
    assert_eq!(phantom.reason, "not found");
    assert!(phantom.hints.is_empty());
  }

  #[test]
  fn phantom_with_hint() {
    let mut cache = test_span_cache();
    let entry = dummy_symbol_entry(&mut cache);
    let phantom = PhantomSymbol::<DV, SI, TP>::new(
      "function".to_string(),
      SI::new("foo"),
      "not found".to_string(),
      entry,
    )
    .with_hint("did you mean bar?".to_string());
    assert_eq!(phantom.hints.len(), 1);
  }

  #[test]
  fn phantom_with_hints() {
    let mut cache = test_span_cache();
    let entry = dummy_symbol_entry(&mut cache);
    let phantom = PhantomSymbol::<DV, SI, TP>::new(
      "function".to_string(),
      SI::new("foo"),
      "not found".to_string(),
      entry,
    )
    .with_hints(vec!["hint1".to_string(), "hint2".to_string()]);
    assert_eq!(phantom.hints.len(), 2);
  }

  #[test]
  fn phantom_has_hints() {
    let mut cache = test_span_cache();
    let entry = dummy_symbol_entry(&mut cache);
    let phantom_no_hints = PhantomSymbol::<DV, SI, TP>::new(
      "function".to_string(),
      SI::new("foo"),
      "not found".to_string(),
      entry,
    );
    assert!(!phantom_no_hints.has_hints());

    let phantom_with = phantom_no_hints.with_hint("a hint".to_string());
    assert!(phantom_with.has_hints());
  }

  #[test]
  fn phantom_display_name() {
    let mut cache = test_span_cache();
    let entry = dummy_symbol_entry(&mut cache);
    let phantom = PhantomSymbol::<DV, SI, TP>::new(
      "function".to_string(),
      SI::new("my_func"),
      "not found".to_string(),
      entry,
    );
    let resolver = test_source_resolver();
    // A `StringIdent` carries no span and no `laburnum::Ident`, so it never
    // resolves through the resolver and renders as the placeholder.
    assert_eq!(phantom.display_name(&resolver), "<unresolved>");
  }

  // -- ResolutionStep tests ------------------------------------------------

  #[test]
  fn step_new() {
    let mut cache = test_span_cache();
    let entry = dummy_symbol_entry(&mut cache);
    let span = test_span(&mut cache, 1);
    let segment = SI::new("foo");
    let step = ResolutionStep::<DV, SI, TP>::new(
      segment.clone(),
      span,
      Resolution::Resolved(entry.clone()),
      vec![segment],
    );
    assert_eq!(step.span, span);
    assert!(step.resolution.is_resolved());
  }

  #[test]
  fn step_is_successful_when_resolved() {
    let mut cache = test_span_cache();
    let entry = dummy_symbol_entry(&mut cache);
    let span = test_span(&mut cache, 1);
    let step = ResolutionStep::<DV, SI, TP>::new(
      SI::new("foo"),
      span,
      Resolution::Resolved(entry.clone()),
      vec![SI::new("foo")],
    );
    assert!(step.is_successful());
  }

  #[test]
  fn step_is_successful_when_failed() {
    let mut cache = test_span_cache();
    let span = test_span(&mut cache, 1);
    let step = ResolutionStep::<DV, SI, TP>::new(
      SI::new("foo"),
      span,
      Resolution::Unresolved,
      vec![SI::new("foo")],
    );
    assert!(!step.is_successful());
  }

  #[test]
  fn step_description() {
    let mut cache = test_span_cache();
    let span = test_span(&mut cache, 1);
    let step = ResolutionStep::<DV, SI, TP>::new(
      SI::new("my_segment"),
      span,
      Resolution::Unresolved,
      vec![SI::new("my_segment")],
    );
    let resolver = test_source_resolver();
    let desc = step.description(&resolver);
    // The span-free segment renders as the placeholder, wrapped with the
    // unresolved resolution's message.
    assert_eq!(desc, "Resolving '<unresolved>': Unresolved: no such definition");
  }

  // -- ResolutionResult tests ----------------------------------------------

  #[test]
  fn result_simple_resolved() {
    let mut cache = test_span_cache();
    let entry = dummy_symbol_entry(&mut cache);
    let result = ResolutionResult::<DV, SI, TP>::simple(Resolution::Resolved(
      entry.clone(),
    ));
    assert!(result.is_successful());
  }

  #[test]
  fn result_simple_failed() {
    let result = ResolutionResult::<DV, SI, TP>::simple(Resolution::Unresolved);
    assert!(!result.is_successful());
  }

  #[test]
  fn result_step_count_zero_for_simple() {
    let mut cache = test_span_cache();
    let entry = dummy_symbol_entry(&mut cache);
    let result = ResolutionResult::<DV, SI, TP>::simple(Resolution::Resolved(
      entry.clone(),
    ));
    assert_eq!(result.step_count(), 0);
  }

  #[test]
  fn result_is_qualified_path_false_for_simple() {
    let mut cache = test_span_cache();
    let entry = dummy_symbol_entry(&mut cache);
    let result = ResolutionResult::<DV, SI, TP>::simple(Resolution::Resolved(
      entry.clone(),
    ));
    assert!(!result.is_qualified_path());
  }

  #[test]
  fn result_symbol_id() {
    let mut cache = test_span_cache();
    let entry = dummy_symbol_entry(&mut cache);
    let result = ResolutionResult::<DV, SI, TP>::simple(Resolution::Resolved(
      entry.clone(),
    ));
    assert!(result.symbol_id().is_some());
  }

  #[test]
  fn result_report_simple() {
    let mut cache = test_span_cache();
    let entry = dummy_symbol_entry(&mut cache);
    let result = ResolutionResult::<DV, SI, TP>::simple(Resolution::Resolved(
      entry.clone(),
    ));
    let resolver = test_source_resolver();
    let report = result.report(&resolver);
    assert!(report.contains("Simple resolution"));
  }

  #[test]
  fn result_new_with_steps() {
    let mut cache = test_span_cache();
    let entry = dummy_symbol_entry(&mut cache);
    let span = test_span(&mut cache, 1);
    let step = ResolutionStep::<DV, SI, TP>::new(
      SI::new("a"),
      span,
      Resolution::Resolved(entry.clone()),
      vec![SI::new("a")],
    );
    let result = ResolutionResult::<DV, SI, TP>::new(
      vec![step],
      Resolution::Resolved(entry.clone()),
    );
    assert_eq!(result.step_count(), 1);
  }

  #[test]
  fn result_is_qualified_path_with_multiple_steps() {
    let mut cache = test_span_cache();
    let entry = dummy_symbol_entry(&mut cache);
    let span = test_span(&mut cache, 1);
    let step_a = ResolutionStep::<DV, SI, TP>::new(
      SI::new("a"),
      span,
      Resolution::Resolved(entry.clone()),
      vec![SI::new("a")],
    );
    let step_b = ResolutionStep::<DV, SI, TP>::new(
      SI::new("b"),
      span,
      Resolution::Resolved(entry.clone()),
      vec![SI::new("a"), SI::new("b")],
    );
    let result = ResolutionResult::<DV, SI, TP>::new(
      vec![step_a, step_b],
      Resolution::Resolved(entry.clone()),
    );
    assert!(result.is_qualified_path());
  }

  #[test]
  fn result_report_multi_step() {
    let mut cache = test_span_cache();
    let entry = dummy_symbol_entry(&mut cache);
    let span = test_span(&mut cache, 1);
    let step_a = ResolutionStep::<DV, SI, TP>::new(
      SI::new("a"),
      span,
      Resolution::Resolved(entry.clone()),
      vec![SI::new("a")],
    );
    let step_b = ResolutionStep::<DV, SI, TP>::new(
      SI::new("b"),
      span,
      Resolution::Resolved(entry.clone()),
      vec![SI::new("a"), SI::new("b")],
    );
    let result = ResolutionResult::<DV, SI, TP>::new(
      vec![step_a, step_b],
      Resolution::Resolved(entry.clone()),
    );
    let resolver = test_source_resolver();
    let report = result.report(&resolver);
    assert!(report.contains("Multi-step resolution"));
  }

  // -- SymboliqueResolver tests --------------------------------------------

  struct TestResolver {
    known: std::collections::HashMap<String, SymbolEntry<DV, SI, TP>>,
  }

  impl SymboliqueResolver<DV, SI, TP> for TestResolver {
    fn resolve(
      &self,
      _ctx: &dyn ResolveCtx<DV, SI, TP>,
      path: &TP,
      _from_path: &TP,
      _from_scope: SymbolEntry<DV, SI, TP>,
    ) -> Resolution<DV, SI, TP> {
      match self.known.get(path) {
        | Some(entry) => Resolution::Resolved(entry.clone()),
        | None => Resolution::Unresolved,
      }
    }
  }

  #[test]
  fn default_resolver_returns_failed() {
    let mut cache = test_span_cache();
    let scope = dummy_symbol_entry(&mut cache);
    let resolver = DefaultResolver;
    let path = "some.path".to_string();
    let from = "caller".to_string();
    let result: Resolution<DV, SI, TP> =
      resolver.resolve(&EmptyResolveCtx, &path, &from, scope);
    assert!(result.is_failed());
  }

  #[test]
  fn test_resolver_resolves_known_path() {
    let mut cache = test_span_cache();
    let entry = dummy_symbol_entry(&mut cache);
    let mut known = std::collections::HashMap::new();
    known.insert("foo.bar".to_string(), entry);
    let resolver = TestResolver { known };

    let scope = dummy_symbol_entry(&mut cache);
    let path = "foo.bar".to_string();
    let from = "caller".to_string();
    let result = resolver.resolve(&EmptyResolveCtx, &path, &from, scope);
    assert!(result.is_resolved());
  }

  #[test]
  fn test_resolver_fails_unknown_path() {
    let mut cache = test_span_cache();
    let entry = dummy_symbol_entry(&mut cache);
    let mut known = std::collections::HashMap::new();
    known.insert("foo.bar".to_string(), entry);
    let resolver = TestResolver { known };

    let scope = dummy_symbol_entry(&mut cache);
    let path = "baz.qux".to_string();
    let from = "caller".to_string();
    let result = resolver.resolve(&EmptyResolveCtx, &path, &from, scope);
    assert!(result.is_failed());
  }

  // -- Visibility-aware resolver -------------------------------------------

  /// Worked example: a resolver that enforces visibility via
  /// [`Visibility::is_visible_from`].
  struct VisibilityResolver {
    known: std::collections::HashMap<
      String,
      (SymbolEntry<DV, SI, TP>, SymbolVisibility),
    >,
  }

  impl SymboliqueResolver<DV, SI, TP> for VisibilityResolver {
    fn resolve(
      &self,
      _ctx: &dyn ResolveCtx<DV, SI, TP>,
      path: &TP,
      from_path: &TP,
      _from_scope: SymbolEntry<DV, SI, TP>,
    ) -> Resolution<DV, SI, TP> {
      match self.known.get(path) {
        | Some((entry, visibility)) => {
          if visibility.is_visible_from(path, from_path) {
            Resolution::Resolved(entry.clone())
          } else {
            Resolution::Inaccessible(entry.clone())
          }
        },
        | None => Resolution::Unresolved,
      }
    }
  }

  #[test]
  fn visibility_resolver_public_visible_anywhere() {
    let mut cache = test_span_cache();
    let entry = dummy_symbol_entry(&mut cache);
    let scope = dummy_symbol_entry(&mut cache);
    let mut known = std::collections::HashMap::new();
    known.insert(
      "mod_a.pub_fn".to_string(),
      (entry, SymbolVisibility::Public),
    );
    let resolver = VisibilityResolver { known };

    let result = resolver.resolve(
      &EmptyResolveCtx,
      &"mod_a.pub_fn".to_string(),
      &"mod_b.caller".to_string(),
      scope,
    );
    assert!(result.is_resolved());
  }

  #[test]
  fn visibility_resolver_private_visible_within_scope() {
    let mut cache = test_span_cache();
    let entry = dummy_symbol_entry(&mut cache);
    let scope = dummy_symbol_entry(&mut cache);
    let mut known = std::collections::HashMap::new();
    known.insert(
      "mod_a.priv_fn".to_string(),
      (entry, SymbolVisibility::Private),
    );
    let resolver = VisibilityResolver { known };

    let result = resolver.resolve(
      &EmptyResolveCtx,
      &"mod_a.priv_fn".to_string(),
      &"mod_a.priv_fn.body".to_string(),
      scope,
    );
    assert!(result.is_resolved());
  }

  #[test]
  fn visibility_resolver_private_hidden_outside_scope() {
    let mut cache = test_span_cache();
    let entry = dummy_symbol_entry(&mut cache);
    let scope = dummy_symbol_entry(&mut cache);
    let mut known = std::collections::HashMap::new();
    known.insert(
      "mod_a.priv_fn".to_string(),
      (entry, SymbolVisibility::Private),
    );
    let resolver = VisibilityResolver { known };

    let result = resolver.resolve(
      &EmptyResolveCtx,
      &"mod_a.priv_fn".to_string(),
      &"mod_b.caller".to_string(),
      scope,
    );
    assert!(result.is_failed());
  }
}
