use crate::error::Error;
use crate::path::{IpfsPath, PathRoot, SlashedPath};
use crate::repo::Repo;
use crate::{Block, Ipfs};
use futures::future::BoxFuture;
use futures::FutureExt;
use libipld::serde::{from_ipld, to_ipld};
use libipld::{
    cid::{
        multihash::{Code, MultihashDigest},
        Cid, Version,
    },
    codec::Codec,
    Ipld, IpldCodec,
};
use libp2p::PeerId;
use rust_unixfs::{
    dagpb::{wrap_node_data, NodeData},
    dir::{Cache, ShardedLookup},
    resolve, MaybeResolved,
};
use serde::de::DeserializeOwned;
use std::convert::TryFrom;
use std::error::Error as StdError;
use std::iter::Peekable;
use std::marker::PhantomData;
use std::time::Duration;
use thiserror::Error;
use tracing::{Instrument, Span};
#[derive(Debug, Error)]
pub enum ResolveError {
    #[error("block loading failed")]
    Loading(Cid, #[source] crate::Error),
    #[error("unsupported document")]
    UnsupportedDocument(Cid, #[source] Box<dyn StdError + Send + Sync + 'static>),
    #[error("list index out of range 0..{elements}: {index}")]
    ListIndexOutOfRange {
        document: Cid,
        path: SlashedPath,
        index: usize,
        elements: usize,
    },
    #[error("tried to resolve through an object that had no links")]
    NoLinks(Cid, SlashedPath),
    #[error("no link named {:?} under {0}", .1.iter().last().unwrap())]
    NotFound(Cid, SlashedPath),
    #[error("the path neiter contains nor resolves to a Cid")]
    NoCid(IpfsPath),
    #[error("can't resolve an IPNS path")]
    IpnsResolutionFailed(IpfsPath),
    #[error("path is not provided or is invalid")]
    PathNotProvided,
}
#[derive(Debug, Error)]
pub enum UnexpectedResolved {
    #[error("path resolved to unexpected type of document: {:?} or {}", .0, .1.source())]
    UnexpectedCodec(u64, ResolvedNode),
    #[error("path did not resolve to a block on {}", .0.source())]
    NonBlock(ResolvedNode),
}
#[derive(Debug)]
enum RawResolveLocalError {
    Loading(Cid, crate::Error),
    UnsupportedDocument(Cid, Box<dyn StdError + Send + Sync + 'static>),
    ListIndexOutOfRange {
        document: Cid,
        segment_index: usize,
        index: usize,
        elements: usize,
    },
    InvalidIndex {
        document: Cid,
        segment_index: usize,
    },
    NoLinks {
        document: Cid,
        segment_index: usize,
    },
    NotFound {
        document: Cid,
        segment_index: usize,
    },
}
impl RawResolveLocalError {
    fn add_starting_point_in_path(&mut self, start: usize) {
        use RawResolveLocalError::*;
        match self {
            ListIndexOutOfRange {
                ref mut segment_index,
                ..
            }
            | InvalidIndex {
                ref mut segment_index,
                ..
            }
            | NoLinks {
                ref mut segment_index,
                ..
            }
            | NotFound {
                ref mut segment_index,
                ..
            } => {
                *segment_index += start;
            }
            _ => {}
        }
    }
    fn with_path(self, path: IpfsPath) -> ResolveError {
        use RawResolveLocalError::*;
        match self {
            Loading(cid, e) => ResolveError::Loading(cid, e),
            UnsupportedDocument(cid, e) => ResolveError::UnsupportedDocument(cid, e),
            ListIndexOutOfRange {
                document,
                segment_index,
                index,
                elements,
            } => ResolveError::ListIndexOutOfRange {
                document,
                path: path.into_truncated(segment_index + 1),
                index,
                elements,
            },
            NoLinks {
                document,
                segment_index,
            } => ResolveError::NoLinks(document, path.into_truncated(segment_index + 1)),
            InvalidIndex {
                document,
                segment_index,
            }
            | NotFound {
                document,
                segment_index,
            } => ResolveError::NotFound(document, path.into_truncated(segment_index + 1)),
        }
    }
}
#[derive(Clone, Debug)]
pub struct IpldDag {
    ipfs: Option<Ipfs>,
    repo: Repo,
}
impl From<Repo> for IpldDag {
    fn from(repo: Repo) -> Self {
        IpldDag { ipfs: None, repo }
    }
}
impl IpldDag {
    pub fn new(ipfs: Ipfs) -> Self {
        let repo = ipfs.repo().clone();
        IpldDag {
            ipfs: Some(ipfs),
            repo,
        }
    }
    pub fn put_dag(&self, ipld: Ipld) -> DagPut {
        self.put().ipld(ipld)
    }
    pub fn get_dag<I: Into<IpfsPath>>(&self, path: I) -> DagGet {
        self.get().path(path)
    }
    pub fn put(&self) -> DagPut {
        DagPut::new(self.clone())
    }
    pub fn get(&self) -> DagGet {
        DagGet::new(self.clone())
    }
    pub(crate) async fn get_with_session(
        &self,
        session: Option<u64>,
        path: IpfsPath,
        providers: &[PeerId],
        local_only: bool,
        timeout: Option<Duration>,
    ) -> Result<Ipld, ResolveError> {
        let resolved_path = match &self.ipfs {
            Some(ipfs) => ipfs
                .resolve_ipns(&path, true)
                .await
                .map_err(|_| ResolveError::IpnsResolutionFailed(path))?,
            None => {
                if !matches!(path.root(), PathRoot::Ipld(_)) {
                    return Err(ResolveError::IpnsResolutionFailed(path));
                }
                path
            }
        };
        let cid = match resolved_path.root().cid() {
            Some(cid) => cid,
            None => return Err(ResolveError::NoCid(resolved_path)),
        };
        let mut iter = resolved_path.iter().peekable();
        let (node, _) = match self
            .resolve0(
                session, cid, &mut iter, true, providers, local_only, timeout,
            )
            .await
        {
            Ok(t) => t,
            Err(e) => {
                drop(iter);
                return Err(e.with_path(resolved_path));
            }
        };
        Ipld::try_from(node)
    }
    pub async fn resolve(
        &self,
        path: IpfsPath,
        follow_links: bool,
        providers: &[PeerId],
        local_only: bool,
    ) -> Result<(ResolvedNode, SlashedPath), ResolveError> {
        self.resolve_with_session(None, path, follow_links, providers, local_only, None)
            .await
    }
    pub(crate) async fn resolve_with_session(
        &self,
        session: Option<u64>,
        path: IpfsPath,
        follow_links: bool,
        providers: &[PeerId],
        local_only: bool,
        timeout: Option<Duration>,
    ) -> Result<(ResolvedNode, SlashedPath), ResolveError> {
        let resolved_path = match &self.ipfs {
            Some(ipfs) => ipfs
                .resolve_ipns(&path, true)
                .await
                .map_err(|_| ResolveError::IpnsResolutionFailed(path))?,
            None => {
                if !matches!(path.root(), PathRoot::Ipld(_)) {
                    return Err(ResolveError::IpnsResolutionFailed(path));
                }
                path
            }
        };
        let cid = match resolved_path.root().cid() {
            Some(cid) => cid,
            None => return Err(ResolveError::NoCid(resolved_path)),
        };
        let (node, matched_segments) = {
            let mut iter = resolved_path.iter().peekable();
            match self
                .resolve0(
                    session,
                    cid,
                    &mut iter,
                    follow_links,
                    providers,
                    local_only,
                    timeout,
                )
                .await
            {
                Ok(t) => t,
                Err(e) => {
                    drop(iter);
                    return Err(e.with_path(resolved_path));
                }
            }
        };
        let remaining_path = resolved_path.into_shifted(matched_segments);
        Ok((node, remaining_path))
    }
    #[allow(clippy::too_many_arguments)]
    async fn resolve0<'a>(
        &self,
        session: Option<u64>,
        cid: &Cid,
        segments: &mut Peekable<impl Iterator<Item = &'a str>>,
        follow_links: bool,
        providers: &[PeerId],
        local_only: bool,
        timeout: Option<Duration>,
    ) -> Result<(ResolvedNode, usize), RawResolveLocalError> {
        use LocallyResolved::*;
        let mut current = *cid;
        let mut total = 0;
        let mut cache = None;
        loop {
            let block = match self
                .repo
                .get_block_with_session(session, ¤t, providers, local_only, timeout)
                .await
            {
                Ok(block) => block,
                Err(e) => return Err(RawResolveLocalError::Loading(current, e)),
            };
            let start = total;
            let (resolution, matched) = match resolve_local(block, segments, &mut cache) {
                Ok(t) => t,
                Err(mut e) => {
                    e.add_starting_point_in_path(start);
                    return Err(e);
                }
            };
            total += matched;
            let (src, dest) = match resolution {
                Complete(ResolvedNode::Link(src, dest)) => (src, dest),
                Incomplete(src, lookup) => match self
                    .resolve_hamt(lookup, &mut cache, providers, local_only)
                    .await
                {
                    Ok(dest) => (src, dest),
                    Err(e) => return Err(RawResolveLocalError::UnsupportedDocument(src, e.into())),
                },
                Complete(other) => {
                    return Ok((other, start));
                }
            };
            if !follow_links {
                return Ok((ResolvedNode::Link(src, dest), total));
            } else {
                current = dest;
            }
        }
    }
    async fn resolve_hamt(
        &self,
        mut lookup: ShardedLookup<'_>,
        cache: &mut Option<Cache>,
        providers: &[PeerId],
        local_only: bool,
    ) -> Result<Cid, Error> {
        use MaybeResolved::*;
        loop {
            let (next, _) = lookup.pending_links();
            let block = self.repo.get_block(next, providers, local_only).await?;
            match lookup.continue_walk(block.data(), cache)? {
                NeedToLoadMore(next) => lookup = next,
                Found(cid) => return Ok(cid),
                NotFound => return Err(anyhow::anyhow!("key not found: ???")),
            }
        }
    }
}
#[must_use = "futures do nothing unless you `.await` or poll them"]
pub struct DagGet {
    dag_ipld: IpldDag,
    session: Option<u64>,
    path: Option<IpfsPath>,
    providers: Vec<PeerId>,
    local: bool,
    timeout: Option<Duration>,
    span: Option<Span>,
}
impl DagGet {
    pub fn new(dag: IpldDag) -> Self {
        Self {
            dag_ipld: dag,
            session: None,
            path: None,
            providers: vec![],
            local: false,
            timeout: None,
            span: None,
        }
    }
    #[allow(dead_code)]
    pub(crate) fn session(mut self, session: u64) -> Self {
        self.session = Some(session);
        self
    }
    pub fn path<P: Into<IpfsPath>>(mut self, path: P) -> Self {
        let path = path.into();
        self.path = Some(path);
        self
    }
    pub fn provider(mut self, peer_id: PeerId) -> Self {
        if !self.providers.contains(&peer_id) {
            self.providers.push(peer_id);
        }
        self
    }
    pub fn providers(mut self, providers: &[PeerId]) -> Self {
        self.providers = providers.into();
        self
    }
    pub fn local(mut self) -> Self {
        self.local = true;
        self
    }
    pub fn set_local(mut self, local: bool) -> Self {
        self.local = local;
        self
    }
    pub fn timeout(mut self, timeout: Duration) -> Self {
        self.timeout = Some(timeout);
        self
    }
    pub fn deserialized<D: DeserializeOwned>(self) -> DagGetDeserialize<D> {
        DagGetDeserialize {
            dag_get: self,
            _marker: PhantomData,
        }
    }
    pub fn span(mut self, span: Span) -> Self {
        self.span = Some(span);
        self
    }
}
impl std::future::IntoFuture for DagGet {
    type Output = Result<Ipld, ResolveError>;
    type IntoFuture = BoxFuture<'static, Self::Output>;
    fn into_future(self) -> Self::IntoFuture {
        let span = self.span.unwrap_or(Span::current());
        async move {
            let path = self.path.ok_or(ResolveError::PathNotProvided)?;
            self.dag_ipld
                .get_with_session(
                    self.session,
                    path,
                    &self.providers,
                    self.local,
                    self.timeout,
                )
                .await
        }
        .instrument(span)
        .boxed()
    }
}
#[must_use = "futures do nothing unless you `.await` or poll them"]
pub struct DagGetDeserialize<D> {
    dag_get: DagGet,
    _marker: PhantomData<D>,
}
impl<D> std::future::IntoFuture for DagGetDeserialize<D>
where
    D: DeserializeOwned,
{
    type Output = Result<D, anyhow::Error>;
    type IntoFuture = BoxFuture<'static, Self::Output>;
    fn into_future(self) -> Self::IntoFuture {
        let fut = self.dag_get.into_future();
        async move {
            let document = fut.await?;
            let data = from_ipld(document)?;
            Ok(data)
        }
        .boxed()
    }
}
#[must_use = "futures do nothing unless you `.await` or poll them"]
pub struct DagPut {
    dag_ipld: IpldDag,
    codec: IpldCodec,
    data: Box<dyn FnOnce() -> anyhow::Result<Ipld> + Send + 'static>,
    hash: Code,
    pinned: Option<bool>,
    span: Span,
    provide: bool,
}
impl DagPut {
    pub fn new(dag: IpldDag) -> Self {
        Self {
            dag_ipld: dag,
            codec: IpldCodec::DagCbor,
            data: Box::new(|| anyhow::bail!("data not available")),
            hash: Code::Sha2_256,
            pinned: None,
            span: Span::current(),
            provide: false,
        }
    }
    pub fn ipld(mut self, data: Ipld) -> Self {
        self.data = Box::new(move || Ok(data));
        self
    }
    pub fn serialize<S: serde::Serialize>(mut self, data: S) -> Self {
        let result = to_ipld(data).map_err(anyhow::Error::from);
        self.data = Box::new(move || result);
        self
    }
    pub fn pin(mut self, recursive: bool) -> Self {
        self.pinned = Some(recursive);
        self
    }
    pub fn provide(mut self) -> Self {
        self.provide = true;
        self
    }
    pub fn hash(mut self, code: Code) -> Self {
        self.hash = code;
        self
    }
    pub fn codec(mut self, codec: IpldCodec) -> Self {
        self.codec = codec;
        self
    }
    pub fn span(mut self, span: Span) -> Self {
        self.span = span;
        self
    }
}
impl std::future::IntoFuture for DagPut {
    type Output = Result<Cid, anyhow::Error>;
    type IntoFuture = BoxFuture<'static, Self::Output>;
    fn into_future(self) -> Self::IntoFuture {
        let span = self.span;
        async move {
            if self.provide && self.dag_ipld.ipfs.is_none() {
                anyhow::bail!("Ipfs is offline");
            }
            let _g = self.dag_ipld.repo.gc_guard().await;
            let data = (self.data)()?;
            let bytes = self.codec.encode(&data)?;
            let code = self.hash;
            let hash = code.digest(&bytes);
            let version = if self.codec == IpldCodec::DagPb {
                Version::V0
            } else {
                Version::V1
            };
            let cid = Cid::new(version, self.codec.into(), hash)?;
            let block = Block::new(cid, bytes)?;
            let cid = self.dag_ipld.repo.put_block(block).await?;
            if let Some(opt) = self.pinned {
                if !self.dag_ipld.repo.is_pinned(&cid).await? {
                    self.dag_ipld.repo.insert_pin(&cid, opt, true).await?;
                }
            }
            if self.provide {
                if let Some(ipfs) = &self.dag_ipld.ipfs {
                    if let Err(e) = ipfs.provide(cid).await {
                        error!("Failed to provide content over DHT: {e}")
                    }
                }
            }
            Ok(cid)
        }
        .instrument(span)
        .boxed()
    }
}
#[derive(Debug, PartialEq)]
pub enum ResolvedNode {
    Block(Block),
    DagPbData(Cid, NodeData<Box<[u8]>>),
    Projection(Cid, Ipld),
    Link(Cid, Cid),
}
impl ResolvedNode {
    pub fn source(&self) -> &Cid {
        match self {
            ResolvedNode::Block(block) => block.cid(),
            ResolvedNode::DagPbData(cid, ..)
            | ResolvedNode::Projection(cid, ..)
            | ResolvedNode::Link(cid, ..) => cid,
        }
    }
    pub fn into_unixfs_block(self) -> Result<Block, UnexpectedResolved> {
        if self.source().codec() != <IpldCodec as Into<u64>>::into(IpldCodec::DagPb) {
            Err(UnexpectedResolved::UnexpectedCodec(
                IpldCodec::DagPb.into(),
                self,
            ))
        } else {
            match self {
                ResolvedNode::Block(b) => Ok(b),
                _ => Err(UnexpectedResolved::NonBlock(self)),
            }
        }
    }
}
impl TryFrom<ResolvedNode> for Ipld {
    type Error = ResolveError;
    fn try_from(r: ResolvedNode) -> Result<Ipld, Self::Error> {
        use ResolvedNode::*;
        match r {
            Block(block) => Ok(block
                .decode::<IpldCodec, Ipld>()
                .map_err(move |e| ResolveError::UnsupportedDocument(*block.cid(), e.into()))?),
            DagPbData(_, node_data) => Ok(Ipld::Bytes(node_data.node_data().to_vec())),
            Projection(_, ipld) => Ok(ipld),
            Link(_, cid) => Ok(Ipld::Link(cid)),
        }
    }
}
#[derive(Debug)]
enum LocallyResolved<'a> {
    Complete(ResolvedNode),
    Incomplete(Cid, ShardedLookup<'a>),
}
#[cfg(test)]
impl LocallyResolved<'_> {
    fn unwrap_complete(self) -> ResolvedNode {
        match self {
            LocallyResolved::Complete(rn) => rn,
            x => unreachable!("{:?}", x),
        }
    }
}
impl From<ResolvedNode> for LocallyResolved<'static> {
    fn from(r: ResolvedNode) -> LocallyResolved<'static> {
        LocallyResolved::Complete(r)
    }
}
fn resolve_local<'a>(
    block: Block,
    segments: &mut Peekable<impl Iterator<Item = &'a str>>,
    cache: &mut Option<Cache>,
) -> Result<(LocallyResolved<'a>, usize), RawResolveLocalError> {
    if segments.peek().is_none() {
        return Ok((LocallyResolved::Complete(ResolvedNode::Block(block)), 0));
    }
    if block.cid().codec() == <IpldCodec as Into<u64>>::into(IpldCodec::DagPb) {
        let segment = segments.next().unwrap();
        let (cid, data) = block.into_inner();
        Ok(resolve_local_dagpb(
            cid,
            data.into(),
            segment,
            segments.peek().is_none(),
            cache,
        )?)
    } else {
        let ipld = match block.decode::<IpldCodec, Ipld>() {
            Ok(ipld) => ipld,
            Err(e) => {
                return Err(RawResolveLocalError::UnsupportedDocument(
                    *block.cid(),
                    e.into(),
                ))
            }
        };
        resolve_local_ipld(*block.cid(), ipld, segments)
    }
}
fn resolve_local_dagpb<'a>(
    cid: Cid,
    data: Box<[u8]>,
    segment: &'a str,
    is_last: bool,
    cache: &mut Option<Cache>,
) -> Result<(LocallyResolved<'a>, usize), RawResolveLocalError> {
    match resolve(&data, segment, cache) {
        Ok(MaybeResolved::NeedToLoadMore(lookup)) => {
            Ok((LocallyResolved::Incomplete(cid, lookup), 0))
        }
        Ok(MaybeResolved::Found(dest)) => {
            Ok((LocallyResolved::Complete(ResolvedNode::Link(cid, dest)), 1))
        }
        Ok(MaybeResolved::NotFound) => {
            if segment == "Data" && is_last {
                let wrapped = wrap_node_data(data).expect("already deserialized once");
                return Ok((
                    LocallyResolved::Complete(ResolvedNode::DagPbData(cid, wrapped)),
                    1,
                ));
            }
            Err(RawResolveLocalError::NotFound {
                document: cid,
                segment_index: 0,
            })
        }
        Err(rust_unixfs::ResolveError::UnexpectedType(ut)) if ut.is_file() => {
            Err(RawResolveLocalError::NotFound {
                document: cid,
                segment_index: 0,
            })
        }
        Err(e) => Err(RawResolveLocalError::UnsupportedDocument(cid, e.into())),
    }
}
fn resolve_local_ipld<'a>(
    document: Cid,
    mut ipld: Ipld,
    segments: &mut Peekable<impl Iterator<Item = &'a str>>,
) -> Result<(LocallyResolved<'a>, usize), RawResolveLocalError> {
    let mut matched_count = 0;
    loop {
        ipld = match ipld {
            Ipld::Link(cid) => {
                if segments.peek() != Some(&".") {
                    return Ok((ResolvedNode::Link(document, cid).into(), matched_count));
                } else {
                    Ipld::Link(cid)
                }
            }
            ipld => ipld,
        };
        ipld = match (ipld, segments.next()) {
            (Ipld::Link(cid), Some(".")) => {
                return Ok((ResolvedNode::Link(document, cid).into(), matched_count + 1));
            }
            (Ipld::Link(_), Some(_)) => {
                unreachable!("case already handled above before advancing the iterator")
            }
            (Ipld::Map(mut map), Some(segment)) => {
                let found = match map.remove(segment) {
                    Some(f) => f,
                    None => {
                        return Err(RawResolveLocalError::NotFound {
                            document,
                            segment_index: matched_count,
                        })
                    }
                };
                matched_count += 1;
                found
            }
            (Ipld::List(mut vec), Some(segment)) => match segment.parse::<usize>() {
                Ok(index) if index < vec.len() => {
                    matched_count += 1;
                    vec.swap_remove(index)
                }
                Ok(index) => {
                    return Err(RawResolveLocalError::ListIndexOutOfRange {
                        document,
                        segment_index: matched_count,
                        index,
                        elements: vec.len(),
                    });
                }
                Err(_) => {
                    return Err(RawResolveLocalError::InvalidIndex {
                        document,
                        segment_index: matched_count,
                    })
                }
            },
            (_, Some(_)) => {
                return Err(RawResolveLocalError::NoLinks {
                    document,
                    segment_index: matched_count,
                });
            }
            (anything, None) => {
                return Ok((
                    ResolvedNode::Projection(document, anything).into(),
                    matched_count,
                ))
            }
        };
    }
}
#[cfg(test)]
mod tests {
    use super::*;
    use crate::Node;
    use libipld::{cbor::DagCborCodec, ipld};
    #[tokio::test]
    async fn test_resolve_root_cid() {
        let Node { ipfs, .. } = Node::new("test_node").await;
        let dag = IpldDag::new(ipfs);
        let data = ipld!([1, 2, 3]);
        let cid = dag.put_dag(data.clone()).await.unwrap();
        let res = dag.get_dag(IpfsPath::from(cid)).await.unwrap();
        assert_eq!(res, data);
    }
    #[tokio::test]
    async fn test_resolve_array_elem() {
        let Node { ipfs, .. } = Node::new("test_node").await;
        let dag = IpldDag::new(ipfs);
        let data = ipld!([1, 2, 3]);
        let cid = dag.put_dag(data.clone()).await.unwrap();
        let res = dag
            .get_dag(IpfsPath::from(cid).sub_path("1").unwrap())
            .await
            .unwrap();
        assert_eq!(res, ipld!(2));
    }
    #[tokio::test]
    async fn test_resolve_nested_array_elem() {
        let Node { ipfs, .. } = Node::new("test_node").await;
        let dag = IpldDag::new(ipfs);
        let data = ipld!([1, [2], 3,]);
        let cid = dag.put_dag(data).await.unwrap();
        let res = dag
            .get_dag(IpfsPath::from(cid).sub_path("1/0").unwrap())
            .await
            .unwrap();
        assert_eq!(res, ipld!(2));
    }
    #[tokio::test]
    async fn test_resolve_object_elem() {
        let Node { ipfs, .. } = Node::new("test_node").await;
        let dag = IpldDag::new(ipfs);
        let data = ipld!({
            "key": false,
        });
        let cid = dag.put_dag(data).await.unwrap();
        let res = dag
            .get_dag(IpfsPath::from(cid).sub_path("key").unwrap())
            .await
            .unwrap();
        assert_eq!(res, ipld!(false));
    }
    #[tokio::test]
    async fn test_resolve_cid_elem() {
        let Node { ipfs, .. } = Node::new("test_node").await;
        let dag = IpldDag::new(ipfs);
        let data1 = ipld!([1]);
        let cid1 = dag.put_dag(data1).await.unwrap();
        let data2 = ipld!([cid1]);
        let cid2 = dag.put_dag(data2).await.unwrap();
        let res = dag
            .get_dag(IpfsPath::from(cid2).sub_path("0/0").unwrap())
            .await
            .unwrap();
        assert_eq!(res, ipld!(1));
    }
    fn example_doc_and_cid() -> (Cid, Ipld, Cid) {
        let cid = Cid::try_from("QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n").unwrap();
        let doc = ipld!({
            "nested": {
                "even": [
                    {
                        "more": 5
                    },
                    {
                        "or": "this",
                    },
                    {
                        "or": cid,
                    },
                    {
                        "5": "or",
                    }
                ],
            }
        });
        let root =
            Cid::try_from("bafyreielwgy762ox5ndmhx6kpi6go6il3gzahz3ngagb7xw3bj3aazeita").unwrap();
        (root, doc, cid)
    }
    #[test]
    fn resolve_cbor_locally_to_end() {
        let (root, example_doc, _) = example_doc_and_cid();
        let good_examples = [
            (
                "bafyreielwgy762ox5ndmhx6kpi6go6il3gzahz3ngagb7xw3bj3aazeita/nested/even/0/more",
                Ipld::Integer(5),
            ),
            (
                "bafyreielwgy762ox5ndmhx6kpi6go6il3gzahz3ngagb7xw3bj3aazeita/nested/even/1/or",
                Ipld::from("this"),
            ),
            (
                "bafyreielwgy762ox5ndmhx6kpi6go6il3gzahz3ngagb7xw3bj3aazeita/nested/even/3/5",
                Ipld::from("or"),
            ),
        ];
        for (path, expected) in &good_examples {
            let p = IpfsPath::try_from(*path).unwrap();
            let (resolved, matched_segments) =
                super::resolve_local_ipld(root, example_doc.clone(), &mut p.iter().peekable())
                    .unwrap();
            assert_eq!(matched_segments, 4);
            match resolved.unwrap_complete() {
                ResolvedNode::Projection(_, p) if &p == expected => {}
                x => unreachable!("unexpected {:?}", x),
            }
            let remaining_path = p.iter().skip(matched_segments).collect::<Vec<&str>>();
            assert!(remaining_path.is_empty(), "{remaining_path:?}");
        }
    }
    #[test]
    fn resolve_cbor_locally_to_link() {
        let (root, example_doc, target) = example_doc_and_cid();
        let p = IpfsPath::try_from(
            "bafyreielwgy762ox5ndmhx6kpi6go6il3gzahz3ngagb7xw3bj3aazeita/nested/even/2/or/foobar/trailer"
            ).unwrap();
        let (resolved, matched_segments) =
            super::resolve_local_ipld(root, example_doc, &mut p.iter().peekable()).unwrap();
        match resolved.unwrap_complete() {
            ResolvedNode::Link(_, cid) if cid == target => {}
            x => unreachable!("{:?}", x),
        }
        assert_eq!(matched_segments, 4);
        let remaining_path = p.iter().skip(matched_segments).collect::<Vec<&str>>();
        assert_eq!(remaining_path, &["foobar", "trailer"]);
    }
    #[test]
    fn resolve_cbor_locally_to_link_with_dot() {
        let (root, example_doc, cid) = example_doc_and_cid();
        let p = IpfsPath::try_from(
            "bafyreielwgy762ox5ndmhx6kpi6go6il3gzahz3ngagb7xw3bj3aazeita/nested/even/2/or/./foobar/trailer",
            )
        .unwrap();
        let (resolved, matched_segments) =
            super::resolve_local_ipld(root, example_doc, &mut p.iter().peekable()).unwrap();
        assert_eq!(resolved.unwrap_complete(), ResolvedNode::Link(root, cid));
        assert_eq!(matched_segments, 5);
        let remaining_path = p.iter().skip(matched_segments).collect::<Vec<&str>>();
        assert_eq!(remaining_path, &["foobar", "trailer"]);
    }
    #[test]
    fn resolve_cbor_locally_not_found_map_key() {
        let (root, example_doc, _) = example_doc_and_cid();
        let p = IpfsPath::try_from(
            "bafyreielwgy762ox5ndmhx6kpi6go6il3gzahz3ngagb7xw3bj3aazeita/foobar/trailer",
        )
        .unwrap();
        let e = super::resolve_local_ipld(root, example_doc, &mut p.iter().peekable()).unwrap_err();
        assert!(
            matches!(
                e,
                RawResolveLocalError::NotFound {
                    segment_index: 0,
                    ..
                }
            ),
            "{e:?}"
        );
    }
    #[test]
    fn resolve_cbor_locally_too_large_list_index() {
        let (root, example_doc, _) = example_doc_and_cid();
        let p = IpfsPath::try_from(
            "bafyreielwgy762ox5ndmhx6kpi6go6il3gzahz3ngagb7xw3bj3aazeita/nested/even/3000",
        )
        .unwrap();
        let e = super::resolve_local_ipld(root, example_doc, &mut p.iter().peekable()).unwrap_err();
        assert!(
            matches!(
                e,
                RawResolveLocalError::ListIndexOutOfRange {
                    segment_index: 2,
                    index: 3000,
                    elements: 4,
                    ..
                }
            ),
            "{e:?}"
        );
    }
    #[test]
    fn resolve_cbor_locally_non_usize_index() {
        let (root, example_doc, _) = example_doc_and_cid();
        let p = IpfsPath::try_from(
            "bafyreielwgy762ox5ndmhx6kpi6go6il3gzahz3ngagb7xw3bj3aazeita/nested/even/-1",
        )
        .unwrap();
        let e = super::resolve_local_ipld(root, example_doc, &mut p.iter().peekable()).unwrap_err();
        assert!(
            matches!(
                e,
                RawResolveLocalError::InvalidIndex {
                    segment_index: 2,
                    ..
                }
            ),
            "{e:?}"
        );
    }
    #[tokio::test]
    async fn resolve_through_link() {
        let Node { ipfs, .. } = Node::new("test_node").await;
        let dag = IpldDag::new(ipfs);
        let ipld = ipld!([1]);
        let cid1 = dag.put_dag(ipld).await.unwrap();
        let ipld = ipld!([cid1]);
        let cid2 = dag.put_dag(ipld).await.unwrap();
        let prefix = IpfsPath::from(cid2);
        let equiv_paths = vec![
            prefix.sub_path("0/0").unwrap(),
            prefix.sub_path("0/./0").unwrap(),
        ];
        for p in equiv_paths {
            let cloned = p.clone();
            match dag.resolve(p, true, &[], false).await.unwrap() {
                (ResolvedNode::Projection(_, Ipld::Integer(1)), remaining_path) => {
                    assert_eq!(remaining_path, ["0"][..], "{cloned}");
                }
                x => unreachable!("{:?}", x),
            }
        }
    }
    #[tokio::test]
    async fn fail_resolving_first_segment() {
        let Node { ipfs, .. } = Node::new("test_node").await;
        let dag = IpldDag::new(ipfs);
        let ipld = ipld!([1]);
        let cid1 = dag.put_dag(ipld).await.unwrap();
        let ipld = ipld!({ "0": cid1 });
        let cid2 = dag.put_dag(ipld).await.unwrap();
        let path = IpfsPath::from(cid2).sub_path("1/a").unwrap();
        let e = dag.resolve(path, true, &[], false).await.unwrap_err();
        assert_eq!(e.to_string(), format!("no link named \"1\" under {cid2}"));
    }
    #[tokio::test]
    async fn fail_resolving_last_segment() {
        let Node { ipfs, .. } = Node::new("test_node").await;
        let dag = IpldDag::new(ipfs);
        let ipld = ipld!([1]);
        let cid1 = dag.put_dag(ipld).await.unwrap();
        let ipld = ipld!([cid1]);
        let cid2 = dag.put_dag(ipld).await.unwrap();
        let path = IpfsPath::from(cid2).sub_path("0/a").unwrap();
        let e = dag.resolve(path, true, &[], false).await.unwrap_err();
        assert_eq!(e.to_string(), format!("no link named \"a\" under {cid1}"));
    }
    #[tokio::test]
    async fn fail_resolving_through_file() {
        let Node { ipfs, .. } = Node::new("test_node").await;
        let mut adder = rust_unixfs::file::adder::FileAdder::default();
        let (mut blocks, _) = adder.push(b"foobar\n");
        assert_eq!(blocks.next(), None);
        let mut blocks = adder.finish();
        let (cid, data) = blocks.next().unwrap();
        assert_eq!(blocks.next(), None);
        ipfs.put_block(Block::new(cid, data).unwrap())
            .await
            .unwrap();
        let path = IpfsPath::from(cid).sub_path("anything-here").unwrap();
        let e = ipfs
            .dag()
            .resolve(path, true, &[], false)
            .await
            .unwrap_err();
        assert_eq!(
            e.to_string(),
            format!("no link named \"anything-here\" under {cid}")
        );
    }
    #[tokio::test]
    async fn fail_resolving_through_dir() {
        let Node { ipfs, .. } = Node::new("test_node").await;
        let mut adder = rust_unixfs::file::adder::FileAdder::default();
        let (mut blocks, _) = adder.push(b"foobar\n");
        assert_eq!(blocks.next(), None);
        let mut blocks = adder.finish();
        let (cid, data) = blocks.next().unwrap();
        assert_eq!(blocks.next(), None);
        let total_size = data.len();
        ipfs.put_block(Block::new(cid, data).unwrap())
            .await
            .unwrap();
        let mut opts = rust_unixfs::dir::builder::TreeOptions::default();
        opts.wrap_with_directory();
        let mut tree = rust_unixfs::dir::builder::BufferingTreeBuilder::new(opts);
        tree.put_link("something/best-file-in-the-world", cid, total_size as u64)
            .unwrap();
        let mut iter = tree.build();
        let mut cids = Vec::new();
        while let Some(node) = iter.next_borrowed() {
            let node = node.unwrap();
            let block = Block::new(node.cid.to_owned(), node.block.into()).unwrap();
            ipfs.put_block(block).await.unwrap();
            cids.push(node.cid.to_owned());
        }
        cids.reverse();
        let path = IpfsPath::from(cids[0].to_owned())
            .sub_path("something/second-best-file")
            .unwrap();
        let e = ipfs
            .dag()
            .resolve(path, true, &[], false)
            .await
            .unwrap_err();
        assert_eq!(
            e.to_string(),
            format!("no link named \"second-best-file\" under {}", cids[1])
        );
    }
    #[test]
    fn observes_strict_order_of_map_keys() {
        let map = ipld!({
            "omega": Ipld::Null,
            "bar": Ipld::Null,
            "alpha": Ipld::Null,
            "foo": Ipld::Null,
        });
        let bytes = DagCborCodec.encode(&map).unwrap();
        assert_eq!(
            bytes.as_slice(),
            &[
                164, 99, 98, 97, 114, 246, 99, 102, 111, 111, 246, 101, 97, 108, 112, 104, 97, 246,
                101, 111, 109, 101, 103, 97, 246
            ]
        );
    }
}