// Copyright 2021 Centrifuge Foundation (centrifuge.io).
//
// This file is part of the Centrifuge chain project.
// Centrifuge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version (see http://www.gnu.org/licenses).
// Centrifuge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.

use cfg_primitives::{AccountId, Balance};
use cfg_types::{
	domain_address::{Domain, DomainAddress},
	tokens::CurrencyId,
};
use ethabi::{ethereum_types::U256, Token};
use frame_support::traits::OriginTrait;
use frame_system::pallet_prelude::OriginFor;
use pallet_liquidity_pools::Message;
use sp_core::ByteArray;

use crate::{
	cases::lp::{
		self, names,
		utils::{as_h160_32bytes, pool_a_tranche_1_id, Decoder},
		LocalUSDC, CENTRIFUGE_CHAIN_ID, DECIMALS_6, DEFAULT_BALANCE, DOMAIN_CENTRIFUGE, DOMAIN_EVM,
		EVM_DOMAIN_CHAIN_ID, POOL_A, USDC,
	},
	config::Runtime,
	env::{Blocks, Env, EnvEvmExtension, EvmEnv},
	utils::{accounts::Keyring, currency::CurrencyInfo, give_tokens, invest_and_collect},
};

// The default amount of invested stable coins
const AMOUNT: Balance = DEFAULT_BALANCE * DECIMALS_6;

mod utils {
	use super::*;

	pub fn prepare_hold_tt_domain<T: Runtime>(env: &mut impl EnvEvmExtension<T>) {
		env.state_mut(|evm| {
			assert_eq!(
				Decoder::<Balance>::decode(&evm.view(
					Keyring::Alice,
					names::POOL_A_T_1,
					"balanceOf",
					Some(&[Token::Address(Keyring::TrancheInvestor(1).in_eth())]),
				)),
				0
			);
		});

		// Invest, close epoch and collect tranche tokens with 1-to-1 conversion
		env.pass(Blocks::ByNumber(2));
		env.state_mut(|_evm| {
			give_tokens::<T>(Keyring::TrancheInvestor(1).id(), LocalUSDC.id(), AMOUNT);
			invest_and_collect::<T>(
				Keyring::TrancheInvestor(1).into(),
				Keyring::Admin,
				POOL_A,
				pool_a_tranche_1_id::<T>(),
				AMOUNT,
			);
			assert_eq!(
				orml_tokens::Accounts::<T>::get(
					Keyring::TrancheInvestor(1).id(),
					CurrencyId::Tranche(POOL_A, pool_a_tranche_1_id::<T>()),
				)
				.free,
				AMOUNT
			);
		});

		env.state_mut(|_evm| {
			pallet_liquidity_pools::Pallet::<T>::transfer_tranche_tokens(
				OriginFor::<T>::signed(Keyring::TrancheInvestor(1).into()),
				POOL_A,
				pool_a_tranche_1_id::<T>(),
				DomainAddress::Evm(EVM_DOMAIN_CHAIN_ID, Keyring::TrancheInvestor(1).in_eth()),
				AMOUNT,
			)
			.unwrap();
			lp::utils::process_gateway_message::<T>(lp::utils::verify_gateway_message_success::<T>);
		});

		env.state(|evm| {
			assert_eq!(
				Decoder::<Balance>::decode(&evm.view(
					Keyring::Alice,
					names::POOL_A_T_1,
					"balanceOf",
					Some(&[Token::Address(Keyring::TrancheInvestor(1).in_eth())]),
				)),
				AMOUNT
			);
		});
	}

	pub fn prepare_hold_usdc_local<T: Runtime>(env: &mut impl EnvEvmExtension<T>) {
		env.state_mut(|evm| {
			evm.call(
				Keyring::Alice,
				Default::default(),
				names::USDC,
				"approve",
				Some(&[
					Token::Address(evm.deployed(names::POOL_MANAGER).address()),
					Token::Uint(U256::from(AMOUNT)),
				]),
			)
			.unwrap();

			let balance = Decoder::<Balance>::decode(&evm.view(
				Keyring::Alice,
				names::USDC,
				"balanceOf",
				Some(&[Token::Address(Keyring::Alice.in_eth())]),
			));
			assert!(
				balance >= AMOUNT,
				"Insufficient USDC funds by Alice. Required {AMOUNT} but only has {balance}"
			);

			evm.call(
				Keyring::Alice,
				Default::default(),
				names::POOL_MANAGER,
				"transferAssets",
				Some(&[
					Token::Address(evm.deployed(names::USDC).address()),
					Token::FixedBytes(Keyring::Ferdie.id().to_raw_vec()),
					Token::Uint(U256::from(AMOUNT)),
				]),
			)
			.unwrap();
		});

		env.pass(Blocks::ByNumber(1));

		env.state(|_| {
			assert_eq!(
				orml_tokens::Accounts::<T>::get(Keyring::Ferdie.id(), USDC.id()).free,
				AMOUNT
			);
		});
	}
}

#[test_runtimes([centrifuge, development])]
fn transfer_tokens_from_local<T: Runtime>() {
	let mut env = super::setup_full::<T>();
	utils::prepare_hold_usdc_local::<T>(&mut env);

	env.state_mut(|_evm| {
		pallet_liquidity_pools::Pallet::<T>::transfer(
			OriginFor::<T>::signed(Keyring::Ferdie.into()),
			USDC.id(),
			DomainAddress::Evm(EVM_DOMAIN_CHAIN_ID, Keyring::Ferdie.in_eth()),
			AMOUNT,
		)
		.unwrap();
		lp::utils::process_gateway_message::<T>(lp::utils::verify_gateway_message_success::<T>);
	});

	env.state(|evm| {
		assert_eq!(
			Decoder::<Balance>::decode(&evm.view(
				Keyring::Alice,
				names::USDC,
				"balanceOf",
				Some(&[Token::Address(Keyring::Ferdie.in_eth())]),
			)),
			AMOUNT
		);
	});
}

#[test_runtimes([centrifuge, development])]
fn transfer_tranche_tokens_from_local<T: Runtime>() {
	let mut env = super::setup_full::<T>();

	env.state_mut(|evm| {
		assert_eq!(
			Decoder::<Balance>::decode(
				&evm.view(
					Keyring::Alice,
					names::POOL_A_T_1,
					"balanceOf",
					Some(&[Token::Address(Keyring::TrancheInvestor(1).in_eth())]),
				)
				.unwrap()
				.value,
			),
			0
		);
	});

	// Invest, close epoch and collect tranche tokens with 1-to-1 conversion
	env.pass(Blocks::ByNumber(2));
	env.state_mut(|_evm| {
		give_tokens::<T>(Keyring::TrancheInvestor(1).id(), LocalUSDC.id(), AMOUNT);
		invest_and_collect::<T>(
			Keyring::TrancheInvestor(1).into(),
			Keyring::Admin,
			POOL_A,
			pool_a_tranche_1_id::<T>(),
			AMOUNT,
		);
	});

	env.state_mut(|_evm| {
		pallet_liquidity_pools::Pallet::<T>::transfer_tranche_tokens(
			OriginFor::<T>::signed(Keyring::TrancheInvestor(1).into()),
			POOL_A,
			pool_a_tranche_1_id::<T>(),
			DomainAddress::Evm(EVM_DOMAIN_CHAIN_ID, Keyring::TrancheInvestor(1).in_eth()),
			AMOUNT,
		)
		.unwrap();
		lp::utils::process_gateway_message::<T>(lp::utils::verify_gateway_message_success::<T>);
	});

	env.state(|evm| {
		assert_eq!(
			Decoder::<Balance>::decode(
				&evm.view(
					Keyring::Alice,
					names::POOL_A_T_1,
					"balanceOf",
					Some(&[Token::Address(Keyring::TrancheInvestor(1).in_eth())]),
				)
				.unwrap()
				.value,
			),
			AMOUNT
		);
	});
}

#[test_runtimes([centrifuge, development])]
fn transfer_tranche_tokens_domain_to_local_to_domain<T: Runtime>() {
	let mut env = super::setup_full::<T>();
	utils::prepare_hold_tt_domain::<T>(&mut env);

	env.state_mut(|evm| {
		assert!(
			Decoder::<Balance>::decode(&evm.view(
				Keyring::TrancheInvestor(1),
				names::POOL_A_T_1,
				"balanceOf",
				Some(&[Token::Address(Keyring::TrancheInvestor(1).in_eth())]),
			)) >= AMOUNT,
			"Insufficient POOL_A_T_1 funds by TrancheInvestor(1)"
		);
		evm.call(
			Keyring::TrancheInvestor(1),
			Default::default(),
			names::POOL_A_T_1,
			"approve",
			Some(&[
				Token::Address(evm.deployed(names::POOL_MANAGER).address()),
				Token::Uint(U256::from(AMOUNT)),
			]),
		)
		.unwrap();

		evm.call(
			Keyring::TrancheInvestor(1),
			sp_core::U256::zero(),
			names::POOL_MANAGER,
			"transferTrancheTokens",
			Some(&[
				Token::Uint(POOL_A.into()),
				Token::FixedBytes(pool_a_tranche_1_id::<T>().into()),
				Token::Uint(DOMAIN_EVM.into()),
				Token::Uint(EVM_DOMAIN_CHAIN_ID.into()),
				Token::FixedBytes(
					AccountId::from(as_h160_32bytes(Keyring::TrancheInvestor(2))).to_raw_vec(),
				),
				Token::Uint(AMOUNT.into()),
			]),
		)
		.unwrap();
	});

	env.state_mut(|_evm| {
		lp::utils::process_gateway_message::<T>(|msg| {
			assert_eq!(
				msg,
				Message::TransferTrancheTokens {
					pool_id: POOL_A,
					tranche_id: pool_a_tranche_1_id::<T>(),
					domain: Domain::Evm(EVM_DOMAIN_CHAIN_ID).into(),
					receiver: as_h160_32bytes(Keyring::TrancheInvestor(2)),
					amount: AMOUNT,
				},
			);
		});
	});

	env.pass(Blocks::ByNumber(1));

	env.state(|evm| {
		assert_eq!(
			Decoder::<Balance>::decode(
				&evm.view(
					Keyring::Alice,
					names::POOL_A_T_1,
					"balanceOf",
					Some(&[Token::Address(Keyring::TrancheInvestor(2).in_eth())]),
				)
				.unwrap()
				.value,
			),
			AMOUNT
		);
	});
}

#[test_runtimes([centrifuge, development])]
fn transfer_tranche_tokens_domain_to_local<T: Runtime>() {
	let mut env = super::setup_full::<T>();
	utils::prepare_hold_tt_domain::<T>(&mut env);

	env.state_mut(|evm| {
		assert!(
			Decoder::<Balance>::decode(&evm.view(
				Keyring::TrancheInvestor(1),
				names::POOL_A_T_1,
				"balanceOf",
				Some(&[Token::Address(Keyring::TrancheInvestor(1).in_eth())]),
			)) >= AMOUNT,
			"Insufficient POOL_A_T_1 funds by TrancheInvestor(1)"
		);
		evm.call(
			Keyring::TrancheInvestor(1),
			Default::default(),
			names::POOL_A_T_1,
			"approve",
			Some(&[
				Token::Address(evm.deployed(names::POOL_MANAGER).address()),
				Token::Uint(U256::from(AMOUNT)),
			]),
		)
		.unwrap();
		evm.call(
			Keyring::TrancheInvestor(1),
			sp_core::U256::zero(),
			names::POOL_MANAGER,
			"transferTrancheTokens",
			Some(&[
				Token::Uint(POOL_A.into()),
				Token::FixedBytes(pool_a_tranche_1_id::<T>().into()),
				Token::Uint(DOMAIN_CENTRIFUGE.into()),
				Token::Uint(CENTRIFUGE_CHAIN_ID.into()),
				Token::FixedBytes(Keyring::TrancheInvestor(2).id().to_raw_vec()),
				Token::Uint(AMOUNT.into()),
			]),
		)
		.unwrap();
	});

	env.pass(Blocks::ByNumber(1));

	env.state(|_evm| {
		assert_eq!(
			orml_tokens::Accounts::<T>::get(
				Keyring::TrancheInvestor(2).id(),
				CurrencyId::Tranche(POOL_A, pool_a_tranche_1_id::<T>()),
			)
			.free,
			AMOUNT
		);
	});
}
