// SPDX-FileCopyrightText: 2025 Slatian
//
// SPDX-License-Identifier: LGPL-3.0-only

//! Matches against textual values.

pub mod direct_match;

#[cfg(feature = "rusqlite")]
pub mod rusqlite;

#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};

use crate::NumberCriterium;

/// The StringCriterium describes a set of comparisons that
/// can be made against a textual field.
///
/// No automatic type conversion will happen.
///
/// If the field is some form of null, undefined or unset
/// only the IsNone condition is allowed to match.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum StringCriterium {
	/// The field is equal to the given string (`f == s`).
	Equals(String),

	/// The field starts with the given string (`f.starts_with(s)`).
	HasPrefix(String),

	/// The field ends with the given string (`f.ends_with(s)`).
	HasSuffix(String),

	/// The field contains the given string (`f.contains(s)`).
	Contains(String),

	/// Matches the length of the string in characters (unicode codepoints).
	Length(NumberCriterium<usize>),

	/// The field has no value.
	IsNone,
}

// Conversion implementations

impl From<String> for StringCriterium {
	/// Converts any String to a `StringCriterium` with an `Equals` condition.
	fn from(string: String) -> Self {
		Self::Equals(string)
	}
}

impl From<Option<String>> for StringCriterium {
	/// Converts `Some(String)` to a `StringCriterium` with an `Equals` condition.
	/// If `None` is passed, the `IsNone` condition will be used.
	fn from(string_opt: Option<String>) -> Self {
		match string_opt {
			Some(s) => s.into(),
			None => Self::IsNone,
		}
	}
}

impl From<&str> for StringCriterium {
	/// Converts any `&str` to a `StringCriterium` with an `Equals` condition.
	fn from(string: &str) -> Self {
		Self::Equals(string.to_string())
	}
}

impl From<Option<&str>> for StringCriterium {
	/// Converts `Some(String)` to a `StringCriterium` with an `Equals` condition.
	/// If `None` is passed, the `IsNone` condition will be used.
	fn from(string_opt: Option<&str>) -> Self {
		match string_opt {
			Some(s) => s.into(),
			None => Self::IsNone,
		}
	}
}

#[cfg(test)]
mod test {
	use super::*;
	use crate::direct_match::DirectMatch;

	#[test]
	fn direct_match_string_equal() {
		let c_empty = StringCriterium::Equals("".to_string());
		let str_opt_none: Option<&str> = None;
		assert!(c_empty.criterium_match(""));
		assert!(c_empty.criterium_match(&("".to_string())));
		assert!(!c_empty.criterium_match("test"));
		assert!(!c_empty.criterium_match(&("test".to_string())));
		assert!(!c_empty.criterium_match(&str_opt_none));
		assert!(!c_empty.criterium_match(&(" ".to_string())));

		let c_none = StringCriterium::IsNone;
		assert!(!c_none.criterium_match(""));
		assert!(!c_none.criterium_match(&("".to_string())));
		assert!(!c_none.criterium_match("test"));
		assert!(!c_none.criterium_match(&("test".to_string())));
		assert!(c_none.criterium_match(&str_opt_none));
		assert!(!c_none.criterium_match(&(" ".to_string())));

		let c_test = StringCriterium::Equals("test".to_string());
		assert!(!c_test.criterium_match(""));
		assert!(!c_test.criterium_match(&("".to_string())));
		assert!(c_test.criterium_match("test"));
		assert!(c_test.criterium_match(&("test".to_string())));
		assert!(!c_test.criterium_match(&str_opt_none));
		assert!(!c_test.criterium_match(&(" ".to_string())));
	}

	#[test]
	fn direct_match_string_substring() {
		let c_prefix = StringCriterium::HasPrefix("foo".to_string());
		assert!(c_prefix.criterium_match("foo"));
		assert!(c_prefix.criterium_match("foo_"));
		assert!(!c_prefix.criterium_match("_foo"));
		assert!(!c_prefix.criterium_match("fo_foo_"));
		assert!(!c_prefix.criterium_match(""));
		let c_suffix = StringCriterium::HasSuffix("foo".to_string());
		assert!(c_suffix.criterium_match("foo"));
		assert!(!c_suffix.criterium_match("foo_"));
		assert!(c_suffix.criterium_match("blafoo"));
		assert!(!c_suffix.criterium_match("_foo_"));
		assert!(!c_suffix.criterium_match(""));
		let c_contains = StringCriterium::Contains("foo".to_string());
		assert!(c_contains.criterium_match("foo"));
		assert!(c_contains.criterium_match("foo_"));
		assert!(c_contains.criterium_match("_foo"));
		assert!(c_contains.criterium_match("_foo_"));
		assert!(!c_contains.criterium_match("_bar_"));
		assert!(!c_contains.criterium_match(""));
	}
}
