#![allow(deprecated)]

use crate::compat::{self, AsModern, Compat, Modern};
pub use crate::ephem::deprecated::v0::{
    commit_accounts, commit_and_undelegate_accounts, create_schedule_commit_ix,
};
use crate::ephem::deprecated::v1::utils;
pub use crate::ephem::deprecated::v1::{
    ActionCallback, CallHandler, CommitAndUndelegate, CommitType, MagicAction,
    MagicInstructionBuilder, UndelegateType,
};
pub use cau_intent_builder::{CommitAndUndelegateIntentBuilder, FoldableCauIntentBuilder};
pub use commit_intent_builder::{CommitIntentBuilder, FoldableCommitIntentBuilder};
use magicblock_magic_program_api::args::MagicIntentBundleArgs;
use magicblock_magic_program_api::instruction::MagicBlockInstruction;
pub use magicblock_magic_program_api::response::MagicResponse;
use solana_program::instruction::{AccountMeta, Instruction};
use solana_program::program::{invoke, invoke_signed};
use std::collections::HashMap;

pub mod action_builder;
pub mod cau_intent_builder;
pub mod commit_intent_builder;
pub mod deprecated;

/// Intent to be scheduled for execution on the base layer.
///
/// This enum represents the different types of operations that can be bundled
/// and executed through the Magic program.
pub enum MagicIntent<'info> {
    /// Standalone actions to execute on base layer without commit/undelegate semantics.
    StandaloneActions(Vec<CallHandler<'info>>),
    /// Commit accounts to base layer, optionally with post-commit actions.
    Commit(CommitType<'info>),
    /// Commit accounts and undelegate them, optionally with post-commit and post-undelegate actions.
    CommitAndUndelegate(CommitAndUndelegate<'info>),
    /// CommitFinalize accounts to base layer, optionally with post-commit actions.
    CommitFinalize(CommitType<'info>),
    /// CommitFinalize accounts and undelegate them, optionally with post-commit and post-undelegate actions.
    CommitFinalizeAndUndelegate(CommitAndUndelegate<'info>),
}

/// The result of [`MagicIntentBundleBuilder::build`].
///
/// Holds the `ScheduleIntentBundle` instruction and any `AddActionCallback` instructions
/// that must be invoked after it. Call [`invoke`](IntentInstructions::invoke) to execute
/// all of them in order, or inspect the fields to drive the CPIs yourself.
pub struct IntentInstructions<'info> {
    pub schedule_intent_ix: (Vec<compat::AccountInfo<'info>>, compat::Instruction),
    pub add_callback_ixs: Vec<(Vec<compat::AccountInfo<'info>>, compat::Instruction)>,
}

impl<'info> IntentInstructions<'info> {
    /// Invokes `ScheduleIntentBundle` followed by each `AddActionCallback` in order.
    pub fn invoke(self) -> compat::ProgramResult {
        let (accounts, ix) = self.schedule_intent_ix;
        invoke(&ix.modern(), &accounts.modern()).compat()?;

        self.add_callback_ixs
            .into_iter()
            .try_for_each(|(accounts, ix)| invoke(&ix.modern(), &accounts.modern()))
            .compat()
    }

    /// Signed variant of [`Self::invoke`]: uses `invoke_signed` with the provided
    /// PDA seeds for both the `ScheduleIntentBundle` and every `AddActionCallback`.
    pub fn invoke_signed(self, signers_seeds: &[&[&[u8]]]) -> compat::ProgramResult {
        let (accounts, ix) = self.schedule_intent_ix;
        invoke_signed(&ix.modern(), &accounts.modern(), signers_seeds).compat()?;

        self.add_callback_ixs
            .into_iter()
            .try_for_each(|(accounts, ix)| {
                invoke_signed(&ix.modern(), &accounts.modern(), signers_seeds)
            })
            .compat()
    }
}

/// Builds a single `MagicBlockInstruction::ScheduleIntentBundle` instruction by aggregating
/// multiple independent intents (base actions, commits, commit+undelegate), normalizing them,
/// and producing a deduplicated account list plus the corresponding CPI `Instruction`.
pub struct MagicIntentBundleBuilder<'info> {
    payer: compat::AccountInfo<'info>,
    magic_context: compat::AccountInfo<'info>,
    magic_program: compat::AccountInfo<'info>,
    magic_fee_vault: Option<compat::AccountInfo<'info>>,
    intent_bundle: MagicIntentBundle<'info>,
}

impl<'info> MagicIntentBundleBuilder<'info> {
    pub fn new(
        payer: compat::AccountInfo<'info>,
        magic_context: compat::AccountInfo<'info>,
        magic_program: compat::AccountInfo<'info>,
    ) -> Self {
        Self {
            payer,
            magic_context,
            magic_program,
            magic_fee_vault: None,
            intent_bundle: MagicIntentBundle::default(),
        }
    }

    /// Sets an optional magic fee vault account to be passed at index 2
    /// (right after payer and magic_context). Required when the payer is delegated.
    pub fn magic_fee_vault(mut self, vault: compat::AccountInfo<'info>) -> Self {
        self.magic_fee_vault = Some(vault);
        self
    }

    /// Starts building a Commit intent. Returns a [`CommitIntentBuilder`] that owns this parent.
    ///
    /// The returned builder lets you chain `.add_post_commit_actions()`, transition to other
    /// intents via `.commit_and_undelegate()`, or finalize via `.build()` / `.build_and_invoke()`.
    pub fn commit(self, accounts: &[compat::AccountInfo<'info>]) -> CommitIntentBuilder<'info> {
        CommitIntentBuilder {
            parent: self,
            accounts: accounts.to_vec(),
            actions: vec![],
            callbacks: vec![],
        }
    }

    /// Starts building a CommitAndUndelegate intent. Returns a [`CommitAndUndelegateIntentBuilder`]
    /// that owns this parent.
    ///
    /// The returned builder lets you chain `.add_post_commit_actions()`,
    /// `.add_post_undelegate_actions()`, transition to other intents via `.commit()`,
    /// or finalize via `.build()` / `.build_and_invoke()`.
    pub fn commit_and_undelegate(
        self,
        accounts: &[compat::AccountInfo<'info>],
    ) -> CommitAndUndelegateIntentBuilder<'info> {
        CommitAndUndelegateIntentBuilder {
            parent: self,
            accounts: accounts.to_vec(),
            post_commit_actions: vec![],
            post_commit_callbacks: vec![],
            post_undelegate_actions: vec![],
            post_undelegate_callbacks: vec![],
        }
    }

    /// Adds standalone base-layer actions to be executed without any commit/undelegate semantics.
    pub fn add_standalone_actions(
        mut self,
        actions: impl IntoIterator<Item = CallHandler<'info>>,
    ) -> Self {
        self.intent_bundle
            .add_intent(MagicIntent::StandaloneActions(
                actions.into_iter().collect(),
            ));
        self
    }

    /// Adds a single standalone action. Returns an [`ActionBuilder`] that lets you
    /// optionally attach a callback via `.then()` before continuing the chain.
    pub fn add_standalone_action(
        self,
        action: CallHandler<'info>,
    ) -> action_builder::ActionBuilder<
        'info,
        MagicIntentBundleBuilder<'info>,
        impl FnOnce(
            MagicIntentBundleBuilder<'info>,
            CallHandler<'info>,
            Option<ActionCallback>,
        ) -> MagicIntentBundleBuilder<'info>,
    > {
        action_builder::ActionBuilder::new(self, action, |mut parent, action, callback| {
            parent.intent_bundle.standalone_actions.push(action);
            parent.intent_bundle.standalone_callbacks.push(callback);
            parent
        })
    }

    fn build_callback_ixs(
        &mut self,
    ) -> Vec<(Vec<compat::AccountInfo<'info>>, compat::Instruction)> {
        let callbacks = self.intent_bundle.extract_callbacks();
        if callbacks.is_empty() {
            return vec![];
        }

        callbacks
            .into_iter()
            .map(|(action_index, callback)| {
                let args = callback.into_args(action_index);
                let payer_meta = AccountMeta {
                    pubkey: *self.payer.key.as_modern(),
                    is_signer: true,
                    is_writable: true,
                };
                let ctx_meta = AccountMeta {
                    pubkey: *self.magic_context.key.as_modern(),
                    is_signer: false,
                    is_writable: true,
                };
                let mut accounts = vec![payer_meta, ctx_meta];
                let mut account_infos = vec![self.payer.clone(), self.magic_context.clone()];
                if let Some(ref magic_fee_vault) = self.magic_fee_vault {
                    account_infos.push(magic_fee_vault.clone());
                    accounts.push(AccountMeta::new(*magic_fee_vault.key.as_modern(), false));
                };

                let ix = Instruction::new_with_bincode(
                    *self.magic_program.key.as_modern(),
                    &MagicBlockInstruction::AddActionCallback(args),
                    accounts,
                )
                .compat();
                (account_infos, ix)
            })
            .collect()
    }

    /// Builds [`IntentInstructions`] that schedules IntentBundle in magic program
    pub fn build(mut self) -> IntentInstructions<'info> {
        self.intent_bundle.normalize();

        // Build AddActionCallback instructions
        let add_callback_ixs = self.build_callback_ixs();

        // Build ScheduleIntentBundle instruction
        let mut all_accounts = vec![self.payer, self.magic_context];
        if let Some(vault) = self.magic_fee_vault {
            all_accounts.push(vault);
        }
        self.intent_bundle.collect_accounts(&mut all_accounts);
        let indices_map = utils::filter_duplicates_with_map(&mut all_accounts);
        let args = self.intent_bundle.into_args(&indices_map);
        let metas = all_accounts
            .iter()
            .map(|ai| AccountMeta {
                pubkey: *ai.key.as_modern(),
                is_signer: ai.is_signer,
                is_writable: ai.is_writable,
            })
            .collect();
        let schedule_ix = Instruction::new_with_bincode(
            *self.magic_program.key.as_modern(),
            &MagicBlockInstruction::ScheduleIntentBundle(args),
            metas,
        )
        .compat();

        IntentInstructions {
            schedule_intent_ix: (all_accounts, schedule_ix),
            add_callback_ixs,
        }
    }

    /// Convenience wrapper: builds all instructions and invokes them in order.
    pub fn build_and_invoke(self) -> compat::ProgramResult {
        self.build().invoke()
    }

    /// Convenience wrapper: builds all instructions and invokes them signed with
    /// the provided PDA seeds.
    pub fn build_and_invoke_signed(self, signers_seeds: &[&[&[u8]]]) -> compat::ProgramResult {
        self.build().invoke_signed(signers_seeds)
    }
}

/// Bundle of Intents
///
/// Note: if `CommitIntent` & `CommitAndUndelegateIntent` has an account overlap
/// they will be undelegated
///
/// Intents assumed to be independent and self-sufficient,
/// hence order in which they were inserted doesn't matter
#[derive(Default)]
struct MagicIntentBundle<'info> {
    standalone_actions: Vec<CallHandler<'info>>,
    standalone_callbacks: Vec<Option<ActionCallback>>, // parallel to standalone_actions
    commit_intent: Option<CommitType<'info>>,
    commit_and_undelegate_intent: Option<CommitAndUndelegate<'info>>,
    commit_finalize_intent: Option<CommitType<'info>>,
    commit_finalize_and_undelegate_intent: Option<CommitAndUndelegate<'info>>,
}

impl<'info> MagicIntentBundle<'info> {
    /// Inserts an intent into the bundle, merging with any existing intent of the same category.
    fn add_intent(&mut self, intent: MagicIntent<'info>) {
        match intent {
            MagicIntent::StandaloneActions(value) => {
                self.standalone_callbacks
                    .extend((0..value.len()).map(|_| None));
                self.standalone_actions.extend(value);
            }
            MagicIntent::Commit(value) => {
                if let Some(ref mut commit_accounts) = self.commit_intent {
                    commit_accounts.merge(value);
                } else {
                    self.commit_intent = Some(value);
                }
            }
            MagicIntent::CommitAndUndelegate(value) => {
                if let Some(ref mut commit_and_undelegate) = self.commit_and_undelegate_intent {
                    commit_and_undelegate.merge(value);
                } else {
                    self.commit_and_undelegate_intent = Some(value);
                }
            }
            MagicIntent::CommitFinalize(value) => {
                if let Some(ref mut commit_finalize_accounts) = self.commit_finalize_intent {
                    commit_finalize_accounts.merge(value);
                } else {
                    self.commit_finalize_intent = Some(value);
                }
            }
            MagicIntent::CommitFinalizeAndUndelegate(value) => {
                if let Some(ref mut commit_finalize_and_undelegate) =
                    self.commit_finalize_and_undelegate_intent
                {
                    commit_finalize_and_undelegate.merge(value);
                } else {
                    self.commit_finalize_and_undelegate_intent = Some(value);
                }
            }
        }
    }

    /// Extracts callbacks with correct Action indices
    fn extract_callbacks(&mut self) -> Vec<(u8, ActionCallback)> {
        let mut out = Vec::new();
        let mut idx: u8 = 0;
        self.commit_intent
            .iter_mut()
            .for_each(|c| c.extract_callbacks(&mut idx, &mut out));
        self.commit_and_undelegate_intent
            .iter_mut()
            .for_each(|cau| cau.extract_callbacks(&mut idx, &mut out));
        self.commit_finalize_intent
            .iter_mut()
            .for_each(|c| c.extract_callbacks(&mut idx, &mut out));
        self.commit_finalize_and_undelegate_intent
            .iter_mut()
            .for_each(|cau| cau.extract_callbacks(&mut idx, &mut out));
        self.standalone_callbacks
            .iter_mut()
            .take(self.standalone_actions.len())
            .for_each(|cb_opt| {
                if let Some(cb) = cb_opt.take() {
                    out.push((idx, cb));
                }
                idx += 1;
            });
        out
    }

    /// Consumes the bundle and encodes it into `MagicIntentBundleArgs` using a `compat::Pubkey -> u8` indices map.
    fn into_args(self, indices_map: &HashMap<compat::Pubkey, u8>) -> MagicIntentBundleArgs {
        MagicIntentBundleArgs {
            commit: self.commit_intent.map(|c| c.into_args(indices_map)),

            commit_and_undelegate: self
                .commit_and_undelegate_intent
                .map(|c| c.into_args(indices_map)),

            commit_finalize: self
                .commit_finalize_intent
                .map(|c| c.into_args(indices_map)),

            commit_finalize_and_undelegate: self
                .commit_finalize_and_undelegate_intent
                .map(|c| c.into_args(indices_map)),

            standalone_actions: self
                .standalone_actions
                .into_iter()
                .map(|ch| ch.into_args(indices_map))
                .collect::<Vec<_>>(),
        }
    }

    /// Collects all accounts referenced by intents in this bundle.
    fn collect_accounts(&self, all_accounts: &mut Vec<compat::AccountInfo<'info>>) {
        for el in &self.standalone_actions {
            el.collect_accounts(all_accounts);
        }
        if let Some(commit) = &self.commit_intent {
            commit.collect_accounts(all_accounts);
        }
        if let Some(cau) = &self.commit_and_undelegate_intent {
            cau.collect_accounts(all_accounts);
        }
        if let Some(commit_finalize) = &self.commit_finalize_intent {
            commit_finalize.collect_accounts(all_accounts);
        }
        if let Some(cau_finalize) = &self.commit_finalize_and_undelegate_intent {
            cau_finalize.collect_accounts(all_accounts);
        }
    }

    /// Normalizes the bundle into a valid, canonical form.
    ///
    /// Effects:
    /// - Deduplicates committed accounts within each intent by pubkey (stable; first occurrence wins).
    /// - Resolves overlap between `Commit` and `CommitAndUndelegate`:
    ///   any account present in both will be removed from `Commit` and kept in `CommitAndUndelegate`.
    /// - If `Commit` becomes empty after overlap removal, it is removed from the bundle, and any
    ///   post-commit handlers from the commit intent are merged into the commit-side handlers
    ///   of `CommitAndUndelegate`.
    fn normalize(&mut self) {
        // Remove duplicates inside individual intents
        if let Some(ref mut value) = self.commit_intent {
            value.dedup();
        }
        let cau = self.commit_and_undelegate_intent.as_mut().map(|value| {
            let seen = value.dedup();
            (seen, value)
        });

        // Remove cross intent duplicates
        // Only proceed if both intents exist; otherwise no cross-intent dedup needed
        let (mut commit, cau, cau_pubkeys) = match (self.commit_intent.take(), cau) {
            (Some(commit), Some((cau_pubkeys, cau))) => (commit, cau, cau_pubkeys),
            // In case only one Intent exists, put commit_intent back if it was taken
            (Some(commit), None) => {
                self.commit_intent = Some(commit);
                return;
            }
            // No commit_intent or neither intent exists - nothing to restore
            _ => return,
        };

        // If accounts in CommitAndUndelegate and Commit intents overlap
        // we keep them only in CommitAndUndelegate Intent and remove from Commit
        commit
            .committed_accounts_mut()
            .retain(|el| !cau_pubkeys.contains(el.key));

        // edge case
        if commit.committed_accounts().is_empty() {
            // if after dedup Commit intent becomes empty
            // 1. remove it from bundle
            // 2. if it has action - move them in CommitAndUndelegate intent
            cau.commit_type.merge(commit);
        } else {
            // set deduped intent
            self.commit_intent = Some(commit);
        }
    }
}

/// Shared transition and terminal methods for intent sub-builders.
///
/// Implementors provide only `fold`, which collapses the sub-builder back into
/// the parent [`MagicIntentBundleBuilder`]. All transition/terminal methods are
/// then available as defaults.
pub trait FoldableIntentBuilder<'info>: Sized {
    fn fold_builder(self) -> MagicIntentBundleBuilder<'info>;

    fn commit(self, accounts: &[compat::AccountInfo<'info>]) -> CommitIntentBuilder<'info> {
        self.fold_builder().commit(accounts)
    }

    fn commit_and_undelegate(
        self,
        accounts: &[compat::AccountInfo<'info>],
    ) -> CommitAndUndelegateIntentBuilder<'info> {
        self.fold_builder().commit_and_undelegate(accounts)
    }

    fn add_standalone_actions(
        self,
        actions: impl IntoIterator<Item = CallHandler<'info>>,
    ) -> MagicIntentBundleBuilder<'info> {
        self.fold_builder().add_standalone_actions(actions)
    }

    fn add_standalone_action(
        self,
        action: CallHandler<'info>,
    ) -> action_builder::ActionBuilder<
        'info,
        MagicIntentBundleBuilder<'info>,
        impl FnOnce(
            MagicIntentBundleBuilder<'info>,
            CallHandler<'info>,
            Option<ActionCallback>,
        ) -> MagicIntentBundleBuilder<'info>,
    > {
        self.fold_builder().add_standalone_action(action)
    }

    fn build(self) -> IntentInstructions<'info> {
        self.fold_builder().build()
    }

    fn build_and_invoke(self) -> compat::ProgramResult {
        self.fold_builder().build_and_invoke()
    }

    fn build_and_invoke_signed(self, signers_seeds: &[&[&[u8]]]) -> compat::ProgramResult {
        self.fold_builder().build_and_invoke_signed(signers_seeds)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::compat;
    use magicblock_magic_program_api::args::ActionArgs;
    use magicblock_magic_program_api::instruction::MagicBlockInstruction;
    use std::cell::RefCell;
    use std::rc::Rc;

    /// Helper to create a mock compat::AccountInfo for testing
    fn create_mock_account_info<'a>(
        key: &'a compat::Pubkey,
        lamports: &'a mut u64,
        data: &'a mut [u8],
        owner: &'a compat::Pubkey,
        is_signer: bool,
        is_writable: bool,
    ) -> compat::AccountInfo<'a> {
        compat::AccountInfo {
            key,
            is_signer,
            is_writable,
            lamports: Rc::new(RefCell::new(lamports)),
            data: Rc::new(RefCell::new(data)),
            owner,
            executable: false,
            #[cfg(feature = "backward-compat")]
            rent_epoch: 0,
            #[cfg(not(feature = "backward-compat"))]
            _unused: 0,
        }
    }

    fn make_info(acc: &mut TestAccount) -> compat::AccountInfo<'_> {
        create_mock_account_info(
            &acc.key,
            &mut acc.lamports,
            &mut acc.data,
            &acc.owner,
            true,
            false,
        )
    }

    /// Helper struct to hold account data for tests
    #[allow(dead_code)]
    struct TestAccount {
        key: compat::Pubkey,
        lamports: u64,
        data: Vec<u8>,
        owner: compat::Pubkey,
    }

    impl TestAccount {
        fn new() -> Self {
            Self {
                key: compat::Pubkey::new_unique(),
                lamports: 1_000_000,
                data: vec![0u8; 32],
                owner: compat::Pubkey::new_unique(),
            }
        }

        fn with_key(key: compat::Pubkey) -> Self {
            Self {
                key,
                lamports: 1_000_000,
                data: vec![0u8; 32],
                owner: compat::Pubkey::new_unique(),
            }
        }
    }

    /// Helper to create a CallHandler for testing
    fn create_test_call_handler(escrow_authority: compat::AccountInfo) -> CallHandler {
        CallHandler {
            args: ActionArgs::new(vec![1, 2, 3]),
            compute_units: 100_000,
            escrow_authority,
            destination_program: compat::Pubkey::new_unique(),
            accounts: vec![],
        }
    }

    // ----- Fluent CommitIntentBuilder Tests -----

    /// Helper to create a MagicIntentBundleBuilder for testing.
    fn create_test_builder<'a>(
        payer: &'a mut TestAccount,
        magic_ctx: &'a mut TestAccount,
        magic_prog: &'a mut TestAccount,
        owner: &'a compat::Pubkey,
    ) -> (MagicIntentBundleBuilder<'a>, compat::Pubkey) {
        let payer_info = create_mock_account_info(
            &payer.key,
            &mut payer.lamports,
            &mut payer.data,
            owner,
            true,
            true,
        );
        let ctx_info = create_mock_account_info(
            &magic_ctx.key,
            &mut magic_ctx.lamports,
            &mut magic_ctx.data,
            owner,
            false,
            true,
        );
        let prog_info = create_mock_account_info(
            &magic_prog.key,
            &mut magic_prog.lamports,
            &mut magic_prog.data,
            owner,
            false,
            false,
        );
        let prog_key = magic_prog.key;
        (
            MagicIntentBundleBuilder::new(payer_info, ctx_info, prog_info),
            prog_key,
        )
    }

    #[test]
    fn test_fluent_commit_standalone() {
        let owner = compat::Pubkey::new_unique();
        let mut payer = TestAccount::new();
        let mut magic_ctx = TestAccount::new();
        let mut magic_prog = TestAccount::new();
        let mut acc1 = TestAccount::new();
        let mut acc2 = TestAccount::new();

        let (builder, prog_key) =
            create_test_builder(&mut payer, &mut magic_ctx, &mut magic_prog, &owner);
        let info1 = create_mock_account_info(
            &acc1.key,
            &mut acc1.lamports,
            &mut acc1.data,
            &owner,
            false,
            true,
        );
        let info2 = create_mock_account_info(
            &acc2.key,
            &mut acc2.lamports,
            &mut acc2.data,
            &owner,
            false,
            true,
        );

        let (accounts, ix) = builder.commit(&[info1, info2]).build().schedule_intent_ix;

        // payer + magic_ctx + 2 committed accounts
        assert_eq!(accounts.len(), 4);
        assert_eq!(*accounts[2].key, acc1.key);
        assert_eq!(*accounts[3].key, acc2.key);
        assert_eq!(ix.program_id, prog_key);
    }

    #[test]
    fn test_fluent_commit_with_handler() {
        let owner = compat::Pubkey::new_unique();
        let mut payer = TestAccount::new();
        let mut magic_ctx = TestAccount::new();
        let mut magic_prog = TestAccount::new();
        let mut acc1 = TestAccount::new();
        let mut escrow_acc = TestAccount::new();

        let (builder, _) = create_test_builder(&mut payer, &mut magic_ctx, &mut magic_prog, &owner);
        let info1 = create_mock_account_info(
            &acc1.key,
            &mut acc1.lamports,
            &mut acc1.data,
            &owner,
            false,
            true,
        );
        let escrow_info = create_mock_account_info(
            &escrow_acc.key,
            &mut escrow_acc.lamports,
            &mut escrow_acc.data,
            &owner,
            true,
            false,
        );
        let handler = create_test_call_handler(escrow_info);

        let IntentInstructions {
            schedule_intent_ix: (accounts, _ix),
            add_callback_ixs,
        } = builder
            .commit(&[info1])
            .add_post_commit_actions([handler])
            .build();

        // payer + magic_ctx + committed acc + escrow
        assert_eq!(accounts.len(), 4);
        assert_eq!(add_callback_ixs.len(), 0);
    }

    // ----- Fluent CommitAndUndelegateIntentBuilder Tests -----

    #[test]
    fn test_fluent_commit_and_undelegate_standalone() {
        let owner = compat::Pubkey::new_unique();
        let mut payer = TestAccount::new();
        let mut magic_ctx = TestAccount::new();
        let mut magic_prog = TestAccount::new();
        let mut acc1 = TestAccount::new();

        let (builder, _) = create_test_builder(&mut payer, &mut magic_ctx, &mut magic_prog, &owner);
        let info1 = create_mock_account_info(
            &acc1.key,
            &mut acc1.lamports,
            &mut acc1.data,
            &owner,
            false,
            true,
        );

        let (accounts, _ix) = builder
            .commit_and_undelegate(&[info1])
            .build()
            .schedule_intent_ix;

        // payer + magic_ctx + committed acc
        assert_eq!(accounts.len(), 3);
    }

    #[test]
    fn test_fluent_commit_and_undelegate_with_actions() {
        let owner = compat::Pubkey::new_unique();
        let mut payer = TestAccount::new();
        let mut magic_ctx = TestAccount::new();
        let mut magic_prog = TestAccount::new();
        let mut acc1 = TestAccount::new();
        let mut escrow_acc1 = TestAccount::new();
        let mut escrow_acc2 = TestAccount::new();

        let (builder, _) = create_test_builder(&mut payer, &mut magic_ctx, &mut magic_prog, &owner);
        let info1 = create_mock_account_info(
            &acc1.key,
            &mut acc1.lamports,
            &mut acc1.data,
            &owner,
            false,
            true,
        );
        let escrow_info1 = create_mock_account_info(
            &escrow_acc1.key,
            &mut escrow_acc1.lamports,
            &mut escrow_acc1.data,
            &owner,
            true,
            false,
        );
        let escrow_info2 = create_mock_account_info(
            &escrow_acc2.key,
            &mut escrow_acc2.lamports,
            &mut escrow_acc2.data,
            &owner,
            true,
            false,
        );
        let post_commit_handler = create_test_call_handler(escrow_info1);
        let post_undelegate_handler = create_test_call_handler(escrow_info2);

        let (accounts, _ix) = builder
            .commit_and_undelegate(&[info1])
            .add_post_commit_action(post_commit_handler)
            .add_post_undelegate_actions([post_undelegate_handler])
            .build()
            .schedule_intent_ix;

        // payer + magic_ctx + committed acc + 2 escrows
        assert_eq!(accounts.len(), 5);
    }

    // ----- Fluent Transition Tests -----

    #[test]
    fn test_fluent_commit_then_commit_and_undelegate() {
        let owner = compat::Pubkey::new_unique();
        let mut payer = TestAccount::new();
        let mut magic_ctx = TestAccount::new();
        let mut magic_prog = TestAccount::new();
        let mut acc1 = TestAccount::new();
        let mut acc2 = TestAccount::new();

        let (builder, _) = create_test_builder(&mut payer, &mut magic_ctx, &mut magic_prog, &owner);
        let info1 = create_mock_account_info(
            &acc1.key,
            &mut acc1.lamports,
            &mut acc1.data,
            &owner,
            false,
            true,
        );
        let info2 = create_mock_account_info(
            &acc2.key,
            &mut acc2.lamports,
            &mut acc2.data,
            &owner,
            false,
            true,
        );

        // Transition from commit to commit_and_undelegate
        let (accounts, _ix) = builder
            .commit(&[info1])
            .commit_and_undelegate(&[info2])
            .build()
            .schedule_intent_ix;

        // payer + magic_ctx + commit_acc + cau_acc
        assert_eq!(accounts.len(), 4);
    }

    #[test]
    fn test_fluent_commit_and_undelegate_then_commit() {
        let owner = compat::Pubkey::new_unique();
        let mut payer = TestAccount::new();
        let mut magic_ctx = TestAccount::new();
        let mut magic_prog = TestAccount::new();
        let mut acc1 = TestAccount::new();
        let mut acc2 = TestAccount::new();

        let (builder, _) = create_test_builder(&mut payer, &mut magic_ctx, &mut magic_prog, &owner);
        let info1 = create_mock_account_info(
            &acc1.key,
            &mut acc1.lamports,
            &mut acc1.data,
            &owner,
            false,
            true,
        );
        let info2 = create_mock_account_info(
            &acc2.key,
            &mut acc2.lamports,
            &mut acc2.data,
            &owner,
            false,
            true,
        );

        // Transition from commit_and_undelegate to commit
        let (accounts, _ix) = builder
            .commit_and_undelegate(&[info1])
            .commit(&[info2])
            .build()
            .schedule_intent_ix;

        // payer + magic_ctx + cau_acc + commit_acc
        assert_eq!(accounts.len(), 4);
    }

    #[test]
    fn test_fluent_full_chain_with_actions() {
        let owner = compat::Pubkey::new_unique();
        let mut payer = TestAccount::new();
        let mut magic_ctx = TestAccount::new();
        let mut magic_prog = TestAccount::new();
        let mut acc1 = TestAccount::new();
        let mut acc2 = TestAccount::new();
        let mut escrow_acc1 = TestAccount::new();
        let mut escrow_acc2 = TestAccount::new();

        let (builder, _) = create_test_builder(&mut payer, &mut magic_ctx, &mut magic_prog, &owner);
        let info1 = create_mock_account_info(
            &acc1.key,
            &mut acc1.lamports,
            &mut acc1.data,
            &owner,
            false,
            true,
        );
        let info2 = create_mock_account_info(
            &acc2.key,
            &mut acc2.lamports,
            &mut acc2.data,
            &owner,
            false,
            true,
        );
        let escrow_info1 = create_mock_account_info(
            &escrow_acc1.key,
            &mut escrow_acc1.lamports,
            &mut escrow_acc1.data,
            &owner,
            true,
            false,
        );
        let escrow_info2 = create_mock_account_info(
            &escrow_acc2.key,
            &mut escrow_acc2.lamports,
            &mut escrow_acc2.data,
            &owner,
            true,
            false,
        );
        let handler1 = create_test_call_handler(escrow_info1);
        let handler2 = create_test_call_handler(escrow_info2);

        // Full chain: commit with action -> transition to commit_and_undelegate with actions
        let (accounts, ix) = builder
            .commit(&[info1])
            .add_post_commit_actions([handler1])
            .commit_and_undelegate(&[info2])
            .add_post_commit_actions([handler2])
            .build()
            .schedule_intent_ix;

        // payer + magic_ctx + commit_acc + escrow1 + cau_acc + escrow2
        assert_eq!(accounts.len(), 6);
        assert!(!ix.data.is_empty());
    }

    // ----- MagicIntentBundle Normalization Tests -----

    #[test]
    fn test_bundle_dedup_within_commit_intent() {
        let shared_key = compat::Pubkey::new_unique();
        let mut acc1 = TestAccount::with_key(shared_key);
        let mut acc2 = TestAccount::with_key(shared_key); // duplicate key
        let mut acc3 = TestAccount::new();
        let owner = compat::Pubkey::new_unique();

        let info1 = create_mock_account_info(
            &acc1.key,
            &mut acc1.lamports,
            &mut acc1.data,
            &owner,
            false,
            true,
        );
        let info2 = create_mock_account_info(
            &acc2.key,
            &mut acc2.lamports,
            &mut acc2.data,
            &owner,
            false,
            true,
        );
        let info3 = create_mock_account_info(
            &acc3.key,
            &mut acc3.lamports,
            &mut acc3.data,
            &owner,
            false,
            true,
        );

        let mut bundle = MagicIntentBundle::default();
        bundle.add_intent(MagicIntent::Commit(CommitType::Standalone(vec![
            info1, info2, info3,
        ])));

        bundle.normalize();

        let commit = bundle.commit_intent.expect("commit should exist");
        let accounts = commit.committed_accounts();
        assert_eq!(accounts.len(), 2, "Duplicates should be removed");
    }

    #[test]
    fn test_bundle_cross_intent_overlap_resolution() {
        // Accounts in both Commit and CommitAndUndelegate should only be in CommitAndUndelegate
        let shared_key = compat::Pubkey::new_unique();
        let mut shared_acc1 = TestAccount::with_key(shared_key);
        let mut shared_acc2 = TestAccount::with_key(shared_key);
        let mut unique_acc = TestAccount::new();
        let owner = compat::Pubkey::new_unique();

        let shared_info1 = create_mock_account_info(
            &shared_acc1.key,
            &mut shared_acc1.lamports,
            &mut shared_acc1.data,
            &owner,
            false,
            true,
        );
        let shared_info2 = create_mock_account_info(
            &shared_acc2.key,
            &mut shared_acc2.lamports,
            &mut shared_acc2.data,
            &owner,
            false,
            true,
        );
        let unique_info = create_mock_account_info(
            &unique_acc.key,
            &mut unique_acc.lamports,
            &mut unique_acc.data,
            &owner,
            false,
            true,
        );

        let mut bundle = MagicIntentBundle::default();

        // Add shared account to Commit intent along with a unique one
        bundle.add_intent(MagicIntent::Commit(CommitType::Standalone(vec![
            shared_info1,
            unique_info,
        ])));

        // Add shared account to CommitAndUndelegate intent
        bundle.add_intent(MagicIntent::CommitAndUndelegate(CommitAndUndelegate {
            commit_type: CommitType::Standalone(vec![shared_info2]),
            undelegate_type: UndelegateType::Standalone,
        }));

        bundle.normalize();

        // Commit intent should only have the unique account
        let commit = bundle.commit_intent.expect("commit should exist");
        assert_eq!(commit.committed_accounts().len(), 1);
        assert_eq!(*commit.committed_accounts()[0].key, unique_acc.key);

        // CommitAndUndelegate should have the shared account
        let cau = bundle
            .commit_and_undelegate_intent
            .expect("cau should exist");
        assert_eq!(cau.commit_type.committed_accounts().len(), 1);
        assert_eq!(*cau.commit_type.committed_accounts()[0].key, shared_key);
    }

    #[test]
    fn test_bundle_commit_becomes_empty_after_overlap() {
        // When all Commit accounts are in CommitAndUndelegate, Commit is removed
        // and its handlers are merged into CommitAndUndelegate
        let shared_key = compat::Pubkey::new_unique();
        let mut shared_acc1 = TestAccount::with_key(shared_key);
        let mut shared_acc2 = TestAccount::with_key(shared_key);
        let mut escrow_acc = TestAccount::new();
        let owner = compat::Pubkey::new_unique();

        let shared_info1 = create_mock_account_info(
            &shared_acc1.key,
            &mut shared_acc1.lamports,
            &mut shared_acc1.data,
            &owner,
            false,
            true,
        );
        let shared_info2 = create_mock_account_info(
            &shared_acc2.key,
            &mut shared_acc2.lamports,
            &mut shared_acc2.data,
            &owner,
            false,
            true,
        );
        let escrow_info = create_mock_account_info(
            &escrow_acc.key,
            &mut escrow_acc.lamports,
            &mut escrow_acc.data,
            &owner,
            true,
            false,
        );

        let handler = create_test_call_handler(escrow_info);

        let mut bundle = MagicIntentBundle::default();

        // Add shared account to Commit intent with a handler
        bundle.add_intent(MagicIntent::Commit(CommitType::WithHandler {
            commited_accounts: vec![shared_info1],
            call_handlers: vec![handler],
            callbacks: vec![None],
        }));

        // Add same account to CommitAndUndelegate
        bundle.add_intent(MagicIntent::CommitAndUndelegate(CommitAndUndelegate {
            commit_type: CommitType::Standalone(vec![shared_info2]),
            undelegate_type: UndelegateType::Standalone,
        }));

        bundle.normalize();

        // Commit intent should be removed (was empty after overlap resolution)
        assert!(
            bundle.commit_intent.is_none(),
            "commit should be removed when empty"
        );

        // CommitAndUndelegate should have the handler merged
        let cau = bundle
            .commit_and_undelegate_intent
            .expect("cau should exist");
        match &cau.commit_type {
            CommitType::WithHandler { call_handlers, .. } => {
                assert_eq!(call_handlers.len(), 1, "Handler should be merged");
            }
            CommitType::Standalone(_) => panic!("Expected WithHandler after merge"),
        }
    }

    // ----- MagicIntentBundle Intent Merging Tests -----

    #[test]
    fn test_bundle_merge_multiple_commit_intents() {
        let mut acc1 = TestAccount::new();
        let mut acc2 = TestAccount::new();
        let owner = compat::Pubkey::new_unique();

        let info1 = create_mock_account_info(
            &acc1.key,
            &mut acc1.lamports,
            &mut acc1.data,
            &owner,
            false,
            true,
        );
        let info2 = create_mock_account_info(
            &acc2.key,
            &mut acc2.lamports,
            &mut acc2.data,
            &owner,
            false,
            true,
        );

        let mut bundle = MagicIntentBundle::default();

        // Add first commit intent
        bundle.add_intent(MagicIntent::Commit(CommitType::Standalone(vec![info1])));

        // Add second commit intent - should merge
        bundle.add_intent(MagicIntent::Commit(CommitType::Standalone(vec![info2])));

        let commit = bundle.commit_intent.expect("commit should exist");
        assert_eq!(
            commit.committed_accounts().len(),
            2,
            "Accounts should be merged"
        );
    }

    #[test]
    fn test_bundle_merge_base_actions() {
        let mut escrow_acc1 = TestAccount::new();
        let mut escrow_acc2 = TestAccount::new();
        let owner = compat::Pubkey::new_unique();

        let escrow_info1 = create_mock_account_info(
            &escrow_acc1.key,
            &mut escrow_acc1.lamports,
            &mut escrow_acc1.data,
            &owner,
            true,
            false,
        );
        let escrow_info2 = create_mock_account_info(
            &escrow_acc2.key,
            &mut escrow_acc2.lamports,
            &mut escrow_acc2.data,
            &owner,
            true,
            false,
        );

        let handler1 = create_test_call_handler(escrow_info1);
        let handler2 = create_test_call_handler(escrow_info2);

        let mut bundle = MagicIntentBundle::default();

        bundle.add_intent(MagicIntent::StandaloneActions(vec![handler1]));
        bundle.add_intent(MagicIntent::StandaloneActions(vec![handler2]));

        assert_eq!(bundle.standalone_actions.len(), 2);
    }

    // ----- MagicIntentBundleBuilder Tests -----

    #[test]
    fn test_builder_creates_instruction_with_commit_only() {
        let mut payer = TestAccount::new();
        let mut magic_ctx = TestAccount::new();
        let mut magic_prog = TestAccount::new();
        let mut acc1 = TestAccount::new();
        let owner = compat::Pubkey::new_unique();

        let payer_info = create_mock_account_info(
            &payer.key,
            &mut payer.lamports,
            &mut payer.data,
            &owner,
            true,
            true,
        );
        let ctx_info = create_mock_account_info(
            &magic_ctx.key,
            &mut magic_ctx.lamports,
            &mut magic_ctx.data,
            &owner,
            false,
            true,
        );
        let prog_info = create_mock_account_info(
            &magic_prog.key,
            &mut magic_prog.lamports,
            &mut magic_prog.data,
            &owner,
            false,
            false,
        );
        let acc1_info = create_mock_account_info(
            &acc1.key,
            &mut acc1.lamports,
            &mut acc1.data,
            &owner,
            false,
            true,
        );

        let (accounts, ix) = MagicIntentBundleBuilder::new(payer_info, ctx_info, prog_info)
            .commit(&[acc1_info])
            .build()
            .schedule_intent_ix;

        // Verify accounts: payer, context, acc1
        assert_eq!(accounts.len(), 3);
        assert_eq!(*accounts[0].key, payer.key);
        assert_eq!(*accounts[1].key, magic_ctx.key);
        assert_eq!(*accounts[2].key, acc1.key);

        // Verify instruction program
        assert_eq!(ix.program_id, magic_prog.key);
    }

    #[test]
    fn test_builder_deduplicates_accounts() {
        let mut payer = TestAccount::new();
        let mut magic_ctx = TestAccount::new();
        let mut magic_prog = TestAccount::new();

        // Same key used multiple times
        let shared_key = compat::Pubkey::new_unique();
        let mut acc1 = TestAccount::with_key(shared_key);
        let mut acc2 = TestAccount::with_key(shared_key);
        let owner = compat::Pubkey::new_unique();

        let payer_info = create_mock_account_info(
            &payer.key,
            &mut payer.lamports,
            &mut payer.data,
            &owner,
            true,
            true,
        );
        let ctx_info = create_mock_account_info(
            &magic_ctx.key,
            &mut magic_ctx.lamports,
            &mut magic_ctx.data,
            &owner,
            false,
            true,
        );
        let prog_info = create_mock_account_info(
            &magic_prog.key,
            &mut magic_prog.lamports,
            &mut magic_prog.data,
            &owner,
            false,
            false,
        );
        let acc1_info = create_mock_account_info(
            &acc1.key,
            &mut acc1.lamports,
            &mut acc1.data,
            &owner,
            false,
            true,
        );
        let acc2_info = create_mock_account_info(
            &acc2.key,
            &mut acc2.lamports,
            &mut acc2.data,
            &owner,
            false,
            true,
        );

        let (accounts, _ix) = MagicIntentBundleBuilder::new(payer_info, ctx_info, prog_info)
            .commit(&[acc1_info, acc2_info])
            .build()
            .schedule_intent_ix;

        // Should be: payer, context, shared_account (deduplicated)
        assert_eq!(accounts.len(), 3, "Duplicate accounts should be removed");
    }

    #[test]
    fn test_builder_with_all_intent_types() {
        let mut payer = TestAccount::new();
        let mut magic_ctx = TestAccount::new();
        let mut magic_prog = TestAccount::new();
        let mut commit_acc = TestAccount::new();
        let mut cau_acc = TestAccount::new();
        let mut escrow_acc = TestAccount::new();
        let owner = compat::Pubkey::new_unique();

        let payer_info = create_mock_account_info(
            &payer.key,
            &mut payer.lamports,
            &mut payer.data,
            &owner,
            true,
            true,
        );
        let ctx_info = create_mock_account_info(
            &magic_ctx.key,
            &mut magic_ctx.lamports,
            &mut magic_ctx.data,
            &owner,
            false,
            true,
        );
        let prog_info = create_mock_account_info(
            &magic_prog.key,
            &mut magic_prog.lamports,
            &mut magic_prog.data,
            &owner,
            false,
            false,
        );
        let commit_info = create_mock_account_info(
            &commit_acc.key,
            &mut commit_acc.lamports,
            &mut commit_acc.data,
            &owner,
            false,
            true,
        );
        let cau_info = create_mock_account_info(
            &cau_acc.key,
            &mut cau_acc.lamports,
            &mut cau_acc.data,
            &owner,
            false,
            true,
        );
        let escrow_info = create_mock_account_info(
            &escrow_acc.key,
            &mut escrow_acc.lamports,
            &mut escrow_acc.data,
            &owner,
            true,
            false,
        );

        let handler = create_test_call_handler(escrow_info);

        let (accounts, ix) = MagicIntentBundleBuilder::new(payer_info, ctx_info, prog_info)
            .commit(&[commit_info])
            .commit_and_undelegate(&[cau_info])
            .add_standalone_actions([handler])
            .build()
            .schedule_intent_ix;

        // Should have: payer, context, commit_acc, cau_acc, escrow_acc
        assert_eq!(accounts.len(), 5);

        // Verify instruction data contains ScheduleIntentBundle
        assert!(!ix.data.is_empty());
    }

    #[test]
    fn test_builder_fluent_api() {
        let mut payer = TestAccount::new();
        let mut magic_ctx = TestAccount::new();
        let mut magic_prog = TestAccount::new();
        let mut acc1 = TestAccount::new();
        let owner = compat::Pubkey::new_unique();

        let payer_info = create_mock_account_info(
            &payer.key,
            &mut payer.lamports,
            &mut payer.data,
            &owner,
            true,
            true,
        );
        let ctx_info = create_mock_account_info(
            &magic_ctx.key,
            &mut magic_ctx.lamports,
            &mut magic_ctx.data,
            &owner,
            false,
            true,
        );
        let prog_info = create_mock_account_info(
            &magic_prog.key,
            &mut magic_prog.lamports,
            &mut magic_prog.data,
            &owner,
            false,
            false,
        );
        let acc1_info = create_mock_account_info(
            &acc1.key,
            &mut acc1.lamports,
            &mut acc1.data,
            &owner,
            false,
            true,
        );

        // Test that fluent builder methods chain properly
        let (accounts, _ix) = MagicIntentBundleBuilder::new(payer_info, ctx_info, prog_info)
            .commit(&[acc1_info])
            .build()
            .schedule_intent_ix;

        assert_eq!(accounts.len(), 3);
    }

    // ----- Account Collection Tests -----

    #[test]
    fn test_collect_accounts_from_call_handler() {
        let mut escrow_acc = TestAccount::new();
        let owner = compat::Pubkey::new_unique();

        let escrow_info = create_mock_account_info(
            &escrow_acc.key,
            &mut escrow_acc.lamports,
            &mut escrow_acc.data,
            &owner,
            true,
            false,
        );

        let handler = create_test_call_handler(escrow_info);
        let mut container = Vec::new();
        handler.collect_accounts(&mut container);

        assert_eq!(container.len(), 1);
        assert_eq!(*container[0].key, escrow_acc.key);
    }

    #[test]
    fn test_collect_accounts_from_commit_with_handler() {
        let mut acc1 = TestAccount::new();
        let mut escrow_acc = TestAccount::new();
        let owner = compat::Pubkey::new_unique();

        let acc1_info = create_mock_account_info(
            &acc1.key,
            &mut acc1.lamports,
            &mut acc1.data,
            &owner,
            false,
            true,
        );
        let escrow_info = create_mock_account_info(
            &escrow_acc.key,
            &mut escrow_acc.lamports,
            &mut escrow_acc.data,
            &owner,
            true,
            false,
        );

        let handler = create_test_call_handler(escrow_info);
        let commit = CommitType::WithHandler {
            commited_accounts: vec![acc1_info],
            call_handlers: vec![handler],
            callbacks: vec![None],
        };

        let mut container = Vec::new();
        commit.collect_accounts(&mut container);

        // Should have both the committed account and escrow from handler
        assert_eq!(container.len(), 2);
    }

    // Verifies flat action index ordering:
    #[test]
    fn test_callback_action_index_ordering() {
        let owner = compat::Pubkey::new_unique();
        let mut payer = TestAccount::new();
        let mut magic_ctx = TestAccount::new();
        let mut magic_prog = TestAccount::new();
        let mut commit_acc = TestAccount::new();
        let mut cau_acc = TestAccount::new();
        let mut escrow_commit = TestAccount::new();
        let mut escrow_cau_commit = TestAccount::new();
        let mut escrow_cau_undelegate = TestAccount::new();
        let mut escrow_standalone = TestAccount::new();

        let (builder, _) = create_test_builder(&mut payer, &mut magic_ctx, &mut magic_prog, &owner);

        let make_callback = || ActionCallback {
            destination_program: compat::Pubkey::new_unique(),
            discriminator: vec![0u8; 8],
            payload: vec![],
            compute_units: 10_000,
            accounts: vec![],
        };

        let IntentInstructions {
            add_callback_ixs, ..
        } = builder
            .commit(&[make_info(&mut commit_acc)])
            .add_post_commit_action(create_test_call_handler(make_info(&mut escrow_commit)))
            .then(make_callback())
            .commit_and_undelegate(&[make_info(&mut cau_acc)])
            .add_post_commit_action(create_test_call_handler(make_info(&mut escrow_cau_commit)))
            .then(make_callback())
            .add_post_undelegate_action(create_test_call_handler(make_info(
                &mut escrow_cau_undelegate,
            )))
            .then(make_callback())
            .add_standalone_action(create_test_call_handler(make_info(&mut escrow_standalone)))
            .then(make_callback())
            .build();

        assert_eq!(add_callback_ixs.len(), 4);

        let action_index = |ix: &Instruction| -> u8 {
            match bincode::deserialize::<MagicBlockInstruction>(&ix.data).unwrap() {
                MagicBlockInstruction::AddActionCallback(args) => args.action_index,
                _ => panic!("expected AddActionCallback"),
            }
        };

        assert_eq!(action_index(&add_callback_ixs[0].1), 0, "commit action");
        assert_eq!(action_index(&add_callback_ixs[1].1), 1, "cau commit action");
        assert_eq!(
            action_index(&add_callback_ixs[2].1),
            2,
            "cau undelegate action"
        );
        assert_eq!(action_index(&add_callback_ixs[3].1), 3, "standalone action");
    }
}
