//! Cargo test subprocess runner implementation

use std::collections::HashMap;
use std::path::PathBuf;
use std::process::Stdio;
use std::time::{Duration, Instant};
use tempfile::TempDir;
use tokio::io::{AsyncBufReadExt, AsyncReadExt, BufReader};
use tokio::process::{Child, Command};
use tokio::time::interval;

/// A single subprocess in a cargo test-based test
pub struct TestProcess {
	pub name: String,
	pub test_function_name: String,
	pub data_dir: TempDir,
	pub child: Option<Child>,
	pub output: String,
}

/// Cargo test-based multi-process test runner
pub struct CargoTestRunner {
	processes: Vec<TestProcess>,
	global_timeout: Duration,
	test_file_name: String,
	test_binary_path: Option<PathBuf>,
}

impl CargoTestRunner {
	/// Create a new cargo test runner
	pub fn new() -> Self {
		Self {
			processes: Vec::new(),
			global_timeout: Duration::from_secs(60),
			test_file_name: "test_core_pairing".to_string(),
			test_binary_path: None,
		}
	}

	/// Create a new cargo test runner for a specific test file
	pub fn for_test_file(test_file_name: impl Into<String>) -> Self {
		Self {
			processes: Vec::new(),
			global_timeout: Duration::from_secs(60),
			test_file_name: test_file_name.into(),
			test_binary_path: None,
		}
	}

	/// Set global timeout for all operations
	pub fn with_timeout(mut self, timeout: Duration) -> Self {
		self.global_timeout = timeout;
		self
	}

	/// Add a subprocess with a test function name
	pub fn add_subprocess(
		mut self,
		name: impl Into<String>,
		test_function_name: impl Into<String>,
	) -> Self {
		let name = name.into();
		let test_function_name = test_function_name.into();
		let data_dir = TempDir::new().expect("Failed to create temp dir");

		let process = TestProcess {
			name,
			test_function_name,
			data_dir,
			child: None,
			output: String::new(),
		};

		self.processes.push(process);
		self
	}

	/// Build the test binary once and cache the path
	async fn build_test_binary(&mut self) -> Result<PathBuf, String> {
		// Return cached path if already built
		if let Some(ref path) = self.test_binary_path {
			return Ok(path.clone());
		}

		println!("Building test binary for {}...", self.test_file_name);

		// Run cargo test --no-run to build the test binary
		let output = Command::new("cargo")
			.args(&[
				"test",
				"--no-run",
				"--test",
				&self.test_file_name,
				"--message-format=json",
			])
			.output()
			.await
			.map_err(|e| format!("Failed to run cargo test --no-run: {}", e))?;

		if !output.status.success() {
			return Err(format!(
				"cargo test --no-run failed: {}",
				String::from_utf8_lossy(&output.stderr)
			));
		}

		// Parse JSON output to find the test binary
		let stdout = String::from_utf8_lossy(&output.stdout);
		for line in stdout.lines() {
			if let Ok(json) = serde_json::from_str::<serde_json::Value>(line) {
				if json["reason"] == "compiler-artifact"
					&& json["target"]["kind"]
						.as_array()
						.map(|arr| arr.iter().any(|v| v == "test"))
						.unwrap_or(false)
					&& json["target"]["name"] == self.test_file_name
				{
					if let Some(executable) = json["executable"].as_str() {
						let path = PathBuf::from(executable);
						println!("Test binary built: {}", path.display());
						self.test_binary_path = Some(path.clone());
						return Ok(path);
					}
				}
			}
		}

		Err(format!(
			"Could not find test binary path in cargo output for {}",
			self.test_file_name
		))
	}

	/// Run all subprocesses and wait until success condition is met
	pub async fn run_until_success<F>(&mut self, condition: F) -> Result<(), String>
	where
		F: Fn(&HashMap<String, String>) -> bool,
	{
		// Spawn all subprocesses
		self.spawn_all_processes().await?;

		// Wait for success condition
		self.wait_until_condition(condition).await?;

		// Cleanup
		self.kill_all().await;

		Ok(())
	}

	/// Spawn a single subprocess by name
	pub async fn spawn_single_process(&mut self, name: &str) -> Result<(), String> {
		// Build the test binary once (or use cached path)
		let binary_path = self.build_test_binary().await?;

		let process = self
			.processes
			.iter_mut()
			.find(|p| p.name == name)
			.ok_or_else(|| format!("Process '{}' not found", name))?;

		// Execute the test binary directly instead of running cargo test
		let mut command = Command::new(&binary_path);
		command
			.args(&[
				&process.test_function_name,
				"--nocapture",
				"--ignored", // Run ignored tests
			])
			.env("TEST_ROLE", &process.name)
			.env("TEST_DATA_DIR", process.data_dir.path().to_str().unwrap());

		// Forward RUST_LOG from parent or default to debug for subprocess visibility
		if let Ok(rust_log) = std::env::var("RUST_LOG") {
			command.env("RUST_LOG", rust_log);
		} else {
			command.env("RUST_LOG", "debug");
		}

		let child = command
			.stdout(Stdio::inherit())
			.stderr(Stdio::inherit())
			.spawn()
			.map_err(|e| format!("Failed to spawn process '{}': {}", process.name, e))?;

		process.child = Some(child);
		println!(
			"Spawned test process: {} (test: {})",
			process.name, process.test_function_name
		);

		Ok(())
	}

	/// Wait for success condition without spawning processes
	pub async fn wait_for_success<F>(&mut self, condition: F) -> Result<(), String>
	where
		F: Fn(&HashMap<String, String>) -> bool,
	{
		// Wait for success condition
		self.wait_until_condition(condition).await?;

		// Cleanup
		self.kill_all().await;

		Ok(())
	}

	/// Spawn all subprocesses using the test binary
	async fn spawn_all_processes(&mut self) -> Result<(), String> {
		// Build the test binary once (or use cached path)
		let binary_path = self.build_test_binary().await?;

		for process in &mut self.processes {
			// Execute the test binary directly instead of running cargo test
			let mut command = Command::new(&binary_path);
			command
				.args(&[
					&process.test_function_name,
					"--nocapture",
					"--ignored", // Run ignored tests
				])
				.env("TEST_ROLE", &process.name)
				.env("TEST_DATA_DIR", process.data_dir.path().to_str().unwrap());

			// Forward RUST_LOG from parent or default to debug for subprocess visibility
			if let Ok(rust_log) = std::env::var("RUST_LOG") {
				command.env("RUST_LOG", rust_log);
			} else {
				command.env("RUST_LOG", "debug");
			}

			let child = command
				.stdout(Stdio::inherit())
				.stderr(Stdio::inherit())
				.spawn()
				.map_err(|e| format!("Failed to spawn process '{}': {}", process.name, e))?;

			process.child = Some(child);
			println!(
				"Spawned test process: {} (test: {})",
				process.name, process.test_function_name
			);
		}

		Ok(())
	}

	/// Wait until the success condition is met
	async fn wait_until_condition<F>(&mut self, condition: F) -> Result<(), String>
	where
		F: Fn(&HashMap<String, String>) -> bool,
	{
		let mut check_interval = interval(Duration::from_millis(100));
		let start_time = Instant::now();

		loop {
			tokio::select! {
				_ = check_interval.tick() => {
					// Read output from all processes
					self.read_all_output().await;

					// Build output map for condition check
					let outputs: HashMap<String, String> = self.processes.iter()
						.map(|p| (p.name.clone(), p.output.clone()))
						.collect();

					// Check condition
					if condition(&outputs) {
						println!("Success condition met after {:?}", start_time.elapsed());
						return Ok(());
					}

					// Check for timeout
					if start_time.elapsed() > self.global_timeout {
						return Err("Timeout waiting for success condition".to_string());
					}

					// Check for failed processes
					self.check_process_health()?;
				}
			}
		}
	}

	/// Read output from all running processes
	async fn read_all_output(&mut self) {
		// Output is handled via stdio inheritance - just track what we see in output
		for process in &mut self.processes {
			if let Some(child) = &mut process.child {
				// Check if process has exited to capture final output
				if let Ok(Some(_)) = child.try_wait() {
					// Process has exited, mark its output as complete
				}
			}
		}
	}

	/// Check if any processes have failed
	fn check_process_health(&mut self) -> Result<(), String> {
		for process in &mut self.processes {
			if let Some(child) = &mut process.child {
				if let Ok(Some(exit_status)) = child.try_wait() {
					if !exit_status.success() {
						return Err(format!(
							"Process '{}' exited with failure: {:?}",
							process.name,
							exit_status.code()
						));
					}
				}
			}
		}
		Ok(())
	}

	/// Kill all processes
	pub async fn kill_all(&mut self) {
		for process in &mut self.processes {
			if let Some(mut child) = process.child.take() {
				let _ = child.kill().await;
				let _ = child.wait().await;
			}
		}
		println!("Killed all cargo test processes");
	}

	/// Get output from a specific process
	pub fn get_output(&self, name: &str) -> Option<&str> {
		self.processes
			.iter()
			.find(|p| p.name == name)
			.map(|p| p.output.as_str())
	}

	/// Get all outputs as a map
	pub fn get_all_outputs(&self) -> HashMap<String, String> {
		self.processes
			.iter()
			.map(|p| (p.name.clone(), p.output.clone()))
			.collect()
	}
}

impl Drop for CargoTestRunner {
	fn drop(&mut self) {
		// Best effort cleanup
		for process in &mut self.processes {
			if let Some(mut child) = process.child.take() {
				let _ = child.start_kill();
			}
		}
	}
}

impl Default for CargoTestRunner {
	fn default() -> Self {
		Self::new()
	}
}
