/// line-level diff: + for added, - for removed, order-preserving LCS-lite
pub fn lines(old: &str, new: &str) -> String {
	let a: Vec<&str> = old.lines().collect();
	let b: Vec<&str> = new.lines().collect();
	let old_set: std::collections::HashSet<&str> = a.iter().copied().collect();
	let new_set: std::collections::HashSet<&str> = b.iter().copied().collect();

	let mut out = String::new();
	for l in &a {
		if !new_set.contains(l) {
			out.push_str("- ");
			out.push_str(l);
			out.push('\n');
		}
	}
	for l in &b {
		if !old_set.contains(l) {
			out.push_str("+ ");
			out.push_str(l);
			out.push('\n');
		}
	}
	out
}

#[cfg(test)]
mod tests {
	use super::*;

	#[test]
	fn identical_produces_no_diff() {
		assert_eq!(lines("a\nb\nc", "a\nb\nc"), "");
	}

	#[test]
	fn added_lines_prefixed_plus() {
		assert_eq!(lines("a\nb", "a\nb\nc"), "+ c\n");
	}

	#[test]
	fn removed_lines_prefixed_minus() {
		assert_eq!(lines("a\nb\nc", "a\nc"), "- b\n");
	}

	#[test]
	fn removed_come_before_added() {
		// removals are emitted first, then additions
		assert_eq!(lines("old", "new"), "- old\n+ new\n");
	}

	#[test]
	fn empty_old_marks_everything_added() {
		assert_eq!(lines("", "x\ny"), "+ x\n+ y\n");
	}

	#[test]
	fn empty_new_marks_everything_removed() {
		assert_eq!(lines("x\ny", ""), "- x\n- y\n");
	}
}
