use std::collections::BTreeMap;
use std::fs;
use std::path::Path;

use monochange_core::Ecosystem;
use monochange_core::EcosystemAdapter;
use monochange_core::PackageRecord;
use monochange_core::PublishAttestationSettings;
use monochange_core::PublishMode;
use monochange_core::PublishState;
use monochange_core::RegistryKind;
use monochange_core::TrustedPublishingSettings;
use monochange_publish::PublishRequest;
use semver::Version;
use serde_yaml_ng::Mapping;
use serde_yaml_ng::Value;
use tempfile::tempdir;

fn yaml_bool(mapping: &Mapping, key: &str) -> Option<bool> {
	mapping
		.get(Value::String(key.to_string()))
		.and_then(Value::as_bool)
}

use crate::DartVersionedFileKind;
use crate::adapter;
use crate::default_lockfile_commands;
use crate::discover_dart_packages;
use crate::discover_lockfiles;
use crate::discover_workspace_packages;
use crate::has_workspace_section;
use crate::parse_manifest;
use crate::parse_yaml_manifest;
use crate::supported_versioned_file_kind;
use crate::update_dependency_fields;
use crate::update_manifest_text;
use crate::update_pubspec_lock;
use crate::write_dart_placeholder_manifest;
use crate::yaml_array_strings;
use crate::yaml_mapping;
use crate::yaml_string;

#[test]
fn discovers_dart_workspace_packages() {
	let fixture_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/dart/workspace");
	let discovery = discover_dart_packages(&fixture_root)
		.unwrap_or_else(|error| panic!("dart discovery: {error}"));

	assert_eq!(discovery.packages.len(), 2);
	assert!(
		discovery
			.packages
			.iter()
			.any(|package| package.name == "dart_shared")
	);
	assert!(
		discovery
			.packages
			.iter()
			.any(|package| package.name == "dart_app")
	);
}

#[test]
fn marks_flutter_packages_with_dart_ecosystem_and_metadata() {
	let fixture_root =
		Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/flutter/workspace");
	let discovery = discover_dart_packages(&fixture_root)
		.unwrap_or_else(|error| panic!("flutter discovery: {error}"));

	for package in &discovery.packages {
		assert_eq!(
			package.ecosystem.as_str(),
			"dart",
			"all Dart/Flutter packages should have ecosystem = dart"
		);
	}
}

#[test]
fn adapter_reports_dart_ecosystem() {
	assert_eq!(adapter().ecosystem(), Ecosystem::Dart);
}

fn sample_publish_request(root: &Path) -> PublishRequest {
	PublishRequest {
		package_id: "dart-package".to_string(),
		package_name: "dart_package".to_string(),
		ecosystem: Ecosystem::Dart,
		manifest_path: root.join("source-pubspec.yaml"),
		package_root: root.to_path_buf(),
		registry: RegistryKind::PubDev,
		package_manager: None,
		package_metadata: BTreeMap::new(),
		mode: PublishMode::Builtin,
		version: "0.0.0".to_string(),
		placeholder: true,
		trusted_publishing: TrustedPublishingSettings::default(),
		attestations: PublishAttestationSettings::default(),
		placeholder_readme: String::new(),
	}
}

#[test]
fn dart_placeholder_manifest_uses_source_environment() {
	let tempdir = tempdir().unwrap_or_else(|error| panic!("tempdir: {error}"));
	let request = sample_publish_request(tempdir.path());
	fs::write(
		&request.manifest_path,
		"name: dart_package\nenvironment:\n  sdk: ^3.11.0\n  flutter: '>=3.30.0'\n",
	)
	.unwrap_or_else(|error| panic!("write source pubspec: {error}"));

	write_dart_placeholder_manifest(tempdir.path(), &request, None)
		.unwrap_or_else(|error| panic!("write placeholder manifest: {error}"));

	let manifest = fs::read_to_string(tempdir.path().join("pubspec.yaml"))
		.unwrap_or_else(|error| panic!("read pubspec: {error}"));

	assert!(manifest.contains("environment:\n  sdk: ^3.11.0\n  flutter: '>=3.30.0'\n"));
}

#[test]
fn dart_placeholder_manifest_falls_back_to_default_sdk_constraint() {
	let tempdir = tempdir().unwrap_or_else(|error| panic!("tempdir: {error}"));
	let request = sample_publish_request(tempdir.path());

	write_dart_placeholder_manifest(tempdir.path(), &request, None)
		.unwrap_or_else(|error| panic!("write placeholder manifest: {error}"));

	let manifest = fs::read_to_string(tempdir.path().join("pubspec.yaml"))
		.unwrap_or_else(|error| panic!("read pubspec: {error}"));

	assert!(manifest.contains("environment:\n  sdk: '>=3.0.0 <4.0.0'\n"));
}

#[test]
fn flutter_placeholder_manifest_falls_back_to_default_flutter_constraint() {
	let tempdir = tempdir().unwrap_or_else(|error| panic!("tempdir: {error}"));
	let mut request = sample_publish_request(tempdir.path());
	request
		.package_metadata
		.insert("is_flutter".to_string(), "true".to_string());

	write_dart_placeholder_manifest(tempdir.path(), &request, None)
		.unwrap_or_else(|error| panic!("write placeholder manifest: {error}"));

	let manifest = fs::read_to_string(tempdir.path().join("pubspec.yaml"))
		.unwrap_or_else(|error| panic!("read pubspec: {error}"));

	assert!(manifest.contains("environment:\n  sdk: '>=3.0.0 <4.0.0'\n  flutter: '>=3.0.0'\n"));
}

#[test]
fn supported_versioned_file_kind_recognizes_pubspec_files() {
	assert_eq!(
		supported_versioned_file_kind(Path::new("pubspec.yaml")),
		Some(DartVersionedFileKind::Manifest)
	);
	assert_eq!(
		supported_versioned_file_kind(Path::new("pubspec.yml")),
		Some(DartVersionedFileKind::Manifest)
	);
	assert_eq!(
		supported_versioned_file_kind(Path::new("pubspec.lock")),
		Some(DartVersionedFileKind::Lock)
	);
	assert_eq!(supported_versioned_file_kind(Path::new("README.md")), None);
}

#[test]
fn discover_lockfiles_prefers_workspace_root_then_manifest_directory() {
	let fixture_root =
		Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/tests/dart/lockfile-workspace");
	let package = PackageRecord::new(
		Ecosystem::Dart,
		"dart_app",
		fixture_root.join("packages/app/pubspec.yaml"),
		fixture_root.clone(),
		Some(Version::new(1, 0, 0)),
		PublishState::Public,
	);
	let lockfiles = discover_lockfiles(&package);
	assert_eq!(lockfiles.len(), 1);
	assert_eq!(
		lockfiles.first(),
		Some(&monochange_core::normalize_path(
			&fixture_root.join("pubspec.lock")
		))
	);
}

#[test]
fn discover_lockfiles_falls_back_to_manifest_directory() {
	let fixture_root = Path::new(env!("CARGO_MANIFEST_DIR"))
		.join("../../fixtures/tests/dart/manifest-lockfile-workspace");
	let package = PackageRecord::new(
		Ecosystem::Dart,
		"nested_dart_app",
		fixture_root.join("packages/app/pubspec.yaml"),
		fixture_root.clone(),
		Some(Version::new(1, 0, 0)),
		PublishState::Public,
	);
	let lockfiles = discover_lockfiles(&package);
	assert_eq!(lockfiles.len(), 1);
	assert_eq!(
		lockfiles.first(),
		Some(&monochange_core::normalize_path(
			&fixture_root.join("packages/app/pubspec.lock")
		))
	);
}

#[test]
fn default_lockfile_commands_choose_dart_or_flutter_pub_get() {
	let fixture_root = Path::new(env!("CARGO_MANIFEST_DIR"))
		.join("../../fixtures/tests/dart/manifest-lockfile-workspace");
	let dart_package = PackageRecord::new(
		Ecosystem::Dart,
		"nested_dart_app",
		fixture_root.join("packages/app/pubspec.yaml"),
		fixture_root.clone(),
		Some(Version::new(1, 0, 0)),
		PublishState::Public,
	);
	assert_eq!(
		default_lockfile_commands(&dart_package),
		vec![monochange_core::LockfileCommandExecution {
			command: "dart pub get".to_string(),
			cwd: monochange_core::normalize_path(&fixture_root.join("packages/app")),
			shell: monochange_core::ShellConfig::None,
		}]
	);

	let mut flutter_package = PackageRecord::new(
		Ecosystem::Dart,
		"nested_flutter_app",
		fixture_root.join("packages/app/pubspec.yaml"),
		fixture_root.clone(),
		Some(Version::new(1, 0, 0)),
		PublishState::Public,
	);
	flutter_package
		.metadata
		.insert("is_flutter".to_string(), "true".to_string());
	assert_eq!(
		default_lockfile_commands(&flutter_package),
		vec![monochange_core::LockfileCommandExecution {
			command: "flutter pub get".to_string(),
			cwd: monochange_core::normalize_path(&fixture_root.join("packages/app")),
			shell: monochange_core::ShellConfig::None,
		}]
	);

	let cargo_package = PackageRecord::new(
		Ecosystem::Cargo,
		"not-dart",
		fixture_root.join("packages/app/pubspec.yaml"),
		fixture_root,
		Some(Version::new(1, 0, 0)),
		PublishState::Public,
	);
	assert!(default_lockfile_commands(&cargo_package).is_empty());
}

#[test]
fn update_dependency_fields_only_changes_declared_dependencies() {
	let mut manifest: Mapping = serde_yaml_ng::from_str(
		r"
dependencies:
  core: ^1.0.0
dev_dependencies:
  test: ^1.0.0
",
	)
	.unwrap_or_else(|error| panic!("pubspec yaml: {error}"));
	let versions = BTreeMap::from([("core".to_string(), "2.0.0".to_string())]);

	update_dependency_fields(
		&mut manifest,
		&["dependencies", "dev_dependencies"],
		&versions,
	);

	let rendered = serde_yaml_ng::to_string(&Value::Mapping(manifest))
		.unwrap_or_else(|error| panic!("render manifest: {error}"));
	assert!(rendered.contains("core: 2.0.0"));
	assert!(rendered.contains("test: ^1.0.0"));
}

#[test]
fn update_manifest_text_preserves_pubspec_formatting() {
	let manifest = r"name: sample_app
version: '1.0.0' # keep quote

dependencies:
  shared:
    path: ../shared
    version: ^1.0.0
  http: ^1.0.0

dev_dependencies:
  test: ^1.0.0
";
	let updated = update_manifest_text(
		manifest,
		Some("2.0.0"),
		&["dependencies", "dev_dependencies"],
		&BTreeMap::from([
			("shared".to_string(), "^2.0.0".to_string()),
			("test".to_string(), "^2.0.0".to_string()),
		]),
	)
	.unwrap_or_else(|error| panic!("update pubspec text: {error}"));
	assert!(updated.contains("version: '2.0.0' # keep quote"));
	assert!(updated.contains("path: ../shared"));
	assert!(updated.contains("version: ^2.0.0"));
	assert!(updated.contains("test: ^2.0.0"));
	assert!(updated.contains("http: ^1.0.0"));
}

#[test]
fn yaml_helper_functions_cover_missing_and_inline_paths() {
	let contents = "version: # comment only\n\n  nested: value\nshared:\n  path: ../shared\n";
	let ranges = crate::yaml_line_ranges(contents);
	assert_eq!(ranges.len(), 6);
	assert!(crate::parse_yaml_line(contents, *ranges.get(1).expect("blank line range")).is_none());
	assert!(crate::parse_yaml_line(": nope", (0, 6)).is_none());
	assert!(crate::yaml_value_span("version: # comment", 0, 8).is_none());
	assert_eq!(crate::find_yaml_quote_end("\"1.0.0\"", '"'), Some(6));
	assert_eq!(crate::find_yaml_quote_end("\"1.0.0", '"'), None);
	assert_eq!(crate::render_yaml_scalar("\"1.0.0\"", "2.0.0"), "\"2.0.0\"");
	assert_eq!(crate::render_yaml_scalar("'1.0.0'", "2.0.0"), "'2.0.0'");
	assert_eq!(crate::render_yaml_scalar("1.0.0", "2.0.0"), "2.0.0");

	let nested = r"dependencies:
  shared:
    path: ../shared

    # keep spacing
  other: ^1.0.0
";
	let nested_ranges = crate::yaml_line_ranges(nested);
	let section_index = crate::find_yaml_key_line(nested, &nested_ranges, 0, "dependencies")
		.unwrap_or_else(|| panic!("expected dependencies section"));
	assert_eq!(
		crate::find_yaml_dependency_scalar(nested, &nested_ranges, section_index, "shared"),
		None
	);
}

#[test]
fn update_pubspec_lock_rewrites_known_package_versions() {
	let mut lock: Mapping = serde_yaml_ng::from_str(
		r"
packages:
  core:
    version: 1.0.0
  app:
    version: 1.0.0
",
	)
	.unwrap_or_else(|error| panic!("pubspec lock yaml: {error}"));
	let versions = BTreeMap::from([
		("core".to_string(), "2.0.0".to_string()),
		("app".to_string(), "1.1.0".to_string()),
	]);

	update_pubspec_lock(&mut lock, &versions);

	let rendered = serde_yaml_ng::to_string(&Value::Mapping(lock))
		.unwrap_or_else(|error| panic!("render pubspec lock: {error}"));
	assert!(rendered.contains("core:\n    version: 2.0.0"));
	assert!(rendered.contains("app:\n    version: 1.1.0"));
}

#[test]
fn update_pubspec_lock_ignores_missing_package_section() {
	let mut lock: Mapping = serde_yaml_ng::from_str("root: true\n")
		.unwrap_or_else(|error| panic!("pubspec lock yaml: {error}"));
	update_pubspec_lock(
		&mut lock,
		&BTreeMap::from([("core".to_string(), "2.0.0".to_string())]),
	);
	let rendered = serde_yaml_ng::to_string(&Value::Mapping(lock))
		.unwrap_or_else(|error| panic!("render pubspec lock: {error}"));
	assert_eq!(rendered, "root: true\n");
}

#[test]
fn workspace_and_manifest_helpers_cover_yaml_and_error_paths() {
	let fixture_root = Path::new(env!("CARGO_MANIFEST_DIR"))
		.join("../../fixtures/tests/dart/workspace-pattern-warnings");
	let workspace_manifest = fixture_root.join("pubspec.yaml");
	assert!(has_workspace_section(&workspace_manifest).unwrap());
	let parsed = parse_yaml_manifest(&workspace_manifest)
		.unwrap_or_else(|error| panic!("workspace yaml: {error}"));
	assert_eq!(
		yaml_array_strings(&parsed, "workspace"),
		vec!["packages/*".to_string(), "missing/*".to_string()]
	);
	assert_eq!(yaml_string(&parsed, "name"), None);
	assert_eq!(yaml_bool(&parsed, "publish_to"), None);
	assert_eq!(yaml_mapping(&parsed, "dependencies"), None);

	let app_manifest = fixture_root.join("packages/app/pubspec.yaml");
	let app = parse_manifest(&app_manifest, &fixture_root)
		.unwrap_or_else(|error| panic!("parse app manifest: {error}"))
		.unwrap_or_else(|| panic!("expected app package"));
	assert_eq!(app.ecosystem, Ecosystem::Dart);
	assert_eq!(app.publish_state, PublishState::Public);
	assert_eq!(
		app.current_version
			.as_ref()
			.map(ToString::to_string)
			.as_deref(),
		Some("1.2.3")
	);
	assert!(app.declared_dependencies.iter().any(|dependency| {
		dependency.name == "shared"
			&& dependency.version_constraint.as_deref() == Some("^1.0.0")
			&& dependency.source_field.as_deref() == Some("dependencies")
	}));

	let private_manifest = fixture_root.join("packages/private/pubspec.yaml");
	let private = parse_manifest(&private_manifest, &fixture_root)
		.unwrap_or_else(|error| panic!("parse private manifest: {error}"))
		.unwrap_or_else(|| panic!("expected private package"));
	assert_eq!(private.ecosystem, Ecosystem::Dart);
	assert_eq!(private.publish_state, PublishState::Private);
	assert_eq!(private.current_version, None);

	let workspace_root_manifest =
		Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/dart/monorepo/pubspec.yaml");
	let workspace_root = parse_manifest(
		&workspace_root_manifest,
		workspace_root_manifest
			.parent()
			.expect("workspace root parent"),
	)
	.unwrap_or_else(|error| panic!("parse workspace root manifest: {error}"))
	.unwrap_or_else(|| panic!("expected workspace root package"));
	assert_eq!(workspace_root.publish_state, PublishState::Private);

	let discovery = discover_workspace_packages(&workspace_manifest)
		.unwrap_or_else(|error| panic!("workspace discovery: {error}"));
	assert_eq!(discovery.0.len(), 2);
	assert!(discovery.1.iter().any(|warning| {
		warning.contains("missing/*") && warning.contains("matched no packages")
	}));

	let nameless_manifest: Mapping = serde_yaml_ng::from_str(
		r"
dependencies:
  core: ^1.0.0
",
	)
	.unwrap_or_else(|error| panic!("yaml: {error}"));
	assert_eq!(yaml_string(&nameless_manifest, "name"), None);
	assert!(yaml_mapping(&nameless_manifest, "dependencies").is_some());
	assert_eq!(yaml_bool(&nameless_manifest, "publish_to"), None);

	let invalid_workspace = Path::new(env!("CARGO_MANIFEST_DIR"))
		.join("../../fixtures/tests/dart/invalid-workspace/invalid-workspace.yaml");
	let invalid_workspace_error = has_workspace_section(&invalid_workspace)
		.err()
		.unwrap_or_else(|| panic!("expected invalid workspace error"));
	assert!(
		invalid_workspace_error
			.to_string()
			.contains("failed to parse")
	);

	let invalid_package = Path::new(env!("CARGO_MANIFEST_DIR"))
		.join("../../fixtures/tests/dart/invalid-package/invalid-package.yaml");
	let invalid_package_error = parse_manifest(&invalid_package, Path::new("."))
		.err()
		.unwrap_or_else(|| panic!("expected invalid package error"));
	assert!(
		invalid_package_error
			.to_string()
			.contains("failed to parse")
	);
}

#[test]
fn default_dependency_version_prefix_is_correct() {
	assert_eq!(super::default_dependency_version_prefix(), "^");
}

#[test]
fn default_dependency_fields_are_non_empty() {
	assert!(!super::default_dependency_fields().is_empty());
}

#[test]
fn validate_versioned_file_accepts_valid_pubspec_yaml() {
	let tempdir = tempdir().unwrap_or_else(|error| panic!("tempdir: {error}"));
	let path = tempdir.path().join("pubspec.yaml");
	fs::write(&path, "version: 1.0.0\n").unwrap_or_else(|error| panic!("write: {error}"));
	assert!(super::validate_versioned_file(&path, "pubspec.yaml", None).is_ok());
}

#[test]
fn validate_versioned_file_rejects_invalid_yaml() {
	let tempdir = tempdir().unwrap_or_else(|error| panic!("tempdir: {error}"));
	let path = tempdir.path().join("pubspec.yaml");
	fs::write(&path, "not valid yaml: [").unwrap_or_else(|error| panic!("write: {error}"));
	let result = super::validate_versioned_file(&path, "pubspec.yaml", None);
	assert!(result.is_err());
	assert!(result.unwrap_err().to_string().contains("not valid YAML"));
}

#[test]
fn validate_versioned_file_rejects_missing_version() {
	let tempdir = tempdir().unwrap_or_else(|error| panic!("tempdir: {error}"));
	let path = tempdir.path().join("pubspec.yaml");
	fs::write(&path, "name: test\n").unwrap_or_else(|error| panic!("write: {error}"));
	let result = super::validate_versioned_file(&path, "pubspec.yaml", None);
	assert!(result.is_err());
	assert!(
		result
			.unwrap_err()
			.to_string()
			.contains("does not contain a `version` string field")
	);
}

#[test]
fn validate_versioned_file_rejects_missing_file() {
	let tempdir = tempdir().unwrap_or_else(|error| panic!("tempdir: {error}"));
	let path = tempdir.path().join("missing.yaml");
	let result = super::validate_versioned_file(&path, "missing.yaml", None);
	assert!(result.is_err());
	assert!(result.unwrap_err().to_string().contains("not readable"));
}
