//! Parses the Tx3 language.
//!
//! This module takes a string and parses it into Tx3 AST.

use std::sync::LazyLock;

use miette::SourceOffset;
use pest::{
    iterators::Pair,
    pratt_parser::{Assoc, Op, PrattParser},
    Parser,
};
use pest_derive::Parser;

use crate::{
    ast::*,
    cardano::{PlutusWitnessBlock, PlutusWitnessField},
};
#[derive(Parser)]
#[grammar = "tx3.pest"]
pub(crate) struct Tx3Grammar;

#[derive(Debug, thiserror::Error, miette::Diagnostic)]
#[error("Parsing error: {message}")]
#[diagnostic(code(tx3::parsing))]
pub struct Error {
    pub message: String,

    #[source_code]
    pub src: String,

    #[label]
    pub span: Span,
}

impl From<pest::error::Error<Rule>> for Error {
    fn from(error: pest::error::Error<Rule>) -> Self {
        match &error.variant {
            pest::error::ErrorVariant::ParsingError { positives, .. } => Error {
                message: format!("expected {positives:?}"),
                src: error.line().to_string(),
                span: error.location.into(),
            },
            pest::error::ErrorVariant::CustomError { message } => Error {
                message: message.clone(),
                src: error.line().to_string(),
                span: error.location.into(),
            },
        }
    }
}

impl From<pest::error::InputLocation> for Span {
    fn from(value: pest::error::InputLocation) -> Self {
        match value {
            pest::error::InputLocation::Pos(pos) => Self::new(pos, pos),
            pest::error::InputLocation::Span((start, end)) => Self::new(start, end),
        }
    }
}

impl From<pest::Span<'_>> for Span {
    fn from(span: pest::Span<'_>) -> Self {
        Self::new(span.start(), span.end())
    }
}

impl From<Span> for miette::SourceSpan {
    fn from(span: Span) -> Self {
        miette::SourceSpan::new(SourceOffset::from(span.start), span.end - span.start)
    }
}

pub trait AstNode: Sized {
    const RULE: Rule;

    fn parse(pair: Pair<Rule>) -> Result<Self, Error>;

    fn span(&self) -> &Span;
}

impl AstNode for Program {
    const RULE: Rule = Rule::program;

    fn parse(pair: Pair<Rule>) -> Result<Self, Error> {
        let span = pair.as_span().into();
        let inner = pair.into_inner();

        let mut program = Self {
            env: None,
            txs: Vec::new(),
            assets: Vec::new(),
            types: Vec::new(),
            aliases: Vec::new(),
            parties: Vec::new(),
            policies: Vec::new(),
            scope: None,
            span,
        };

        for pair in inner {
            match pair.as_rule() {
                Rule::env_def => program.env = Some(EnvDef::parse(pair)?),
                Rule::tx_def => program.txs.push(TxDef::parse(pair)?),
                Rule::asset_def => program.assets.push(AssetDef::parse(pair)?),
                Rule::record_def => program.types.push(TypeDef::parse(pair)?),
                Rule::variant_def => program.types.push(TypeDef::parse(pair)?),
                Rule::alias_def => program.aliases.push(AliasDef::parse(pair)?),
                Rule::party_def => program.parties.push(PartyDef::parse(pair)?),
                Rule::policy_def => program.policies.push(PolicyDef::parse(pair)?),
                Rule::EOI => break,
                x => unreachable!("Unexpected rule in program: {:?}", x),
            }
        }

        Ok(program)
    }

    fn span(&self) -> &Span {
        &self.span
    }
}

impl AstNode for EnvField {
    const RULE: Rule = Rule::env_field;

    fn parse(pair: Pair<Rule>) -> Result<Self, Error> {
        let span = pair.as_span().into();
        let mut inner = pair.into_inner();
        let identifier = inner.next().unwrap().as_str().to_string();
        let r#type = Type::parse(inner.next().unwrap())?;

        Ok(EnvField {
            name: identifier,
            r#type,
            span,
        })
    }

    fn span(&self) -> &Span {
        &self.span
    }
}

impl AstNode for EnvDef {
    const RULE: Rule = Rule::env_def;

    fn parse(pair: Pair<Rule>) -> Result<Self, Error> {
        let span = pair.as_span().into();
        let inner = pair.into_inner();

        let fields = inner
            .map(|x| EnvField::parse(x))
            .collect::<Result<Vec<_>, _>>()?;

        Ok(EnvDef { fields, span })
    }

    fn span(&self) -> &Span {
        &self.span
    }
}

impl AstNode for ParameterList {
    const RULE: Rule = Rule::parameter_list;

    fn parse(pair: Pair<Rule>) -> Result<Self, Error> {
        let span = pair.as_span().into();
        let inner = pair.into_inner();

        let mut parameters = Vec::new();

        for param in inner {
            let mut inner = param.into_inner();
            let name = Identifier::parse(inner.next().unwrap())?;
            let r#type = Type::parse(inner.next().unwrap())?;

            parameters.push(ParamDef { name, r#type });
        }

        Ok(ParameterList { parameters, span })
    }

    fn span(&self) -> &Span {
        &self.span
    }
}

impl AstNode for TxDef {
    const RULE: Rule = Rule::tx_def;

    fn parse(pair: Pair<Rule>) -> Result<Self, Error> {
        let span = pair.as_span().into();
        let mut inner = pair.into_inner();

        let name = Identifier::parse(inner.next().unwrap())?;
        let parameters = ParameterList::parse(inner.next().unwrap())?;

        let mut locals = None;
        let mut references = Vec::new();
        let mut inputs = Vec::new();
        let mut outputs = Vec::new();
        let mut validity = None;
        let mut burns = Vec::new();
        let mut mints = Vec::new();
        let mut adhoc = Vec::new();
        let mut collateral = Vec::new();
        let mut signers = None;
        let mut metadata = None;

        for item in inner {
            match item.as_rule() {
                Rule::locals_block => locals = Some(LocalsBlock::parse(item)?),
                Rule::reference_block => references.push(ReferenceBlock::parse(item)?),
                Rule::input_block => inputs.push(InputBlock::parse(item)?),
                Rule::output_block => outputs.push(OutputBlock::parse(item)?),
                Rule::validity_block => validity = Some(ValidityBlock::parse(item)?),
                Rule::mint_block => mints.push(MintBlock::parse(item)?),
                Rule::burn_block => burns.push(MintBlock::parse(item)?),
                Rule::chain_specific_block => adhoc.push(ChainSpecificBlock::parse(item)?),
                Rule::collateral_block => collateral.push(CollateralBlock::parse(item)?),
                Rule::signers_block => signers = Some(SignersBlock::parse(item)?),
                Rule::metadata_block => metadata = Some(MetadataBlock::parse(item)?),
                x => unreachable!("Unexpected rule in tx_def: {:?}", x),
            }
        }

        Ok(TxDef {
            name,
            parameters,
            locals,
            references,
            inputs,
            outputs,
            validity,
            mints,
            burns,
            signers,
            adhoc,
            scope: None,
            span,
            collateral,
            metadata,
        })
    }

    fn span(&self) -> &Span {
        &self.span
    }
}

impl AstNode for Identifier {
    const RULE: Rule = Rule::identifier;

    fn parse(pair: Pair<Rule>) -> Result<Self, Error> {
        Ok(Identifier {
            value: pair.as_str().to_string(),
            symbol: None,
            span: pair.as_span().into(),
        })
    }

    fn span(&self) -> &Span {
        &self.span
    }
}

impl AstNode for StringLiteral {
    const RULE: Rule = Rule::string;

    fn parse(pair: Pair<Rule>) -> Result<Self, Error> {
        Ok(StringLiteral {
            value: pair.as_str()[1..pair.as_str().len() - 1].to_string(),
            span: pair.as_span().into(),
        })
    }

    fn span(&self) -> &Span {
        &self.span
    }
}

impl AstNode for HexStringLiteral {
    const RULE: Rule = Rule::hex_string;

    fn parse(pair: Pair<Rule>) -> Result<Self, Error> {
        Ok(HexStringLiteral {
            value: pair.as_str()[2..].to_string(),
            span: pair.as_span().into(),
        })
    }

    fn span(&self) -> &Span {
        &self.span
    }
}

impl AstNode for PartyDef {
    const RULE: Rule = Rule::party_def;

    fn parse(pair: Pair<Rule>) -> Result<Self, Error> {
        let span = pair.as_span().into();
        let mut inner = pair.into_inner();
        let identifier = Identifier::parse(inner.next().unwrap())?;

        Ok(PartyDef {
            name: identifier,
            span,
        })
    }

    fn span(&self) -> &Span {
        &self.span
    }
}

impl AstNode for LocalsAssign {
    const RULE: Rule = Rule::locals_assign;

    fn parse(pair: Pair<Rule>) -> Result<Self, Error> {
        let span = pair.as_span().into();
        let mut inner = pair.into_inner();

        let name = Identifier::parse(inner.next().unwrap())?;
        let value = DataExpr::parse(inner.next().unwrap())?;

        Ok(LocalsAssign { name, value, span })
    }

    fn span(&self) -> &Span {
        &self.span
    }
}

impl AstNode for LocalsBlock {
    const RULE: Rule = Rule::locals_block;

    fn parse(pair: Pair<Rule>) -> Result<Self, Error> {
        let span = pair.as_span().into();
        let inner = pair.into_inner();

        let assigns = inner
            .map(|x| LocalsAssign::parse(x))
            .collect::<Result<Vec<_>, _>>()?;

        Ok(LocalsBlock { assigns, span })
    }

    fn span(&self) -> &Span {
        &self.span
    }
}

impl AstNode for ReferenceBlock {
    const RULE: Rule = Rule::reference_block;

    fn parse(pair: Pair<Rule>) -> Result<Self, Error> {
        let span = pair.as_span().into();
        let mut inner = pair.into_inner();

        let name = inner.next().unwrap().as_str().to_string();

        let pair = inner.next().unwrap();
        match pair.as_rule() {
            Rule::input_block_ref => {
                let pair = pair.into_inner().next().unwrap();
                let r#ref = DataExpr::parse(pair)?;
                Ok(ReferenceBlock { name, r#ref, span })
            }
            x => unreachable!("Unexpected rule in ref_input_block: {:?}", x),
        }
    }

    fn span(&self) -> &Span {
        &self.span
    }
}

impl AstNode for CollateralBlockField {
    const RULE: Rule = Rule::collateral_block_field;

    fn parse(pair: Pair<Rule>) -> Result<Self, Error> {
        match pair.as_rule() {
            Rule::input_block_from => {
                let pair = pair.into_inner().next().unwrap();
                let x = CollateralBlockField::From(DataExpr::parse(pair)?);
                Ok(x)
            }
            Rule::input_block_min_amount => {
                let pair = pair.into_inner().next().unwrap();
                let x = CollateralBlockField::MinAmount(DataExpr::parse(pair)?);
                Ok(x)
            }
            Rule::input_block_ref => {
                let pair = pair.into_inner().next().unwrap();
                let x = CollateralBlockField::Ref(DataExpr::parse(pair)?);
                Ok(x)
            }
            x => unreachable!("Unexpected rule in collateral_block: {:?}", x),
        }
    }

    fn span(&self) -> &Span {
        match self {
            Self::From(x) => x.span(),
            Self::MinAmount(x) => x.span(),
            Self::Ref(x) => x.span(),
        }
    }
}

impl AstNode for CollateralBlock {
    const RULE: Rule = Rule::collateral_block;

    fn parse(pair: Pair<Rule>) -> Result<Self, Error> {
        let span = pair.as_span().into();
        let inner = pair.into_inner();

        let fields = inner
            .map(|x| CollateralBlockField::parse(x))
            .collect::<Result<Vec<_>, _>>()?;

        Ok(CollateralBlock { fields, span })
    }

    fn span(&self) -> &Span {
        &self.span
    }
}

impl AstNode for MetadataBlockField {
    const RULE: Rule = Rule::metadata_block_field;

    fn parse(pair: Pair<Rule>) -> Result<Self, Error> {
        let span = pair.as_span().into();
        match pair.as_rule() {
            Rule::metadata_block_field => {
                let mut inner = pair.into_inner();
                let key = inner.next().unwrap();
                let value = inner.next().unwrap();
                Ok(MetadataBlockField {
                    key: DataExpr::parse(key)?,
                    value: DataExpr::parse(value)?,
                    span,
                })
            }
            x => unreachable!("Unexpected rule in metadata_block: {:?}", x),
        }
    }

    fn span(&self) -> &Span {
        &self.span
    }
}

impl AstNode for MetadataBlock {
    const RULE: Rule = Rule::metadata_block;

    fn parse(pair: Pair<Rule>) -> Result<Self, Error> {
        let span = pair.as_span().into();
        let inner = pair.into_inner();

        let fields = inner
            .map(|x| MetadataBlockField::parse(x))
            .collect::<Result<Vec<_>, _>>()?;

        Ok(MetadataBlock { fields, span })
    }

    fn span(&self) -> &Span {
        &self.span
    }
}

impl AstNode for InputBlockField {
    const RULE: Rule = Rule::input_block_field;

    fn parse(pair: Pair<Rule>) -> Result<Self, Error> {
        match pair.as_rule() {
            Rule::input_block_from => {
                let pair = pair.into_inner().next().unwrap();
                let x = InputBlockField::From(DataExpr::parse(pair)?);
                Ok(x)
            }
            Rule::input_block_datum_is => {
                let pair = pair.into_inner().next().unwrap();
                let x = InputBlockField::DatumIs(Type::parse(pair)?);
                Ok(x)
            }
            Rule::input_block_min_amount => {
                let pair = pair.into_inner().next().unwrap();
                let x = InputBlockField::MinAmount(DataExpr::parse(pair)?);
                Ok(x)
            }
            Rule::input_block_redeemer => {
                let pair = pair.into_inner().next().unwrap();
                let x = InputBlockField::Redeemer(DataExpr::parse(pair)?);
                Ok(x)
            }
            Rule::input_block_ref => {
                let pair = pair.into_inner().next().unwrap();
                let x = InputBlockField::Ref(DataExpr::parse(pair)?);
                Ok(x)
            }
            x => unreachable!("Unexpected rule in input_block: {:?}", x),
        }
    }

    fn span(&self) -> &Span {
        match self {
            Self::From(x) => x.span(),
            Self::DatumIs(x) => x.span(),
            Self::MinAmount(x) => x.span(),
            Self::Redeemer(x) => x.span(),
            Self::Ref(x) => x.span(),
        }
    }
}

impl AstNode for InputBlock {
    const RULE: Rule = Rule::input_block;

    fn parse(pair: Pair<Rule>) -> Result<Self, Error> {
        let span = pair.as_span().into();
        let mut inner = pair.into_inner();

        let next = inner.next().unwrap();

        let (many, name) = match next.as_rule() {
            Rule::input_many => (true, inner.next().unwrap().as_str().to_string()),
            Rule::identifier => (false, next.as_str().to_string()),
            _ => unreachable!("Unexpected rule in input_block: {:?}", next.as_rule()),
        };

        let fields = inner
            .map(|x| InputBlockField::parse(x))
            .collect::<Result<Vec<_>, _>>()?;

        Ok(InputBlock {
            name,
            many,
            fields,
            span,
        })
    }

    fn span(&self) -> &Span {
        &self.span
    }
}

impl AstNode for OutputBlockField {
    const RULE: Rule = Rule::output_block_field;

    fn parse(pair: Pair<Rule>) -> Result<Self, Error> {
        match pair.as_rule() {
            Rule::output_block_to => {
                let pair = pair.into_inner().next().unwrap();
                let x = OutputBlockField::To(Box::new(DataExpr::parse(pair)?));
                Ok(x)
            }
            Rule::output_block_amount => {
                let pair = pair.into_inner().next().unwrap();
                let x = OutputBlockField::Amount(DataExpr::parse(pair)?.into());
                Ok(x)
            }
            Rule::output_block_datum => {
                let pair = pair.into_inner().next().unwrap();
                let x = OutputBlockField::Datum(DataExpr::parse(pair)?.into());
                Ok(x)
            }
            x => unreachable!("Unexpected rule in output_block_field: {:?}", x),
        }
    }

    fn span(&self) -> &Span {
        match self {
            Self::To(x) => x.span(),
            Self::Amount(x) => x.span(),
            Self::Datum(x) => x.span(),
        }
    }
}

impl AstNode for OutputBlock {
    const RULE: Rule = Rule::output_block;

    fn parse(pair: Pair<Rule>) -> Result<Self, Error> {
        let span = pair.as_span().into();
        let mut inner = pair.into_inner();

        let optional = inner
            .peek()
            .map_or(false, |first| first.as_rule() == Rule::output_optional);

        if optional {
            inner.next();
        }

        let has_name = inner
            .peek()
            .map(|x| x.as_rule() == Rule::identifier)
            .unwrap_or_default();

        let name = if has_name {
            Some(Identifier::parse(inner.next().unwrap())?)
        } else {
            None
        };

        let fields = inner
            .map(|x| OutputBlockField::parse(x))
            .collect::<Result<Vec<_>, _>>()?;

        Ok(OutputBlock {
            name,
            optional,
            fields,
            span,
        })
    }

    fn span(&self) -> &Span {
        &self.span
    }
}

impl AstNode for ValidityBlockField {
    const RULE: Rule = Rule::validity_block_field;

    fn parse(pair: Pair<Rule>) -> Result<Self, Error> {
        match pair.as_rule() {
            Rule::validity_since_slot => {
                let pair = pair.into_inner().next().unwrap();
                let x = ValidityBlockField::SinceSlot(DataExpr::parse(pair)?.into());
                Ok(x)
            }
            Rule::validity_until_slot => {
                let pair = pair.into_inner().next().unwrap();
                let x = ValidityBlockField::UntilSlot(DataExpr::parse(pair)?.into());
                Ok(x)
            }
            x => unreachable!("Unexpected rule in validity_block: {:?}", x),
        }
    }

    fn span(&self) -> &Span {
        match self {
            Self::UntilSlot(x) => x.span(),
            Self::SinceSlot(x) => x.span(),
        }
    }
}

impl AstNode for ValidityBlock {
    const RULE: Rule = Rule::validity_block;

    fn parse(pair: Pair<Rule>) -> Result<Self, Error> {
        let span = pair.as_span().into();
        let inner = pair.into_inner();

        let fields = inner
            .map(|x| ValidityBlockField::parse(x))
            .collect::<Result<Vec<_>, _>>()?;

        Ok(ValidityBlock { fields, span })
    }

    fn span(&self) -> &Span {
        &self.span
    }
}

impl AstNode for MintBlockField {
    const RULE: Rule = Rule::mint_block_field;

    fn parse(pair: Pair<Rule>) -> Result<Self, Error> {
        match pair.as_rule() {
            Rule::mint_block_amount => {
                let pair = pair.into_inner().next().unwrap();
                let x = MintBlockField::Amount(DataExpr::parse(pair)?.into());
                Ok(x)
            }
            Rule::mint_block_redeemer => {
                let pair = pair.into_inner().next().unwrap();
                let x = MintBlockField::Redeemer(DataExpr::parse(pair)?.into());
                Ok(x)
            }
            x => unreachable!("Unexpected rule in output_block_field: {:?}", x),
        }
    }

    fn span(&self) -> &Span {
        match self {
            Self::Amount(x) => x.span(),
            Self::Redeemer(x) => x.span(),
        }
    }
}

impl AstNode for SignersBlock {
    const RULE: Rule = Rule::signers_block;

    fn parse(pair: Pair<Rule>) -> Result<Self, Error> {
        let span = pair.as_span().into();
        let inner = pair.into_inner();

        let signers = inner
            .map(|x| DataExpr::parse(x))
            .collect::<Result<Vec<_>, _>>()?;

        Ok(SignersBlock { signers, span })
    }

    fn span(&self) -> &Span {
        &self.span
    }
}

impl AstNode for MintBlock {
    const RULE: Rule = Rule::mint_block;

    fn parse(pair: Pair<Rule>) -> Result<Self, Error> {
        let span = pair.as_span().into();
        let inner = pair.into_inner();

        let fields = inner
            .map(|x| MintBlockField::parse(x))
            .collect::<Result<Vec<_>, _>>()?;

        Ok(MintBlock { fields, span })
    }

    fn span(&self) -> &Span {
        &self.span
    }
}

impl AstNode for RecordField {
    const RULE: Rule = Rule::record_field;

    fn parse(pair: Pair<Rule>) -> Result<Self, Error> {
        let span = pair.as_span().into();
        let mut inner = pair.into_inner();
        let identifier = Identifier::parse(inner.next().unwrap())?;
        let r#type = Type::parse(inner.next().unwrap())?;

        Ok(RecordField {
            name: identifier,
            r#type,
            span,
        })
    }

    fn span(&self) -> &Span {
        &self.span
    }
}

impl AstNode for PolicyField {
    const RULE: Rule = Rule::policy_def_field;

    fn parse(pair: Pair<Rule>) -> Result<Self, Error> {
        match pair.as_rule() {
            Rule::policy_def_hash => Ok(PolicyField::Hash(DataExpr::parse(
                pair.into_inner().next().unwrap(),
            )?)),
            Rule::policy_def_script => Ok(PolicyField::Script(DataExpr::parse(
                pair.into_inner().next().unwrap(),
            )?)),
            Rule::policy_def_ref => Ok(PolicyField::Ref(DataExpr::parse(
                pair.into_inner().next().unwrap(),
            )?)),
            x => unreachable!("Unexpected rule in policy_field: {:?}", x),
        }
    }

    fn span(&self) -> &Span {
        match self {
            Self::Hash(x) => x.span(),
            Self::Script(x) => x.span(),
            Self::Ref(x) => x.span(),
        }
    }
}

impl AstNode for PolicyConstructor {
    const RULE: Rule = Rule::policy_def_constructor;

    fn parse(pair: Pair<Rule>) -> Result<Self, Error> {
        let span = pair.as_span().into();
        let inner = pair.into_inner();

        let fields = inner
            .map(|x| PolicyField::parse(x))
            .collect::<Result<Vec<_>, _>>()?;

        Ok(PolicyConstructor { fields, span })
    }

    fn span(&self) -> &Span {
        &self.span
    }
}

impl AstNode for PolicyValue {
    const RULE: Rule = Rule::policy_def_value;

    fn parse(pair: Pair<Rule>) -> Result<Self, Error> {
        match pair.as_rule() {
            Rule::policy_def_constructor => {
                Ok(PolicyValue::Constructor(PolicyConstructor::parse(pair)?))
            }
            Rule::policy_def_assign => Ok(PolicyValue::Assign(HexStringLiteral::parse(
                pair.into_inner().next().unwrap(),
            )?)),
            x => unreachable!("Unexpected rule in policy_value: {:?}", x),
        }
    }

    fn span(&self) -> &Span {
        match self {
            Self::Constructor(x) => x.span(),
            Self::Assign(x) => x.span(),
        }
    }
}

impl AstNode for PolicyDef {
    const RULE: Rule = Rule::policy_def;

    fn parse(pair: Pair<Rule>) -> Result<Self, Error> {
        let span = pair.as_span().into();
        let mut inner = pair.into_inner();
        let name = Identifier::parse(inner.next().unwrap())?;
        let value = PolicyValue::parse(inner.next().unwrap())?;

        Ok(PolicyDef { name, value, span })
    }

    fn span(&self) -> &Span {
        &self.span
    }
}

impl AstNode for AnyAssetConstructor {
    const RULE: Rule = Rule::any_asset_constructor;

    fn parse(pair: Pair<Rule>) -> Result<Self, Error> {
        let span = pair.as_span().into();
        let mut inner = pair.into_inner();

        let policy = DataExpr::parse(inner.next().unwrap())?;
        let asset_name = DataExpr::parse(inner.next().unwrap())?;
        let amount = DataExpr::parse(inner.next().unwrap())?;

        Ok(AnyAssetConstructor {
            policy: Box::new(policy),
            asset_name: Box::new(asset_name),
            amount: Box::new(amount),
            span,
        })
    }

    fn span(&self) -> &Span {
        &self.span
    }
}

impl AstNode for ConcatOp {
    const RULE: Rule = Rule::concat_constructor;

    fn parse(pair: Pair<Rule>) -> Result<Self, Error> {
        let span = pair.as_span().into();
        let mut inner = pair.into_inner();

        let lhs = DataExpr::parse(inner.next().unwrap())?;
        let rhs = DataExpr::parse(inner.next().unwrap())?;

        Ok(ConcatOp {
            lhs: Box::new(lhs),
            rhs: Box::new(rhs),
            span,
        })
    }

    fn span(&self) -> &Span {
        &self.span
    }
}

impl AstNode for crate::ast::FnCall {
    const RULE: Rule = Rule::fn_call;

    fn parse(pair: Pair<Rule>) -> Result<Self, Error> {
        let span = pair.as_span().into();
        let mut inner = pair.into_inner();

        let callee = Identifier::parse(inner.next().unwrap())?;

        let mut args = Vec::new();
        for arg_pair in inner {
            args.push(DataExpr::parse(arg_pair)?);
        }

        Ok(crate::ast::FnCall { callee, args, span })
    }

    fn span(&self) -> &Span {
        &self.span
    }
}

impl AstNode for RecordConstructorField {
    const RULE: Rule = Rule::record_constructor_field;

    fn parse(pair: Pair<Rule>) -> Result<Self, Error> {
        let span = pair.as_span().into();
        let mut inner = pair.into_inner();

        let name = Identifier::parse(inner.next().unwrap())?;
        let value = DataExpr::parse(inner.next().unwrap())?;

        Ok(RecordConstructorField {
            name,
            value: Box::new(value),
            span,
        })
    }

    fn span(&self) -> &Span {
        &self.span
    }
}

impl AstNode for UtxoRef {
    const RULE: Rule = Rule::utxo_ref;

    fn parse(pair: Pair<Rule>) -> Result<Self, Error> {
        let span = pair.as_span().into();
        let raw_ref = pair.as_span().as_str()[2..].to_string();
        let (raw_txid, raw_output_ix) = raw_ref.split_once("#").expect("Invalid utxo ref");

        Ok(UtxoRef {
            txid: hex::decode(raw_txid).expect("Invalid hex txid"),
            index: raw_output_ix.parse().expect("Invalid output index"),
            span,
        })
    }

    fn span(&self) -> &Span {
        &self.span
    }
}

impl AstNode for StructConstructor {
    const RULE: Rule = Rule::struct_constructor;

    fn parse(pair: Pair<Rule>) -> Result<Self, Error> {
        let span = pair.as_span().into();
        let mut inner = pair.into_inner();

        let r#type = Identifier::parse(inner.next().unwrap())?;
        let case = VariantCaseConstructor::parse(inner.next().unwrap())?;

        Ok(StructConstructor {
            r#type,
            case,
            scope: None,
            span,
        })
    }

    fn span(&self) -> &Span {
        &self.span
    }
}

impl VariantCaseConstructor {
    fn implicit_parse(pair: Pair<Rule>) -> Result<Self, Error> {
        let span = pair.as_span().into();
        let inner = pair.into_inner();

        let mut fields = Vec::new();
        let mut spread = None;

        for pair in inner {
            match pair.as_rule() {
                Rule::record_constructor_field => {
                    fields.push(RecordConstructorField::parse(pair)?);
                }
                Rule::spread_expression => {
                    spread = Some(DataExpr::parse(pair.into_inner().next().unwrap())?);
                }
                x => unreachable!("Unexpected rule in datum_constructor: {:?}", x),
            }
        }

        Ok(VariantCaseConstructor {
            name: Identifier::new("Default"),
            fields,
            spread: spread.map(Box::new),
            scope: None,
            span,
        })
    }

    fn explicit_parse(pair: Pair<Rule>) -> Result<Self, Error> {
        let span = pair.as_span().into();
        let mut inner = pair.into_inner();

        let name = Identifier::parse(inner.next().unwrap())?;

        let mut fields = Vec::new();
        let mut spread = None;

        for pair in inner {
            match pair.as_rule() {
                Rule::record_constructor_field => {
                    fields.push(RecordConstructorField::parse(pair)?);
                }
                Rule::spread_expression => {
                    spread = Some(DataExpr::parse(pair.into_inner().next().unwrap())?);
                }
                x => unreachable!("Unexpected rule in datum_constructor: {:?}", x),
            }
        }

        Ok(VariantCaseConstructor {
            name,
            fields,
            spread: spread.map(Box::new),
            scope: None,
            span,
        })
    }
}

impl AstNode for VariantCaseConstructor {
    const RULE: Rule = Rule::variant_case_constructor;

    fn parse(pair: Pair<Rule>) -> Result<Self, Error> {
        match pair.as_rule() {
            Rule::implicit_variant_case_constructor => Self::implicit_parse(pair),
            Rule::explicit_variant_case_constructor => Self::explicit_parse(pair),
            x => unreachable!("Unexpected rule in datum_constructor: {:?}", x),
        }
    }

    fn span(&self) -> &Span {
        &self.span
    }
}

impl AstNode for ListConstructor {
    const RULE: Rule = Rule::list_constructor;

    fn parse(pair: Pair<Rule>) -> Result<Self, Error> {
        let span = pair.as_span().into();
        let inner = pair.into_inner();

        let elements = inner.map(DataExpr::parse).collect::<Result<Vec<_>, _>>()?;

        Ok(ListConstructor { elements, span })
    }

    fn span(&self) -> &Span {
        &self.span
    }
}

impl AstNode for MapField {
    const RULE: Rule = Rule::map_field;

    fn parse(pair: Pair<Rule>) -> Result<Self, Error> {
        let span = pair.as_span().into();
        let mut inner = pair.into_inner();

        let key = DataExpr::parse(inner.next().unwrap())?;
        let value = DataExpr::parse(inner.next().unwrap())?;

        Ok(MapField { key, value, span })
    }

    fn span(&self) -> &Span {
        &self.span
    }
}

impl AstNode for MapConstructor {
    const RULE: Rule = Rule::map_constructor;

    fn parse(pair: Pair<Rule>) -> Result<Self, Error> {
        let span = pair.as_span().into();
        let inner = pair.into_inner();

        let fields = inner.map(MapField::parse).collect::<Result<Vec<_>, _>>()?;

        Ok(MapConstructor { fields, span })
    }

    fn span(&self) -> &Span {
        &self.span
    }
}

impl DataExpr {
    fn number_parse(pair: Pair<Rule>) -> Result<Self, Error> {
        Ok(DataExpr::Number(pair.as_str().parse().unwrap()))
    }

    fn bool_parse(pair: Pair<Rule>) -> Result<Self, Error> {
        Ok(DataExpr::Bool(pair.as_str().parse().unwrap()))
    }

    fn identifier_parse(pair: Pair<Rule>) -> Result<Self, Error> {
        Ok(DataExpr::Identifier(Identifier::parse(pair)?))
    }

    fn struct_constructor_parse(pair: Pair<Rule>) -> Result<Self, Error> {
        Ok(DataExpr::StructConstructor(StructConstructor::parse(pair)?))
    }

    fn list_constructor_parse(pair: Pair<Rule>) -> Result<Self, Error> {
        Ok(DataExpr::ListConstructor(ListConstructor::parse(pair)?))
    }

    fn map_constructor_parse(pair: Pair<Rule>) -> Result<Self, Error> {
        Ok(DataExpr::MapConstructor(MapConstructor::parse(pair)?))
    }

    fn utxo_ref_parse(pair: Pair<Rule>) -> Result<Self, Error> {
        Ok(DataExpr::UtxoRef(UtxoRef::parse(pair)?))
    }

    fn any_asset_constructor_parse(pair: Pair<Rule>) -> Result<Self, Error> {
        Ok(DataExpr::AnyAssetConstructor(AnyAssetConstructor::parse(
            pair,
        )?))
    }

    fn concat_constructor_parse(pair: Pair<Rule>) -> Result<Self, Error> {
        Ok(DataExpr::ConcatOp(ConcatOp::parse(pair)?))
    }

    fn fn_call_parse(pair: Pair<Rule>) -> Result<Self, Error> {
        Ok(DataExpr::FnCall(crate::ast::FnCall::parse(pair)?))
    }

    fn negate_op_parse(pair: Pair<Rule>, right: DataExpr) -> Result<Self, Error> {
        Ok(DataExpr::NegateOp(NegateOp {
            operand: Box::new(right),
            span: pair.as_span().into(),
        }))
    }

    fn property_op_parse(pair: Pair<Rule>, left: DataExpr) -> Result<Self, Error> {
        let span: Span = pair.as_span().into();
        let mut inner = pair.into_inner();

        Ok(DataExpr::PropertyOp(PropertyOp {
            operand: Box::new(left),
            property: Box::new(DataExpr::Identifier(Identifier::parse(
                inner.next().unwrap(),
            )?)),
            span,
            scope: None,
        }))
    }

    fn index_op_parse(pair: Pair<Rule>, left: DataExpr) -> Result<Self, Error> {
        let span: Span = pair.as_span().into();
        let mut inner = pair.into_inner();

        Ok(DataExpr::PropertyOp(PropertyOp {
            operand: Box::new(left),
            property: Box::new(DataExpr::parse(inner.next().unwrap())?),
            span,
            scope: None,
        }))
    }

    fn add_op_parse(left: DataExpr, pair: Pair<Rule>, right: DataExpr) -> Result<Self, Error> {
        let span = pair.as_span().into();

        Ok(DataExpr::AddOp(AddOp {
            lhs: Box::new(left),
            rhs: Box::new(right),
            span,
        }))
    }

    fn sub_op_parse(left: DataExpr, pair: Pair<Rule>, right: DataExpr) -> Result<Self, Error> {
        let span = pair.as_span().into();

        Ok(DataExpr::SubOp(SubOp {
            lhs: Box::new(left),
            rhs: Box::new(right),
            span,
        }))
    }
}

static DATA_EXPR_PRATT_PARSER: LazyLock<PrattParser<Rule>> = LazyLock::new(|| {
    PrattParser::new()
        .op(Op::infix(Rule::data_add, Assoc::Left) | Op::infix(Rule::data_sub, Assoc::Left))
        .op(Op::prefix(Rule::data_negate))
        .op(Op::postfix(Rule::data_property) | Op::postfix(Rule::data_index))
});

impl AstNode for DataExpr {
    const RULE: Rule = Rule::data_expr;

    fn parse(pair: Pair<Rule>) -> Result<Self, Error> {
        let inner = pair.into_inner();

        DATA_EXPR_PRATT_PARSER
            .map_primary(|x| match x.as_rule() {
                Rule::number => DataExpr::number_parse(x),
                Rule::string => Ok(DataExpr::String(StringLiteral::parse(x)?)),
                Rule::bool => DataExpr::bool_parse(x),
                Rule::hex_string => Ok(DataExpr::HexString(HexStringLiteral::parse(x)?)),
                Rule::struct_constructor => DataExpr::struct_constructor_parse(x),
                Rule::list_constructor => DataExpr::list_constructor_parse(x),
                Rule::map_constructor => DataExpr::map_constructor_parse(x),
                Rule::unit => Ok(DataExpr::Unit),
                Rule::identifier => DataExpr::identifier_parse(x),
                Rule::utxo_ref => DataExpr::utxo_ref_parse(x),
                Rule::any_asset_constructor => DataExpr::any_asset_constructor_parse(x),
                Rule::concat_constructor => DataExpr::concat_constructor_parse(x),
                Rule::fn_call => DataExpr::fn_call_parse(x),
                Rule::data_expr => DataExpr::parse(x),
                x => unreachable!("unexpected rule as data primary: {:?}", x),
            })
            .map_prefix(|op, right| match op.as_rule() {
                Rule::data_negate => DataExpr::negate_op_parse(op, right?),
                x => unreachable!("Unexpected rule as data prefix: {:?}", x),
            })
            .map_postfix(|left, op| match op.as_rule() {
                Rule::data_property => DataExpr::property_op_parse(op, left?),
                Rule::data_index => DataExpr::index_op_parse(op, left?),
                x => unreachable!("Unexpected rule as data postfix: {:?}", x),
            })
            .map_infix(|left, op, right| match op.as_rule() {
                Rule::data_add => DataExpr::add_op_parse(left?, op, right?),
                Rule::data_sub => DataExpr::sub_op_parse(left?, op, right?),
                x => unreachable!("Unexpected rule as data infix: {:?}", x),
            })
            .parse(inner)
    }

    fn span(&self) -> &Span {
        match self {
            DataExpr::None => &Span::DUMMY,      // TODO
            DataExpr::Unit => &Span::DUMMY,      // TODO
            DataExpr::Number(_) => &Span::DUMMY, // TODO
            DataExpr::Bool(_) => &Span::DUMMY,   // TODO
            DataExpr::String(x) => x.span(),
            DataExpr::HexString(x) => x.span(),
            DataExpr::StructConstructor(x) => x.span(),
            DataExpr::ListConstructor(x) => x.span(),
            DataExpr::MapConstructor(x) => x.span(),
            DataExpr::AnyAssetConstructor(x) => x.span(),
            DataExpr::Identifier(x) => x.span(),
            DataExpr::AddOp(x) => &x.span,
            DataExpr::SubOp(x) => &x.span,
            DataExpr::ConcatOp(x) => &x.span,
            DataExpr::NegateOp(x) => &x.span,
            DataExpr::PropertyOp(x) => &x.span,
            DataExpr::UtxoRef(x) => x.span(),
            DataExpr::MinUtxo(x) => x.span(),
            DataExpr::SlotToTime(x) => x.span(),
            DataExpr::TimeToSlot(x) => x.span(),
            DataExpr::ComputeTipSlot => &Span::DUMMY, // TODO
            DataExpr::FnCall(x) => &x.span,
        }
    }
}

impl AstNode for Type {
    const RULE: Rule = Rule::r#type;

    fn parse(pair: Pair<Rule>) -> Result<Self, Error> {
        let inner = pair.into_inner().next().unwrap();

        match inner.as_rule() {
            Rule::primitive_type => match inner.as_str() {
                "Int" => Ok(Type::Int),
                "Bool" => Ok(Type::Bool),
                "Bytes" => Ok(Type::Bytes),
                "Address" => Ok(Type::Address),
                "UtxoRef" => Ok(Type::UtxoRef),
                "AnyAsset" => Ok(Type::AnyAsset),
                _ => unreachable!("Unexpected string in primitive_type: {:?}", inner.as_str()),
            },
            Rule::list_type => {
                let inner = inner.into_inner().next().unwrap();
                Ok(Type::List(Box::new(Type::parse(inner)?)))
            }
            Rule::map_type => {
                let mut inner = inner.into_inner();
                let key_type = Type::parse(inner.next().unwrap())?;
                let value_type = Type::parse(inner.next().unwrap())?;
                Ok(Type::Map(Box::new(key_type), Box::new(value_type)))
            }
            Rule::custom_type => Ok(Type::Custom(Identifier::new(inner.as_str().to_owned()))),
            x => unreachable!("Unexpected rule in type: {:?}", x),
        }
    }

    fn span(&self) -> &Span {
        &Span::DUMMY // TODO
    }
}

impl TypeDef {
    fn parse_variant_format(pair: Pair<Rule>) -> Result<Self, Error> {
        let span = pair.as_span().into();
        let mut inner = pair.into_inner();

        let identifier = Identifier::parse(inner.next().unwrap())?;

        let cases = inner
            .map(VariantCase::parse)
            .collect::<Result<Vec<_>, _>>()?;

        Ok(TypeDef {
            name: identifier,
            cases,
            span,
        })
    }

    fn parse_record_format(pair: Pair<Rule>) -> Result<Self, Error> {
        let span: Span = pair.as_span().into();
        let mut inner = pair.into_inner();

        let identifier = Identifier::parse(inner.next().unwrap())?;

        let fields = inner
            .map(RecordField::parse)
            .collect::<Result<Vec<_>, _>>()?;

        Ok(TypeDef {
            name: identifier.clone(),
            cases: vec![VariantCase {
                name: Identifier::new("Default"),
                fields,
                span: span.clone(),
            }],
            span,
        })
    }
}

impl AstNode for TypeDef {
    const RULE: Rule = Rule::type_def;

    fn parse(pair: Pair<Rule>) -> Result<Self, Error> {
        match pair.as_rule() {
            Rule::variant_def => Ok(Self::parse_variant_format(pair)?),
            Rule::record_def => Ok(Self::parse_record_format(pair)?),
            x => unreachable!("Unexpected rule in type_def: {:?}", x),
        }
    }

    fn span(&self) -> &Span {
        &self.span
    }
}

impl AstNode for AliasDef {
    const RULE: Rule = Rule::alias_def;

    fn parse(pair: Pair<Rule>) -> Result<Self, Error> {
        let span: Span = pair.as_span().into();
        let mut inner = pair.into_inner();

        let identifier = Identifier::parse(inner.next().unwrap())?;
        let r#type = Type::parse(inner.next().unwrap())?;

        Ok(AliasDef {
            name: identifier,
            alias_type: r#type,
            span,
        })
    }

    fn span(&self) -> &Span {
        &self.span
    }
}

impl VariantCase {
    fn struct_case_parse(pair: pest::iterators::Pair<Rule>) -> Result<Self, Error> {
        let span = pair.as_span().into();
        let mut inner = pair.into_inner();

        let identifier = Identifier::parse(inner.next().unwrap())?;

        let fields = inner
            .map(RecordField::parse)
            .collect::<Result<Vec<_>, _>>()?;

        Ok(Self {
            name: identifier,
            fields,
            span,
        })
    }

    fn unit_case_parse(pair: pest::iterators::Pair<Rule>) -> Result<Self, Error> {
        let span = pair.as_span().into();
        let mut inner = pair.into_inner();

        let identifier = Identifier::parse(inner.next().unwrap())?;

        Ok(Self {
            name: identifier,
            fields: vec![],
            span,
        })
    }
}

impl AstNode for VariantCase {
    const RULE: Rule = Rule::variant_case;

    fn parse(pair: Pair<Rule>) -> Result<Self, Error> {
        let case = match pair.as_rule() {
            Rule::variant_case_struct => Self::struct_case_parse(pair),
            Rule::variant_case_tuple => todo!("parse variant case tuple"),
            Rule::variant_case_unit => Self::unit_case_parse(pair),
            x => unreachable!("Unexpected rule in datum_variant: {:?}", x),
        }?;

        Ok(case)
    }

    fn span(&self) -> &Span {
        &self.span
    }
}

impl AstNode for AssetDef {
    const RULE: Rule = Rule::asset_def;

    fn parse(pair: Pair<Rule>) -> Result<Self, Error> {
        let span = pair.as_span().into();
        let mut inner = pair.into_inner();

        let identifier = Identifier::parse(inner.next().unwrap())?;
        let policy = DataExpr::parse(inner.next().unwrap())?;
        let asset_name = DataExpr::parse(inner.next().unwrap())?;

        Ok(AssetDef {
            name: identifier,
            policy,
            asset_name,
            span,
        })
    }

    fn span(&self) -> &Span {
        &self.span
    }
}

impl AstNode for ChainSpecificBlock {
    const RULE: Rule = Rule::chain_specific_block;

    fn parse(pair: Pair<Rule>) -> Result<Self, Error> {
        let mut inner = pair.into_inner();

        let block = inner.next().unwrap();

        match block.as_rule() {
            Rule::cardano_block => {
                let block = crate::cardano::CardanoBlock::parse(block)?;
                Ok(ChainSpecificBlock::Cardano(block))
            }
            x => unreachable!("Unexpected rule in chain_specific_block: {:?}", x),
        }
    }

    fn span(&self) -> &Span {
        match self {
            Self::Cardano(x) => x.span(),
        }
    }
}

/// Parses a Tx3 source string into a Program AST.
///
/// # Arguments
///
/// * `input` - String containing Tx3 source code
///
/// # Returns
///
/// * `Result<Program, Error>` - The parsed Program AST or an error
///
/// # Errors
///
/// Returns an error if:
/// - The input string is not valid Tx3 syntax
/// - The AST construction fails
///
/// # Example
///
/// ```
/// use tx3_lang::parsing::parse_string;
/// let program = parse_string("tx swap() {}").unwrap();
/// ```
pub fn parse_string(input: &str) -> Result<Program, Error> {
    let pairs = Tx3Grammar::parse(Rule::program, input)?;
    Program::parse(pairs.into_iter().next().unwrap())
}

#[cfg(test)]
pub fn parse_well_known_example(example: &str) -> Program {
    let manifest_dir = env!("CARGO_MANIFEST_DIR");
    let test_file = format!("{}/../../examples/{}.tx3", manifest_dir, example);
    let input = std::fs::read_to_string(&test_file).unwrap();
    parse_string(&input).unwrap()
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::ast;
    use assert_json_diff::assert_json_eq;
    use paste::paste;
    use pest::Parser;

    #[test]
    fn smoke_test_parse_string() {
        let _ = parse_string("tx swap() {}").unwrap();
    }

    macro_rules! input_to_ast_check {
        ($ast:ty, $name:expr, $input:expr, $expected:expr) => {
            paste::paste! {
                #[test]
                fn [<test_parse_ $ast:snake _ $name>]() {
                    let pairs = super::Tx3Grammar::parse(<$ast>::RULE, $input).unwrap();
                    let single_match = pairs.into_iter().next().unwrap();
                    let result = <$ast>::parse(single_match).unwrap();

                    assert_eq!(result, $expected);
                }
            }
        };
    }

    input_to_ast_check!(
        ConcatOp,
        "basic",
        r#"concat("hello", "world")"#,
        ast::ConcatOp {
            lhs: Box::new(ast::DataExpr::String(ast::StringLiteral {
                value: "hello".to_string(),
                span: ast::Span::DUMMY,
            })),
            rhs: Box::new(ast::DataExpr::String(ast::StringLiteral {
                value: "world".to_string(),
                span: ast::Span::DUMMY,
            })),
            span: ast::Span::DUMMY,
        }
    );
    input_to_ast_check!(Type, "int", "Int", Type::Int);

    input_to_ast_check!(Type, "bool", "Bool", Type::Bool);

    input_to_ast_check!(Type, "bytes", "Bytes", Type::Bytes);

    input_to_ast_check!(Type, "address", "Address", Type::Address);

    input_to_ast_check!(Type, "utxo_ref", "UtxoRef", Type::UtxoRef);

    input_to_ast_check!(Type, "any_asset", "AnyAsset", Type::AnyAsset);

    input_to_ast_check!(Type, "list", "List<Int>", Type::List(Box::new(Type::Int)));

    input_to_ast_check!(
        Type,
        "identifier",
        "MyType",
        Type::Custom(Identifier::new("MyType".to_string()))
    );

    input_to_ast_check!(
        Type,
        "other_type",
        "List<Bytes>",
        Type::List(Box::new(Type::Bytes))
    );

    input_to_ast_check!(
        Type,
        "within_list",
        "List<List<Int>>",
        Type::List(Box::new(Type::List(Box::new(Type::Int))))
    );

    input_to_ast_check!(
        TypeDef,
        "type_def_record",
        "type MyRecord {
            field1: Int,
            field2: Bytes,
        }",
        TypeDef {
            name: Identifier::new("MyRecord"),
            cases: vec![VariantCase {
                name: Identifier::new("Default"),
                fields: vec![
                    RecordField::new("field1", Type::Int),
                    RecordField::new("field2", Type::Bytes)
                ],
                span: Span::DUMMY,
            }],
            span: Span::DUMMY,
        }
    );

    input_to_ast_check!(
        TypeDef,
        "type_def_variant",
        "type MyVariant {
            Case1 {
                field1: Int,
                field2: Bytes,
            },
            Case2,
        }",
        TypeDef {
            name: Identifier::new("MyVariant"),
            cases: vec![
                VariantCase {
                    name: Identifier::new("Case1"),
                    fields: vec![
                        RecordField::new("field1", Type::Int),
                        RecordField::new("field2", Type::Bytes)
                    ],
                    span: Span::DUMMY,
                },
                VariantCase {
                    name: Identifier::new("Case2"),
                    fields: vec![],
                    span: Span::DUMMY,
                },
            ],
            span: Span::DUMMY,
        }
    );

    input_to_ast_check!(
        AliasDef,
        "type_def_alias",
        "type MyAlias = Bytes;",
        AliasDef {
            name: Identifier::new("MyAlias"),
            alias_type: Type::Bytes,
            span: Span::DUMMY,
        }
    );

    input_to_ast_check!(
        AliasDef,
        "type_alias_custom_type",
        "type UserAlias = UserType;",
        AliasDef {
            name: Identifier::new("UserAlias"),
            alias_type: Type::Custom(Identifier::new("UserType")),
            span: Span::DUMMY,
        }
    );

    input_to_ast_check!(
        AliasDef,
        "type_alias_list",
        "type StringList = List<Bytes>;",
        AliasDef {
            name: Identifier::new("StringList"),
            alias_type: Type::List(Box::new(Type::Bytes)),
            span: Span::DUMMY,
        }
    );

    input_to_ast_check!(
        AliasDef,
        "type_alias_map",
        "type StringIntMap = Map<Bytes, Int>;",
        AliasDef {
            name: Identifier::new("StringIntMap"),
            alias_type: Type::Map(Box::new(Type::Bytes), Box::new(Type::Int)),
            span: Span::DUMMY,
        }
    );

    input_to_ast_check!(
        AliasDef,
        "type_alias_complex_nested",
        "type ComplexType = List<Map<Bytes, Int>>;",
        AliasDef {
            name: Identifier::new("ComplexType"),
            alias_type: Type::List(Box::new(Type::Map(
                Box::new(Type::Bytes),
                Box::new(Type::Int)
            ))),
            span: Span::DUMMY,
        }
    );

    input_to_ast_check!(
        AliasDef,
        "type_alias_all_primitives",
        "type MyInt = Int;",
        AliasDef {
            name: Identifier::new("MyInt"),
            alias_type: Type::Int,
            span: Span::DUMMY,
        }
    );

    input_to_ast_check!(
        AliasDef,
        "type_alias_bool",
        "type MyBool = Bool;",
        AliasDef {
            name: Identifier::new("MyBool"),
            alias_type: Type::Bool,
            span: Span::DUMMY,
        }
    );

    input_to_ast_check!(
        AliasDef,
        "type_alias_address",
        "type MyAddress = Address;",
        AliasDef {
            name: Identifier::new("MyAddress"),
            alias_type: Type::Address,
            span: Span::DUMMY,
        }
    );

    input_to_ast_check!(
        AliasDef,
        "type_alias_utxo_ref",
        "type MyUtxoRef = UtxoRef;",
        AliasDef {
            name: Identifier::new("MyUtxoRef"),
            alias_type: Type::UtxoRef,
            span: Span::DUMMY,
        }
    );

    input_to_ast_check!(
        AliasDef,
        "type_alias_any_asset",
        "type MyAsset = AnyAsset;",
        AliasDef {
            name: Identifier::new("MyAsset"),
            alias_type: Type::AnyAsset,
            span: Span::DUMMY,
        }
    );

    input_to_ast_check!(
        StringLiteral,
        "literal_string",
        "\"Hello, world!\"",
        StringLiteral::new("Hello, world!".to_string())
    );

    input_to_ast_check!(
        HexStringLiteral,
        "hex_string",
        "0xAFAFAF",
        HexStringLiteral::new("AFAFAF".to_string())
    );

    input_to_ast_check!(
        StringLiteral,
        "literal_string_address",
        "\"addr1qx234567890abcdefghijklmnopqrstuvwxyz\"",
        StringLiteral::new("addr1qx234567890abcdefghijklmnopqrstuvwxyz".to_string())
    );

    input_to_ast_check!(
        ListConstructor,
        "empty_list",
        "[]",
        ListConstructor {
            elements: vec![],
            span: Span::DUMMY,
        }
    );

    input_to_ast_check!(
        ListConstructor,
        "trailing_comma",
        "[1, 2,]",
        ListConstructor {
            elements: vec![DataExpr::Number(1), DataExpr::Number(2),],
            span: Span::DUMMY,
        }
    );

    input_to_ast_check!(
        ListConstructor,
        "int_list",
        "[1, 2]",
        ListConstructor {
            elements: vec![DataExpr::Number(1), DataExpr::Number(2),],
            span: Span::DUMMY,
        }
    );

    input_to_ast_check!(
        ListConstructor,
        "string_list",
        "[\"Hello\", \"World\"]",
        ListConstructor {
            elements: vec![
                DataExpr::String(StringLiteral::new("Hello".to_string())),
                DataExpr::String(StringLiteral::new("World".to_string()))
            ],
            span: Span::DUMMY,
        }
    );

    input_to_ast_check!(
        ListConstructor,
        "mixed_list",
        "[1, \"Hello\", true]",
        ListConstructor {
            elements: vec![
                DataExpr::Number(1),
                DataExpr::String(StringLiteral::new("Hello".to_string())),
                DataExpr::Bool(true)
            ],
            span: Span::DUMMY,
        }
    );

    input_to_ast_check!(
        ListConstructor,
        "list_within_list",
        "[[1, 2], [3, 4]]",
        ListConstructor {
            elements: vec![
                DataExpr::ListConstructor(ListConstructor {
                    elements: vec![DataExpr::Number(1), DataExpr::Number(2),],
                    span: Span::DUMMY,
                }),
                DataExpr::ListConstructor(ListConstructor {
                    elements: vec![DataExpr::Number(3), DataExpr::Number(4),],
                    span: Span::DUMMY,
                }),
            ],
            span: Span::DUMMY,
        }
    );

    input_to_ast_check!(DataExpr, "literal_bool_true", "true", DataExpr::Bool(true));

    input_to_ast_check!(
        DataExpr,
        "literal_bool_false",
        "false",
        DataExpr::Bool(false)
    );

    input_to_ast_check!(DataExpr, "unit_value", "())", DataExpr::Unit);

    input_to_ast_check!(DataExpr, "number_value", "123", DataExpr::Number(123));

    input_to_ast_check!(
        PolicyDef,
        "policy_def_assign",
        "policy MyPolicy = 0xAFAFAF;",
        PolicyDef {
            name: Identifier::new("MyPolicy"),
            value: PolicyValue::Assign(HexStringLiteral::new("AFAFAF".to_string())),
            span: Span::DUMMY,
        }
    );

    input_to_ast_check!(
        PolicyDef,
        "policy_def_constructor",
        "policy MyPolicy {
            hash: 0x1234567890,
            script: 0x1234567890,
            ref: 0x1234567890,
        };",
        PolicyDef {
            name: Identifier::new("MyPolicy"),
            value: PolicyValue::Constructor(PolicyConstructor {
                fields: vec![
                    PolicyField::Hash(DataExpr::HexString(HexStringLiteral::new(
                        "1234567890".to_string()
                    ))),
                    PolicyField::Script(DataExpr::HexString(HexStringLiteral::new(
                        "1234567890".to_string()
                    ))),
                    PolicyField::Ref(DataExpr::HexString(HexStringLiteral::new(
                        "1234567890".to_string()
                    ))),
                ],
                span: Span::DUMMY,
            }),
            span: Span::DUMMY,
        }
    );

    input_to_ast_check!(
        AssetDef,
        "hex_hex",
        "asset MyToken = 0xef7a1cebb2dc7de884ddf82f8fcbc91fe9750dcd8c12ec7643a99bbe.0xef7a1ceb;",
        AssetDef {
            name: Identifier::new("MyToken"),
            policy: DataExpr::HexString(HexStringLiteral::new(
                "ef7a1cebb2dc7de884ddf82f8fcbc91fe9750dcd8c12ec7643a99bbe".to_string()
            )),
            asset_name: DataExpr::HexString(HexStringLiteral::new("ef7a1ceb".to_string())),
            span: Span::DUMMY,
        }
    );

    input_to_ast_check!(
        AssetDef,
        "hex_string",
        "asset MyToken = 0xef7a1cebb2dc7de884ddf82f8fcbc91fe9750dcd8c12ec7643a99bbe.\"MY TOKEN\";",
        AssetDef {
            name: Identifier::new("MyToken"),
            policy: DataExpr::HexString(HexStringLiteral::new(
                "ef7a1cebb2dc7de884ddf82f8fcbc91fe9750dcd8c12ec7643a99bbe".to_string()
            )),
            asset_name: DataExpr::String(StringLiteral::new("MY TOKEN".to_string())),
            span: Span::DUMMY,
        }
    );

    input_to_ast_check!(
        DataExpr,
        "type_and_literal",
        "MyToken(15)",
        DataExpr::FnCall(crate::ast::FnCall {
            callee: Identifier::new("MyToken"),
            args: vec![DataExpr::Number(15)],
            span: Span::DUMMY,
        })
    );

    input_to_ast_check!(
        AnyAssetConstructor,
        "any_asset_constructor",
        "AnyAsset(0x1234567890, \"MyToken\", 15)",
        AnyAssetConstructor {
            policy: Box::new(DataExpr::HexString(HexStringLiteral::new(
                "1234567890".to_string()
            ))),
            asset_name: Box::new(DataExpr::String(StringLiteral::new("MyToken".to_string()))),
            amount: Box::new(DataExpr::Number(15)),
            span: Span::DUMMY,
        }
    );

    input_to_ast_check!(
        AnyAssetConstructor,
        "any_asset_identifiers",
        "AnyAsset(my_policy, my_token, my_amount)",
        AnyAssetConstructor {
            policy: Box::new(DataExpr::Identifier(Identifier::new("my_policy"))),
            asset_name: Box::new(DataExpr::Identifier(Identifier::new("my_token"))),
            amount: Box::new(DataExpr::Identifier(Identifier::new("my_amount"))),
            span: Span::DUMMY,
        }
    );

    input_to_ast_check!(
        AnyAssetConstructor,
        "any_asset_property_access",
        "AnyAsset(input1.policy, input1.asset_name, input1.amount)",
        AnyAssetConstructor {
            policy: Box::new(DataExpr::PropertyOp(PropertyOp {
                operand: Box::new(DataExpr::Identifier(Identifier::new("input1"))),
                property: Box::new(DataExpr::Identifier(Identifier::new("policy"))),
                span: Span::DUMMY,
                scope: None,
            })),
            asset_name: Box::new(DataExpr::PropertyOp(PropertyOp {
                operand: Box::new(DataExpr::Identifier(Identifier::new("input1"))),
                property: Box::new(DataExpr::Identifier(Identifier::new("asset_name"))),
                span: Span::DUMMY,
                scope: None,
            })),
            amount: Box::new(DataExpr::PropertyOp(PropertyOp {
                operand: Box::new(DataExpr::Identifier(Identifier::new("input1"))),
                property: Box::new(DataExpr::Identifier(Identifier::new("amount"))),
                span: Span::DUMMY,
                scope: None,
            })),
            span: Span::DUMMY,
        }
    );

    input_to_ast_check!(DataExpr, "literal", "5", DataExpr::Number(5));

    input_to_ast_check!(
        DataExpr,
        "add_op",
        "5 + var1",
        DataExpr::AddOp(AddOp {
            lhs: Box::new(DataExpr::Number(5)),
            rhs: Box::new(DataExpr::Identifier(Identifier::new("var1"))),
            span: Span::DUMMY,
        })
    );

    input_to_ast_check!(
        DataExpr,
        "concat_op",
        r#"concat("hello", "world")"#,
        DataExpr::ConcatOp(ConcatOp {
            lhs: Box::new(ast::DataExpr::String(ast::StringLiteral {
                value: "hello".to_string(),
                span: ast::Span::DUMMY,
            })),
            rhs: Box::new(ast::DataExpr::String(ast::StringLiteral {
                value: "world".to_string(),
                span: ast::Span::DUMMY,
            })),
            span: ast::Span::DUMMY,
        })
    );

    input_to_ast_check!(
        DataExpr,
        "property_access",
        "subject.property",
        DataExpr::PropertyOp(PropertyOp {
            operand: Box::new(DataExpr::Identifier(Identifier::new("subject"))),
            property: Box::new(DataExpr::Identifier(Identifier::new("property"))),
            span: Span::DUMMY,
            scope: None,
        })
    );

    input_to_ast_check!(
        DataExpr,
        "multiple_properties",
        "subject.property.subproperty",
        DataExpr::PropertyOp(PropertyOp {
            operand: Box::new(DataExpr::PropertyOp(PropertyOp {
                operand: Box::new(DataExpr::Identifier(Identifier::new("subject"))),
                property: Box::new(DataExpr::Identifier(Identifier::new("property"))),
                span: Span::DUMMY,
                scope: None,
            })),
            property: Box::new(DataExpr::Identifier(Identifier::new("subproperty"))),
            span: Span::DUMMY,
            scope: None,
        })
    );

    input_to_ast_check!(DataExpr, "empty_parentheses", "()", DataExpr::Unit);

    input_to_ast_check!(DataExpr, "nested_parentheses", "((()))", DataExpr::Unit);

    input_to_ast_check!(
        DataExpr,
        "nested_arithmetic_expression",
        "(1 + ((6 - 3) + 4))",
        DataExpr::AddOp(AddOp {
            lhs: Box::new(DataExpr::Number(1)),
            rhs: Box::new(DataExpr::AddOp(AddOp {
                lhs: Box::new(DataExpr::SubOp(SubOp {
                    lhs: Box::new(DataExpr::Number(6)),
                    rhs: Box::new(DataExpr::Number(3)),
                    span: Span::DUMMY,
                })),
                rhs: Box::new(DataExpr::Number(4)),
                span: Span::DUMMY,
            })),
            span: Span::DUMMY,
        })
    );

    input_to_ast_check!(
        DataExpr,
        "negate_op",
        "!a",
        DataExpr::NegateOp(NegateOp {
            operand: Box::new(DataExpr::Identifier(Identifier::new("a"))),
            span: Span::DUMMY,
        })
    );

    input_to_ast_check!(
        DataExpr,
        "negate_precedence",
        "!a.b",
        DataExpr::NegateOp(NegateOp {
            operand: Box::new(DataExpr::PropertyOp(PropertyOp {
                operand: Box::new(DataExpr::Identifier(Identifier::new("a"))),
                property: Box::new(DataExpr::Identifier(Identifier::new("b"))),
                span: Span::DUMMY,
                scope: None,
            })),
            span: Span::DUMMY,
        })
    );

    input_to_ast_check!(
        DataExpr,
        "negate_override_precedence",
        "(!a).b",
        DataExpr::PropertyOp(PropertyOp {
            operand: Box::new(DataExpr::NegateOp(NegateOp {
                operand: Box::new(DataExpr::Identifier(Identifier::new("a"))),
                span: Span::DUMMY,
            })),
            property: Box::new(DataExpr::Identifier(Identifier::new("b"))),
            span: Span::DUMMY,
            scope: None,
        })
    );

    input_to_ast_check!(
        DataExpr,
        "overly_complex",
        "(1 + 5) - ((a.b.c - 3) + !d.f)",
        DataExpr::SubOp(SubOp {
            lhs: Box::new(DataExpr::AddOp(AddOp {
                lhs: Box::new(DataExpr::Number(1)),
                rhs: Box::new(DataExpr::Number(5)),
                span: Span::DUMMY,
            })),
            rhs: Box::new(DataExpr::AddOp(AddOp {
                lhs: Box::new(DataExpr::SubOp(SubOp {
                    lhs: Box::new(DataExpr::PropertyOp(PropertyOp {
                        operand: Box::new(DataExpr::PropertyOp(PropertyOp {
                            operand: Box::new(DataExpr::Identifier(Identifier::new("a"))),
                            property: Box::new(DataExpr::Identifier(Identifier::new("b"))),
                            span: Span::DUMMY,
                            scope: None,
                        })),
                        property: Box::new(DataExpr::Identifier(Identifier::new("c"))),
                        span: Span::DUMMY,
                        scope: None,
                    })),
                    rhs: Box::new(DataExpr::Number(3)),
                    span: Span::DUMMY,
                })),
                rhs: Box::new(DataExpr::NegateOp(NegateOp {
                    operand: Box::new(DataExpr::PropertyOp(PropertyOp {
                        operand: Box::new(DataExpr::Identifier(Identifier::new("d"))),
                        property: Box::new(DataExpr::Identifier(Identifier::new("f"))),
                        span: Span::DUMMY,
                        scope: None,
                    })),
                    span: Span::DUMMY,
                })),

                span: Span::DUMMY,
            })),
            span: Span::DUMMY,
        })
    );

    input_to_ast_check!(
        DataExpr,
        "min_utxo_basic",
        "min_utxo(output1)",
        DataExpr::FnCall(crate::ast::FnCall {
            callee: Identifier::new("min_utxo"),
            args: vec![DataExpr::Identifier(Identifier::new("output1"))],
            span: Span::DUMMY,
        })
    );

    input_to_ast_check!(
        DataExpr,
        "min_utxo_in_expression",
        "Ada(100) + min_utxo(my_output)",
        DataExpr::AddOp(AddOp {
            lhs: Box::new(DataExpr::FnCall(crate::ast::FnCall {
                callee: Identifier::new("Ada"),
                args: vec![DataExpr::Number(100)],
                span: Span::DUMMY,
            })),
            rhs: Box::new(DataExpr::FnCall(crate::ast::FnCall {
                callee: Identifier::new("min_utxo"),
                args: vec![DataExpr::Identifier(Identifier::new("my_output"))],
                span: Span::DUMMY,
            })),
            span: Span::DUMMY,
        })
    );

    input_to_ast_check!(
        DataExpr,
        "tip_slot_basic",
        "tip_slot()",
        DataExpr::FnCall(crate::ast::FnCall {
            callee: Identifier::new("tip_slot"),
            args: vec![],
            span: Span::DUMMY,
        })
    );

    input_to_ast_check!(
        DataExpr,
        "tip_slot_in_expression",
        "1000 + tip_slot()",
        DataExpr::AddOp(AddOp {
            lhs: Box::new(DataExpr::Number(1000)),
            rhs: Box::new(DataExpr::FnCall(crate::ast::FnCall {
                callee: Identifier::new("tip_slot"),
                args: vec![],
                span: Span::DUMMY,
            })),
            span: Span::DUMMY,
        })
    );

    input_to_ast_check!(
        StructConstructor,
        "struct_constructor_record",
        "MyRecord {
            field1: 10,
            field2: abc,
        }",
        StructConstructor {
            r#type: Identifier::new("MyRecord"),
            case: VariantCaseConstructor {
                name: Identifier::new("Default"),
                fields: vec![
                    RecordConstructorField {
                        name: Identifier::new("field1"),
                        value: Box::new(DataExpr::Number(10)),
                        span: Span::DUMMY,
                    },
                    RecordConstructorField {
                        name: Identifier::new("field2"),
                        value: Box::new(DataExpr::Identifier(Identifier::new("abc"))),
                        span: Span::DUMMY,
                    },
                ],
                spread: None,
                scope: None,
                span: Span::DUMMY,
            },
            scope: None,
            span: Span::DUMMY,
        }
    );

    input_to_ast_check!(
        StructConstructor,
        "struct_constructor_variant",
        "ShipCommand::MoveShip {
            delta_x: delta_x,
            delta_y: delta_y,
        }",
        StructConstructor {
            r#type: Identifier::new("ShipCommand"),
            case: VariantCaseConstructor {
                name: Identifier::new("MoveShip"),
                fields: vec![
                    RecordConstructorField {
                        name: Identifier::new("delta_x"),
                        value: Box::new(DataExpr::Identifier(Identifier::new("delta_x"))),
                        span: Span::DUMMY,
                    },
                    RecordConstructorField {
                        name: Identifier::new("delta_y"),
                        value: Box::new(DataExpr::Identifier(Identifier::new("delta_y"))),
                        span: Span::DUMMY,
                    },
                ],
                spread: None,
                scope: None,
                span: Span::DUMMY,
            },
            scope: None,
            span: Span::DUMMY,
        }
    );

    input_to_ast_check!(
        StructConstructor,
        "struct_constructor_variant_with_spread",
        "ShipCommand::MoveShip {
            delta_x: delta_x,
            delta_y: delta_y,
            ...abc
        }",
        StructConstructor {
            r#type: Identifier::new("ShipCommand"),
            case: VariantCaseConstructor {
                name: Identifier::new("MoveShip"),
                fields: vec![
                    RecordConstructorField {
                        name: Identifier::new("delta_x"),
                        value: Box::new(DataExpr::Identifier(Identifier::new("delta_x"))),
                        span: Span::DUMMY,
                    },
                    RecordConstructorField {
                        name: Identifier::new("delta_y"),
                        value: Box::new(DataExpr::Identifier(Identifier::new("delta_y"))),
                        span: Span::DUMMY,
                    },
                ],
                spread: Some(Box::new(DataExpr::Identifier(Identifier::new(
                    "abc".to_string()
                )))),
                scope: None,
                span: Span::DUMMY,
            },
            scope: None,
            span: Span::DUMMY,
        }
    );

    input_to_ast_check!(
        LocalsBlock,
        "basic",
        "locals {
            a: 10,
        }",
        LocalsBlock {
            assigns: vec![LocalsAssign {
                name: Identifier::new("a"),
                value: DataExpr::Number(10),
                span: Span::DUMMY,
            },],
            span: Span::DUMMY,
        }
    );

    input_to_ast_check!(
        LocalsBlock,
        "multiple",
        "locals {
            a: 10,
            b: 20,
        }",
        LocalsBlock {
            assigns: vec![
                LocalsAssign {
                    name: Identifier::new("a"),
                    value: DataExpr::Number(10),
                    span: Span::DUMMY,
                },
                LocalsAssign {
                    name: Identifier::new("b"),
                    value: DataExpr::Number(20),
                    span: Span::DUMMY,
                },
            ],
            span: Span::DUMMY,
        }
    );

    input_to_ast_check!(
        LocalsBlock,
        "complex_expression",
        "locals {
            a: (10 + 20) - 8,
            b: a.b.c + (5 - d),
        }",
        LocalsBlock {
            assigns: vec![
                LocalsAssign {
                    name: Identifier::new("a"),
                    value: DataExpr::SubOp(SubOp {
                        lhs: Box::new(DataExpr::AddOp(AddOp {
                            lhs: Box::new(DataExpr::Number(10)),
                            rhs: Box::new(DataExpr::Number(20)),
                            span: Span::DUMMY,
                        })),
                        rhs: Box::new(DataExpr::Number(8)),
                        span: Span::DUMMY,
                    }),
                    span: Span::DUMMY,
                },
                LocalsAssign {
                    name: Identifier::new("b"),
                    value: DataExpr::AddOp(AddOp {
                        lhs: Box::new(DataExpr::PropertyOp(PropertyOp {
                            operand: Box::new(DataExpr::PropertyOp(PropertyOp {
                                operand: Box::new(DataExpr::Identifier(Identifier::new("a"))),
                                property: Box::new(DataExpr::Identifier(Identifier::new("b"))),
                                span: Span::DUMMY,
                                scope: None,
                            })),
                            property: Box::new(DataExpr::Identifier(Identifier::new("c"))),
                            span: Span::DUMMY,
                            scope: None,
                        })),
                        rhs: Box::new(DataExpr::SubOp(SubOp {
                            lhs: Box::new(DataExpr::Number(5)),
                            rhs: Box::new(DataExpr::Identifier(Identifier::new("d"))),
                            span: Span::DUMMY,
                        })),
                        span: Span::DUMMY,
                    }),
                    span: Span::DUMMY,
                },
            ],
            span: Span::DUMMY,
        }
    );

    input_to_ast_check!(
        InputBlock,
        "single",
        r#"input source {}"#,
        InputBlock {
            many: false,
            name: "source".to_string(),
            fields: vec![],
            span: Span::DUMMY,
        }
    );

    input_to_ast_check!(
        InputBlock,
        "multiple",
        r#"input* source {}"#,
        InputBlock {
            many: true,
            name: "source".to_string(),
            fields: vec![],
            span: Span::DUMMY,
        }
    );

    input_to_ast_check!(
        OutputBlock,
        "output_block_anonymous",
        r#"output {
            to: my_party,
            amount: Ada(100),
        }"#,
        OutputBlock {
            name: None,
            optional: false,
            fields: vec![
                OutputBlockField::To(Box::new(DataExpr::Identifier(Identifier::new(
                    "my_party".to_string(),
                )))),
                OutputBlockField::Amount(Box::new(DataExpr::FnCall(crate::ast::FnCall {
                    callee: Identifier::new("Ada"),
                    args: vec![DataExpr::Number(100)],
                    span: Span::DUMMY,
                }))),
            ],
            span: Span::DUMMY,
        }
    );

    input_to_ast_check!(
        ChainSpecificBlock,
        "chain_specific_block_cardano",
        "cardano::vote_delegation_certificate {
            drep: 0x1234567890,
            stake: 0x1234567890,
        }",
        ChainSpecificBlock::Cardano(crate::cardano::CardanoBlock::VoteDelegationCertificate(
            crate::cardano::VoteDelegationCertificate {
                drep: DataExpr::HexString(HexStringLiteral::new("1234567890".to_string())),
                stake: DataExpr::HexString(HexStringLiteral::new("1234567890".to_string())),
                span: Span::DUMMY,
            },
        ))
    );

    input_to_ast_check!(
        ChainSpecificBlock,
        "chain_specific_block_cardano_treasury",
        "cardano::treasury_donation {
           coin: 20,
        }",
        ChainSpecificBlock::Cardano(crate::cardano::CardanoBlock::TreasuryDonation(
            crate::cardano::TreasuryDonationBlock {
                coin: DataExpr::Number(20),
                span: Span::DUMMY,
            },
        ))
    );

    input_to_ast_check!(
        EnvDef,
        "basic",
        "env {
            field_a: Int,
            field_b: Bytes,
        }",
        EnvDef {
            fields: vec![
                EnvField {
                    name: "field_a".to_string(),
                    r#type: Type::Int,
                    span: Span::DUMMY,
                },
                EnvField {
                    name: "field_b".to_string(),
                    r#type: Type::Bytes,
                    span: Span::DUMMY,
                },
            ],
            span: Span::DUMMY,
        }
    );

    input_to_ast_check!(
        TxDef,
        "empty",
        "tx my_tx() {}",
        TxDef {
            name: Identifier::new("my_tx"),
            parameters: ParameterList {
                parameters: vec![],
                span: Span::DUMMY,
            },
            locals: None,
            references: vec![],
            inputs: vec![],
            outputs: vec![],
            validity: None,
            mints: vec![],
            burns: vec![],
            signers: None,
            adhoc: vec![],
            collateral: vec![],
            metadata: None,
            scope: None,
            span: Span::DUMMY,
        }
    );

    input_to_ast_check!(
        TxDef,
        "with_parameters",
        "tx my_tx(a: Int, b: Bytes) {}",
        TxDef {
            name: Identifier::new("my_tx"),
            parameters: ParameterList {
                parameters: vec![
                    ParamDef {
                        name: Identifier::new("a"),
                        r#type: Type::Int,
                    },
                    ParamDef {
                        name: Identifier::new("b"),
                        r#type: Type::Bytes,
                    },
                ],
                span: Span::DUMMY,
            },
            locals: None,
            references: vec![],
            inputs: vec![],
            outputs: vec![],
            validity: None,
            mints: vec![],
            burns: vec![],
            signers: None,
            adhoc: vec![],
            collateral: vec![],
            metadata: None,
            scope: None,
            span: Span::DUMMY,
        }
    );

    input_to_ast_check!(
        Program,
        "basic",
        "party Abc; tx my_tx() {}",
        Program {
            parties: vec![PartyDef {
                name: Identifier::new("Abc"),
                span: Span::DUMMY,
            }],
            types: vec![],
            aliases: vec![],
            txs: vec![TxDef {
                name: Identifier::new("my_tx"),
                parameters: ParameterList {
                    parameters: vec![],
                    span: Span::DUMMY,
                },
                locals: None,
                references: vec![],
                inputs: vec![],
                outputs: vec![],
                validity: None,
                mints: vec![],
                burns: vec![],
                signers: None,
                adhoc: vec![],
                collateral: vec![],
                metadata: None,
                scope: None,
                span: Span::DUMMY,
            }],
            env: None,
            assets: vec![],
            policies: vec![],
            span: Span::DUMMY,
            scope: None,
        }
    );

    input_to_ast_check!(
        DataExpr,
        "array_index_literal",
        "my_list[0]",
        DataExpr::PropertyOp(PropertyOp {
            operand: Box::new(DataExpr::Identifier(Identifier::new("my_list"))),
            property: Box::new(DataExpr::Number(0)),
            span: Span::DUMMY,
            scope: None,
        })
    );

    input_to_ast_check!(
        DataExpr,
        "array_index_variable",
        "my_list[index]",
        DataExpr::PropertyOp(PropertyOp {
            operand: Box::new(DataExpr::Identifier(Identifier::new("my_list"))),
            property: Box::new(DataExpr::Identifier(Identifier::new("index"))),
            span: Span::DUMMY,
            scope: None,
        })
    );

    input_to_ast_check!(
        DataExpr,
        "nested_array_index",
        "matrix[row][col]",
        DataExpr::PropertyOp(PropertyOp {
            operand: Box::new(DataExpr::PropertyOp(PropertyOp {
                operand: Box::new(DataExpr::Identifier(Identifier::new("matrix"))),
                property: Box::new(DataExpr::Identifier(Identifier::new("row"))),
                span: Span::DUMMY,
                scope: None,
            })),
            property: Box::new(DataExpr::Identifier(Identifier::new("col"))),
            span: Span::DUMMY,
            scope: None,
        })
    );

    input_to_ast_check!(
        DataExpr,
        "array_index_with_property_access",
        "items[index].name",
        DataExpr::PropertyOp(PropertyOp {
            operand: Box::new(DataExpr::PropertyOp(PropertyOp {
                operand: Box::new(DataExpr::Identifier(Identifier::new("items"))),
                property: Box::new(DataExpr::Identifier(Identifier::new("index"))),
                span: Span::DUMMY,
                scope: None,
            })),
            property: Box::new(DataExpr::Identifier(Identifier::new("name"))),
            span: Span::DUMMY,
            scope: None,
        })
    );

    input_to_ast_check!(
        DataExpr,
        "property_access_then_array_index",
        "object.list[0]",
        DataExpr::PropertyOp(PropertyOp {
            operand: Box::new(DataExpr::PropertyOp(PropertyOp {
                operand: Box::new(DataExpr::Identifier(Identifier::new("object"))),
                property: Box::new(DataExpr::Identifier(Identifier::new("list"))),
                span: Span::DUMMY,
                scope: None,
            })),
            property: Box::new(DataExpr::Number(0)),
            span: Span::DUMMY,
            scope: None,
        })
    );

    input_to_ast_check!(
        DataExpr,
        "array_index_with_function_call",
        "values[min_utxo(output)]",
        DataExpr::PropertyOp(PropertyOp {
            operand: Box::new(DataExpr::Identifier(Identifier::new("values"))),
            property: Box::new(DataExpr::FnCall(crate::ast::FnCall {
                callee: Identifier::new("min_utxo"),
                args: vec![DataExpr::Identifier(Identifier::new("output"))],
                span: Span::DUMMY,
            })),
            span: Span::DUMMY,
            scope: None,
        })
    );

    input_to_ast_check!(
        DataExpr,
        "mixed_property_and_index_access",
        "container.items[index].metadata[\"key\"]",
        DataExpr::PropertyOp(PropertyOp {
            operand: Box::new(DataExpr::PropertyOp(PropertyOp {
                operand: Box::new(DataExpr::PropertyOp(PropertyOp {
                    operand: Box::new(DataExpr::PropertyOp(PropertyOp {
                        operand: Box::new(DataExpr::Identifier(Identifier::new("container"))),
                        property: Box::new(DataExpr::Identifier(Identifier::new("items"))),
                        span: Span::DUMMY,
                        scope: None,
                    })),
                    property: Box::new(DataExpr::Identifier(Identifier::new("index"))),
                    span: Span::DUMMY,
                    scope: None,
                })),
                property: Box::new(DataExpr::Identifier(Identifier::new("metadata"))),
                span: Span::DUMMY,
                scope: None,
            })),
            property: Box::new(DataExpr::String(StringLiteral::new("key".to_string()))),
            span: Span::DUMMY,
            scope: None,
        })
    );

    #[test]
    fn test_spans_are_respected() {
        let program = parse_well_known_example("spans");
        assert_eq!(program.span, Span::new(0, 759));

        assert_eq!(program.parties[0].span, Span::new(27, 41));

        assert_eq!(program.types[0].span, Span::new(43, 77));
    }

    fn make_snapshot_if_missing(example: &str, program: &Program) {
        let manifest_dir = env!("CARGO_MANIFEST_DIR");
        let path = format!("{}/../../examples/{}.ast", manifest_dir, example);

        if !std::fs::exists(&path).unwrap() {
            let ast = serde_json::to_string_pretty(program).unwrap();
            std::fs::write(&path, ast).unwrap();
        }
    }

    fn test_parsing_example(example: &str) {
        let program = parse_well_known_example(example);
        make_snapshot_if_missing(example, &program);

        let manifest_dir = env!("CARGO_MANIFEST_DIR");
        let ast_file = format!("{}/../../examples/{}.ast", manifest_dir, example);
        let ast = std::fs::read_to_string(ast_file).unwrap();

        let expected: Program = serde_json::from_str(&ast).unwrap();

        assert_json_eq!(program, expected);
    }

    #[macro_export]
    macro_rules! test_parsing {
        ($name:ident) => {
            paste! {
                #[test]
                fn [<test_example_ $name>]() {
                    test_parsing_example(stringify!($name));
                }
            }
        };
    }

    test_parsing!(lang_tour);

    test_parsing!(transfer);

    test_parsing!(swap);

    test_parsing!(asteria);

    test_parsing!(vesting);

    test_parsing!(faucet);

    test_parsing!(disordered);

    test_parsing!(input_datum);

    test_parsing!(withdrawal);

    test_parsing!(env_vars);

    test_parsing!(local_vars);

    test_parsing!(cardano_witness);

    test_parsing!(reference_script);
    test_parsing!(map);
    test_parsing!(burn);

    test_parsing!(donation);

    test_parsing!(list_concat);

    test_parsing!(buidler_fest_2026);
}
