use std::path::Path;

use monochange_config::load_workspace_configuration;
use monochange_core::lint::LintSuite;
use monochange_core::lint::LintTarget;
use monochange_test_helpers::fixture_path;
use serde_json::json;
use serde_yaml_ng::Mapping;

use super::*;

fn metadata_publishability_root() -> std::path::PathBuf {
	fixture_path!("dart-lints/metadata-publishability/workspace")
}

fn metadata_publishability_targets() -> Vec<LintTarget> {
	let root = metadata_publishability_root();
	let configuration = load_workspace_configuration(&root)
		.unwrap_or_else(|error| panic!("load dart lint fixture config: {error}"));
	lint_suite()
		.collect_targets(&root, &configuration)
		.unwrap_or_else(|error| panic!("collect dart lint fixture targets: {error}"))
}

fn sdk_dependency_hygiene_root() -> std::path::PathBuf {
	fixture_path!("dart-lints/sdk-dependency-hygiene/workspace")
}

fn sdk_dependency_hygiene_targets() -> Vec<LintTarget> {
	let root = sdk_dependency_hygiene_root();
	let configuration = load_workspace_configuration(&root)
		.unwrap_or_else(|error| panic!("load dart sdk fixture config: {error}"));
	lint_suite()
		.collect_targets(&root, &configuration)
		.unwrap_or_else(|error| panic!("collect dart sdk fixture targets: {error}"))
}

fn advanced_workspace_flutter_root() -> std::path::PathBuf {
	fixture_path!("dart-lints/advanced-workspace-flutter/workspace")
}

fn advanced_workspace_flutter_targets() -> Vec<LintTarget> {
	let root = advanced_workspace_flutter_root();
	let configuration = load_workspace_configuration(&root)
		.unwrap_or_else(|error| panic!("load dart advanced fixture config: {error}"));
	lint_suite()
		.collect_targets(&root, &configuration)
		.unwrap_or_else(|error| panic!("collect dart advanced fixture targets: {error}"))
}

fn find_target<'a>(targets: &'a [LintTarget], package_name: &str) -> &'a LintTarget {
	targets
		.iter()
		.find(|target| target.metadata.package_name.as_deref() == Some(package_name))
		.unwrap_or_else(|| panic!("missing target for {package_name}"))
}

fn ctx(target: &LintTarget) -> LintContext<'_> {
	LintContext {
		workspace_root: &target.workspace_root,
		manifest_path: &target.manifest_path,
		contents: &target.contents,
		metadata: &target.metadata,
		parsed: target.parsed.as_ref(),
	}
}

fn config() -> LintRuleConfig {
	LintRuleConfig::Detailed {
		level: LintSeverity::Error,
		options: BTreeMap::from([("fix".to_string(), json!(true))]),
	}
}

#[test]
fn presets_are_exposed() {
	let presets = DartLintSuite.presets();
	assert_eq!(presets.len(), 2);

	let recommended = presets.first().expect("expected recommended preset");
	assert_eq!(recommended.id, "dart/recommended");
	assert_eq!(
		recommended.rules.get("dart/dependency-sorted"),
		Some(&LintRuleConfig::Severity(LintSeverity::Warning))
	);
	assert_eq!(
		recommended.rules.get("dart/sdk-constraint-present"),
		Some(&LintRuleConfig::Severity(LintSeverity::Error))
	);
	assert!(
		!recommended
			.rules
			.contains_key("dart/internal-path-dependency-policy")
	);

	let strict = presets.get(1).expect("expected strict preset");
	assert_eq!(strict.id, "dart/strict");
	assert!(strict.rules.contains_key("dart/assets-sorted"));
	assert!(
		strict
			.rules
			.contains_key("dart/flutter-package-metadata-consistent")
	);
	assert!(
		strict
			.rules
			.contains_key("dart/workspace-internal-version-consistency")
	);
}

#[test]
fn required_package_fields_rule_reports_missing_metadata_for_managed_publishable_packages() {
	let targets = metadata_publishability_targets();
	let failing = find_target(&targets, "published_missing");
	let passing = find_target(&targets, "published_ok");

	let failing_results = RequiredPackageFieldsRule::new().run(&ctx(failing), &config());
	assert_eq!(failing_results.len(), 2);
	assert!(
		failing_results
			.iter()
			.any(|result| result.message.contains("description"))
	);
	assert!(
		failing_results
			.iter()
			.any(|result| result.message.contains("repository"))
	);

	let passing_results = RequiredPackageFieldsRule::new().run(&ctx(passing), &config());
	assert!(passing_results.is_empty());
}

#[test]
fn required_package_fields_rule_supports_custom_fields() {
	let targets = metadata_publishability_targets();
	let target = find_target(&targets, "published_missing");
	let config = LintRuleConfig::Detailed {
		level: LintSeverity::Error,
		options: BTreeMap::from([("fields".to_string(), json!(["description"]))]),
	};
	let results = RequiredPackageFieldsRule::new().run(&ctx(target), &config);
	assert_eq!(results.len(), 1);
	assert!(
		results
			.first()
			.expect("expected lint result")
			.message
			.contains("description")
	);
}

#[test]
fn sdk_constraint_present_rule_reports_missing_constraints() {
	let targets = sdk_dependency_hygiene_targets();
	let missing = find_target(&targets, "missing_sdk");
	let modern = find_target(&targets, "modern_ok");

	let results = SdkConstraintPresentRule::new().run(&ctx(missing), &config());
	assert_eq!(results.len(), 1);
	assert!(
		results
			.first()
			.expect("expected lint result")
			.message
			.contains("environment.sdk")
	);
	assert!(
		SdkConstraintPresentRule::new()
			.run(&ctx(modern), &config())
			.is_empty()
	);
}

#[test]
fn sdk_constraint_modern_rule_reports_legacy_and_overly_broad_constraints() {
	let targets = sdk_dependency_hygiene_targets();
	let legacy = find_target(&targets, "legacy_sdk");
	let wide = find_target(&targets, "wide_sdk");

	let legacy_results = SdkConstraintModernRule::new().run(&ctx(legacy), &config());
	assert_eq!(legacy_results.len(), 1);
	assert!(
		legacy_results
			.first()
			.expect("expected lint result")
			.message
			.contains("3.0.0")
	);

	let wide_results = SdkConstraintModernRule::new().run(&ctx(wide), &config());
	assert_eq!(wide_results.len(), 1);
	assert!(
		wide_results
			.first()
			.expect("expected lint result")
			.message
			.contains("upper bound")
	);

	let tuned_config = LintRuleConfig::Detailed {
		level: LintSeverity::Error,
		options: BTreeMap::from([
			("minimum".to_string(), json!("2.19.0")),
			("require_upper_bound".to_string(), json!(false)),
		]),
	};
	assert!(
		SdkConstraintModernRule::new()
			.run(&ctx(legacy), &tuned_config)
			.is_empty()
	);
	assert!(
		SdkConstraintModernRule::new()
			.run(&ctx(wide), &tuned_config)
			.is_empty()
	);
}

#[test]
fn dependency_sorted_rule_reports_unsorted_sections_and_emits_parseable_fix() {
	let targets = sdk_dependency_hygiene_targets();
	let target = find_target(&targets, "unsorted_deps");
	let results = DependencySortedRule::new().run(&ctx(target), &config());
	assert_eq!(results.len(), 3);

	let replacement = results
		.first()
		.and_then(|result| result.fix.as_ref())
		.and_then(|fix| fix.edits.first())
		.map(|edit| edit.replacement.clone())
		.expect("expected fix replacement");
	assert!(serde_yaml_ng::from_str::<Mapping>(&replacement).is_ok());
	assert_eq!(
		source_key_order(&replacement, "dependencies"),
		Some(vec!["alpha".to_string(), "zebra".to_string()])
	);
	assert_eq!(
		source_key_order(&replacement, "dev_dependencies"),
		Some(vec!["beta".to_string(), "zeta".to_string()])
	);
	assert_eq!(
		source_key_order(&replacement, "dependency_overrides"),
		Some(vec!["analyzer".to_string(), "yaml".to_string()])
	);
}

#[test]
fn assets_sorted_rule_reports_unsorted_flutter_assets_and_families() {
	let targets = advanced_workspace_flutter_targets();
	let target = find_target(&targets, "flutter_assets_unsorted");
	let results = AssetsSortedRule::new().run(&ctx(target), &config());
	assert_eq!(results.len(), 4);

	let replacement = results
		.first()
		.and_then(|result| result.fix.as_ref())
		.and_then(|fix| fix.edits.first())
		.map(|edit| edit.replacement.clone())
		.expect("expected fix replacement");
	let rewritten = serde_yaml_ng::from_str::<Mapping>(&replacement)
		.unwrap_or_else(|error| panic!("expected yaml replacement: {error}"));
	let flutter = flutter_section(&rewritten).expect("expected flutter section");
	assert_eq!(
		sequence_order(yaml_sequence(flutter, "assets").expect("expected assets")),
		vec![
			"assets/icons/alpha.png".to_string(),
			"assets/icons/zebra.png".to_string(),
		]
	);
}

#[test]
fn flutter_package_metadata_consistent_rule_is_flutter_only() {
	let targets = advanced_workspace_flutter_targets();
	let failing = find_target(&targets, "flutter_missing_sdk");
	let passing = find_target(&targets, "path_ok");

	let results = FlutterPackageMetadataConsistentRule::new().run(&ctx(failing), &config());
	assert_eq!(results.len(), 1);
	assert!(
		results
			.first()
			.expect("expected lint result")
			.message
			.contains("dependencies.flutter")
	);
	assert!(
		FlutterPackageMetadataConsistentRule::new()
			.run(&ctx(passing), &config())
			.is_empty()
	);
}

#[test]
fn internal_path_dependency_policy_rule_supports_path_and_hosted_modes() {
	let targets = advanced_workspace_flutter_targets();
	let path_ok = find_target(&targets, "path_ok");
	let path_fail = find_target(&targets, "path_fail");
	let workspace_resolution = find_target(&targets, "workspace_resolution");
	let workspace_path_fail = find_target(&targets, "workspace_path_fail");

	// Without resolution: workspace, path mode requires path: references.
	assert!(
		InternalPathDependencyPolicyRule::new()
			.run(&ctx(path_ok), &config())
			.is_empty()
	);

	// With resolution: workspace, versioned internal deps should pass.
	assert!(
		InternalPathDependencyPolicyRule::new()
			.run(&ctx(workspace_resolution), &config())
			.is_empty()
	);

	// Without resolution: workspace, versioned internal deps should fail.
	let failing = InternalPathDependencyPolicyRule::new().run(&ctx(path_fail), &config());
	assert_eq!(failing.len(), 1);
	assert!(
		failing
			.first()
			.expect("expected lint result")
			.message
			.contains("use `path:` references")
	);

	// With resolution: workspace, path: references should fail.
	let ws_failing =
		InternalPathDependencyPolicyRule::new().run(&ctx(workspace_path_fail), &config());
	assert_eq!(ws_failing.len(), 1);
	assert!(
		ws_failing
			.first()
			.expect("expected lint result")
			.message
			.contains("use version constraints")
	);

	let hosted_config = LintRuleConfig::Detailed {
		level: LintSeverity::Error,
		options: BTreeMap::from([("mode".to_string(), json!("hosted"))]),
	};
	assert!(
		InternalPathDependencyPolicyRule::new()
			.run(&ctx(path_fail), &hosted_config)
			.is_empty()
	);
}

#[test]
fn workspace_internal_version_consistency_rule_reports_workspace_drift() {
	let targets = advanced_workspace_flutter_targets();
	let mismatch = find_target(&targets, "version_mismatch");
	let ok = find_target(&targets, "path_fail");

	let results = WorkspaceInternalVersionConsistencyRule::new().run(&ctx(mismatch), &config());
	assert_eq!(results.len(), 1);
	assert!(
		results
			.first()
			.expect("expected lint result")
			.message
			.contains("workspace version is `1.2.3`")
	);
	assert!(
		WorkspaceInternalVersionConsistencyRule::new()
			.run(&ctx(ok), &config())
			.is_empty()
	);
}

#[test]
fn no_git_dependencies_rule_reports_published_git_dependencies_and_supports_allow_list() {
	let targets = metadata_publishability_targets();
	let failing = find_target(&targets, "git_dep_fail");
	let private = find_target(&targets, "private_git_dep");

	let results = NoGitDependenciesInPublishedPackagesRule::new().run(&ctx(failing), &config());
	assert_eq!(results.len(), 1);
	assert!(
		results
			.first()
			.expect("expected lint result")
			.message
			.contains("git_dep")
	);

	let allow_config = LintRuleConfig::Detailed {
		level: LintSeverity::Error,
		options: BTreeMap::from([("allow".to_string(), json!(["git_dep"]))]),
	};
	assert!(
		NoGitDependenciesInPublishedPackagesRule::new()
			.run(&ctx(failing), &allow_config)
			.is_empty()
	);
	assert!(
		NoGitDependenciesInPublishedPackagesRule::new()
			.run(&ctx(private), &config())
			.is_empty()
	);
}

#[test]
fn no_unexpected_dependency_overrides_rule_supports_allow_list_and_private_packages() {
	let targets = sdk_dependency_hygiene_targets();
	let failing = find_target(&targets, "override_fail");
	let private = find_target(&targets, "override_private");
	let allowed = find_target(&targets, "override_allowed");

	let results = NoUnexpectedDependencyOverridesRule::new().run(&ctx(failing), &config());
	assert_eq!(results.len(), 1);
	assert!(
		results
			.first()
			.expect("expected lint result")
			.message
			.contains("override_fail")
	);
	assert!(
		NoUnexpectedDependencyOverridesRule::new()
			.run(&ctx(private), &config())
			.is_empty()
	);

	let allow_config = LintRuleConfig::Detailed {
		level: LintSeverity::Error,
		options: BTreeMap::from([("allow_packages".to_string(), json!(["override_allowed"]))]),
	};
	assert!(
		NoUnexpectedDependencyOverridesRule::new()
			.run(&ctx(allowed), &allow_config)
			.is_empty()
	);
}

#[test]
fn unlisted_package_private_rule_reports_for_public_unmanaged_packages() {
	let targets = metadata_publishability_targets();
	let failing = find_target(&targets, "unmanaged_public");
	let passing = find_target(&targets, "unmanaged_private");

	let results = UnlistedPackagePrivateRule::new().run(&ctx(failing), &config());
	assert_eq!(results.len(), 1);
	assert!(
		results
			.first()
			.and_then(|result| result.fix.as_ref())
			.is_some()
	);
	assert!(
		UnlistedPackagePrivateRule::new()
			.run(&ctx(passing), &config())
			.is_empty()
	);
}

#[test]
fn collect_targets_loads_managed_workspace_dart_packages() {
	let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/dart/workspace");
	let configuration = load_workspace_configuration(&root)
		.unwrap_or_else(|error| panic!("load dart workspace config: {error}"));
	let targets = lint_suite()
		.collect_targets(&root, &configuration)
		.unwrap_or_else(|error| panic!("collect dart lint targets: {error}"));

	assert_eq!(targets.len(), 2);
	assert!(
		targets
			.iter()
			.all(|target| target.metadata.ecosystem == "dart")
	);
	assert!(targets.iter().all(|target| target.metadata.managed));
	assert!(
		targets
			.iter()
			.all(|target| target.parsed.downcast_ref::<DartLintFile>().is_some())
	);
	assert!(
		targets
			.iter()
			.any(|target| target.metadata.package_name.as_deref() == Some("dart_app"))
	);
	assert!(
		targets
			.iter()
			.any(|target| target.metadata.package_name.as_deref() == Some("dart_shared"))
	);
}

#[test]
fn collect_targets_mark_workspace_versions_for_internal_rules() {
	let targets = advanced_workspace_flutter_targets();
	let target = find_target(&targets, "path_fail");
	let file = target
		.parsed
		.downcast_ref::<DartLintFile>()
		.expect("expected dart lint file");
	assert_eq!(
		file.workspace_package_versions
			.get("core")
			.map(ToString::to_string)
			.as_deref(),
		Some("1.2.3")
	);
}

#[test]
fn collect_targets_marks_private_packages_from_publish_to_none() {
	let targets = metadata_publishability_targets();
	let private = find_target(&targets, "private_git_dep");
	assert_eq!(private.metadata.private, Some(true));
	assert_eq!(private.metadata.publishable, Some(false));
}

#[test]
fn collect_targets_ignores_fixture_manifests_outside_workspace_packages() {
	let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
	let configuration = load_workspace_configuration(&root)
		.unwrap_or_else(|error| panic!("load workspace config: {error}"));
	let targets = lint_suite()
		.collect_targets(&root, &configuration)
		.unwrap_or_else(|error| panic!("collect repo dart lint targets: {error}"));

	assert!(
		targets
			.iter()
			.all(|target| !target.manifest_path.starts_with(root.join("fixtures")))
	);
}
