use sophia_api::{
    MownStr,
    quad::Spog,
    term::{BaseDirection, BnodeId, IriRef, LanguageTag, Term, TermKind, VarName},
};

use crate::_stash::StringStash;

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum IndexedTerm {
    /// IRI stored in the buffer with the given index
    Iri(usize),
    /// IRI in the `rdf:` namespace (the argument is a relative IRI reference, e.g. `#type`)
    RdfBuiltin(IriRef<&'static str>),
    /// Blank node whose label in stored in the buffer with the given index
    BlankNode(usize),
    /// Typed literal whose lexical form and datatype IRI are stored in the buffers with the given indices
    TypedLiteral(usize, usize),
    /// Language tagged string (with or without base direction) whose lexical form and language tag are stored in the buffers with the given indices
    LangString(usize, usize, Option<BaseDirection>),
    /// Typed literal whose lexical form is stored in the buffer with the given index, and whose datatype is given relative to the `xsd:` namespace (e.g. `#integer`)
    XsdLiteral(usize, IriRef<&'static str>),
    /// Typed literal of type `xsd:boolean`
    BoolLiteral(bool),
    /// Triple term (whose object, predicate, and subject are stored below in the term stack)
    TripleTerm,
    /// **inverse** `rdf:reifies` edge generated by the Turtle syntactic sugar for reifying triples
    Reifier,
    /// `rdf:rest` edge generated by the Turtle syntactic sugar for collections
    Rest,
    /// Variable (in Sophia's generalized RDF)
    Variable(usize),
}

impl IndexedTerm {
    /// Copy term `t` at the top of `stack`, creating buffers as needed in `buffers`,
    /// and return the index of the copied term in `stack`.
    pub fn copy_term<T: Term>(
        t: T,
        stack: &mut Vec<IndexedTerm>,
        buffers: &mut StringStash,
    ) -> usize {
        use sophia_api::term::SimpleTerm::*;
        match t.as_simple() {
            Iri(iri_ref) => {
                let i = buffers.len();
                buffers.push().push_str(&iri_ref);
                stack.push(IndexedTerm::Iri(i));
            }
            BlankNode(bnode_id) => {
                let i = buffers.len();
                buffers.push().push_str(&bnode_id);
                stack.push(IndexedTerm::BlankNode(i));
            }
            LiteralDatatype(lex, dt) => {
                let ilex = buffers.len();
                buffers.push().push_str(&lex);
                buffers.push().push_str(&dt);
                stack.push(IndexedTerm::TypedLiteral(ilex, ilex + 1));
            }
            LiteralLanguage(lex, tag, dir) => {
                let ilex = buffers.len();
                buffers.push().push_str(&lex);
                buffers.push().push_str(&tag);
                stack.push(IndexedTerm::LangString(ilex, ilex + 1, dir));
            }
            Triple(t) => {
                Self::copy_term(&t[0], stack, buffers);
                Self::copy_term(&t[1], stack, buffers);
                Self::copy_term(&t[2], stack, buffers);
                stack.push(IndexedTerm::TripleTerm);
            }
            Variable(var_name) => {
                let i = buffers.len();
                buffers.push().push_str(&var_name);
                stack.push(IndexedTerm::Variable(i));
            }
        }
        stack.len() - 1
    }

    /// Pop the last term of the stack, freeing the corresponding buffers
    pub fn pop(stack: &mut Vec<IndexedTerm>, buffers: &mut StringStash) {
        use IndexedTerm::*;
        match stack.pop().unwrap() {
            Iri(_) | BlankNode(_) | XsdLiteral(..) | Variable(..) => buffers.pop(),
            TypedLiteral(..) | LangString(..) => {
                for _ in 0..2 {
                    buffers.pop()
                }
            }
            TripleTerm => {
                for _ in 0..3 {
                    Self::pop(stack, buffers);
                }
            }
            Reifier | RdfBuiltin(_) | BoolLiteral(_) | Rest => (),
        }
    }
}

pub(crate) trait IndexedTermSliceExt: AsRef<[IndexedTerm]> {
    /// Return the index of the term preceding term of index `i`.
    ///
    /// This is usually `i`-1, except for triple terms,
    /// which occupy several slots in the stack.
    ///
    /// This method is optimized for **strict** RDF 1.2.
    /// See also [`IndexedTermSliceExt::generalized_predecessor`]
    fn predecessor(&self, i: usize) -> Option<usize> {
        if matches!(self.as_ref()[i], IndexedTerm::TripleTerm) {
            let oi = i - 1;
            let pi = self.predecessor(oi).unwrap();
            // we are using the knowledge that predicate and subject can not be triple terms,
            // so the occupy only one slot in the stack
            debug_assert!(!matches!(self.as_ref()[pi - 1], IndexedTerm::TripleTerm));
            debug_assert!(!matches!(self.as_ref()[pi - 2], IndexedTerm::TripleTerm));
            Some(pi - 2)
        } else if i > 0 {
            Some(i - 1)
        } else {
            None
        }
    }

    /// Return the index of the term preceding term of index `i`.
    ///
    /// This is usually `i`-1, except for triple terms,
    /// which occupy several slots in the stack.
    fn generalized_predecessor(&self, i: usize) -> Option<usize> {
        if matches!(self.as_ref()[i], IndexedTerm::TripleTerm) {
            let oi = i - 1;
            let pi = self.generalized_predecessor(oi).unwrap();
            let si = self.generalized_predecessor(pi).unwrap();
            self.generalized_predecessor(si)
        } else if i > 0 {
            Some(i - 1)
        } else {
            None
        }
    }
}

impl<T: AsRef<[IndexedTerm]>> IndexedTermSliceExt for T {}

/// The kind of [`Term`]s generated by parsers.
#[derive(Clone, Copy, Debug)]
pub struct StashedTerm<'a> {
    pub(crate) terms: &'a [IndexedTerm],
    pub(crate) buffers: &'a StringStash,
    pub(crate) idx: usize,
}

impl<'a> StashedTerm<'a> {
    /// Construct a triple of [`StashedTerm`]s from the stacks of terms and buffers.
    ///
    /// IMPORTANT: this methods assume that a *strict* RDF 1.2 triple is stored on the stack.
    /// It is optimized for this case,
    /// and would not work for a generalized triple (i.e. with triple terms in the subject, predicate or graph position).
    /// See also [`StashedTerm::new_generialized_triple`]
    pub(crate) fn new_triple(terms: &'a [IndexedTerm], buffers: &'a StringStash) -> [Self; 3] {
        debug_assert!(terms.len() >= 3);
        let o = terms.len() - 1;
        let p = terms.predecessor(o).unwrap();
        debug_assert!(matches!(
            terms[p],
            IndexedTerm::Iri(..)
                | IndexedTerm::RdfBuiltin(..)
                | IndexedTerm::Reifier
                | IndexedTerm::Rest
        ));
        let s = p - 1;
        [
            Self {
                terms,
                buffers,
                idx: s,
            },
            Self {
                terms,
                buffers,
                idx: p,
            },
            Self {
                terms,
                buffers,
                idx: o,
            },
        ]
    }

    /// Construct a quad of [`StashedTerm`]s from the given stacks of terms and buffers,
    /// assuming that S, P, O and G were stacked in this order.
    ///
    /// IMPORTANT: this methods assume that a *strict* RDF 1.2 quad is stored on the stack.
    /// It is optimized for this case,
    /// and would not work for a generalized quad (i.e. with triple terms in the subject, predicate or graph position).
    /// See also [`StashedTerm::new_generialized_spo`]
    pub(crate) fn new_spog(terms: &'a [IndexedTerm], buffers: &'a StringStash) -> Spog<Self> {
        debug_assert!(terms.len() >= 4);
        let g = terms.len() - 1;
        debug_assert!(matches!(
            terms[g],
            IndexedTerm::Iri(..) | IndexedTerm::BlankNode(..)
        ));
        let o = g - 1;
        let p = terms.predecessor(o).unwrap();
        debug_assert!(matches!(
            terms[p],
            IndexedTerm::Iri(..)
                | IndexedTerm::RdfBuiltin(..)
                | IndexedTerm::Reifier
                | IndexedTerm::Rest
        ));
        let s = p - 1;
        (
            [
                Self {
                    terms,
                    buffers,
                    idx: s,
                },
                Self {
                    terms,
                    buffers,
                    idx: p,
                },
                Self {
                    terms,
                    buffers,
                    idx: o,
                },
            ],
            Some(Self {
                terms,
                buffers,
                idx: g,
            }),
        )
    }

    /// Construct a triple of [`StashedTerm`]s from the given stacks of terms and buffers,
    /// assuming that S, P and O were stacked in this order.
    ///
    /// IMPORTANT: this methods assume that a *generalized* RDF 1.2 triple can be stored on the stack.
    /// For a version optimized for strict RDF 1.2 triples, see [`StashedTerm::new_triple`]
    pub(crate) fn new_generalized_triple(
        terms: &'a [IndexedTerm],
        buffers: &'a StringStash,
    ) -> [Self; 3] {
        debug_assert!(terms.len() >= 3);
        let o = terms.len() - 1;
        let p = terms.generalized_predecessor(o).unwrap();
        let s = terms.generalized_predecessor(p).unwrap();
        [
            Self {
                terms,
                buffers,
                idx: s,
            },
            Self {
                terms,
                buffers,
                idx: p,
            },
            Self {
                terms,
                buffers,
                idx: o,
            },
        ]
    }

    /// Construct a quad of [`StashedTerm`]s from the given stacks of terms and buffers,
    /// assuming that S, P, O and G were stacked in this order.
    ///
    /// IMPORTANT: this methods assume that a *generalized* RDF 1.2 quad can be stored on the stack.
    /// For a version optimized for strict RDF 1.2 quads, see [`StashedTerm::new_spog`]
    pub(crate) fn new_generalized_spog(
        terms: &'a [IndexedTerm],
        buffers: &'a StringStash,
    ) -> Spog<Self> {
        debug_assert!(terms.len() >= 4);
        let g = terms.len() - 1;
        let o = terms.generalized_predecessor(g).unwrap();
        let p = terms.generalized_predecessor(o).unwrap();
        let s = terms.generalized_predecessor(p).unwrap();
        (
            [
                Self {
                    terms,
                    buffers,
                    idx: s,
                },
                Self {
                    terms,
                    buffers,
                    idx: p,
                },
                Self {
                    terms,
                    buffers,
                    idx: o,
                },
            ],
            Some(Self {
                terms,
                buffers,
                idx: g,
            }),
        )
    }

    pub(crate) fn indexed_term(&self) -> IndexedTerm {
        self.terms[self.idx]
    }

    pub(crate) fn buf(&self, i: usize) -> MownStr<'_> {
        MownStr::from_ref(&self.buffers[i])
    }
}

impl<'a> Term for StashedTerm<'a> {
    type BorrowTerm<'x>
        = Self
    where
        Self: 'x;

    fn kind(&self) -> sophia_api::prelude::TermKind {
        match self.indexed_term() {
            IndexedTerm::Iri(..) => TermKind::Iri,
            IndexedTerm::RdfBuiltin(_) => TermKind::Iri,
            IndexedTerm::BlankNode(..) => TermKind::BlankNode,
            IndexedTerm::TypedLiteral(..) => TermKind::Literal,
            IndexedTerm::LangString(..) => TermKind::Literal,
            IndexedTerm::XsdLiteral(..) => TermKind::Literal,
            IndexedTerm::BoolLiteral(_) => TermKind::Literal,
            IndexedTerm::TripleTerm => TermKind::Triple,
            IndexedTerm::Reifier => TermKind::Iri,
            IndexedTerm::Rest => TermKind::Iri,
            IndexedTerm::Variable(..) => TermKind::Variable,
        }
    }

    fn borrow_term(&self) -> Self::BorrowTerm<'_> {
        *self
    }

    fn iri(&self) -> Option<IriRef<MownStr<'_>>> {
        match self.indexed_term() {
            IndexedTerm::Iri(bi) => Some(IriRef::new_unchecked(self.buf(bi))),
            IndexedTerm::RdfBuiltin(iriref) => {
                debug_assert!(
                    iriref.starts_with('#'),
                    "{iriref:?} does not start with '#'"
                );
                Some(IriRef::new_unchecked(
                    format!("http://www.w3.org/1999/02/22-rdf-syntax-ns{iriref}").into(),
                ))
            }
            IndexedTerm::Reifier => Some(IriRef::new_unchecked(
                "http://www.w3.org/1999/02/22-rdf-syntax-ns#reifies".into(),
            )),
            IndexedTerm::Rest => Some(IriRef::new_unchecked(
                "http://www.w3.org/1999/02/22-rdf-syntax-ns#rest".into(),
            )),
            _ => None,
        }
    }

    fn bnode_id(&self) -> Option<BnodeId<MownStr<'_>>> {
        match self.indexed_term() {
            IndexedTerm::BlankNode(bi) => Some(BnodeId::new_unchecked(self.buf(bi))),
            _ => None,
        }
    }

    fn lexical_form(&self) -> Option<MownStr<'_>> {
        match self.indexed_term() {
            IndexedTerm::TypedLiteral(bi, _)
            | IndexedTerm::LangString(bi, _, _)
            | IndexedTerm::XsdLiteral(bi, _) => Some(self.buf(bi)),
            IndexedTerm::BoolLiteral(b) => Some((if b { "true" } else { "false" }).into()),
            _ => None,
        }
    }

    fn datatype(&self) -> Option<IriRef<MownStr<'_>>> {
        match self.indexed_term() {
            IndexedTerm::TypedLiteral(_, bi) => Some(IriRef::new_unchecked(self.buf(bi))),
            IndexedTerm::LangString(.., None) => Some(IriRef::new_unchecked(
                "http://www.w3.org/1999/02/22-rdf-syntax-ns#langString".into(),
            )),
            IndexedTerm::LangString(.., Some(_)) => Some(IriRef::new_unchecked(
                "http://www.w3.org/1999/02/22-rdf-syntax-ns#dirLangString".into(),
            )),
            IndexedTerm::XsdLiteral(_, iri_ref) => Some(xsd_dt(&iri_ref)),
            IndexedTerm::BoolLiteral(_) => Some(xsd_dt("#boolean")),
            _ => None,
        }
    }

    fn language_tag(&self) -> Option<LanguageTag<MownStr<'_>>> {
        match self.indexed_term() {
            IndexedTerm::LangString(_, bi, _) => Some(LanguageTag::new_unchecked(self.buf(bi))),
            _ => None,
        }
    }

    fn base_direction(&self) -> Option<sophia_api::term::BaseDirection> {
        match self.indexed_term() {
            IndexedTerm::LangString(_, _, dir) => dir,
            _ => None,
        }
    }

    fn triple(&self) -> Option<[Self::BorrowTerm<'_>; 3]> {
        if self.indexed_term() == IndexedTerm::TripleTerm {
            let oi = self.idx - 1;
            let pi = self.terms.generalized_predecessor(oi).unwrap();
            let si = self.terms.generalized_predecessor(pi).unwrap();
            Some([
                Self { idx: si, ..*self },
                Self { idx: pi, ..*self },
                Self { idx: oi, ..*self },
            ])
        } else {
            None
        }
    }

    fn to_triple(self) -> Option<[Self; 3]>
    where
        Self: Sized,
    {
        self.triple()
    }

    fn variable(&self) -> Option<VarName<MownStr<'_>>> {
        match self.indexed_term() {
            IndexedTerm::Variable(bi) => Some(VarName::new_unchecked(self.buf(bi))),
            _ => None,
        }
    }
}

fn xsd_dt(suffix: &str) -> IriRef<MownStr<'static>> {
    debug_assert!(IriRef::new(suffix).is_ok());
    debug_assert!(suffix.starts_with('#'));
    IriRef::new_unchecked(format!("http://www.w3.org/2001/XMLSchema{suffix}").into())
}

#[cfg(test)]
mod test {
    use super::*;
    use sophia_api::{
        ns::{rdf, xsd},
        term::{SimpleTerm, assert_consistent_term_impl},
        triple::Triple,
    };
    use test_case::test_case;

    #[test]
    fn iri() {
        let terms = [IndexedTerm::Iri(0)];
        let buffers = stash(&["tag:s"]);
        let term = StashedTerm {
            terms: &terms,
            buffers: &buffers,
            idx: terms.len() - 1,
        };
        assert_consistent_term_impl(&term);
        assert_eq!(TermKind::Iri, term.kind());
        assert_eq!(buffers[0].as_str(), term.iri().unwrap().unwrap());
    }

    #[test]
    fn rdf_builtin() {
        let terms = [IndexedTerm::RdfBuiltin(IriRef::new_unchecked("#type"))];
        let buffers = stash(&[]);
        let term = StashedTerm {
            terms: &terms,
            buffers: &buffers,
            idx: terms.len() - 1,
        };
        assert_consistent_term_impl(&term);
        assert_eq!(TermKind::Iri, term.kind());
        assert_eq!(rdf::type_, term.iri().unwrap());
    }

    #[test]
    fn bnode() {
        let terms = [IndexedTerm::BlankNode(0)];
        let buffers = stash(&["bn0001"]);
        let term = StashedTerm {
            terms: &terms,
            buffers: &buffers,
            idx: terms.len() - 1,
        };
        assert_consistent_term_impl(&term);
        assert_eq!(TermKind::BlankNode, term.kind());
        assert_eq!(buffers[0].as_str(), term.bnode_id().unwrap().as_str());
    }

    #[test]
    fn typed_literal() {
        let terms = [IndexedTerm::TypedLiteral(0, 1)];
        let buffers = stash(&["hello", "tag:dt"]);
        let term = StashedTerm {
            terms: &terms,
            buffers: &buffers,
            idx: terms.len() - 1,
        };
        assert_consistent_term_impl(&term);
        assert_eq!(TermKind::Literal, term.kind());
        assert_eq!(buffers[0].as_str(), term.lexical_form().unwrap());
        assert_eq!(buffers[1].as_str(), term.datatype().unwrap().unwrap());
    }

    #[test]
    fn lang_string() {
        let terms = [IndexedTerm::LangString(0, 1, None)];
        let buffers = stash(&["chat", "fr"]);
        let term = StashedTerm {
            terms: &terms,
            buffers: &buffers,
            idx: terms.len() - 1,
        };
        assert_consistent_term_impl(&term);
        assert_eq!(TermKind::Literal, term.kind());
        assert_eq!(buffers[0].as_str(), term.lexical_form().unwrap());
        assert_eq!(rdf::langString, term.datatype().unwrap());
    }

    #[test]
    fn lang_string_dir() {
        let terms = [IndexedTerm::LangString(0, 1, Some(BaseDirection::Ltr))];
        let buffers = stash(&["chat", "fr"]);
        let term = StashedTerm {
            terms: &terms,
            buffers: &buffers,
            idx: terms.len() - 1,
        };
        assert_consistent_term_impl(&term);
        assert_eq!(TermKind::Literal, term.kind());
        assert_eq!(buffers[0].as_str(), term.lexical_form().unwrap());
        assert_eq!(rdf::dirLangString, term.datatype().unwrap());
    }

    #[test]
    fn xsd_literal() {
        let terms = [IndexedTerm::XsdLiteral(
            0,
            IriRef::new_unchecked("#integer"),
        )];
        let buffers = stash(&["42"]);
        let term = StashedTerm {
            terms: &terms,
            buffers: &buffers,
            idx: terms.len() - 1,
        };
        assert_consistent_term_impl(&term);
        assert_eq!(TermKind::Literal, term.kind());
        assert_eq!(buffers[0].as_str(), term.lexical_form().unwrap());
        assert_eq!(xsd::integer, term.datatype().unwrap());
    }

    #[test_case(true)]
    #[test_case(false)]
    fn bool_literal(val: bool) {
        let terms = [IndexedTerm::BoolLiteral(val)];
        let buffers = stash(&[]);
        let term = StashedTerm {
            terms: &terms,
            buffers: &buffers,
            idx: terms.len() - 1,
        };
        assert_consistent_term_impl(&term);
        assert_eq!(TermKind::Literal, term.kind());
        assert_eq!(
            term.lexical_form().unwrap(),
            if val { "true" } else { "false" }
        );
        assert_eq!(xsd::boolean, term.datatype().unwrap());
    }

    #[test]
    fn triple_term() {
        let terms = [
            IndexedTerm::Iri(0),
            IndexedTerm::Iri(1),
            IndexedTerm::Iri(2),
            IndexedTerm::TripleTerm,
        ];
        let buffers = stash(&["tag:s", "tag:p", "tag:o"]);
        let term = StashedTerm {
            terms: &terms,
            buffers: &buffers,
            idx: terms.len() - 1,
        };
        assert_consistent_term_impl(&term);
        assert_eq!(TermKind::Triple, term.kind());
        let t = term.triple().unwrap();
        assert_eq!("tag:s", t.s().iri().unwrap().unwrap());
        assert_eq!("tag:p", t.p().iri().unwrap().unwrap());
        assert_eq!("tag:o", t.o().iri().unwrap().unwrap());
        let t = term.to_triple().unwrap();
        assert_eq!("tag:s", t.s().iri().unwrap().unwrap());
        assert_eq!("tag:p", t.p().iri().unwrap().unwrap());
        assert_eq!("tag:o", t.o().iri().unwrap().unwrap());
    }

    #[test]
    fn triple_term_nested() {
        let terms = [
            IndexedTerm::Iri(0),
            IndexedTerm::Iri(1),
            IndexedTerm::Iri(2),
            IndexedTerm::Iri(3),
            IndexedTerm::Iri(4),
            IndexedTerm::TripleTerm,
            IndexedTerm::TripleTerm,
        ];
        let buffers = stash(&["tag:s", "tag:p", "tag:a", "tag:b", "tag:c"]);
        let term = StashedTerm {
            terms: &terms,
            buffers: &buffers,
            idx: terms.len() - 1,
        };
        assert_consistent_term_impl(&term);
        assert_eq!(TermKind::Triple, term.kind());
        let t = term.triple().unwrap();
        assert_eq!("tag:s", t.s().iri().unwrap().unwrap());
        assert_eq!("tag:p", t.p().iri().unwrap().unwrap());
        assert!(t.o().is_triple());
        let t = term.to_triple().unwrap();
        assert_eq!("tag:s", t.s().iri().unwrap().unwrap());
        assert_eq!("tag:p", t.p().iri().unwrap().unwrap());
        assert!(t.o().is_triple());
    }

    #[test]
    fn reifier() {
        let terms = [IndexedTerm::Reifier];
        let buffers = stash(&[]);
        let term = StashedTerm {
            terms: &terms,
            buffers: &buffers,
            idx: terms.len() - 1,
        };
        assert_consistent_term_impl(&term);
        assert_eq!(TermKind::Iri, term.kind());
        assert_eq!(
            "http://www.w3.org/1999/02/22-rdf-syntax-ns#reifies",
            term.iri().unwrap().unwrap()
        );
    }

    #[test]
    fn rest() {
        let terms = [IndexedTerm::Rest];
        let buffers = stash(&[]);
        let term = StashedTerm {
            terms: &terms,
            buffers: &buffers,
            idx: terms.len() - 1,
        };
        assert_consistent_term_impl(&term);
        assert_eq!(TermKind::Iri, term.kind());
        assert_eq!(
            "http://www.w3.org/1999/02/22-rdf-syntax-ns#rest",
            term.iri().unwrap().unwrap()
        );
    }

    #[test]
    fn variable() {
        let terms = [IndexedTerm::Variable(0)];
        let buffers = stash(&["x"]);
        let term = StashedTerm {
            terms: &terms,
            buffers: &buffers,
            idx: terms.len() - 1,
        };
        assert_consistent_term_impl(&term);
        assert_eq!(TermKind::Variable, term.kind());
        assert_eq!(buffers[0].as_str(), term.variable().unwrap().as_str());
    }

    #[test]
    fn multiple_terms() {
        let terms = [
            IndexedTerm::TypedLiteral(0, 1),
            IndexedTerm::LangString(2, 3, None),
            IndexedTerm::LangString(4, 5, Some(BaseDirection::Ltr)),
            IndexedTerm::XsdLiteral(6, IriRef::new_unchecked("#integer")),
            IndexedTerm::BoolLiteral(true),
            IndexedTerm::Iri(7),
            IndexedTerm::RdfBuiltin(IriRef::new_unchecked("#type")),
            IndexedTerm::BlankNode(8),
            IndexedTerm::TripleTerm,
        ];
        let buffers = stash(&[
            "hello", "tag:dt", "chat", "fr", "chat", "fr", "42", "tag:s", "bn0001",
        ]);
        for _ in 0..terms.len() {
            let term = StashedTerm {
                terms: &terms,
                buffers: &buffers,
                idx: terms.len() - 1,
            };
            assert_consistent_term_impl(&term);
        }
    }

    fn stash(strs: &[&str]) -> StringStash {
        let mut ret = StringStash::default();
        for s in strs {
            ret.push().push_str(s);
        }
        ret
    }

    #[test]
    fn copy_iri() {
        let t = IriRef::new_unchecked("https://example.org/");
        let mut stack = vec![];
        let mut buffers = StringStash::default();
        let idx = IndexedTerm::copy_term(t, &mut stack, &mut buffers);
        let got = StashedTerm {
            terms: &stack,
            buffers: &buffers,
            idx,
        };
        assert!(Term::eq(&got, t));
    }

    #[test]
    fn copy_bnode() {
        let t = BnodeId::new_unchecked("b001");
        let mut stack = vec![];
        let mut buffers = StringStash::default();
        let idx = IndexedTerm::copy_term(t, &mut stack, &mut buffers);
        let got = StashedTerm {
            terms: &stack,
            buffers: &buffers,
            idx,
        };
        assert!(Term::eq(&got, t));
    }

    #[test]
    fn copy_typed_literal() {
        let t = 42;
        let mut stack = vec![];
        let mut buffers = StringStash::default();
        let idx = IndexedTerm::copy_term(t, &mut stack, &mut buffers);
        let got = StashedTerm {
            terms: &stack,
            buffers: &buffers,
            idx,
        };
        assert!(Term::eq(&got, t));
    }

    #[test]
    fn copy_language_string() {
        let t = "chat" * LanguageTag::new_unchecked("fr");
        let mut stack = vec![];
        let mut buffers = StringStash::default();
        let idx = IndexedTerm::copy_term(t, &mut stack, &mut buffers);
        let got = StashedTerm {
            terms: &stack,
            buffers: &buffers,
            idx,
        };
        assert!(Term::eq(&got, t));
    }

    #[test]
    fn copy_directional_language_string() {
        let t = "chat" * LanguageTag::new_unchecked("fr") * BaseDirection::Ltr;
        let mut stack = vec![];
        let mut buffers = StringStash::default();
        let idx = IndexedTerm::copy_term(&t, &mut stack, &mut buffers);
        let got = StashedTerm {
            terms: &stack,
            buffers: &buffers,
            idx,
        };
        assert!(Term::eq(&got, t));
    }

    #[test]
    fn copy_triple() {
        let t = SimpleTerm::from_triple([
            BnodeId::new_unchecked("s").as_simple(),
            IriRef::new_unchecked("x:p").as_simple(),
            "o".as_simple(),
        ]);
        let mut stack = vec![];
        let mut buffers = StringStash::default();
        let idx = IndexedTerm::copy_term(&t, &mut stack, &mut buffers);
        let got = StashedTerm {
            terms: &stack,
            buffers: &buffers,
            idx,
        };
        assert!(Term::eq(&got, t));
    }

    #[test]
    fn copy_variable() {
        let t = VarName::new_unchecked("x");
        let mut stack = vec![];
        let mut buffers = StringStash::default();
        let idx = IndexedTerm::copy_term(t, &mut stack, &mut buffers);
        let got = StashedTerm {
            terms: &stack,
            buffers: &buffers,
            idx,
        };
        assert!(Term::eq(&got, t));
    }
}
