#![allow(clippy::large_futures)]
use std::ffi::OsString;
use std::fs;
use std::future::Future;
use std::path::Path;
use std::process::Command;

use criterion::BatchSize;
use criterion::BenchmarkId;
use criterion::Criterion;
use criterion::async_executor::AsyncExecutor;
use criterion::criterion_group;
use criterion::criterion_main;
use httpmock::Method::GET;
use httpmock::Method::POST;
use httpmock::MockServer;

/// Generate a workspace fixture with N cargo packages and M changesets.
fn generate_fixture(root: &Path, num_packages: usize, num_changesets: usize) {
	use std::fmt::Write;
	let mut workspace_members = String::from("[workspace]\nmembers = [\n");
	for i in 0..num_packages {
		let _ = writeln!(workspace_members, "  \"crates/pkg-{i}\",");
	}
	workspace_members.push_str("]\nresolver = \"2\"\n");
	fs::write(root.join("Cargo.toml"), &workspace_members).unwrap();

	let mut config = String::from("[defaults]\npackage_type = \"cargo\"\n\n");
	for i in 0..num_packages {
		let _ = write!(config, "[package.pkg-{i}]\npath = \"crates/pkg-{i}\"\n\n");
	}
	config.push_str("[ecosystems.cargo]\nenabled = true\n\n");
	config.push_str("[cli.discover]\n[[cli.discover.steps]]\ntype = \"Discover\"\n\n");
	config.push_str("[cli.release]\n[[cli.release.steps]]\ntype = \"PrepareRelease\"\n");
	fs::write(root.join("monochange.toml"), &config).unwrap();

	for i in 0..num_packages {
		let pkg_dir = root.join(format!("crates/pkg-{i}"));
		fs::create_dir_all(pkg_dir.join("src")).unwrap();
		fs::write(
			pkg_dir.join("Cargo.toml"),
			format!("[package]\nname = \"pkg-{i}\"\nversion = \"1.0.0\"\nedition = \"2021\"\n"),
		)
		.unwrap();
		fs::write(
			pkg_dir.join("src/lib.rs"),
			format!("pub const PACKAGE_ID: &str = \"pkg-{i}\";\n"),
		)
		.unwrap();
	}

	let changeset_dir = root.join(".changeset");
	fs::create_dir_all(&changeset_dir).unwrap();
	for i in 0..num_changesets {
		let target_pkg = i % num_packages;
		fs::write(
			changeset_dir.join(format!("change-{i:04}.md")),
			format!("---\npkg-{target_pkg}: patch\n---\n\nFix issue #{i}.\n"),
		)
		.unwrap();
	}
}

fn write_synthetic_cargo_lock(root: &Path, num_packages: usize) {
	use std::fmt::Write;

	let mut cargo_lock = String::from("version = 3\n\n");
	for i in 0..num_packages {
		let _ = writeln!(
			cargo_lock,
			"[[package]]\nname = \"pkg-{i}\"\nversion = \"1.0.0\"\n"
		);
	}
	fs::write(root.join("Cargo.lock"), cargo_lock).unwrap();
}

fn add_dependency_chain_manifests(root: &Path, num_packages: usize) {
	for i in 1..num_packages {
		let manifest_path = root.join(format!("crates/pkg-{i}/Cargo.toml"));
		let manifest = fs::read_to_string(&manifest_path).unwrap();
		fs::write(
			&manifest_path,
			format!(
				"{manifest}\n[dependencies]\npkg-{} = {{ path = \"../pkg-{}\", version = \"1.0.0\" }}\n",
				i - 1,
				i - 1
			),
		)
		.unwrap();
	}
}

fn write_dependency_chain_cargo_lock(root: &Path, num_packages: usize) {
	use std::fmt::Write;

	let mut cargo_lock = String::from("version = 3\n\n");
	for i in 0..num_packages {
		let _ = writeln!(
			cargo_lock,
			"[[package]]\nname = \"pkg-{i}\"\nversion = \"1.0.0\""
		);
		if i > 0 {
			let _ = writeln!(cargo_lock, "dependencies = [\"pkg-{}\"]", i - 1);
		}
		cargo_lock.push('\n');
	}
	fs::write(root.join("Cargo.lock"), cargo_lock).unwrap();
}

fn generate_fixture_with_cargo_lock(root: &Path, num_packages: usize, num_changesets: usize) {
	generate_fixture(root, num_packages, num_changesets);
	let config_path = root.join("monochange.toml");
	let config = fs::read_to_string(&config_path).unwrap();
	fs::write(
		&config_path,
		config.replacen(
			"[defaults]\npackage_type = \"cargo\"\n\n",
			"[defaults]\npackage_type = \"cargo\"\nchangelog = \"{{ path }}/changelog.md\"\n\n",
			1,
		),
	)
	.unwrap();
	for i in 0..num_packages {
		fs::write(
			root.join(format!("crates/pkg-{i}/changelog.md")),
			"## 1.0.0\n\n- Initial release\n",
		)
		.unwrap();
	}
	write_synthetic_cargo_lock(root, num_packages);
}

fn generate_fixture_with_dependency_chain_cargo_lock(
	root: &Path,
	num_packages: usize,
	num_changesets: usize,
) {
	generate_fixture_with_cargo_lock(root, num_packages, num_changesets);
	add_dependency_chain_manifests(root, num_packages);
	write_dependency_chain_cargo_lock(root, num_packages);
}

fn enable_explicit_cargo_refresh_command(root: &Path) {
	let config_path = root.join("monochange.toml");
	let config = fs::read_to_string(&config_path).unwrap();
	fs::write(
		&config_path,
		format!(
			"{config}\n[[ecosystems.cargo.lockfile_commands]]\ncommand = \"cargo generate-lockfile\"\ncwd = \".\"\n"
		),
	)
		.unwrap();
}

fn setup_scenario_workspace(relative: &str) -> tempfile::TempDir {
	monochange_test_helpers::fs::setup_scenario_workspace_from(env!("CARGO_MANIFEST_DIR"), relative)
}

fn git(root: &Path, args: &[&str]) {
	let output = Command::new("git")
		.current_dir(root)
		.arg("-c")
		.arg("commit.gpgsign=false")
		.args(args)
		.output()
		.unwrap_or_else(|error| panic!("git {args:?}: {error}"));
	assert!(
		output.status.success(),
		"git {args:?} failed: {}{}",
		String::from_utf8_lossy(&output.stdout),
		String::from_utf8_lossy(&output.stderr)
	);
}

fn seed_release_pr_git_repository(root: &Path) {
	let bare = root.join(".bench-origin.git");
	git(root, &["init", "-b", "main"]);
	git(root, &["config", "user.name", "monochange Bench"]);
	git(root, &["config", "user.email", "monochange@example.com"]);
	git(root, &["config", "commit.gpgsign", "false"]);
	git(root, &["add", "."]);
	git(root, &["commit", "-m", "initial"]);
	git(root, &["init", "--bare", bare.to_string_lossy().as_ref()]);
	git(
		root,
		&["remote", "add", "origin", bare.to_string_lossy().as_ref()],
	);
	git(root, &["push", "-u", "origin", "main"]);
}

fn release_pr_args(dry_run: bool) -> Vec<OsString> {
	let mut args = vec![OsString::from("mc"), OsString::from("release-pr")];
	if dry_run {
		args.push(OsString::from("--dry-run"));
	}
	args.push(OsString::from("--format"));
	args.push(OsString::from("json"));
	args
}

fn command_args(command: &str, dry_run: bool) -> Vec<OsString> {
	let mut args = vec![OsString::from("mc"), OsString::from(command)];
	if dry_run {
		args.push(OsString::from("--dry-run"));
	}
	args.push(OsString::from("--format"));
	args.push(OsString::from("json"));
	args
}

fn block_on_bench<T>(future: impl Future<Output = T>) -> T {
	let runtime = tokio::runtime::Builder::new_multi_thread()
		.enable_all()
		.build()
		.unwrap_or_else(|error| panic!("tokio runtime: {error}"));

	AsyncExecutor::block_on(&runtime, future)
}

fn run_command(root: &Path, command: &str, dry_run: bool) -> String {
	block_on_bench(temp_env::async_with_vars(
		[("MONOCHANGE_RELEASE_DATE", Some("2026-04-06"))],
		async {
			monochange::run_with_args_in_dir("mc", command_args(command, dry_run), root)
				.await
				.unwrap()
		},
	))
}

fn run_release_pr_command(root: &Path, dry_run: bool, github_api_url: Option<&str>) -> String {
	block_on_bench(temp_env::async_with_vars(
		[
			("MONOCHANGE_RELEASE_DATE", Some("2026-04-06")),
			("GITHUB_TOKEN", Some("bench-token")),
			("GITHUB_API_URL", github_api_url),
		],
		async {
			monochange::run_with_args_in_dir("mc", release_pr_args(dry_run), root)
				.await
				.unwrap()
		},
	))
}

const SCALES: &[(usize, usize)] = &[(5, 10), (20, 50), (50, 200)];
const LOCKFILE_COMPARE_SCALE: (usize, usize) = (20, 50);

fn bench_config_load(c: &mut Criterion) {
	let mut group = c.benchmark_group("config_load");
	group.sample_size(10);

	for &(packages, changesets) in SCALES {
		let label = format!("{packages}pkg_{changesets}cs");
		group.bench_with_input(
			BenchmarkId::new("load_workspace_configuration", &label),
			&(packages, changesets),
			|b, &(packages, changesets)| {
				let tempdir = tempfile::tempdir().unwrap();
				generate_fixture(tempdir.path(), packages, changesets);
				b.iter(|| monochange_config::load_workspace_configuration(tempdir.path()).unwrap());
			},
		);
	}
	group.finish();
}

fn bench_discover(c: &mut Criterion) {
	let mut group = c.benchmark_group("discover");
	group.sample_size(10);

	for &(packages, changesets) in SCALES {
		let label = format!("{packages}pkg_{changesets}cs");
		group.bench_with_input(
			BenchmarkId::new("discover_workspace", &label),
			&(packages, changesets),
			|b, &(packages, changesets)| {
				let tempdir = tempfile::tempdir().unwrap();
				generate_fixture(tempdir.path(), packages, changesets);
				b.iter(|| monochange::discover_workspace(tempdir.path()).unwrap());
			},
		);
	}
	group.finish();
}

fn bench_validate(c: &mut Criterion) {
	let mut group = c.benchmark_group("validate");
	group.sample_size(10);

	for &(packages, changesets) in SCALES {
		let label = format!("{packages}pkg_{changesets}cs");
		group.bench_with_input(
			BenchmarkId::new("validate_workspace", &label),
			&(packages, changesets),
			|b, &(packages, changesets)| {
				let tempdir = tempfile::tempdir().unwrap();
				generate_fixture(tempdir.path(), packages, changesets);
				b.iter(|| monochange_config::validate_workspace(tempdir.path()).unwrap());
			},
		);
	}
	group.finish();
}

fn bench_changeset_loading(c: &mut Criterion) {
	let mut group = c.benchmark_group("changeset_loading");
	group.sample_size(10);

	for &(packages, changesets) in SCALES {
		let label = format!("{packages}pkg_{changesets}cs");
		group.bench_with_input(
			BenchmarkId::new("load_changesets", &label),
			&(packages, changesets),
			|b, &(packages, changesets)| {
				let tempdir = tempfile::tempdir().unwrap();
				generate_fixture(tempdir.path(), packages, changesets);
				let configuration =
					monochange_config::load_workspace_configuration(tempdir.path()).unwrap();
				let discovery = monochange::discover_workspace(tempdir.path()).unwrap();
				let changeset_dir = tempdir.path().join(".changeset");
				let changeset_paths: Vec<_> = fs::read_dir(&changeset_dir)
					.unwrap()
					.filter_map(Result::ok)
					.map(|e| e.path())
					.filter(|p| p.extension().is_some_and(|ext| ext == "md"))
					.collect();
				b.iter(|| {
					changeset_paths
						.iter()
						.map(|path| {
							monochange_config::load_changeset_file(
								path,
								&configuration,
								&discovery.packages,
							)
							.unwrap()
						})
						.collect::<Vec<_>>()
				});
			},
		);
	}
	group.finish();
}

fn bench_versions_dry_run_json(c: &mut Criterion) {
	let mut group = c.benchmark_group("versions_dry_run_json");
	group.sample_size(10);

	group.bench_function("dart_workspace", |b| {
		b.iter_batched(
			|| setup_scenario_workspace("dart-lints/advanced-workspace-flutter/workspace"),
			|tempdir| {
				let _ = run_command(tempdir.path(), "versions", true);
			},
			BatchSize::LargeInput,
		);
	});

	group.bench_function("mixed_unsupported_workspace", |b| {
		b.iter_batched(
			|| setup_scenario_workspace("cli-output/discover-mixed"),
			|tempdir| {
				let _ = run_command(tempdir.path(), "versions", true);
			},
			BatchSize::LargeInput,
		);
	});

	group.finish();
}

fn bench_prepare_release_dry_run(c: &mut Criterion) {
	let mut group = c.benchmark_group("prepare_release_dry_run");
	group.sample_size(10);

	for &(packages, changesets) in SCALES {
		let label = format!("{packages}pkg_{changesets}cs");
		group.bench_with_input(
			BenchmarkId::new("prepare_release", &label),
			&(packages, changesets),
			|b, &(packages, changesets)| {
				let tempdir = tempfile::tempdir().unwrap();
				generate_fixture(tempdir.path(), packages, changesets);
				b.iter(|| {
					block_on_bench(monochange::prepare_release(tempdir.path(), true)).unwrap()
				});
			},
		);
	}
	group.finish();
}

/// Generate a workspace with a real git history.
///
/// Creates `num_packages` packages, then builds git history by:
/// 1. Initial commit with all packages
/// 2. `num_history_commits` filler commits (modifying source files)
/// 3. Changesets added at evenly spaced intervals throughout the history
///
/// This tests whether `git log --follow --diff-filter=A` scales with
/// the depth of git history.
fn generate_fixture_with_git_history(
	root: &Path,
	num_packages: usize,
	num_changesets: usize,
	num_history_commits: usize,
) {
	// Create workspace structure first (no git yet).
	generate_fixture(root, num_packages, num_changesets);
	// Remove changesets — we'll add them interleaved with history.
	let changeset_dir = root.join(".changeset");
	if changeset_dir.exists() {
		fs::remove_dir_all(&changeset_dir).unwrap();
	}
	fs::create_dir_all(&changeset_dir).unwrap();

	// Init git repo.
	let git = |args: &[&str]| {
		let output = monochange_core::git::git_command(root)
			.arg("-c")
			.arg("gc.auto=0")
			.arg("-c")
			.arg("commit.gpgsign=false")
			.args(args)
			.env("GIT_AUTHOR_NAME", "bench")
			.env("GIT_AUTHOR_EMAIL", "bench@test")
			.env("GIT_COMMITTER_NAME", "bench")
			.env("GIT_COMMITTER_EMAIL", "bench@test")
			.output()
			.unwrap_or_else(|e| panic!("git {}: {e}", args.join(" ")));
		assert!(
			output.status.success(),
			"git {} failed: {}",
			args.join(" "),
			String::from_utf8_lossy(&output.stderr)
		);
	};

	git(&["init", "-b", "main"]);
	git(&["add", "."]);
	git(&["commit", "-m", "initial"]);

	// Calculate where to insert changesets in the history.
	let changeset_interval = if num_changesets > 0 {
		num_history_commits.max(1) / num_changesets.max(1)
	} else {
		usize::MAX
	};
	let mut changesets_added = 0;

	for commit_idx in 0..num_history_commits {
		// Add a filler commit (modify a source file).
		let pkg_idx = commit_idx % num_packages;
		let src_file = root.join(format!("crates/pkg-{pkg_idx}/src.rs"));
		fs::write(&src_file, format!("// commit {commit_idx}\n")).unwrap();
		git(&["add", "."]);
		git(&["commit", "-m", &format!("commit {commit_idx}")]);

		// Interleave changeset additions at regular intervals.
		if changesets_added < num_changesets
			&& (changeset_interval == 0 || commit_idx % changeset_interval.max(1) == 0)
		{
			let target_pkg = changesets_added % num_packages;
			let cs_path = changeset_dir.join(format!("change-{changesets_added:04}.md"));
			fs::write(
				&cs_path,
				format!("---\npkg-{target_pkg}: patch\n---\n\nFix issue #{changesets_added}.\n"),
			)
			.unwrap();
			git(&["add", "."]);
			git(&["commit", "-m", &format!("changeset {changesets_added}")]);
			changesets_added += 1;
		}
	}

	// Add any remaining changesets.
	while changesets_added < num_changesets {
		let target_pkg = changesets_added % num_packages;
		let cs_path = changeset_dir.join(format!("change-{changesets_added:04}.md"));
		fs::write(
			&cs_path,
			format!("---\npkg-{target_pkg}: patch\n---\n\nFix issue #{changesets_added}.\n"),
		)
		.unwrap();
		git(&["add", "."]);
		git(&["commit", "-m", &format!("changeset {changesets_added}")]);
		changesets_added += 1;
	}
}

fn bench_prepare_release_apply(c: &mut Criterion) {
	let mut group = c.benchmark_group("prepare_release_apply");
	group.sample_size(10);

	for &(packages, changesets) in SCALES {
		let label = format!("{packages}pkg_{changesets}cs");
		group.bench_with_input(
			BenchmarkId::new("prepare_release", &label),
			&(packages, changesets),
			|b, &(packages, changesets)| {
				b.iter_batched(
					|| {
						let tempdir = tempfile::tempdir().unwrap();
						generate_fixture_with_cargo_lock(tempdir.path(), packages, changesets);
						tempdir
					},
					|tempdir| {
						// Benchmark the real non-dry-run path without rendering diff
						// previews. This is the critical fast path for `mc release`
						// after we switched supported lockfiles to direct rewrites.
						block_on_bench(monochange::prepare_release(tempdir.path(), false)).unwrap();
					},
					BatchSize::LargeInput,
				);
			},
		);
	}
	group.finish();
}

fn bench_prepare_release_apply_cargo_lockfile_refresh(c: &mut Criterion) {
	let mut group = c.benchmark_group("prepare_release_apply_cargo_lockfile_refresh");
	group.sample_size(10);

	let (packages, changesets) = LOCKFILE_COMPARE_SCALE;
	let label = format!("{packages}pkg_{changesets}cs");

	group.bench_with_input(
		BenchmarkId::new("direct_rewrite", &label),
		&(packages, changesets),
		|b, &(packages, changesets)| {
			b.iter_batched(
				|| {
					let tempdir = tempfile::tempdir().unwrap();
					generate_fixture_with_dependency_chain_cargo_lock(
						tempdir.path(),
						packages,
						changesets,
					);
					tempdir
				},
				|tempdir| {
					block_on_bench(monochange::prepare_release(tempdir.path(), false)).unwrap();
				},
				BatchSize::LargeInput,
			);
		},
	);

	group.bench_with_input(
		BenchmarkId::new("full_refresh_command", &label),
		&(packages, changesets),
		|b, &(packages, changesets)| {
			b.iter_batched(
				|| {
					let tempdir = tempfile::tempdir().unwrap();
					generate_fixture_with_dependency_chain_cargo_lock(
						tempdir.path(),
						packages,
						changesets,
					);
					enable_explicit_cargo_refresh_command(tempdir.path());
					tempdir
				},
				|tempdir| {
					block_on_bench(monochange::prepare_release(tempdir.path(), false)).unwrap();
				},
				BatchSize::LargeInput,
			);
		},
	);

	group.finish();
}

fn bench_prepare_release_with_git_history(c: &mut Criterion) {
	if std::env::var_os("CARGO_LLVM_COV").is_some() {
		return;
	}

	let mut group = c.benchmark_group("prepare_release_git_history");
	group.sample_size(10);

	// Test with 10 packages, 20 changesets, varying history depth.
	for &history_commits in &[10, 100, 200] {
		let label = format!("10pkg_20cs_{history_commits}commits");
		group.bench_with_input(
			BenchmarkId::new("prepare_release", &label),
			&history_commits,
			|b, &history_commits| {
				let tempdir = tempfile::tempdir().unwrap();
				generate_fixture_with_git_history(tempdir.path(), 10, 20, history_commits);
				b.iter(|| {
					block_on_bench(monochange::prepare_release(tempdir.path(), true)).unwrap()
				});
			},
		);
	}
	group.finish();
}

fn bench_release_pr(c: &mut Criterion) {
	let mut group = c.benchmark_group("release_pr");
	group.sample_size(10);

	group.bench_function("dry_run_preview", |b| {
		b.iter_batched(
			|| setup_scenario_workspace("source/github"),
			|tempdir| {
				let _ = run_release_pr_command(tempdir.path(), true, None);
			},
			BatchSize::LargeInput,
		);
	});

	group.bench_function("github_create", |b| {
		b.iter_batched(
			|| {
				let tempdir = setup_scenario_workspace("source/github");
				seed_release_pr_git_repository(tempdir.path());
				let server = MockServer::start();
				server.mock(|when, then| {
					when.method(GET).path("/repos/ifiokjr/monochange/pulls");
					then.status(200)
						.header("content-type", "application/json")
						.body("[]");
				});
				server.mock(|when, then| {
					when.method(POST).path("/repos/ifiokjr/monochange/pulls");
					then.status(201)
						.header("content-type", "application/json")
						.body(
							"{\"number\":7,\"html_url\":\"https://example.com/pr/7\",\"node_id\":\"PR_node\"}",
						);
				});
				server.mock(|when, then| {
					when.method(POST)
						.path("/repos/ifiokjr/monochange/issues/7/labels");
					then.status(200)
						.header("content-type", "application/json")
						.body("[]");
				});
				server.mock(|when, then| {
					when.method(POST).path("/graphql");
					then.status(200)
						.header("content-type", "application/json")
						.body(
							"{\"enablePullRequestAutoMerge\":{\"pullRequest\":{\"number\":7}},\"data\":{\"enablePullRequestAutoMerge\":{\"pullRequest\":{\"number\":7}}}}",
						);
				});
				(tempdir, server)
			},
			|(tempdir, server)| {
				let api_url = server.base_url();
				let _ = run_release_pr_command(tempdir.path(), false, Some(api_url.as_str()));
			},
			BatchSize::LargeInput,
		);
	});

	group.finish();
}

fn bench_prepared_release_cache(c: &mut Criterion) {
	let mut group = c.benchmark_group("prepared_release_cache");
	group.sample_size(10);

	group.bench_function("commit_release_after_release", |b| {
		b.iter_batched(
			|| {
				let tempdir = setup_scenario_workspace("prepared-release/source-github-follow-up");
				seed_release_pr_git_repository(tempdir.path());
				let _ = run_command(tempdir.path(), "release", false);
				tempdir
			},
			|tempdir| {
				let _ = run_command(tempdir.path(), "commit-release", false);
			},
			BatchSize::LargeInput,
		);
	});

	group.bench_function("release_pr_dry_run_after_release", |b| {
		b.iter_batched(
			|| {
				let tempdir = setup_scenario_workspace("prepared-release/source-github-follow-up");
				seed_release_pr_git_repository(tempdir.path());
				let _ = run_command(tempdir.path(), "release", false);
				tempdir
			},
			|tempdir| {
				let _ = run_command(tempdir.path(), "release-pr", true);
			},
			BatchSize::LargeInput,
		);
	});

	group.finish();
}

criterion_group!(
	benches,
	bench_config_load,
	bench_discover,
	bench_validate,
	bench_changeset_loading,
	bench_versions_dry_run_json,
	bench_prepare_release_dry_run,
	bench_prepare_release_apply,
	bench_prepare_release_apply_cargo_lockfile_refresh,
	bench_prepare_release_with_git_history,
	bench_release_pr,
	bench_prepared_release_cache,
);
criterion_main!(benches);
