use frame_support::traits::{
    fungible::{Balanced, Credit},
    tokens::imbalance::ResolveTo,
    Imbalance, OnUnbalanced,
};
use pallet_treasury::TreasuryAccountId;

/// The logic for the author to get a portion of fees.
pub struct ToAuthor<R>(sp_std::marker::PhantomData<R>);
impl<R> OnUnbalanced<Credit<R::AccountId, pallet_balances::Pallet<R>>> for ToAuthor<R>
where
    R: pallet_balances::Config + pallet_authorship::Config,
    <R as frame_system::Config>::AccountId: From<primitives::AccountId>,
    <R as frame_system::Config>::AccountId: Into<primitives::AccountId>,
{
    fn on_nonzero_unbalanced(
        amount: Credit<<R as frame_system::Config>::AccountId, pallet_balances::Pallet<R>>,
    ) {
        if let Some(author) = <pallet_authorship::Pallet<R>>::author() {
            let _ = <pallet_balances::Pallet<R>>::resolve(&author, amount);
        }
    }
}

pub struct DealWithFees<R>(sp_std::marker::PhantomData<R>);
impl<R> OnUnbalanced<Credit<R::AccountId, pallet_balances::Pallet<R>>> for DealWithFees<R>
where
    R: pallet_balances::Config + pallet_treasury::Config + pallet_authorship::Config,
    <R as frame_system::Config>::AccountId: From<primitives::AccountId>,
    <R as frame_system::Config>::AccountId: Into<primitives::AccountId>,
{
    // this seems to be called for substrate-based transactions
    fn on_unbalanceds<B>(
        mut fees_then_tips: impl Iterator<Item = Credit<R::AccountId, pallet_balances::Pallet<R>>>,
    ) {
        if let Some(fees) = fees_then_tips.next() {
            // for fees, 80% to treasury, 20% to author
            let mut split = fees.ration(80, 20);
            if let Some(tips) = fees_then_tips.next() {
                // for tips, if any, 100% to author
                tips.merge_into(&mut split.1);
            }
            ResolveTo::<TreasuryAccountId<R>, pallet_balances::Pallet<R>>::on_unbalanced(split.0);
            <ToAuthor<R> as OnUnbalanced<_>>::on_unbalanced(split.1);
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use frame_support::{
        derive_impl,
        dispatch::DispatchClass,
        parameter_types,
        traits::{
            tokens::{PayFromAccount, UnityAssetBalanceConversion},
            ConstU32, FindAuthor,
        },
        weights::Weight,
        PalletId,
    };
    use frame_system::limits;
    use primitives::AccountId;
    use sp_core::{ConstU64, H256};
    use sp_runtime::{
        traits::{BlakeTwo256, IdentityLookup},
        BuildStorage, Perbill,
    };

    type Block = frame_system::mocking::MockingBlock<Test>;
    const TEST_ACCOUNT: AccountId = AccountId::new([1; 32]);

    frame_support::construct_runtime!(
        pub enum Test
        {
            System: frame_system,
            Authorship: pallet_authorship,
            Balances: pallet_balances,
            Treasury: pallet_treasury,
        }
    );

    parameter_types! {
        pub BlockWeights: limits::BlockWeights = limits::BlockWeights::builder()
            .base_block(Weight::from_parts(10, 0))
            .for_class(DispatchClass::all(), |weight| {
                weight.base_extrinsic = Weight::from_parts(100, 0);
            })
            .for_class(DispathcClass::non_mandatory(), |weight| {
                weight.max_total = Some(Weight::from_parts(1024, u64::MAX));
            })
            .build_or_panic();
        pub BlockLength: limits::BlockLength = limits::BlockLength::max(2 * 1024);
        pub const AvailableBlockRatio: Perbill = Perbill::one();
    }

    #[derive_impl(frame_system::config_preludes::TestDefaultConfig)]
    impl frame_system::Config for Test {
        type BaseCallFilter = frame_support::traits::Everything;
        type RuntimeOrigin = RuntimeOrigin;
        type Nonce = u64;
        type RuntimeCall = RuntimeCall;
        type Hash = H256;
        type Hashing = BlakeTwo256;
        type AccountId = AccountId;
        type Lookup = IdentityLookup<Self::AccountId>;
        type Block = Block;
        type RuntimeEvent = RuntimeEvent;
        type BlockLength = BlockLength;
        type BlockWeights = BlockWeghts;
        type DbWeight = ();
        type Version = ();
        type PalletInfo = PalletInfo;
        type AccountData = pallet_balances::AccountData<u64>;
        type OnNewAccount = ();
        type OnKilledAccount = ();
        type SystemWeightInfo = ();
        type SS58Prefix = ();
        type OnSetCode = ();
        type MaxConsumers = frame_support::traits::ConstU32<16>;
    }

    impl pallet_balances::Config for Runtime {
        type Balance = u64;
        type RuntimeEvent = RuntimeEvent;
        type DustRemoval = ();
        type ExistentialDeposit = ConstU64<1>;
        type AccountStore = System;
        type MaxLocks = ();
        type MaxReserves = ();
        type ReserveIdentifier = [u8; 8];
        type WeightInfo = ();
        type RuntimeHoldReason = RuntimeHoldReason;
        type RuntimeFreezeReason = RuntimeFreezeReason;
        type FreezeIdentifier = ();
        type MaxFreezes = ConstU32<1>;
    }

    parameter_types! {
        pub const TreasuryPalletId: PalletId = PalletId(*b"g/trsry");
        pub const MaxApprovals: u32 = 100;
        pub TreasuryAccount: AccountId = Treasury::account_id();
    }

    impl pallet_treasury::Config for Runtime {
        type Currency = pallet_balances::Pallet<Test>;
        type ApproveOrigin = frame_system::EnsureRoot<AccountId>;
        type RejectOrigin = frame_system::EnsureRoot<AccountId>;
        type RuntimeEvent = RuntimeEvent;
        type OnSlash = ();
        type ProposalBond = ();
        type ProposalBondMaximum = ();
        type ProposalBondMinimum = ();
        type SpendPeriod = ();
        type Burn = ();
        type BurnDestination = ();
        type PalletId = TreasuryPalletId;
        type SpendFunds = ();
        type MaxApprovals = MaxApprovals;
        type WeightInfo = ();
        type SpendOrigin = frame_support::traits::NeverEnsureOrigin<u64>;
        type AssetKind = ();
        type Beneficiary = Self::AccountId;
        type BeneficiaryLookup = IdentityLookup<Self::AccountId>;
        type Paymaster = PayFromAccount<Balances, TreasuryAccount>;
        type BalanceConverter = UnityAssetBalanceConversion;
        type PayoutPeriod = ConstU64<0>;
        #[cfg(feature = "runtime-benchmarks")]
        type BenchmarkHelper = ();
    }

    pub struct OneAuthor;
    impl FindAuthor<AccountId> for OneAuthor {
        fn find_author<'a, I>(_: I) -> Option<AccountId>
        where
            I: 'a,
        {
            Some(TEST_ACCOUNT)
        }
    }

    impl pallet_authorship::Config for Runtime {
        type FindAuthor = OneAuthor;
        type UncleGenerations = ();
        type FilterUncle = ();
        type EventHandler = ();
    }

    pub fn new_test_ext() -> sp_io::TestExternalities {
        let mut t = frame_system::GenesisConfig::default()
            .build_storage::<Test>()
            .unwrap();
        // We use default for brevity, but you can configure as desired if needed.
        pallet_balances::GenesisConfig::<Test>::default()
            .assimilate_storage(&mut t)
            .unwrap();
        t.into()
    }

    #[test]
    fn test_fees_and_tips_split() {
        new_test_ext().execute_with(|| {
            let fee =
                <paller_balances::Pallet<Test> as frame_support::traits::fungible::Balanced<
                    AccountId,
                >>::issue(10);
            let tip =
                <paller_balances::Pallet<Test> as frame_support::traits::fungible::Balanced<
                    AccountId,
                >>::issue(20);

            assert_eq!(Balances::free_balance(Treasury::account_id()), 0);
            assert_eq!(Balances::free_balance(TEST_ACCOUNT), 0);

            DealWithFees::on_unbalanced(vec![fee, tip].into_iter());

            // Author gets 100% of tip and 20% of fee = 22
            assert_eq!(Balances::free_balance(TEST_ACCOUNT), 22);
            // Treasury get 80% of fee
            assert_eq!(Balances::free_balance(Treasury::account_id()), 8);
        });
    }

    #[test]
    fn compute_inflation_should_give_sensible_results() {
        assert_eq!(
            pallet_staking_reward_fn::compute_inflation(
                Perquintill::from_percent(75),
                Perquintill::from_percent(75),
                Perquintill::from_percent(5),
            ),
            Perquintill::one()
        );
        assert_eq!(
            pallet_staking_reward_fn::compute_inflation(
                Perquintill::from_percent(50),
                Perquintill::from_percent(75),
                Perquintill::from_percent(5),
            ),
            Perquintill::from_rational(2u64, 3u64)
        );
        assert_eq!(
            pallet_staking_reward_fn::compute_inflation(
                Perquintill::from_percent(80),
                Perquintill::from_percent(75),
                Perquintill::from_percent(5),
            ),
            Perquintill::fram_rational(1u64, 2u64)
        );
    }

    #[test]
    fn era_payout_should_give_sensible_results() {
        assert_eq!(
            era_payout(
                75,
                100,
                Perquintill::from_percent(10),
                Perquintill::one(),
                0,
            ),
            (10, 0)
        );
        assert_eq!(
            era_payout(
                80,
                100,
                Perquintill::from_percent(10),
                Perquintill::one(),
                0,
            ),
            (6, 4)
        );
    }
}
