use proptest::prelude::*;
use proptest::prop_compose;
use proptest::proptest;
use semver::Version;

use crate::BumpSeverity;

fn arbitrary_version() -> impl Strategy<Value = Version> {
	(0..=99u64, 0..=99u64, 0..=99u64)
		.prop_map(|(major, minor, patch)| Version::new(major, minor, patch))
}

prop_compose! {
	fn arbitrary_bump_severity()(n in 0..4u8) -> BumpSeverity {
		match n {
			0 => BumpSeverity::None,
			1 => BumpSeverity::Patch,
			2 => BumpSeverity::Minor,
			3 => BumpSeverity::Major,
			_ => unreachable!(),
		}
	}
}

proptest! {
	#[test]
	fn apply_to_version_is_strictly_increasing_for_release_severity(
		version in arbitrary_version()
	) {
		for severity in [BumpSeverity::Patch, BumpSeverity::Minor, BumpSeverity::Major] {
			let next = severity.apply_to_version(&version);
			let version_s = version.to_string();
			let next_s = next.to_string();
			prop_assert!(
				next > version,
				"apply_to_version({:?}, {}) = {} should be strictly greater",
				severity, version_s, next_s
			);
		}
	}

	#[test]
	fn apply_to_version_preserves_version_for_none_severity(
		version in arbitrary_version()
	) {
		let next = BumpSeverity::None.apply_to_version(&version);
		prop_assert_eq!(next, version);
	}

	#[test]
	fn apply_to_version_resets_pre_and_build_metadata(
		mut version in arbitrary_version(),
		pre in "[a-z]*",
		build in "[a-z]*"
	) {
		if !pre.is_empty() {
			version.pre = semver::Prerelease::new(&pre).unwrap_or_default();
		}
		if !build.is_empty() {
			version.build = semver::BuildMetadata::new(&build).unwrap_or_default();
		}
		for severity in [BumpSeverity::Patch, BumpSeverity::Minor, BumpSeverity::Major] {
			let next = severity.apply_to_version(&version);
			let next_s = next.to_string();
			let version_s = version.to_string();
			prop_assert!(
				next.pre.is_empty(),
				"pre-release metadata should be cleared: {version_s} -> {next_s}"
			);
			prop_assert!(
				next.build.is_empty(),
				"build metadata should be cleared: {version_s} -> {next_s}"
			);
		}
	}

	#[test]
	fn apply_to_version_is_idempotent_for_none_severity(
		version in arbitrary_version()
	) {
		let once = BumpSeverity::None.apply_to_version(&version);
		let twice = BumpSeverity::None.apply_to_version(&once);
		prop_assert_eq!(
			once, twice,
			"None.apply_to_version should be idempotent"
		);
	}

	#[test]
	fn pre_stable_shifting_preserves_release_order(
		version in arbitrary_version()
	) {
		let is_pre = BumpSeverity::is_pre_stable(&version);
		let patch_next = BumpSeverity::Patch.apply_to_version(&version);
		let minor_next = BumpSeverity::Minor.apply_to_version(&version);
		let major_next = BumpSeverity::Major.apply_to_version(&version);

		let (patch_s, minor_s, major_s, version_s) = (
			patch_next.to_string(),
			minor_next.to_string(),
			major_next.to_string(),
			version.to_string(),
		);
		prop_assert!(
			patch_next >= version,
			"Patch should not decrease version: {version_s} -> {patch_s}"
		);
		prop_assert!(
			minor_next >= patch_next,
			"Minor should be >= Patch: {version_s} Patch={patch_s} Minor={minor_s}"
		);
		prop_assert!(
			major_next >= minor_next,
			"Major should be >= Minor: {version_s} Minor={minor_s} Major={major_s}"
		);

		if is_pre {
			prop_assert!(
				minor_next == patch_next,
				"Pre-stable: Minor ({}) should equal Patch ({}) for {}",
				minor_s, patch_s, version_s
			);
		}
	}
}
