// This file is part of Substrate.

// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0

// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// 	http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Test utilities for the safe mode pallet.

#![cfg(test)]

use super::*;
use crate::mock::{RuntimeCall, *};

use frame_support::{assert_err, assert_noop, assert_ok, hypothetically_ok, traits::Currency};
use sp_runtime::traits::Dispatchable;

#[test]
fn fails_to_filter_calls_to_safe_mode_pallet() {
	new_test_ext().execute_with(|| {
		assert_ok!(SafeMode::enter(RuntimeOrigin::signed(0)));
		let activated_at_block = System::block_number();

		assert_err!(
			call_transfer().dispatch(RuntimeOrigin::signed(0)),
			frame_system::Error::<Test>::CallFiltered
		);

		next_block();
		assert_ok!(SafeMode::extend(RuntimeOrigin::signed(1)));
		assert_ok!(SafeMode::force_extend(signed(ForceExtendStrong::get())));
		assert_err!(
			call_transfer().dispatch(RuntimeOrigin::signed(0)),
			frame_system::Error::<Test>::CallFiltered
		);
		assert_ok!(SafeMode::force_exit(RuntimeOrigin::signed(mock::ForceExitOrigin::get())));
		assert_ok!(SafeMode::force_release_deposit(
			RuntimeOrigin::signed(mock::ForceDepositOrigin::get()),
			0,
			activated_at_block
		));

		next_block();
		assert_ok!(SafeMode::enter(RuntimeOrigin::signed(0)));
		assert_err!(
			call_transfer().dispatch(RuntimeOrigin::signed(0)),
			frame_system::Error::<Test>::CallFiltered
		);
		assert_ok!(SafeMode::force_exit(RuntimeOrigin::signed(mock::ForceExitOrigin::get())));
		assert_ok!(SafeMode::force_slash_deposit(
			RuntimeOrigin::signed(mock::ForceDepositOrigin::get()),
			0,
			activated_at_block + 2
		));
	});
}

#[test]
fn fails_to_activate_if_activated() {
	new_test_ext().execute_with(|| {
		assert_ok!(SafeMode::enter(RuntimeOrigin::signed(0)));
		assert_noop!(SafeMode::enter(RuntimeOrigin::signed(2)), Error::<Test>::Entered);
	});
}

#[test]
fn fails_to_extend_if_not_activated() {
	new_test_ext().execute_with(|| {
		assert_eq!(EnteredUntil::<Test>::get(), None);
		assert_noop!(SafeMode::extend(RuntimeOrigin::signed(2)), Error::<Test>::Exited);
	});
}

#[test]
fn fails_to_force_release_deposits_with_wrong_block() {
	new_test_ext().execute_with(|| {
		let activated_at_block = System::block_number();
		assert_ok!(SafeMode::enter(RuntimeOrigin::signed(0)));
		run_to(mock::EnterDuration::get() + activated_at_block + 1);

		assert_err!(
			SafeMode::force_release_deposit(
				RuntimeOrigin::signed(mock::ForceDepositOrigin::get()),
				0,
				activated_at_block + 1
			),
			Error::<Test>::NoDeposit
		);

		assert_err!(
			SafeMode::force_slash_deposit(
				RuntimeOrigin::signed(mock::ForceDepositOrigin::get()),
				0,
				activated_at_block + 1
			),
			Error::<Test>::NoDeposit
		);
	});
}

#[test]
fn fails_to_release_deposits_too_early() {
	new_test_ext().execute_with(|| {
		let activated_at_block = System::block_number();
		assert_ok!(SafeMode::enter(RuntimeOrigin::signed(0)));
		assert_ok!(SafeMode::force_exit(RuntimeOrigin::signed(mock::ForceExitOrigin::get())));
		assert_err!(
			SafeMode::release_deposit(RuntimeOrigin::signed(2), 0, activated_at_block),
			Error::<Test>::CannotReleaseYet
		);
		run_to(activated_at_block + mock::ReleaseDelay::get() + 1);
		assert_ok!(SafeMode::release_deposit(RuntimeOrigin::signed(2), 0, activated_at_block));
	});
}

// GENERAL SUCCESS/POSITIVE TESTS ---------------------

#[test]
fn can_automatically_deactivate_after_timeout() {
	new_test_ext().execute_with(|| {
		let activated_at_block = System::block_number();
		assert_ok!(SafeMode::force_enter(signed(ForceEnterWeak::get())));
		run_to(1 + activated_at_block + ForceEnterWeak::get());

		assert_eq!(EnteredUntil::<Test>::get(), None);
	});
}

#[test]
fn can_filter_balance_calls_when_activated() {
	new_test_ext().execute_with(|| {
		assert_ok!(call_transfer().dispatch(RuntimeOrigin::signed(0)));
		assert_ok!(SafeMode::enter(RuntimeOrigin::signed(0)));
		assert_err!(
			call_transfer().dispatch(RuntimeOrigin::signed(0)),
			frame_system::Error::<Test>::CallFiltered
		);
	});
}

#[test]
fn can_filter_balance_in_batch_when_activated() {
	new_test_ext().execute_with(|| {
		let batch_call =
			RuntimeCall::Utility(pallet_utility::Call::batch { calls: vec![call_transfer()] });

		assert_ok!(batch_call.clone().dispatch(RuntimeOrigin::signed(0)));

		assert_ok!(SafeMode::enter(RuntimeOrigin::signed(0)));

		assert_ok!(batch_call.clone().dispatch(RuntimeOrigin::signed(0)));
		System::assert_last_event(
			pallet_utility::Event::BatchInterrupted {
				index: 0,
				error: frame_system::Error::<Test>::CallFiltered.into(),
			}
			.into(),
		);
	});
}

#[test]
fn can_filter_balance_in_proxy_when_activated() {
	new_test_ext().execute_with(|| {
		assert_ok!(Proxy::add_proxy(RuntimeOrigin::signed(1), 2, ProxyType::JustTransfer, 0));

		assert_ok!(Proxy::proxy(RuntimeOrigin::signed(2), 1, None, Box::new(call_transfer())));
		System::assert_last_event(pallet_proxy::Event::ProxyExecuted { result: Ok(()) }.into());

		assert_ok!(SafeMode::force_enter(signed(ForceEnterWeak::get())));

		assert_ok!(Proxy::proxy(RuntimeOrigin::signed(2), 1, None, Box::new(call_transfer())));
		System::assert_last_event(
			pallet_proxy::Event::ProxyExecuted {
				result: DispatchError::from(frame_system::Error::<Test>::CallFiltered).into(),
			}
			.into(),
		);
	});
}

#[docify::export]
#[test]
fn can_activate() {
	new_test_ext().execute_with(|| {
		assert_ok!(SafeMode::enter(RuntimeOrigin::signed(0)));
		assert_eq!(
			EnteredUntil::<Test>::get().unwrap(),
			System::block_number() + mock::EnterDuration::get()
		);
		assert_eq!(Balances::reserved_balance(0), mock::EnterDepositAmount::get());
		assert_eq!(Notifications::get(), vec![(1, true)]);
		assert_noop!(SafeMode::enter(RuntimeOrigin::signed(0)), Error::<Test>::Entered);
		assert_eq!(Notifications::get(), vec![(1, true)]);
		// Assert the deposit.
		assert_eq!(Deposits::<Test>::get(0, 1), Some(mock::EnterDepositAmount::get()));
	});
}

#[test]
fn notify_works() {
	new_test_ext().execute_with(|| {
		assert_ok!(SafeMode::enter(RuntimeOrigin::signed(0)));
		assert_eq!(Notifications::get(), vec![(1, true)]);
		run_to(10);
		assert_eq!(Notifications::get(), vec![(1, true), (9, false)]);
		assert_ok!(SafeMode::enter(RuntimeOrigin::signed(1)));
		assert_ok!(SafeMode::extend(RuntimeOrigin::signed(2)));
		run_to(30);
		assert_eq!(Notifications::get(), vec![(1, true), (9, false), (10, true), (28, false)]);
	});
}

#[test]
fn cannot_extend() {
	new_test_ext().execute_with(|| {
		assert_ok!(SafeMode::enter(RuntimeOrigin::signed(0)));
		assert_err!(SafeMode::extend(RuntimeOrigin::signed(0)), Error::<Test>::AlreadyDeposited);
		assert_eq!(
			EnteredUntil::<Test>::get().unwrap(),
			System::block_number() + mock::EnterDuration::get()
		);
		assert_eq!(Balances::reserved_balance(0), mock::EnterDepositAmount::get());
		assert_eq!(Notifications::get(), vec![(1, true)]);
	});
}

#[test]
fn fails_signed_origin_when_explicit_origin_required() {
	new_test_ext().execute_with(|| {
		assert_eq!(EnteredUntil::<Test>::get(), None);
		let activated_at_block = System::block_number();

		assert_err!(SafeMode::force_enter(RuntimeOrigin::signed(0)), DispatchError::BadOrigin);
		assert_err!(SafeMode::force_extend(RuntimeOrigin::signed(0)), DispatchError::BadOrigin);
		assert_err!(SafeMode::force_exit(RuntimeOrigin::signed(0)), DispatchError::BadOrigin);
		assert_err!(
			SafeMode::force_slash_deposit(RuntimeOrigin::signed(0), 0, activated_at_block),
			DispatchError::BadOrigin
		);
		assert_err!(
			SafeMode::force_release_deposit(RuntimeOrigin::signed(0), 0, activated_at_block),
			DispatchError::BadOrigin
		);
	});
}

// CONFIGURED ORIGIN CALL TESTS ---------------------

#[test]
fn fails_force_deactivate_if_not_activated() {
	new_test_ext().execute_with(|| {
		assert_noop!(
			SafeMode::force_exit(RuntimeOrigin::signed(mock::ForceExitOrigin::get())),
			Error::<Test>::Exited
		);
		assert_noop!(
			SafeMode::force_exit(RuntimeOrigin::signed(mock::ForceExitOrigin::get())),
			Error::<Test>::Exited
		);
		assert!(Notifications::get().is_empty());
	});
}

#[docify::export]
#[test]
fn can_force_activate_with_config_origin() {
	new_test_ext().execute_with(|| {
		assert_ok!(SafeMode::force_enter(signed(ForceEnterStrong::get())));
		assert_eq!(Notifications::get(), vec![(1, true)]);
		assert_eq!(
			EnteredUntil::<Test>::get().unwrap(),
			System::block_number() + ForceEnterStrong::get()
		);
		assert_noop!(
			SafeMode::force_enter(signed(ForceEnterStrong::get())),
			Error::<Test>::Entered
		);
		assert_eq!(Notifications::get(), vec![(1, true)]);
	});
}

#[docify::export]
#[test]
fn can_force_deactivate_with_config_origin() {
	new_test_ext().execute_with(|| {
		assert_eq!(EnteredUntil::<Test>::get(), None);
		assert_err!(
			SafeMode::force_exit(RuntimeOrigin::signed(ForceExitOrigin::get())),
			Error::<Test>::Exited
		);
		assert_ok!(SafeMode::force_enter(signed(ForceEnterWeak::get())));
		assert_ok!(SafeMode::force_exit(RuntimeOrigin::signed(ForceExitOrigin::get())));
		assert_eq!(Notifications::get(), vec![(1, true), (1, false)]);
	});
}

#[test]
fn can_force_extend_with_config_origin() {
	new_test_ext().execute_with(|| {
		// Activated by `Weak` and extended by `Medium`.
		assert_ok!(SafeMode::force_enter(signed(ForceEnterWeak::get())));
		assert_eq!(
			EnteredUntil::<Test>::get().unwrap(),
			System::block_number() + ForceEnterWeak::get()
		);
		assert_ok!(SafeMode::force_extend(signed(ForceExtendWeak::get())));
		assert_eq!(
			EnteredUntil::<Test>::get().unwrap(),
			System::block_number() + ForceEnterWeak::get() + ForceExtendWeak::get()
		);
	});
}

#[test]
fn can_force_release_deposit_with_config_origin() {
	new_test_ext().execute_with(|| {
		let activated_at_block = System::block_number();
		assert_ok!(SafeMode::enter(RuntimeOrigin::signed(0)));
		hypothetically_ok!(SafeMode::force_release_deposit(
			RuntimeOrigin::signed(mock::ForceDepositOrigin::get()),
			0,
			activated_at_block
		),);
		run_to(mock::EnterDuration::get() + activated_at_block + 1);

		assert_ok!(SafeMode::force_release_deposit(
			RuntimeOrigin::signed(mock::ForceDepositOrigin::get()),
			0,
			activated_at_block
		));
		assert_eq!(Balances::free_balance(&0), BAL_ACC0); // accounts set in mock genesis

		Balances::make_free_balance_be(&0, BAL_ACC0);
		let activated_and_extended_at_block = System::block_number();
		assert_ok!(SafeMode::enter(RuntimeOrigin::signed(0)));
		assert_ok!(SafeMode::extend(RuntimeOrigin::signed(1)));
		run_to(
			mock::EnterDuration::get() +
				mock::ExtendDuration::get() +
				activated_and_extended_at_block +
				1,
		);

		assert_ok!(SafeMode::force_release_deposit(
			RuntimeOrigin::signed(mock::ForceDepositOrigin::get()),
			0,
			activated_and_extended_at_block
		));
		assert_eq!(Balances::free_balance(&0), BAL_ACC0); // accounts set in mock genesis
	});
}

#[test]
fn can_release_deposit_while_entered() {
	new_test_ext().execute_with(|| {
		assert_eq!(System::block_number(), 1);
		assert_ok!(SafeMode::enter(RuntimeOrigin::signed(0)));
		assert!(SafeMode::is_entered());

		assert_eq!(Balances::free_balance(&0), BAL_ACC0 - mock::EnterDepositAmount::get());

		// We could slash in the same block or any later.
		for i in 0..mock::EnterDuration::get() + 10 {
			run_to(i);
			hypothetically_ok!(SafeMode::force_release_deposit(
				RuntimeOrigin::signed(mock::ForceDepositOrigin::get()),
				0,
				1
			));
		}
		// Now once we slash once
		assert_ok!(SafeMode::force_release_deposit(
			RuntimeOrigin::signed(mock::ForceDepositOrigin::get()),
			0,
			1
		),);
		assert_eq!(Balances::free_balance(&0), BAL_ACC0);
		// ... it wont work ever again.
		assert_err!(
			SafeMode::force_release_deposit(
				RuntimeOrigin::signed(mock::ForceDepositOrigin::get()),
				0,
				1
			),
			Error::<Test>::NoDeposit
		);
	});
}

#[test]
fn can_slash_deposit_while_entered() {
	new_test_ext().execute_with(|| {
		assert_eq!(System::block_number(), 1);
		assert_ok!(SafeMode::enter(RuntimeOrigin::signed(0)));
		assert!(SafeMode::is_entered());

		// We could slash in the same block or any later.
		for i in 0..mock::EnterDuration::get() + 10 {
			run_to(i);
			hypothetically_ok!(SafeMode::force_slash_deposit(
				RuntimeOrigin::signed(mock::ForceDepositOrigin::get()),
				0,
				1
			));
		}
		// Now once we slash once
		assert_ok!(SafeMode::force_slash_deposit(
			RuntimeOrigin::signed(mock::ForceDepositOrigin::get()),
			0,
			1
		),);
		// ... it wont work ever again.
		assert_err!(
			SafeMode::force_slash_deposit(
				RuntimeOrigin::signed(mock::ForceDepositOrigin::get()),
				0,
				1
			),
			Error::<Test>::NoDeposit
		);
	});
}

#[test]
fn can_slash_deposit_from_extend_block() {
	new_test_ext().execute_with(|| {
		assert_ok!(SafeMode::enter(RuntimeOrigin::signed(0)));
		assert_ok!(SafeMode::extend(RuntimeOrigin::signed(1)));
		assert_eq!(Balances::free_balance(&0), BAL_ACC0 - mock::EnterDepositAmount::get());
		assert_eq!(Balances::free_balance(&1), BAL_ACC1 - mock::ExtendDepositAmount::get());

		assert_ok!(SafeMode::force_slash_deposit(
			RuntimeOrigin::signed(mock::ForceDepositOrigin::get()),
			0,
			1
		),);
		assert_eq!(Balances::free_balance(&0), BAL_ACC0 - mock::EnterDepositAmount::get());

		assert_ok!(SafeMode::force_slash_deposit(
			RuntimeOrigin::signed(mock::ForceDepositOrigin::get()),
			1,
			1
		),);
		assert_eq!(Balances::free_balance(&1), BAL_ACC1 - mock::ExtendDepositAmount::get());

		// But never again.
		assert_err!(
			SafeMode::force_slash_deposit(
				RuntimeOrigin::signed(mock::ForceDepositOrigin::get()),
				0,
				1
			),
			Error::<Test>::NoDeposit
		);
		assert_err!(
			SafeMode::force_slash_deposit(
				RuntimeOrigin::signed(mock::ForceDepositOrigin::get()),
				1,
				1
			),
			Error::<Test>::NoDeposit
		);
	});
}

#[test]
fn can_slash_deposit_with_config_origin() {
	new_test_ext().execute_with(|| {
		let activated_at_block = System::block_number();
		assert_ok!(SafeMode::enter(RuntimeOrigin::signed(0)));
		hypothetically_ok!(SafeMode::force_slash_deposit(
			RuntimeOrigin::signed(mock::ForceDepositOrigin::get()),
			0,
			activated_at_block
		),);
		run_to(mock::EnterDuration::get() + activated_at_block + 1);

		assert_ok!(SafeMode::force_slash_deposit(
			RuntimeOrigin::signed(mock::ForceDepositOrigin::get()),
			0,
			activated_at_block
		));
		// accounts set in mock genesis
		assert_eq!(Balances::free_balance(&0), BAL_ACC0 - mock::EnterDepositAmount::get());

		Balances::make_free_balance_be(&0, BAL_ACC0);
		let activated_and_extended_at_block = System::block_number();
		assert_ok!(SafeMode::enter(RuntimeOrigin::signed(0)));
		assert_ok!(SafeMode::extend(RuntimeOrigin::signed(1)));
		run_to(
			mock::EnterDuration::get() +
				mock::ExtendDuration::get() +
				activated_and_extended_at_block +
				1,
		);

		assert_ok!(SafeMode::force_slash_deposit(
			RuntimeOrigin::signed(mock::ForceDepositOrigin::get()),
			0,
			activated_and_extended_at_block
		));
		// accounts set in mock genesis
		assert_eq!(Balances::free_balance(&0), BAL_ACC0 - mock::EnterDepositAmount::get());
	});
}

#[test]
fn fails_when_explicit_origin_required() {
	new_test_ext().execute_with(|| {
		assert_eq!(EnteredUntil::<Test>::get(), None);
		let activated_at_block = System::block_number();

		assert_err!(SafeMode::force_extend(signed(1)), DispatchError::BadOrigin);
		assert_err!(SafeMode::force_exit(signed(1)), DispatchError::BadOrigin);
		assert_err!(
			SafeMode::force_slash_deposit(signed(1), 0, activated_at_block),
			DispatchError::BadOrigin
		);
		assert_err!(
			SafeMode::force_release_deposit(signed(1), 0, activated_at_block),
			DispatchError::BadOrigin
		);

		assert_err!(SafeMode::force_enter(signed(1)), DispatchError::BadOrigin);
		assert_err!(SafeMode::force_exit(signed(1)), DispatchError::BadOrigin);
		assert_err!(
			SafeMode::force_slash_deposit(signed(1), 0, activated_at_block),
			DispatchError::BadOrigin
		);
		assert_err!(
			SafeMode::force_release_deposit(signed(1), 0, activated_at_block),
			DispatchError::BadOrigin
		);

		assert_err!(
			SafeMode::force_enter(RuntimeOrigin::signed(mock::ForceExitOrigin::get())),
			DispatchError::BadOrigin
		);
		assert_err!(
			SafeMode::force_extend(RuntimeOrigin::signed(mock::ForceExitOrigin::get())),
			DispatchError::BadOrigin
		);
		assert_err!(
			SafeMode::force_slash_deposit(
				RuntimeOrigin::signed(mock::ForceExitOrigin::get()),
				0,
				activated_at_block
			),
			DispatchError::BadOrigin
		);
		assert_err!(
			SafeMode::force_release_deposit(
				RuntimeOrigin::signed(mock::ForceExitOrigin::get()),
				0,
				activated_at_block
			),
			DispatchError::BadOrigin
		);

		assert_err!(
			SafeMode::force_enter(RuntimeOrigin::signed(mock::ForceDepositOrigin::get())),
			DispatchError::BadOrigin
		);
		assert_err!(
			SafeMode::force_extend(RuntimeOrigin::signed(mock::ForceDepositOrigin::get())),
			DispatchError::BadOrigin
		);
		assert_err!(
			SafeMode::force_exit(RuntimeOrigin::signed(mock::ForceDepositOrigin::get())),
			DispatchError::BadOrigin
		);
	});
}

fn call_transfer() -> RuntimeCall {
	RuntimeCall::Balances(pallet_balances::Call::transfer_allow_death { dest: 1, value: 1 })
}

fn signed(who: u64) -> RuntimeOrigin {
	RuntimeOrigin::signed(who)
}
