//! Assert-or-regen over one document: the testing layer.
//!
//! `Checker` wraps a [`Document`] and a [`PatchAccumulator`] (both pub fields;
//! this layer adds behavior, it doesn't hide anything).
//! In normal mode, [`Checker::check`] compares an actual value against a hunk's body.
//! In regen mode -- captured at construction, from the `TESTMARK_REGEN` env var --
//! a difference is recorded as a pending patch instead of failing.
//! Writing the patched text back out is the caller's job: see [`write_back`].
//!
//! # Use with test harnesses
//!
//! One `Checker` per document per process; the crate does no file locking,
//! so two *processes* regenerating the same file concurrently will lose writes.
//! (For `cargo test`, each integration-test binary is its own process:
//! test binaries that share fixture files must not run regen concurrently.)
//! Within one process, parallel tests can share a `Checker` by wrapping it
//! in a `Mutex` at the point where testmark meets the parallel runner;
//! the crate itself stays thread-unaware so single-threaded use pays nothing.
//!
//! Call `write_back` before anything that calls `process::exit`
//! (for example, libtest_mimic's `Conclusion::exit`), or accumulated regen
//! patches are silently dropped.

use std::fmt;
use std::path::{Path, PathBuf};

use crate::document::Document;
use crate::patch::{PatchAccumulator, PatchError};

/// Assert-or-regen over one document.  See the module docs for the lifecycle.
pub struct Checker {
	pub doc: Document,
	pub patches: PatchAccumulator,
	regen: bool,
	/// The first value each hunk was checked with, for conflict detection.
	/// Kept separately from `patches`, which only holds actual differences:
	/// a first check that *matched* the document still claims the hunk.
	claims: Vec<(String, String)>,
}

impl Checker {
	/// Regen mode iff the `TESTMARK_REGEN` env var is set (to any value).
	pub fn from_env(doc: Document) -> Checker {
		Checker::new(doc, std::env::var_os("TESTMARK_REGEN").is_some())
	}

	/// Explicit mode selection (mostly for testing harness code itself).
	pub fn new(doc: Document, regen: bool) -> Checker {
		Checker {
			doc,
			patches: PatchAccumulator::new(),
			regen,
			claims: Vec::new(),
		}
	}

	/// Compare `actual` against the named code hunk's body.
	///
	/// Normal mode: a difference is [`CheckError::Mismatch`].
	/// Regen mode: a difference records a patch and returns Ok.
	/// Both modes: an absent hunk is an error -- regen rewrites hunks,
	/// it does not invent them -- and a second check of the same hunk with a
	/// *different* value is [`CheckError::Conflict`]: two tests claiming one
	/// hunk is a harness bug, and last-write-wins here would hide it.
	pub fn check(&mut self, hunk: &str, actual: &str) -> Result<(), CheckError> {
		let h = self.doc.hunk(hunk).ok_or_else(|| CheckError::NoSuchHunk {
			name: hunk.to_string(),
		})?;
		let expected = h.body().ok_or_else(|| CheckError::NotACodeHunk {
			name: hunk.to_string(),
		})?;
		if let Some((_, earlier)) = self.claims.iter().find(|(name, _)| name == hunk) {
			if earlier == actual {
				return Ok(());
			}
			return Err(CheckError::Conflict {
				name: hunk.to_string(),
				earlier: earlier.clone(),
				later: actual.to_string(),
			});
		}
		self.claims.push((hunk.to_string(), actual.to_string()));
		if actual == expected {
			return Ok(());
		}
		if self.regen {
			self.patches.put(hunk, actual);
			return Ok(());
		}
		Err(CheckError::Mismatch {
			name: hunk.to_string(),
			expected: expected.to_string(),
			actual: actual.to_string(),
		})
	}
}

/// Write the checker's patched document text to `path`, if any patches
/// accumulated.  A no-op when there are none, so it's harmless (and cheap)
/// outside regen mode: call it unconditionally at the end of a harness.
pub fn write_back(path: &Path, chk: &Checker) -> Result<(), WriteBackError> {
	if chk.patches.is_empty() {
		return Ok(());
	}
	let text = chk.doc.patched_all(&chk.patches)?;
	std::fs::write(path, text).map_err(|source| WriteBackError::Io {
		path: path.to_path_buf(),
		source,
	})
}

/// Problems from [`Checker::check`].
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum CheckError {
	/// The named hunk is not in the document (in regen mode too:
	/// regen rewrites hunks, it does not invent them).
	NoSuchHunk {
		name: String,
	},
	/// The named hunk is a table, not a code hunk.
	NotACodeHunk {
		name: String,
	},
	/// Normal mode: the actual value differs from the hunk body.
	Mismatch {
		name: String,
		expected: String,
		actual: String,
	},
	/// The same hunk was checked twice with different values
	/// (e.g. two tests claiming one hunk, or one test whose subject is
	/// expected to be deterministic producing two different outputs).
	Conflict {
		name: String,
		earlier: String,
		later: String,
	},
}

impl fmt::Display for CheckError {
	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
		match self {
			CheckError::NoSuchHunk {
				name,
			} => write!(f, "no hunk named {name:?} in the document"),
			CheckError::NotACodeHunk {
				name,
			} => write!(f, "hunk {name:?} is not a code hunk"),
			CheckError::Mismatch {
				name,
				expected,
				actual,
			} => write!(
				f,
				"mismatch in {name}:\n--- expected ---\n{expected}--- actual ---\n{actual}--- end ---\n(run with TESTMARK_REGEN=1 to accept the actual output)"
			),
			CheckError::Conflict {
				name,
				earlier,
				later,
			} => write!(
				f,
				"hunk {name} checked twice with different values (a harness bug):\n--- earlier ---\n{earlier}--- later ---\n{later}--- end ---"
			),
		}
	}
}

impl std::error::Error for CheckError {}

/// Problems from [`write_back`].
#[derive(Debug)]
pub enum WriteBackError {
	Patch(PatchError),
	Io {
		path: PathBuf,
		source: std::io::Error,
	},
}

impl From<PatchError> for WriteBackError {
	fn from(e: PatchError) -> WriteBackError {
		WriteBackError::Patch(e)
	}
}

impl fmt::Display for WriteBackError {
	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
		match self {
			WriteBackError::Patch(e) => write!(f, "{e}"),
			WriteBackError::Io {
				path,
				source,
			} => write!(f, "writing {}: {source}", path.display()),
		}
	}
}

impl std::error::Error for WriteBackError {}

include!("check_test.rs");
