// Copyright 2018 The Grin Developers
//
// 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.

extern crate grin_api as api;
extern crate grin_chain as chain;
extern crate grin_core as core;
extern crate grin_p2p as p2p;
extern crate grin_servers as servers;
extern crate grin_util as util;
extern crate grin_wallet as wallet;

mod framework;

use std::default::Default;
use std::{thread, time};

use core::core::hash::Hashed;
use core::global::{self, ChainTypes};

use framework::{config, stratum_config, LocalServerContainerConfig, LocalServerContainerPool,
                LocalServerContainerPoolConfig};

/// Testing the frameworks by starting a fresh server, creating a genesis
/// Block and mining into a wallet for a bit
#[test]
fn basic_genesis_mine() {
	util::init_test_logger();
	global::set_mining_mode(ChainTypes::AutomatedTesting);

	let test_name_dir = "genesis_mine";
	framework::clean_all_output(test_name_dir);

	// Create a server pool
	let mut pool_config = LocalServerContainerPoolConfig::default();
	pool_config.base_name = String::from(test_name_dir);
	pool_config.run_length_in_seconds = 10;

	pool_config.base_api_port = 30000;
	pool_config.base_p2p_port = 31000;
	pool_config.base_wallet_port = 32000;

	let mut pool = LocalServerContainerPool::new(pool_config);

	// Create a server to add into the pool
	let mut server_config = LocalServerContainerConfig::default();
	server_config.start_miner = true;
	server_config.start_wallet = false;
	server_config.burn_mining_rewards = true;

	pool.create_server(&mut server_config);
	pool.run_all_servers();
}

/// Creates 5 servers, first being a seed and check that through peer address
/// messages they all end up connected.
#[test]
fn simulate_seeding() {
	util::init_test_logger();
	global::set_mining_mode(ChainTypes::AutomatedTesting);

	let test_name_dir = "simulate_seeding";
	framework::clean_all_output(test_name_dir);

	// Create a server pool
	let mut pool_config = LocalServerContainerPoolConfig::default();
	pool_config.base_name = String::from(test_name_dir);
	pool_config.run_length_in_seconds = 30;

	// have to use different ports because of tests being run in parallel
	pool_config.base_api_port = 30020;
	pool_config.base_p2p_port = 31020;
	pool_config.base_wallet_port = 32020;

	let mut pool = LocalServerContainerPool::new(pool_config);

	// Create a first seed server to add into the pool
	let mut server_config = LocalServerContainerConfig::default();
	// server_config.start_miner = true;
	server_config.start_wallet = false;
	server_config.burn_mining_rewards = true;
	server_config.is_seeding = true;

	pool.create_server(&mut server_config);

	// point next servers at first seed
	server_config.is_seeding = false;
	server_config.seed_addr = String::from(format!(
		"{}:{}",
		server_config.base_addr, server_config.p2p_server_port
	));

	for _ in 0..4 {
		pool.create_server(&mut server_config);
	}

	// pool.connect_all_peers();

	let _ = pool.run_all_servers();
}

/// Create 1 server, start it mining, then connect 4 other peers mining and
/// using the first
/// as a seed. Meant to test the evolution of mining difficulty with miners
/// running at
/// different rates
// Just going to comment this out as an automatically run test for the time
// being,
// As it's more for actively testing and hurts CI a lot
//#[test]
#[allow(dead_code)]
fn simulate_parallel_mining() {
	global::set_mining_mode(ChainTypes::AutomatedTesting);

	let test_name_dir = "simulate_parallel_mining";
	// framework::clean_all_output(test_name_dir);

	// Create a server pool
	let mut pool_config = LocalServerContainerPoolConfig::default();
	pool_config.base_name = String::from(test_name_dir);
	pool_config.run_length_in_seconds = 60;
	// have to use different ports because of tests being run in parallel
	pool_config.base_api_port = 30040;
	pool_config.base_p2p_port = 31040;
	pool_config.base_wallet_port = 32040;

	let mut pool = LocalServerContainerPool::new(pool_config);

	// Create a first seed server to add into the pool
	let mut server_config = LocalServerContainerConfig::default();
	server_config.start_miner = true;
	server_config.start_wallet = true;
	server_config.is_seeding = true;

	pool.create_server(&mut server_config);

	// point next servers at first seed
	server_config.is_seeding = false;
	server_config.seed_addr = String::from(format!(
		"{}:{}",
		server_config.base_addr, server_config.p2p_server_port
	));

	// And create 4 more, then let them run for a while
	for i in 1..4 {
		// fudge in some slowdown
		server_config.miner_slowdown_in_millis = i * 2;
		pool.create_server(&mut server_config);
	}

	// pool.connect_all_peers();

	let _ = pool.run_all_servers();

	// Check mining difficulty here?, though I'd think it's more valuable
	// to simply output it. Can at least see the evolution of the difficulty target
	// in the debug log output for now
}

// TODO: Convert these tests to newer framework format
/// Create a network of 5 servers and mine a block, verifying that the block
/// gets propagated to all.
#[test]
fn simulate_block_propagation() {
	util::init_test_logger();

	// we actually set the chain_type in the ServerConfig below
	// TODO - avoid needing to set it in two places?
	global::set_mining_mode(ChainTypes::AutomatedTesting);

	let test_name_dir = "grin-prop";
	framework::clean_all_output(test_name_dir);

	// instantiates 5 servers on different ports
	let mut servers = vec![];
	for n in 0..5 {
		let s = servers::Server::new(framework::config(10 * n, test_name_dir, 0)).unwrap();
		servers.push(s);
		thread::sleep(time::Duration::from_millis(100));
	}

	// start mining
	servers[0].start_test_miner(None);

	// monitor for a change of head on a different server and check whether
	// chain height has changed
	loop {
		let mut count = 0;
		for n in 0..5 {
			if servers[n].head().height > 3 {
				count += 1;
			}
		}
		if count == 5 {
			break;
		}
		thread::sleep(time::Duration::from_millis(1_000));
	}
	for n in 0..5 {
		servers[n].stop();
	}
}

/// Creates 2 different disconnected servers, mine a few blocks on one, connect
/// them and check that the 2nd gets all the blocks
#[test]
fn simulate_full_sync() {
	util::init_test_logger();

	// we actually set the chain_type in the ServerConfig below
	global::set_mining_mode(ChainTypes::AutomatedTesting);

	let test_name_dir = "grin-sync";
	framework::clean_all_output(test_name_dir);

	let s1 = servers::Server::new(framework::config(1000, "grin-sync", 1000)).unwrap();
	// mine a few blocks on server 1
	s1.start_test_miner(None);
	thread::sleep(time::Duration::from_secs(8));

	let s2 = servers::Server::new(framework::config(1001, "grin-sync", 1000)).unwrap();

	// Get the current header from s1.
	let s1_header = s1.chain.head_header().unwrap();

	// Wait for s2 to sync up to and including the header from s1.
	while s2.head().height < s1_header.height {
		thread::sleep(time::Duration::from_millis(1_000));
	}

	// Confirm both s1 and s2 see a consistent header at that height.
	let s2_header = s2.chain.get_block_header(&s1_header.hash()).unwrap();
	assert_eq!(s1_header, s2_header);

	// Stop our servers cleanly.
	s1.stop();
	s2.stop();
}

/// Creates 2 different disconnected servers, mine a few blocks on one, connect
/// them and check that the 2nd gets all using fast sync algo
#[test]
fn simulate_fast_sync() {
	util::init_test_logger();

	// we actually set the chain_type in the ServerConfig below
	global::set_mining_mode(ChainTypes::AutomatedTesting);

	let test_name_dir = "grin-fast";
	framework::clean_all_output(test_name_dir);

	// start s1 and mine enough blocks to get beyond the fast sync horizon
	let s1 = servers::Server::new(framework::config(2000, "grin-fast", 2000)).unwrap();
	s1.start_test_miner(None);

	while s1.head().height < 21 {
		thread::sleep(time::Duration::from_millis(1_000));
	}

	let mut conf = config(2001, "grin-fast", 2000);
	conf.archive_mode = Some(false);

	let s2 = servers::Server::new(conf).unwrap();
	while s2.head().height < 21 {
		thread::sleep(time::Duration::from_millis(1_000));
	}

	// Get the current header from s1.
	let s1_header = s1.chain.head_header().unwrap();

	// Wait for s2 to sync up to and including the header from s1.
	while s2.head().height < s1_header.height {
		thread::sleep(time::Duration::from_millis(1_000));
	}

	// Confirm both s1 and s2 see a consistent header at that height.
	let s2_header = s2.chain.get_block_header(&s1_header.hash()).unwrap();
	assert_eq!(s1_header, s2_header);

	// Stop our servers cleanly.
	s1.stop();
	s2.stop();
}

// #[test]
fn simulate_fast_sync_double() {
	util::init_test_logger();

	// we actually set the chain_type in the ServerConfig below
	global::set_mining_mode(ChainTypes::AutomatedTesting);

	framework::clean_all_output("grin-double-fast1");
	framework::clean_all_output("grin-double-fast2");

	let s1 = servers::Server::new(framework::config(3000, "grin-double-fast1", 3000)).unwrap();
	// mine a few blocks on server 1
	s1.start_test_miner(None);
	thread::sleep(time::Duration::from_secs(8));

	{
		let mut conf = config(3001, "grin-double-fast2", 3000);
		conf.archive_mode = Some(false);
		let s2 = servers::Server::new(conf).unwrap();
		while s2.head().height != s2.header_head().height || s2.head().height < 20 {
			thread::sleep(time::Duration::from_millis(1000));
		}
		s2.stop();
	}
	// locks files don't seem to be cleaned properly until process exit
	std::fs::remove_file("target/tmp/grin-double-fast2/grin-sync-1001/chain/LOCK").unwrap();
	std::fs::remove_file("target/tmp/grin-double-fast2/grin-sync-1001/peers/LOCK").unwrap();
	thread::sleep(time::Duration::from_secs(20));

	let mut conf = config(3001, "grin-double-fast2", 3000);
	conf.archive_mode = Some(false);
	let s2 = servers::Server::new(conf).unwrap();
	while s2.head().height != s2.header_head().height || s2.head().height < 50 {
		thread::sleep(time::Duration::from_millis(1000));
	}
	s1.stop();
	s2.stop();
}