#![allow(clippy::large_futures)]
#![allow(clippy::disallowed_methods)]
use std::fs;

use httpmock::Method::GET;
use httpmock::MockServer;
use insta::assert_snapshot;
use insta_cmd::assert_cmd_snapshot;
use monochange_test_helpers::git::git;
use serde_json::Value;

mod test_support;
use test_support::assert_readable_json_snapshot;
use test_support::copy_directory;
use test_support::current_test_name;
use test_support::fixture_path;
use test_support::monochange_command;
use test_support::setup_scenario_workspace;
use test_support::snapshot_settings;

fn release_cli_command() -> std::process::Command {
	monochange_command(Some("2026-04-06"))
}

fn command_stdout_json(command: &mut std::process::Command, expect_success: bool) -> Value {
	let output = command
		.output()
		.unwrap_or_else(|error| panic!("run command: {error}"));
	assert_eq!(
		output.status.success(),
		expect_success,
		"stdout:\n{}\nstderr:\n{}",
		String::from_utf8_lossy(&output.stdout),
		String::from_utf8_lossy(&output.stderr)
	);
	assert!(
		output.stderr.is_empty(),
		"stderr:\n{}",
		String::from_utf8_lossy(&output.stderr)
	);
	serde_json::from_slice(&output.stdout)
		.unwrap_or_else(|error| panic!("parse stdout json: {error}"))
}

fn setup_analyze_cli_repo(first_release: bool) -> tempfile::TempDir {
	let scenario_root = fixture_path("cli-output/analyze-group-release-trajectory");
	let release = scenario_root.join("release");
	let main = scenario_root.join("main");
	let head = scenario_root.join("head");
	let tempdir = tempfile::TempDir::new().unwrap_or_else(|error| panic!("tempdir: {error}"));
	let root = tempdir.path();

	copy_directory(&release, root);
	git(root, &["init"]);
	git(root, &["config", "user.name", "monochange-tests"]);
	git(
		root,
		&["config", "user.email", "monochange-tests@example.com"],
	);
	git(root, &["add", "."]);
	git(root, &["commit", "-m", "release"]);
	git(root, &["branch", "-M", "main"]);
	if !first_release {
		git(root, &["tag", "v1.0.0"]);
	}

	copy_directory(&main, root);
	git(root, &["add", "."]);
	git(root, &["commit", "-m", "main evolution"]);
	git(root, &["checkout", "-b", "feature"]);

	copy_directory(&head, root);
	git(root, &["add", "."]);
	git(root, &["commit", "-m", "head evolution"]);

	tempdir
}

fn mock_missing_publish_versions(server: &MockServer) {
	server.mock(|when, then| {
		when.method(GET).path("/crates/core");
		then.status(200).json_body_obj(&serde_json::json!({
			"versions": []
		}));
	});
	server.mock(|when, then| {
		when.method(GET).path("/packages/dart_pkg");
		then.status(200).json_body_obj(&serde_json::json!({
			"versions": []
		}));
	});
	server.mock(|when, then| {
		when.method(GET).path("/@scope/jsr-pkg/meta.json");
		then.status(200).json_body_obj(&serde_json::json!({
			"versions": {}
		}));
	});
	server.mock(|when, then| {
		when.method(GET).path("/npm-pkg");
		then.status(200).json_body_obj(&serde_json::json!({
			"versions": {}
		}));
	});
}

#[test]
fn validate_cli_succeeds_for_valid_workspace() {
	let mut settings = snapshot_settings();
	settings.set_snapshot_suffix(current_test_name());
	let _guard = settings.bind_to_scope();

	let tempdir = setup_scenario_workspace("monochange/validate-workspace");
	assert_cmd_snapshot!(
		monochange_command(None)
			.current_dir(tempdir.path())
			.arg("step:validate")
	);
}

#[test]
fn placeholder_publish_dry_run_reports_manual_registry_trust_contexts() {
	let mut settings = snapshot_settings();
	settings.set_snapshot_suffix(current_test_name());
	let _guard = settings.bind_to_scope();

	let tempdir = setup_scenario_workspace("cli-output/manual-trust-diagnostics");
	let server = MockServer::start();
	mock_missing_publish_versions(&server);
	assert_cmd_snapshot!(
		monochange_command(None)
			.current_dir(tempdir.path())
			.env(
				"GITHUB_WORKFLOW_REF",
				"ifiokjr/monochange/.github/workflows/publish.yml@refs/heads/main"
			)
			.env("GITHUB_JOB", "release")
			.env("MONOCHANGE_CRATES_IO_API_URL", server.base_url())
			.env("MONOCHANGE_NPM_REGISTRY_URL", server.base_url())
			.env("MONOCHANGE_PUB_DEV_API_URL", server.base_url())
			.env("MONOCHANGE_JSR_BASE_URL", server.base_url())
			.arg("placeholder-publish")
			.arg("--dry-run")
			.arg("--format")
			.arg("text")
	);
}

#[test]
fn placeholder_publish_dry_run_reports_missing_manual_registry_workflow_configuration() {
	let mut settings = snapshot_settings();
	settings.set_snapshot_suffix(current_test_name());
	let _guard = settings.bind_to_scope();

	let tempdir = setup_scenario_workspace("cli-output/manual-trust-diagnostics");
	let server = MockServer::start();
	mock_missing_publish_versions(&server);
	assert_cmd_snapshot!(
		monochange_command(None)
			.current_dir(tempdir.path())
			.env_remove("GITHUB_ACTIONS")
			.env_remove("GITHUB_REPOSITORY")
			.env_remove("GITHUB_WORKFLOW")
			.env_remove("GITHUB_WORKFLOW_REF")
			.env_remove("GITHUB_JOB")
			.env_remove("GITHUB_RUN_ID")
			.env_remove("GITHUB_REF_NAME")
			.env_remove("MONOCHANGE_TRUSTED_PUBLISHING_ENVIRONMENT")
			.env("MONOCHANGE_CRATES_IO_API_URL", server.base_url())
			.env("MONOCHANGE_NPM_REGISTRY_URL", server.base_url())
			.env("MONOCHANGE_PUB_DEV_API_URL", server.base_url())
			.env("MONOCHANGE_JSR_BASE_URL", server.base_url())
			.arg("placeholder-publish")
			.arg("--dry-run")
			.arg("--format")
			.arg("json")
	);
}

#[test]
fn change_cli_help_documents_package_and_group_targeting_rules() {
	let mut settings = snapshot_settings();
	settings.set_snapshot_suffix(current_test_name());
	let _guard = settings.bind_to_scope();

	let tempdir = setup_scenario_workspace("changeset-target-metadata/render-workspace");
	assert_cmd_snapshot!(
		monochange_command(None)
			.current_dir(tempdir.path())
			.arg("change")
			.arg("--help")
	);
}

#[test]
fn discover_cli_json_reports_relative_paths_and_stable_ids() {
	let mut settings = snapshot_settings();
	settings.set_snapshot_suffix(current_test_name());
	let _guard = settings.bind_to_scope();

	let tempdir = setup_scenario_workspace("cli-output/discover-mixed");
	assert_cmd_snapshot!(
		monochange_command(None)
			.current_dir(tempdir.path())
			.arg("discover")
			.arg("--format")
			.arg("json")
	);
}

#[test]
fn analyze_cli_text_defaults_to_group_release_tag_for_selected_package() {
	let mut settings = snapshot_settings();
	settings.set_snapshot_suffix(current_test_name());
	let _guard = settings.bind_to_scope();

	let tempdir = setup_analyze_cli_repo(false);
	assert_cmd_snapshot!(
		monochange_command(None)
			.current_dir(tempdir.path())
			.arg("analyze")
			.arg("--package")
			.arg("core")
	);
}

#[test]
fn analyze_cli_json_falls_back_to_main_head_for_first_release_packages() {
	let mut settings = snapshot_settings();
	settings.set_snapshot_suffix(current_test_name());
	let _guard = settings.bind_to_scope();

	let tempdir = setup_analyze_cli_repo(true);
	assert_cmd_snapshot!(
		monochange_command(None)
			.current_dir(tempdir.path())
			.arg("analyze")
			.arg("--package")
			.arg("core")
			.arg("--format")
			.arg("json")
	);
}

#[test]
fn change_cli_writes_requested_file_contents() {
	let mut settings = snapshot_settings();
	settings.set_snapshot_suffix(current_test_name());
	let _guard = settings.bind_to_scope();

	let tempdir = setup_scenario_workspace("cli-output/ungrouped-basic");
	let output_path = tempdir.path().join("feature.md");

	assert_cmd_snapshot!(
		monochange_command(None)
			.current_dir(tempdir.path())
			.arg("change")
			.arg("--package")
			.arg("core")
			.arg("--bump")
			.arg("minor")
			.arg("--reason")
			.arg("document cli snapshots")
			.arg("--output")
			.arg(&output_path)
	);

	let change_file =
		fs::read_to_string(&output_path).unwrap_or_else(|error| panic!("change file: {error}"));
	assert_snapshot!("change_file", change_file);
}

#[test]
fn change_cli_writes_explicit_versions_when_requested() {
	let mut settings = snapshot_settings();
	settings.set_snapshot_suffix(current_test_name());
	let _guard = settings.bind_to_scope();

	let tempdir = setup_scenario_workspace("cli-output/ungrouped-basic");
	let output_path = tempdir.path().join("versioned.md");

	assert_cmd_snapshot!(
		monochange_command(None)
			.current_dir(tempdir.path())
			.arg("change")
			.arg("--package")
			.arg("core")
			.arg("--bump")
			.arg("major")
			.arg("--version")
			.arg("2.0.0")
			.arg("--reason")
			.arg("promote to stable")
			.arg("--output")
			.arg(&output_path)
	);

	let change_file =
		fs::read_to_string(&output_path).unwrap_or_else(|error| panic!("change file: {error}"));
	assert_snapshot!("change_file", change_file);
}

#[test]
fn release_dry_run_cli_defaults_to_markdown_output() {
	let mut settings = snapshot_settings();
	settings.set_snapshot_suffix(current_test_name());
	let _guard = settings.bind_to_scope();

	let tempdir = setup_scenario_workspace("cli-output/ungrouped-basic");
	assert_cmd_snapshot!(
		release_cli_command()
			.current_dir(tempdir.path())
			.arg("release")
			.arg("--dry-run")
	);
}

#[test]
fn release_dry_run_cli_patches_parent_packages_when_dependencies_change() {
	let mut settings = snapshot_settings();
	settings.set_snapshot_suffix(current_test_name());
	let _guard = settings.bind_to_scope();

	let tempdir = setup_scenario_workspace("cli-output/ungrouped-basic");
	assert_cmd_snapshot!(
		release_cli_command()
			.current_dir(tempdir.path())
			.arg("release")
			.arg("--dry-run")
			.arg("--format")
			.arg("text")
	);
}

#[test]
fn release_dry_run_cli_uses_explicit_group_versions_from_member_changes() {
	let mut settings = snapshot_settings();
	settings.set_snapshot_suffix(current_test_name());
	let _guard = settings.bind_to_scope();

	let tempdir = setup_scenario_workspace("cli-output/group-explicit-version");
	assert_cmd_snapshot!(
		release_cli_command()
			.current_dir(tempdir.path())
			.arg("release")
			.arg("--dry-run")
			.arg("--format")
			.arg("text")
	);
}

#[test]
fn release_dry_run_cli_json_exposes_group_owned_release_targets() {
	let mut settings = snapshot_settings();
	settings.set_snapshot_suffix(current_test_name());
	let _guard = settings.bind_to_scope();

	let tempdir = setup_scenario_workspace("cli-output/group-basic");
	let json = command_stdout_json(
		release_cli_command()
			.current_dir(tempdir.path())
			.arg("release")
			.arg("--dry-run")
			.arg("--format")
			.arg("json"),
		true,
	);
	assert_readable_json_snapshot!(json);
}

#[test]
fn release_dry_run_cli_text_renders_diff_preview() {
	let mut settings = snapshot_settings();
	settings.set_snapshot_suffix(current_test_name());
	let _guard = settings.bind_to_scope();

	let tempdir = setup_scenario_workspace("cli-output/group-basic");
	assert_cmd_snapshot!(
		release_cli_command()
			.current_dir(tempdir.path())
			.arg("release")
			.arg("--dry-run")
			.arg("--diff")
			.arg("--format")
			.arg("text")
	);
}

#[test]
fn release_dry_run_cli_json_renders_diff_preview() {
	let mut settings = snapshot_settings();
	settings.set_snapshot_suffix(current_test_name());
	let _guard = settings.bind_to_scope();

	let tempdir = setup_scenario_workspace("cli-output/group-basic");
	let json = command_stdout_json(
		release_cli_command()
			.current_dir(tempdir.path())
			.arg("release")
			.arg("--dry-run")
			.arg("--diff")
			.arg("--format")
			.arg("json"),
		true,
	);
	assert_readable_json_snapshot!(json);
}

#[test]
fn verify_cli_json_reports_failure_comment() {
	let mut settings = snapshot_settings();
	settings.set_snapshot_suffix(current_test_name());
	let _guard = settings.bind_to_scope();

	let tempdir = setup_scenario_workspace("cli-output/changeset-policy-no-changeset");
	let json = command_stdout_json(
		monochange_command(None)
			.current_dir(tempdir.path())
			.arg("affected")
			.arg("--format")
			.arg("json")
			.arg("--changed-paths")
			.arg("crates/core/src/lib.rs"),
		true,
	);
	assert_readable_json_snapshot!(json);
}

#[test]
fn release_pr_workflow_reports_dry_run_pull_request_preview() {
	let mut settings = snapshot_settings();
	settings.set_snapshot_suffix(current_test_name());
	let _guard = settings.bind_to_scope();

	let tempdir = setup_scenario_workspace("cli-output/release-pr-workflow");
	assert_cmd_snapshot!(
		release_cli_command()
			.current_dir(tempdir.path())
			.arg("release-pr")
			.arg("--dry-run")
	);
}

#[test]
fn prepare_release_writes_manifest_json() {
	let mut settings = snapshot_settings();
	settings.set_snapshot_suffix(current_test_name());
	let _guard = settings.bind_to_scope();

	let tempdir = setup_scenario_workspace("cli-output/release-manifest-workflow");
	assert_cmd_snapshot!(
		release_cli_command()
			.current_dir(tempdir.path())
			.arg("release")
			.arg("--dry-run")
	);

	let manifest_path = tempdir
		.path()
		.join(".monochange/local/release-manifest.json");
	let manifest = fs::read_to_string(&manifest_path)
		.unwrap_or_else(|error| panic!("read manifest {}: {error}", manifest_path.display()));
	let manifest_json: Value = serde_json::from_str(&manifest)
		.unwrap_or_else(|error| panic!("parse manifest {}: {error}", manifest_path.display()));
	assert_readable_json_snapshot!("manifest", manifest_json);
}

#[test]
fn release_cli_reports_missing_changesets_cleanly() {
	let mut settings = snapshot_settings();
	settings.set_snapshot_suffix(current_test_name());
	let _guard = settings.bind_to_scope();

	let tempdir = setup_scenario_workspace("cli-output/ungrouped-no-changeset");
	assert_cmd_snapshot!(
		release_cli_command()
			.current_dir(tempdir.path())
			.arg("release")
	);
}

#[test]
fn release_cli_writes_group_changelog_and_skips_packages_without_changelogs() {
	let tempdir = setup_scenario_workspace("cli-output/group-basic");

	let output = release_cli_command()
		.current_dir(tempdir.path())
		.arg("release")
		.output()
		.unwrap_or_else(|error| panic!("release output: {error}"));
	assert!(
		output.status.success(),
		"{}",
		String::from_utf8_lossy(&output.stderr)
	);

	let root_changelog = fs::read_to_string(tempdir.path().join("changelog.md"))
		.unwrap_or_else(|error| panic!("group changelog: {error}"));
	let core_changelog = fs::read_to_string(tempdir.path().join("crates/core/CHANGELOG.md"))
		.unwrap_or_else(|error| panic!("core changelog: {error}"));
	let workspace_manifest = fs::read_to_string(tempdir.path().join("Cargo.toml"))
		.unwrap_or_else(|error| panic!("workspace manifest: {error}"));
	let group_versioned_file = fs::read_to_string(tempdir.path().join("group.toml"))
		.unwrap_or_else(|error| panic!("group versioned file: {error}"));

	assert!(root_changelog.contains("Grouped release for `sdk`."));
	assert!(!root_changelog.contains("Changed members:"));
	assert!(!root_changelog.contains("Synchronized members:"));
	assert!(core_changelog.contains("## 1.1.0"));
	assert!(core_changelog.contains("add feature"));
	assert!(!tempdir.path().join("crates/app/CHANGELOG.md").exists());
	assert!(!tempdir.path().join("crates/app/changelog.md").exists());
	assert!(workspace_manifest.contains("version = \"1.1.0\""));
	assert!(group_versioned_file.contains("version = \"1.1.0\""));
}

#[test]
fn release_quiet_suppresses_output_and_skips_workspace_mutation() {
	let tempdir = setup_scenario_workspace("cli-output/group-basic");
	let before_root_changelog = fs::read_to_string(tempdir.path().join("changelog.md"))
		.unwrap_or_else(|error| panic!("group changelog before quiet release: {error}"));
	let before_workspace_manifest = fs::read_to_string(tempdir.path().join("Cargo.toml"))
		.unwrap_or_else(|error| panic!("workspace manifest before quiet release: {error}"));

	let output = release_cli_command()
		.current_dir(tempdir.path())
		.arg("--quiet")
		.arg("release")
		.output()
		.unwrap_or_else(|error| panic!("quiet release output: {error}"));
	assert!(output.status.success(), "quiet release failed unexpectedly");
	assert!(
		output.stdout.is_empty(),
		"quiet release should suppress stdout"
	);
	assert!(
		output.stderr.is_empty(),
		"quiet release should suppress stderr"
	);

	let after_root_changelog = fs::read_to_string(tempdir.path().join("changelog.md"))
		.unwrap_or_else(|error| panic!("group changelog after quiet release: {error}"));
	let after_workspace_manifest = fs::read_to_string(tempdir.path().join("Cargo.toml"))
		.unwrap_or_else(|error| panic!("workspace manifest after quiet release: {error}"));

	assert_eq!(before_root_changelog, after_root_changelog);
	assert_eq!(before_workspace_manifest, after_workspace_manifest);
}

#[test]
fn validate_cli_rejects_packages_in_multiple_groups() {
	let mut settings = snapshot_settings();
	settings.set_snapshot_suffix(current_test_name());
	let _guard = settings.bind_to_scope();

	let tempdir = setup_scenario_workspace("cli-output/multiple-groups-validation");
	assert_cmd_snapshot!(
		monochange_command(None)
			.current_dir(tempdir.path())
			.arg("step:validate")
	);
}

#[test]
fn validate_cli_reports_frontmatter_location_and_fix_hint() {
	let tempdir = setup_scenario_workspace("changeset-target-metadata/render-workspace");
	fs::write(
		tempdir.path().join("monochange.toml"),
		"[package.\"@monochange/skill\"]\npath = \"crates/core\"\ntype = \"cargo\"\n",
	)
	.unwrap_or_else(|error| panic!("write config: {error}"));
	fs::create_dir_all(tempdir.path().join(".changeset"))
		.unwrap_or_else(|error| panic!("mkdir changeset dir: {error}"));
	fs::write(
		tempdir.path().join(".changeset/invalid.md"),
		"---\n@monochange/skill: patch\n---\n\n# broken\n",
	)
	.unwrap_or_else(|error| panic!("write changeset: {error}"));

	let output = monochange_command(None)
		.current_dir(tempdir.path())
		.arg("step:validate")
		.output()
		.unwrap_or_else(|error| panic!("validate output: {error}"));
	assert!(!output.status.success());
	let stderr = String::from_utf8_lossy(&output.stderr);
	assert!(stderr.contains("-->"), "stderr: {stderr}");
	assert!(
		stderr.contains(".changeset/invalid.md:2:1"),
		"stderr: {stderr}"
	);
	assert!(
		stderr.contains("2 | @monochange/skill: patch"),
		"stderr: {stderr}"
	);
	assert!(
		stderr.contains("wrap package or group ids that contain characters like `@`, `/`, `:`, or spaces in double quotes"),
		"stderr: {stderr}"
	);
}
