use super::*;

/// Used where the error comes from other pallet impl. unknown from the tests
fn config_mocks(withdraw_amount: Balance) {
	MockPools::mock_withdraw(move |pool_id, to, amount| {
		assert_eq!(to, BORROWER);
		assert_eq!(pool_id, POOL_A);
		assert_eq!(withdraw_amount, amount);
		Ok(())
	});
	MockPrices::mock_get(|id, pool_id| {
		assert_eq!(*pool_id, POOL_A);
		match *id {
			REGISTER_PRICE_ID => Ok((PRICE_VALUE, BLOCK_TIME_MS)),
			_ => Err(PRICE_ID_NO_FOUND),
		}
	});
	MockPrices::mock_register_id(|id, pool_id| {
		assert_eq!(*pool_id, POOL_A);
		match *id {
			REGISTER_PRICE_ID => Ok(()),
			_ => Err(PRICE_ID_NO_FOUND),
		}
	});
}

#[test]
fn with_wrong_loan_id() {
	new_test_ext().execute_with(|| {
		config_mocks(COLLATERAL_VALUE);

		assert_noop!(
			Loans::borrow(
				RuntimeOrigin::signed(BORROWER),
				POOL_A,
				0,
				PrincipalInput::Internal(COLLATERAL_VALUE)
			),
			Error::<Runtime>::LoanNotActiveOrNotFound
		);
	});
}

#[test]
fn from_other_borrower() {
	new_test_ext().execute_with(|| {
		let loan_id = util::create_loan(util::base_internal_loan());

		config_mocks(COLLATERAL_VALUE);

		assert_noop!(
			Loans::borrow(
				RuntimeOrigin::signed(OTHER_BORROWER),
				POOL_A,
				loan_id,
				PrincipalInput::Internal(COLLATERAL_VALUE)
			),
			Error::<Runtime>::NotLoanBorrower
		);
	});
}

#[test]
fn with_restriction_no_written_off() {
	new_test_ext().execute_with(|| {
		let loan_id = util::create_loan(util::base_internal_loan());

		config_mocks(COLLATERAL_VALUE / 2);
		assert_ok!(Loans::borrow(
			RuntimeOrigin::signed(BORROWER),
			POOL_A,
			loan_id,
			PrincipalInput::Internal(COLLATERAL_VALUE / 2)
		));

		advance_time(YEAR + DAY);
		util::write_off_loan(loan_id);

		assert_noop!(
			Loans::borrow(
				RuntimeOrigin::signed(BORROWER),
				POOL_A,
				loan_id,
				PrincipalInput::Internal(COLLATERAL_VALUE / 2)
			),
			Error::<Runtime>::from(BorrowLoanError::Restriction)
		);
	});
}

#[test]
fn with_restriction_full_once() {
	new_test_ext().execute_with(|| {
		let loan_id = util::create_loan(LoanInfo {
			restrictions: LoanRestrictions {
				borrows: BorrowRestrictions::FullOnce,
				repayments: RepayRestrictions::None,
			},
			..util::base_internal_loan()
		});

		config_mocks(COLLATERAL_VALUE / 2);
		assert_noop!(
			Loans::borrow(
				RuntimeOrigin::signed(BORROWER),
				POOL_A,
				loan_id,
				PrincipalInput::Internal(COLLATERAL_VALUE / 2) // Must be full value
			),
			Error::<Runtime>::from(BorrowLoanError::Restriction)
		);

		config_mocks(COLLATERAL_VALUE);
		assert_ok!(Loans::borrow(
			RuntimeOrigin::signed(BORROWER),
			POOL_A,
			loan_id,
			PrincipalInput::Internal(COLLATERAL_VALUE)
		));

		// Borrow was already done
		assert_noop!(
			Loans::borrow(
				RuntimeOrigin::signed(BORROWER),
				POOL_A,
				loan_id,
				PrincipalInput::Internal(0)
			),
			Error::<Runtime>::from(BorrowLoanError::Restriction)
		);
	});
}

#[test]
fn with_maturity_passed() {
	new_test_ext().execute_with(|| {
		let loan_id = util::create_loan(util::base_internal_loan());

		advance_time(YEAR);

		config_mocks(COLLATERAL_VALUE);
		assert_noop!(
			Loans::borrow(
				RuntimeOrigin::signed(BORROWER),
				POOL_A,
				loan_id,
				PrincipalInput::Internal(COLLATERAL_VALUE)
			),
			Error::<Runtime>::from(BorrowLoanError::MaturityDatePassed)
		);
	});
}

#[test]
fn with_wrong_internal_pricing() {
	new_test_ext().execute_with(|| {
		let loan_id = util::create_loan(util::base_external_loan());

		config_mocks(0);
		assert_noop!(
			Loans::borrow(
				RuntimeOrigin::signed(BORROWER),
				POOL_A,
				loan_id,
				PrincipalInput::Internal(0)
			),
			Error::<Runtime>::MismatchedPricingMethod
		);
	});
}

#[test]
fn with_wrong_external_pricing() {
	new_test_ext().execute_with(|| {
		let loan_id = util::create_loan(util::base_internal_loan());

		config_mocks(0);
		assert_noop!(
			Loans::borrow(
				RuntimeOrigin::signed(BORROWER),
				POOL_A,
				loan_id,
				PrincipalInput::External(ExternalAmount::empty())
			),
			Error::<Runtime>::MismatchedPricingMethod
		);
	});
}

#[test]
fn with_wrong_big_amount_internal_pricing() {
	let borrow_inputs = [
		(COLLATERAL_VALUE + 1, util::total_borrowed_rate(1.0)),
		(COLLATERAL_VALUE / 2 + 1, util::total_borrowed_rate(0.5)),
		(1, util::total_borrowed_rate(0.0)),
		(COLLATERAL_VALUE + 1, util::outstanding_debt_rate(1.0)),
		(COLLATERAL_VALUE / 2 + 1, util::outstanding_debt_rate(0.5)),
		(1, util::outstanding_debt_rate(0.0)),
	];

	for (amount, max_borrow_amount) in borrow_inputs {
		new_test_ext().execute_with(|| {
			let loan_id = util::create_loan(LoanInfo {
				pricing: Pricing::Internal(InternalPricing {
					max_borrow_amount,
					..util::base_internal_pricing()
				}),
				..util::base_internal_loan()
			});

			config_mocks(amount);
			assert_noop!(
				Loans::borrow(
					RuntimeOrigin::signed(BORROWER),
					POOL_A,
					loan_id,
					PrincipalInput::Internal(amount)
				),
				Error::<Runtime>::from(BorrowLoanError::MaxAmountExceeded)
			);
		});
	}
}

#[test]
fn with_correct_amount_internal_pricing() {
	let borrow_inputs = [
		(COLLATERAL_VALUE, util::total_borrowed_rate(1.0)),
		(COLLATERAL_VALUE / 2, util::total_borrowed_rate(0.5)),
		(0, util::total_borrowed_rate(0.0)),
		(COLLATERAL_VALUE, util::outstanding_debt_rate(1.0)),
		(COLLATERAL_VALUE / 2, util::outstanding_debt_rate(0.5)),
		(0, util::outstanding_debt_rate(0.0)),
	];

	for (amount, max_borrow_amount) in borrow_inputs {
		new_test_ext().execute_with(|| {
			let loan_id = util::create_loan(LoanInfo {
				pricing: Pricing::Internal(InternalPricing {
					max_borrow_amount,
					..util::base_internal_pricing()
				}),
				..util::base_internal_loan()
			});

			config_mocks(amount);
			assert_ok!(Loans::borrow(
				RuntimeOrigin::signed(BORROWER),
				POOL_A,
				loan_id,
				PrincipalInput::Internal(amount)
			));
			assert_eq!(amount, util::current_loan_debt(loan_id));
		});
	}
}

#[test]
fn with_unregister_price_id_and_oracle_required() {
	new_test_ext().execute_with(|| {
		let loan = LoanInfo {
			pricing: Pricing::External(ExternalPricing {
				price_id: UNREGISTER_PRICE_ID,
				..util::base_external_pricing()
			}),
			restrictions: LoanRestrictions {
				borrows: BorrowRestrictions::OraclePriceRequired,
				repayments: RepayRestrictions::None,
			},
			..util::base_external_loan()
		};

		let loan_id = util::create_loan(loan);

		let amount = ExternalAmount::new(QUANTITY, PRICE_VALUE);
		config_mocks(amount.balance().unwrap());

		assert_noop!(
			Loans::borrow(
				RuntimeOrigin::signed(BORROWER),
				POOL_A,
				loan_id,
				PrincipalInput::External(amount)
			),
			PRICE_ID_NO_FOUND
		);
	});
}

#[test]
fn with_unregister_price_id_and_oracle_not_required() {
	new_test_ext().execute_with(|| {
		let loan = LoanInfo {
			pricing: Pricing::External(ExternalPricing {
				price_id: UNREGISTER_PRICE_ID,
				..util::base_external_pricing()
			}),
			..util::base_external_loan()
		};

		let loan_id = util::create_loan(loan);

		// Borrow half
		let amount = ExternalAmount::new(QUANTITY / 2.into(), PRICE_VALUE * 2);
		config_mocks(amount.balance().unwrap());

		assert_ok!(Loans::borrow(
			RuntimeOrigin::signed(BORROWER),
			POOL_A,
			loan_id,
			PrincipalInput::External(amount)
		));

		assert_eq!(
			(QUANTITY / 2.into()).saturating_mul_int(PRICE_VALUE * 2),
			util::current_loan_pv(loan_id)
		);

		// Borrow half, price changed
		let amount = ExternalAmount::new(QUANTITY / 2.into(), PRICE_VALUE * 4);
		config_mocks(amount.balance().unwrap());

		assert_ok!(Loans::borrow(
			RuntimeOrigin::signed(BORROWER),
			POOL_A,
			loan_id,
			PrincipalInput::External(amount)
		));

		assert_eq!(
			(QUANTITY).saturating_mul_int(PRICE_VALUE * 4),
			util::current_loan_pv(loan_id)
		);

		// Suddenty, the oracle set a value
		MockPrices::mock_get(|_, _| Ok((PRICE_VALUE * 8, BLOCK_TIME_MS)));

		assert_eq!(
			(QUANTITY).saturating_mul_int(PRICE_VALUE * 8),
			util::current_loan_pv(loan_id)
		);
	});
}

#[test]
fn with_wrong_big_amount_external_pricing() {
	new_test_ext().execute_with(|| {
		let loan_id = util::create_loan(util::base_external_loan());

		let amount = ExternalAmount::new(QUANTITY + 1.into(), PRICE_VALUE);
		config_mocks(amount.balance().unwrap());

		assert_noop!(
			Loans::borrow(
				RuntimeOrigin::signed(BORROWER),
				POOL_A,
				loan_id,
				PrincipalInput::External(amount)
			),
			Error::<Runtime>::from(BorrowLoanError::MaxAmountExceeded)
		);
	});
}

#[test]
fn with_incorrect_settlement_price_external_pricing() {
	new_test_ext().execute_with(|| {
		let loan_id = util::create_loan(util::base_external_loan());

		// Much higher
		let amount = ExternalAmount::new(QUANTITY, PRICE_VALUE + PRICE_VALUE + 1);
		config_mocks(amount.balance().unwrap());
		assert_noop!(
			Loans::borrow(
				RuntimeOrigin::signed(BORROWER),
				POOL_A,
				loan_id,
				PrincipalInput::External(amount)
			),
			Error::<Runtime>::SettlementPriceExceedsVariation
		);

		// Higher
		let amount = ExternalAmount::new(
			QUANTITY,
			PRICE_VALUE + (MAX_PRICE_VARIATION.saturating_mul_int(PRICE_VALUE) + 1),
		);
		config_mocks(amount.balance().unwrap());
		assert_noop!(
			Loans::borrow(
				RuntimeOrigin::signed(BORROWER),
				POOL_A,
				loan_id,
				PrincipalInput::External(amount)
			),
			Error::<Runtime>::SettlementPriceExceedsVariation
		);

		// Lower
		let amount = ExternalAmount::new(
			QUANTITY,
			PRICE_VALUE - (MAX_PRICE_VARIATION.saturating_mul_int(PRICE_VALUE) + 1),
		);
		config_mocks(amount.balance().unwrap());
		assert_noop!(
			Loans::borrow(
				RuntimeOrigin::signed(BORROWER),
				POOL_A,
				loan_id,
				PrincipalInput::External(amount)
			),
			Error::<Runtime>::SettlementPriceExceedsVariation
		);
	});
}

#[test]
fn with_correct_settlement_price_external_pricing() {
	new_test_ext().execute_with(|| {
		let loan_id = util::create_loan(util::base_external_loan());

		// Higher
		let amount = ExternalAmount::new(
			QUANTITY / 3.into(),
			PRICE_VALUE + MAX_PRICE_VARIATION.saturating_mul_int(PRICE_VALUE),
		);
		config_mocks(amount.balance().unwrap());

		assert_ok!(Loans::borrow(
			RuntimeOrigin::signed(BORROWER),
			POOL_A,
			loan_id,
			PrincipalInput::External(amount)
		));

		assert_eq!(
			(QUANTITY / 3.into()).saturating_mul_int(PRICE_VALUE),
			util::current_loan_pv(loan_id)
		);

		// Same
		let amount = ExternalAmount::new(QUANTITY / 3.into(), PRICE_VALUE);
		config_mocks(amount.balance().unwrap());

		assert_ok!(Loans::borrow(
			RuntimeOrigin::signed(BORROWER),
			POOL_A,
			loan_id,
			PrincipalInput::External(amount)
		));

		assert_eq!(
			(QUANTITY / 3.into()).saturating_mul_int(PRICE_VALUE) * 2,
			util::current_loan_pv(loan_id)
		);

		// Lower
		let amount = ExternalAmount::new(
			QUANTITY / 3.into(),
			PRICE_VALUE - MAX_PRICE_VARIATION.saturating_mul_int(PRICE_VALUE),
		);
		config_mocks(amount.balance().unwrap());

		assert_ok!(Loans::borrow(
			RuntimeOrigin::signed(BORROWER),
			POOL_A,
			loan_id,
			PrincipalInput::External(amount)
		));

		assert_eq!(
			QUANTITY.saturating_mul_int(PRICE_VALUE),
			util::current_loan_pv(loan_id)
		);
	});
}

#[test]
fn with_unlimited_amount_external_pricing() {
	new_test_ext().execute_with(|| {
		let loan = LoanInfo {
			pricing: Pricing::External(ExternalPricing {
				max_borrow_amount: ExtMaxBorrowAmount::NoLimit,
				..util::base_external_pricing()
			}),
			..util::base_external_loan()
		};

		let loan_id = util::create_loan(loan);

		let amount = ExternalAmount::new(QUANTITY /* Could be any value */, PRICE_VALUE);
		config_mocks(amount.balance().unwrap());

		assert_ok!(Loans::borrow(
			RuntimeOrigin::signed(BORROWER),
			POOL_A,
			loan_id,
			PrincipalInput::External(amount)
		));
	});
}

#[test]
fn twice() {
	new_test_ext().execute_with(|| {
		let loan_id = util::create_loan(util::base_internal_loan());

		config_mocks(COLLATERAL_VALUE / 2);

		assert_ok!(Loans::borrow(
			RuntimeOrigin::signed(BORROWER),
			POOL_A,
			loan_id,
			PrincipalInput::Internal(COLLATERAL_VALUE / 2)
		));
		assert_eq!(COLLATERAL_VALUE / 2, util::current_loan_debt(loan_id));

		assert_ok!(Loans::borrow(
			RuntimeOrigin::signed(BORROWER),
			POOL_A,
			loan_id,
			PrincipalInput::Internal(COLLATERAL_VALUE / 2)
		));
		assert_eq!(COLLATERAL_VALUE, util::current_loan_debt(loan_id));

		// At this point the loan has been fully borrowed.
		let extra = 1;
		assert_noop!(
			Loans::borrow(
				RuntimeOrigin::signed(BORROWER),
				POOL_A,
				loan_id,
				PrincipalInput::Internal(extra)
			),
			Error::<Runtime>::from(BorrowLoanError::MaxAmountExceeded)
		);
	});
}

#[test]
fn twice_with_elapsed_time() {
	new_test_ext().execute_with(|| {
		let loan_id = util::create_loan(util::base_internal_loan());

		config_mocks(COLLATERAL_VALUE / 2);

		assert_ok!(Loans::borrow(
			RuntimeOrigin::signed(BORROWER),
			POOL_A,
			loan_id,
			PrincipalInput::Internal(COLLATERAL_VALUE / 2)
		));
		assert_eq!(COLLATERAL_VALUE / 2, util::current_loan_debt(loan_id));

		advance_time(YEAR / 2);

		assert_eq!(
			util::current_debt_for(
				util::interest_for(DEFAULT_INTEREST_RATE, YEAR / 2),
				COLLATERAL_VALUE / 2,
			),
			util::current_loan_debt(loan_id)
		);

		assert_ok!(Loans::borrow(
			RuntimeOrigin::signed(BORROWER),
			POOL_A,
			loan_id,
			PrincipalInput::Internal(COLLATERAL_VALUE / 2)
		));

		// At this point the loan has been fully borrowed.
		let extra = 1;
		assert_noop!(
			Loans::borrow(
				RuntimeOrigin::signed(BORROWER),
				POOL_A,
				loan_id,
				PrincipalInput::Internal(extra)
			),
			Error::<Runtime>::from(BorrowLoanError::MaxAmountExceeded)
		);
	});
}

#[test]
fn increase_debt_does_not_withdraw() {
	new_test_ext().execute_with(|| {
		let loan = LoanInfo {
			pricing: Pricing::External(ExternalPricing {
				max_borrow_amount: ExtMaxBorrowAmount::NoLimit,
				..util::base_external_pricing()
			}),
			..util::base_external_loan()
		};

		let loan_id = util::create_loan(loan);

		let amount = ExternalAmount::new(QUANTITY, PRICE_VALUE);

		config_mocks(amount.balance().unwrap());
		MockPools::mock_withdraw(|_, _, _| {
			unreachable!("increase debt must not withdraw funds from the pool");
		});

		assert_ok!(Loans::increase_debt(
			RuntimeOrigin::signed(BORROWER),
			POOL_A,
			loan_id,
			PrincipalInput::External(amount)
		));
	});
}

mod cashflow {
	use super::*;

	#[test]
	fn computed_correctly_internal_pricing() {
		new_test_ext().execute_with(|| {
			let loan_id = util::create_loan(util::base_internal_loan());

			config_mocks(COLLATERAL_VALUE / 2);
			assert_ok!(Loans::borrow(
				RuntimeOrigin::signed(BORROWER),
				POOL_A,
				loan_id,
				PrincipalInput::Internal(COLLATERAL_VALUE / 2)
			));

			let loan = util::get_loan(loan_id);

			let principal = COLLATERAL_VALUE / 2;
			let acc_interest_rate_per_year = checked_pow(
				util::default_interest_rate().per_sec().unwrap(),
				SECONDS_PER_YEAR as usize,
			)
			.unwrap();
			let interest = acc_interest_rate_per_year.saturating_mul_int(principal) - principal;

			assert_eq!(
				loan.expected_cashflows()
					.unwrap()
					.into_iter()
					.map(|payment| (payment.when, payment.principal, payment.interest))
					.collect::<Vec<_>>(),
				vec![(loan.maturity_date().unwrap(), principal, interest)]
			);
		});
	}

	#[test]
	fn computed_correctly_external_pricing() {
		new_test_ext().execute_with(|| {
			let loan_id = util::create_loan(util::base_external_loan());

			let amount = ExternalAmount::new(QUANTITY / 2.into(), PRICE_VALUE);
			config_mocks(amount.balance().unwrap());

			assert_ok!(Loans::borrow(
				RuntimeOrigin::signed(BORROWER),
				POOL_A,
				loan_id,
				PrincipalInput::External(amount.clone())
			));

			let loan = util::get_loan(loan_id);

			let principal = amount.balance().unwrap();
			let acc_interest_rate_per_year = checked_pow(
				util::default_interest_rate().per_sec().unwrap(),
				SECONDS_PER_YEAR as usize,
			)
			.unwrap();

			let outstanding_notional = util::current_extenal_pricing(loan_id)
				.outstanding_notional_principal()
				.unwrap();

			let interest =
				acc_interest_rate_per_year.saturating_mul_int(principal) - outstanding_notional;

			assert_eq!(
				loan.expected_cashflows()
					.unwrap()
					.into_iter()
					.map(|payment| (payment.when, payment.principal, payment.interest))
					.collect::<Vec<_>>(),
				vec![(loan.maturity_date().unwrap(), principal, interest)]
			);
		});
	}
}
