use std::{
	ffi::OsString,
	fs::File,
	io::{BufRead, BufReader, Write},
	path::Path,
};

/// Whether the `pretty_errors` feature is enabled.
pub const FEATURE_PRETTY_ERRORS: bool = cfg!(feature = "pretty_errors");
/// Whether the `module_disambiguation` feature is enabled.
pub const FEATURE_MODULE_DISAMBIGUATION: bool = cfg!(feature = "module_disambiguation");
/// The number of enabled features.
pub const NR_FEATURES: usize =
	0 + FEATURE_PRETTY_ERRORS as usize + FEATURE_MODULE_DISAMBIGUATION as usize;
/// A list of the enabled features.
const FEATURES: [&'static str; NR_FEATURES] = get_features();

/// Returns a list of enabled features.
const fn get_features() -> [&'static str; NR_FEATURES]
{
	#[allow(unused_mut)]
	let mut features: [&'static str; NR_FEATURES] = [""; NR_FEATURES];
	#[cfg(feature = "pretty_errors")]
	{
		features[0] = "pretty_errors";
	}
	#[cfg(feature = "module_disambiguation")]
	{
		features[FEATURE_PRETTY_ERRORS as usize] = "module_disambiguation";
	}
	features
}

/// Manages the setting up and running of expansion tests using macrotest
///
/// Expansion test live in a home directory. This directory has a single
/// testing sub-directory that is used during the test. Temporary testing
/// files are put in the testing directory before each test but not removed
/// after. (They may be deleted before each test, though)
///
/// The tester is configured to generate files in the testing directory from
/// files in source directories (sub-directories of the home).
/// Various rules can be configured, e.g. a simple copy of files, or duplicating
/// the source files a number of times in the testing directory with various
/// names.
pub struct ExpansionTester<'a>
{
	/// The home directory for the tests
	dir: &'a str,
	/// The subdirectory (of the home) where test files may be put
	testing_dir: &'a str,
	/// Source sub-directory, and how it's files should be treated before
	/// testing
	source_dirs: Vec<(&'a str, Vec<Box<dyn Fn(&Path, &dyn AsRef<Path>)>>)>,

	/// Whether this tester is testing errors, i.e. that expansions should fail
	error_tests: bool,
}

impl<'a> ExpansionTester<'a>
{
	/// Construct a new tester with a home directory and a testing subdirectory.
	pub fn new(home_dir: &'a str, testing_dir: &'a str) -> Self
	{
		Self {
			dir: home_dir,
			testing_dir,
			source_dirs: Vec::new(),
			error_tests: false,
		}
	}

	/// Construct a new tester with a home directory and a testing subdirectory.
	///
	/// This tester will be testing errors.
	pub fn new_errors(home_dir: &'a str, testing_dir: &'a str) -> Self
	{
		Self {
			dir: home_dir,
			testing_dir,
			source_dirs: Vec::new(),
			error_tests: true,
		}
	}

	/// Add a source directory under the home directory,
	/// with a list of actions that produce files in the testing directory
	/// based on each file in the source directory.
	pub fn add_source_dir(
		&mut self,
		dir: &'a str,
		actions: Vec<Box<dyn Fn(&Path, &dyn AsRef<Path>)>>,
	)
	{
		self.source_dirs.push((dir, actions));
	}

	/// Executes the tests including first setting up the testing directory.
	pub fn execute_tests(&self)
	{
		// Remove old test files
		let testing_dir = self.dir.to_owned() + "/" + self.testing_dir;
		let _ = std::fs::remove_dir_all(&testing_dir);

		// Recreate testing dir
		std::fs::create_dir_all(&testing_dir).unwrap();

		// For each source dir, execute action of each file
		for (source_dir, actions) in self.source_dirs.iter()
		{
			let source_dir_path = self.dir.to_owned() + "/" + source_dir;
			if let Ok(files) = std::fs::read_dir(&source_dir_path)
			{
				for file in files
				{
					if let Ok(file) = file
					{
						for action in actions.iter()
						{
							action(&file.path(), &testing_dir);
						}
					}
					else
					{
						panic!("Error accessing source file: {:?}", file)
					}
				}
			}
		}

		// Prepare feature list for expansion testing
		let mut args = Vec::new();
		let mut features = String::new();
		args.push("--no-default-features");
		if NR_FEATURES > 0
		{
			args.push("--features");
			for f in FEATURES.iter()
			{
				features.push_str(f);
				features.push(',');
			}
			args.push(features.as_str());
		}

		if self.error_tests
		{
			duplicate_macrotest::expand_without_refresh_args_fail(
				testing_dir + "/*.rs",
				args.as_slice(),
			);
		}
		else
		{
			duplicate_macrotest::expand_without_refresh_args(
				testing_dir + "/*.rs",
				args.as_slice(),
			);
		}
	}

	/// Generates an action that copies the file given to the testing
	/// directory with the given prefix added to its name.
	pub fn copy_with_prefix(prefix: &str) -> Box<dyn Fn(&Path, &dyn AsRef<Path>)>
	{
		Self::copy_with_prefix_postfix(prefix, "")
	}

	/// Generates an action that copies the file given to the testing
	/// directory with the given prefix added to its name.
	pub fn copy_with_prefix_postfix(
		prefix: &str,
		postfix: &str,
	) -> Box<dyn Fn(&Path, &dyn AsRef<Path>)>
	{
		let prefix = OsString::from(prefix);
		let postfix = OsString::from(postfix);
		Box::new(move |file, destination| {
			let mut destination_file = destination.as_ref().to_path_buf();
			let mut file_name = prefix.clone();
			file_name.push(file.file_name().unwrap());
			file_name.push(postfix.clone());
			destination_file.push(file_name);
			std::fs::copy(file, &destination_file).unwrap();
		})
	}

	/// Generates an action that simply copies the file given to the testing
	/// directory.
	pub fn copy() -> Box<dyn Fn(&Path, &dyn AsRef<Path>)>
	{
		Self::copy_with_prefix("")
	}

	/// Generates an action that creates two versions of the given file in the
	/// testing directory. The source file must use the attribute
	/// macro, where:
	/// - The invocation must starts with `#[duplicate_item(` or
	///   `#[substitute_item(` on a the first line
	/// (with nothing else). Notice that you must not import the attribute but
	/// use its full path.
	/// - Then the body of the invocation. Both syntaxes are allowed.
	/// - Then the `)]` on its own line, followed immediately by
	///   `//duplicate_end`.
	/// I.e. `)]//duplicate_end`
	/// - Then the item to be duplicated, followed on the next line by
	///   `//item_end` on
	/// its own.
	///
	/// This action will then generate 2 versions of this file. The first is
	/// almost identical the original, but the second will change the invocation
	/// to instead use `duplicate` or `substitute`. It uses the exact rules
	/// specified above to correctly change the code, so any small deviation
	/// from the above rules might result in an error. The name of the first
	/// version is the same as the original and the second version is prefixed
	/// with 'inline_'
	///
	/// ### Example
	/// Original file (`test.rs`):
	/// ```
	/// #[duplicate_item(
	///   name;
	///   [SomeName];
	/// )]//duplicate_end
	/// pub struct name();
	/// //item_end
	/// ```
	/// First version (`test.expanded.rs`):
	/// ```
	/// #[duplicate_item(
	///   name;
	///   [SomeName];
	/// )]
	/// pub struct name();
	/// ```
	/// Second version (`inline_test.expanded.rs`):
	/// ```
	/// duplicate{
	///   [
	///     name;
	///     [SomeName];
	///   ]
	///   pub struct name();
	/// }
	/// ```
	pub fn duplicate_for_inline() -> Box<dyn Fn(&Path, &dyn AsRef<Path>)>
	{
		Self::duplicate_for_inline_with_prefix("")
	}

	/// like 'duplicate_for_inline' except adds the given prefix to the name of
	/// the original files.
	pub fn duplicate_for_inline_with_prefix(
		prefix: &str,
	) -> Box<dyn '_ + Fn(&Path, &dyn AsRef<Path>)>
	{
		Box::new(move |file, destination| {
			let mut inline_file_name = OsString::from("inline_");
			inline_file_name.push(prefix);
			inline_file_name.push(file.file_name().unwrap());
			let mut new_file_name = OsString::from(prefix);
			new_file_name.push(file.file_name().unwrap());

			let mut dest_file_path = destination.as_ref().to_path_buf();
			let mut dest_inline_file_path = destination.as_ref().to_path_buf();

			dest_file_path.push(new_file_name);
			dest_inline_file_path.push(inline_file_name);

			let mut dest_file = File::create(dest_file_path).unwrap();
			let mut dest_inline_file = File::create(dest_inline_file_path).unwrap();

			for line in BufReader::new(File::open(file).unwrap()).lines()
			{
				let line = line.unwrap();
				let line = line.trim();

				match line
				{
					"#[duplicate_item(" =>
					{
						dest_file.write_all("#[duplicate_item(".as_bytes()).unwrap();
						dest_inline_file
							.write_all("duplicate!{[".as_bytes())
							.unwrap();
					},
					"#[substitute_item(" =>
					{
						dest_file
							.write_all("#[substitute_item(".as_bytes())
							.unwrap();
						dest_inline_file
							.write_all("substitute!{[".as_bytes())
							.unwrap();
					},
					")]//duplicate_end" =>
					{
						dest_file.write_all(")]".as_bytes()).unwrap();
						dest_inline_file.write_all("]".as_bytes()).unwrap();
					},
					"//item_end" =>
					{
						dest_inline_file.write_all("}".as_bytes()).unwrap();
					},
					_ =>
					{
						dest_file.write_all(line.as_bytes()).unwrap();
						dest_inline_file.write_all(line.as_bytes()).unwrap();
					},
				}
				dest_file.write_all("\n".as_bytes()).unwrap();
				dest_inline_file.write_all("\n".as_bytes()).unwrap();
			}
		})
	}

	/// Sets up and runs tests in a specific directory using our standard test
	/// setup.
	pub fn run_default_test_setup(home_dir: &str, test_subdir: &str)
	{
		Self::run_default_test_setup_errors(home_dir, test_subdir, false)
	}

	/// Sets up and runs tests in a specific directory using our standard test
	/// setup.
	pub fn run_default_test_setup_errors(home_dir: &str, test_subdir: &str, test_errors: bool)
	{
		let mut test = if test_errors
		{
			ExpansionTester::new_errors(home_dir, test_subdir)
		}
		else
		{
			ExpansionTester::new(home_dir, test_subdir)
		};
		test.add_source_dir("from", vec![ExpansionTester::duplicate_for_inline()]);
		test.add_source_dir(
			"expected",
			vec![
				ExpansionTester::copy(),
				ExpansionTester::copy_with_prefix("inline_"),
			],
		);
		test.add_source_dir(
			"expected_both",
			vec![
				ExpansionTester::copy_with_prefix("inline_short_"),
				ExpansionTester::copy_with_prefix("inline_verbose_"),
				ExpansionTester::copy_with_prefix("short_"),
				ExpansionTester::copy_with_prefix("verbose_"),
			],
		);
		test.execute_tests();
	}
}
