use std::borrow::Cow;
use std::cell::RefCell;
use std::convert::TryFrom;
use std::fs;
use std::io;
use std::num::NonZeroUsize;
use std::path::Path;
use std::process::Command;
use std::str;

use colored::{Color, ColoredString, Colorize, Styles};
use docopt::Docopt;
use lazy_static::lazy_static;
use numfmt::{Formatter, Numeric, Precision};
use rand::prelude::*;
use rand_chacha::ChaCha8Rng;
use rand_seeder::Seeder;
use regex::{Captures, Regex};
use serde::de::DeserializeOwned;
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;

use crate::CliResult;
use crate::config::{Config, Delimiter};
use crate::select::SelectedColumns;
use crate::temporal;

pub fn version() -> String {
    let (maj, min, pat, pre) = (
        option_env!("CARGO_PKG_VERSION_MAJOR"),
        option_env!("CARGO_PKG_VERSION_MINOR"),
        option_env!("CARGO_PKG_VERSION_PATCH"),
        option_env!("CARGO_PKG_VERSION_PRE"),
    );
    match (maj, min, pat, pre) {
        (Some(maj), Some(min), Some(pat), Some(pre)) => {
            if pre.is_empty() {
                format!("{}.{}.{}", maj, min, pat)
            } else {
                format!("{}.{}.{}-{}", maj, min, pat, pre)
            }
        }
        _ => "".to_owned(),
    }
}

pub fn default_num_cpus() -> usize {
    num_cpus::get().min(16)
}

pub fn parallelization(parallel: bool, threads: Option<usize>) -> Option<usize> {
    match (parallel, threads) {
        (_, Some(t)) => Some(if t == 0 { default_num_cpus() } else { t }),
        (true, None) => Some(default_num_cpus()),
        _ => None,
    }
}

lazy_static! {
    static ref FLAG_REGEX: Regex = Regex::new(r"([\s,/\(])(--?[A-Za-z§][\w\-=]*)").unwrap();
    static ref SECTION_REGEX: Regex = Regex::new("(?im)^.*(?:usage|options):|---+").unwrap();
    static ref DIMMED_REGEX: Regex = Regex::new(
        r"\[--\]|\[-\w\s+<\w+>\.\.\.\]|\[?<[\w|\-]+>(?:\.{3})?\]?|\[[\w\s:§|\-.]+\]|\s+[\$>][^\n]+|\*[^*\n]+\*"
    )
    .unwrap();
    static ref QUOTE_REGEX: Regex = Regex::new(r#"(?m)"[^"\n]+"|'[^'\n]+'|`[^`\n]+`"#).unwrap();
    static ref MAIN_SECTION_REGEX: Regex = Regex::new("(?m)^#+.+").unwrap();
    static ref MAIN_COMMAND_REGEX: Regex = Regex::new(r"(?m)^\s{4}[\w\-]+").unwrap();
    static ref MAIN_ALIAS_REGEX: Regex = Regex::new(r"\([^\)\s]+\)").unwrap();
    static ref URL_REGEX: Regex = Regex::new(r"https?://\S+").unwrap();
}

pub fn colorize_help(help: &str) -> String {
    let help = FLAG_REGEX.replace_all(help, |caps: &Captures| {
        caps[1].to_string() + &caps[2].cyan().to_string()
    });
    let help = MAIN_SECTION_REGEX
        .replace_all(&help, |caps: &Captures| caps[0].yellow().bold().to_string());
    let help =
        SECTION_REGEX.replace_all(&help, |caps: &Captures| caps[0].yellow().bold().to_string());
    let help = QUOTE_REGEX.replace_all(&help, |caps: &Captures| caps[0].green().to_string());

    let help = DIMMED_REGEX.replace_all(&help, |caps: &Captures| {
        caps[0].dimmed().white().to_string()
    });

    let help = URL_REGEX.replace_all(&help, |caps: &Captures| caps[0].blue().to_string());

    help.into_owned()
}

pub fn colorize_main_help(help: &str) -> String {
    let help =
        MAIN_SECTION_REGEX.replace_all(help, |caps: &Captures| caps[0].yellow().bold().to_string());
    let help = MAIN_COMMAND_REGEX.replace_all(&help, |caps: &Captures| {
        "    ".to_string() + &caps[0][4..].cyan().bold().to_string()
    });
    let help = MAIN_ALIAS_REGEX.replace_all(&help, |caps: &Captures| caps[0].dimmed().to_string());

    help.replace("xan", &"xan".red().to_string())
}

#[derive(Debug, Clone, Copy, Default, Deserialize)]
pub enum ColorMode {
    #[default]
    Auto,
    Never,
    Always,
}

impl ColorMode {
    pub fn is_auto(&self) -> bool {
        matches!(self, Self::Auto)
    }

    pub fn is_never(&self) -> bool {
        matches!(self, Self::Never)
    }

    pub fn apply(&self) {
        match self {
            Self::Never => colored::control::set_override(false),
            Self::Always => colored::control::set_override(true),
            _ => (),
        }
    }
}

pub fn get_args<T>(usage: &str, argv: &[&str]) -> CliResult<T>
where
    T: DeserializeOwned,
{
    Docopt::new(usage)
        .and_then(|d| {
            d.argv(argv.iter().copied())
                .version(Some(version()))
                .help(true)
                .deserialize()
        })
        .map_err(From::from)
}

pub fn many_configs(
    inps: &[String],
    delim: Option<Delimiter>,
    no_headers: bool,
    select: Option<&SelectedColumns>,
) -> Result<Vec<Config>, String> {
    let mut inps = inps.to_vec();
    if inps.is_empty() {
        inps.push("-".to_owned()); // stdin
    }
    let confs = inps
        .into_iter()
        .map(|p| {
            let mut conf = Config::new(&Some(p))
                .delimiter(delim)
                .no_headers(no_headers);

            if let Some(sel) = select {
                conf = conf.select(sel.clone());
            }

            conf
        })
        .collect::<Vec<_>>();
    errif_greater_one_stdin(&confs)?;
    Ok(confs)
}

pub fn errif_greater_one_stdin(inps: &[Config]) -> Result<(), String> {
    let nstd = inps.iter().filter(|inp| inp.is_std()).count();
    if nstd > 1 {
        return Err("At most one <stdin> input is allowed.".to_owned());
    }
    Ok(())
}

pub type Idx = Option<usize>;

pub fn range(start: Idx, end: Idx, len: Idx, index: Idx) -> Result<(usize, usize), String> {
    match (start, end, len, index) {
        (None, None, None, Some(i)) => Ok((i, i + 1)),
        (_, _, _, Some(_)) => Err("--index cannot be used with --start, --end or --len".to_owned()),
        (_, Some(_), Some(_), None) => {
            Err("--end and --len cannot be used at the same time.".to_owned())
        }
        (_, None, None, None) => Ok((start.unwrap_or(0), usize::MAX)),
        (_, Some(e), None, None) => {
            let s = start.unwrap_or(0);
            if s > e {
                Err(format!(
                    "The end of the range ({}) must be greater than or\n\
                             equal to the start of the range ({}).",
                    e, s
                ))
            } else {
                Ok((s, e))
            }
        }
        (_, None, Some(l), None) => {
            let s = start.unwrap_or(0);
            Ok((s, s + l))
        }
    }
}

/// Represents a filename template of the form `"{}.csv"`, where `"{}"` is
/// the splace to insert the part of the filename generated by `xan`.
#[derive(Clone, Debug, Deserialize)]
#[serde(try_from = "String")]
pub struct FilenameTemplate {
    prefix: String,
    suffix: String,
}

impl FilenameTemplate {
    /// Generate a new filename using `unique_value` to replace the `"{}"`
    /// in the template.
    pub fn filename(&self, unique_value: &str) -> String {
        format!("{}{}{}", &self.prefix, unique_value, &self.suffix)
    }

    /// Create a new, writable file in directory `path` with a filename
    /// using `unique_value` to replace the `"{}"` in the template.  Note
    /// that we do not output headers; the caller must do that if
    /// desired.
    pub fn writer<P>(
        &self,
        path: P,
        unique_value: &str,
    ) -> io::Result<simd_csv::Writer<Box<dyn io::Write + 'static>>>
    where
        P: AsRef<Path>,
    {
        let filename = self.filename(unique_value);
        let full_path = path.as_ref().join(filename);
        if let Some(parent) = full_path.parent() {
            // We may be called concurrently, especially by parallel `xan
            // split`, so be careful to avoid the `create_dir_all` race
            // condition.
            fs::create_dir_all(parent)?;
        }
        let spath = Some(full_path.display().to_string());
        Config::new(&spath).simd_writer_with_options(
            fs::OpenOptions::new()
                .write(true)
                .create(true)
                .truncate(true),
        )
    }
}

impl TryFrom<String> for FilenameTemplate {
    type Error = &'static str;

    fn try_from(value: String) -> Result<Self, Self::Error> {
        let chunks = value.split("{}").collect::<Vec<_>>();
        if chunks.len() == 2 {
            Ok(FilenameTemplate {
                prefix: chunks[0].to_owned(),
                suffix: chunks[1].to_owned(),
            })
        } else {
            Err("The template argument must contain one '{}'.")
        }
    }
}

pub fn acquire_rng(seed: Option<usize>) -> Box<dyn Rng> {
    match seed {
        None => Box::new(rand::rng()),
        Some(seed) => Box::new(Seeder::from(seed).into_rng::<ChaCha8Rng>()),
    }
}

pub fn acquire_stty_size() -> Option<termsize::Size> {
    if let Ok(output) = Command::new("/bin/sh")
        .arg("-c")
        .arg("stty size < /dev/tty")
        .output()
    {
        let text = String::from_utf8_lossy(&output.stdout);
        let parts = text.split_whitespace().take(2).collect::<Vec<_>>();

        if parts.len() < 2 {
            return None;
        }

        let cols: u16 = if let Ok(c) = parts[1].parse() {
            c
        } else {
            return None;
        };

        let rows: u16 = if let Ok(r) = parts[0].parse() {
            r
        } else {
            return None;
        };

        return Some(termsize::Size { cols, rows });
    }

    None
}

pub fn acquire_term_cols(cols_override: &Option<usize>) -> usize {
    match cols_override {
        None => match termsize::get() {
            None => match acquire_stty_size() {
                None => 80,
                Some(size) => size.cols as usize,
            },
            Some(size) => size.cols as usize,
        },
        Some(c) => *c,
    }
}

pub fn acquire_term_rows(rows_override: &Option<usize>) -> usize {
    match rows_override {
        None => match termsize::get() {
            None => match acquire_stty_size() {
                None => 30,
                Some(size) => size.rows as usize,
            },
            Some(size) => size.rows as usize,
        },
        Some(c) => *c,
    }
}

fn parse_size_ratio(baseline: usize, spec: &str) -> Option<usize> {
    if let Some(prefix) = spec.strip_suffix('%') {
        prefix
            .parse::<f64>()
            .ok()
            .map(|ratio| (baseline as f64 * (ratio / 100.0)).trunc().abs() as usize)
    } else if spec.contains('.') {
        spec.parse::<f64>()
            .ok()
            .map(|ratio| (baseline as f64 * ratio).trunc().abs() as usize)
    } else {
        spec.parse::<usize>().ok()
    }
}

pub fn acquire_term_cols_ratio(cols_override: &Option<String>) -> Result<usize, &str> {
    let cols = acquire_term_cols(&None);

    if let Some(spec) = cols_override {
        parse_size_ratio(cols, spec).ok_or("--cols is invalid!")
    } else {
        Ok(cols)
    }
}

pub fn acquire_term_rows_ratio(rows_override: &Option<String>) -> Result<usize, &str> {
    let rows = acquire_term_rows(&None);

    if let Some(spec) = rows_override {
        parse_size_ratio(rows, spec).ok_or("--rows is invalid!")
    } else {
        Ok(rows)
    }
}

thread_local! {
    static NUMBER_FORMATTER: RefCell<numfmt::Formatter> = RefCell::new(
        Formatter::new()
            .precision(Precision::Significance(5))
            .separator(',')
            .unwrap()
    );
}

pub fn format_number_with_formatter<T: Numeric>(formatter: &mut numfmt::Formatter, x: T) -> String {
    let mut string = formatter.fmt2(x).to_string();

    if let Some(i) = string.find('.') {
        if string[i + 1..].chars().all(|c| c == '0') {
            string.truncate(i);
        }
    }

    string
}

pub fn format_number<T: Numeric>(x: T) -> String {
    NUMBER_FORMATTER.with_borrow_mut(|f| format_number_with_formatter(f, x))
}

pub fn could_be_url(string: &str) -> bool {
    if string.starts_with("http://") || string.starts_with("https://") {
        return !string.contains(' ');
    }

    false
}

#[derive(PartialEq, Debug, Clone, Copy)]
pub enum ColorOrStyles {
    Color(Color),
    Styles(Styles),
}

impl ColorOrStyles {
    pub fn dimmed() -> Self {
        Self::Styles(Styles::Dimmed)
    }

    pub fn highlightable_color(&self) -> Option<Cow<'static, str>> {
        match self {
            Self::Color(color) => {
                if color != &Color::Red {
                    Some(color.to_fg_str())
                } else {
                    None
                }
            }
            _ => None,
        }
    }

    pub fn colorize(&self, string: &str) -> ColoredString {
        match self {
            Self::Color(color) => string.color(*color),
            Self::Styles(styles) => match styles {
                Styles::Dimmed => string.dimmed(),
                _ => unimplemented!(),
            },
        }
    }
}

pub fn colorizer_by_type(string: &str) -> ColorOrStyles {
    match string {
        "true" | "TRUE" | "True" | "false" | "FALSE" | "False" | "yes" | "no" => {
            return ColorOrStyles::Color(Color::Cyan);
        }
        "NULL" | "null" | "na" | "NA" | "None" | "n/a" | "N/A" | "nan" | "NaN" | "<empty>"
        | "<rest>" | "." | "-" => return ColorOrStyles::Styles(Styles::Dimmed),
        _ => (),
    };

    match string.trim_start().parse::<f64>() {
        Ok(_) => ColorOrStyles::Color(Color::Red),
        Err(_) => {
            if could_be_url(string) {
                ColorOrStyles::Color(Color::Blue)
            } else if temporal::looks_temporal(string) {
                ColorOrStyles::Color(Color::Magenta)
            } else {
                ColorOrStyles::Color(Color::Green)
            }
        }
    }
}

pub fn colorizer_by_rainbow(index: usize, string: &str) -> ColorOrStyles {
    if string == "<empty>" {
        return ColorOrStyles::Styles(Styles::Dimmed);
    }

    let index = index % 7;

    match index {
        0 => ColorOrStyles::Color(Color::Red),
        1 => ColorOrStyles::Color(Color::Green),
        2 => ColorOrStyles::Color(Color::Yellow),
        3 => ColorOrStyles::Color(Color::Blue),
        4 => ColorOrStyles::Color(Color::Magenta),
        5 => ColorOrStyles::Color(Color::Cyan),
        6 => ColorOrStyles::Color(Color::BrightBlack),
        _ => unreachable!(),
    }
}

pub fn colorizer_by_rainbow_with_fallback(
    mut index: usize,
    hash: usize,
    string: &str,
) -> ColorOrStyles {
    if string == "<empty>" {
        return ColorOrStyles::Styles(Styles::Dimmed);
    }

    if index < 7 {
        index = (index + hash) % 7;
    }

    match index {
        0 => ColorOrStyles::Color(Color::Red),
        1 => ColorOrStyles::Color(Color::Green),
        2 => ColorOrStyles::Color(Color::Yellow),
        3 => ColorOrStyles::Color(Color::Blue),
        4 => ColorOrStyles::Color(Color::Magenta),
        5 => ColorOrStyles::Color(Color::Cyan),
        6 => ColorOrStyles::Color(Color::BrightBlack),
        _ => ColorOrStyles::Styles(Styles::Dimmed),
    }
}

lazy_static! {
    static ref SPARKLINE_PATTERN: Regex = Regex::new(r"[▁▂▃▄▅▆▇█]").unwrap();
    static ref ESCAPED_WHITESPACE_REPLACER: Regex = Regex::new(r"\\[nrtf]").unwrap();
}

pub fn highlight_problematic_string_features(string: &str) -> String {
    if SPARKLINE_PATTERN.is_match(string) {
        return string.to_string();
    }

    let start = string.len() - string.trim_start().len();
    let end = start + string[start..].trim_end().len();

    let replaced = format!(
        "{}{}{}",
        if (0..start).is_empty() {
            "".normal()
        } else {
            "·".repeat((0..start).len()).white().dimmed()
        },
        &string[start..end],
        if (end..string.len()).is_empty() {
            "".normal()
        } else {
            "·".repeat((end..string.len()).len()).white().dimmed()
        }
    );

    ESCAPED_WHITESPACE_REPLACER
        .replace_all(&replaced, |caps: &Captures| caps[0].dimmed().to_string())
        .into_owned()
}

pub fn sanitize_text_for_multi_line_printing(string: &str) -> String {
    // Soft-hyphens
    let mut string = string.replace('\u{00ad}', "");

    // Control characters
    string.retain(|c| c > '\x1f' || c.is_ascii_whitespace());

    string
}

lazy_static! {
    static ref WHITESPACE_REPLACER: Regex = Regex::new(r"\r\n|[\n\r\t\f]").unwrap();
}

pub fn sanitize_text_for_single_line_printing(string: &str) -> String {
    let sanitized = sanitize_text_for_multi_line_printing(string);

    match WHITESPACE_REPLACER.replace_all(&sanitized, |caps: &Captures| match &caps[0] {
        "\r\n" => "\\r\\n",
        "\n" => "\\n",
        "\r" => "\\r",
        "\t" => "\\t",
        "\x0C" => "\\f",
        _ => unreachable!(),
    }) {
        Cow::Borrowed(_) => sanitized,
        Cow::Owned(s) => s,
    }
}

pub fn unicode_aware_ellipsis(string: &str, max_width: usize) -> String {
    let mut width: usize = 0;
    let graphemes = string.graphemes(true).collect::<Vec<_>>();
    let graphemes_count = graphemes.len();

    let mut take: usize = 0;

    for grapheme in graphemes.iter() {
        width += grapheme.width();

        if width <= max_width {
            take += 1;
            continue;
        }

        break;
    }

    let mut parts = graphemes.into_iter().take(take).collect::<Vec<&str>>();

    if graphemes_count > parts.len() {
        parts.pop();

        let mut elided_width = parts.iter().map(|part| part.width()).sum::<usize>() + 1;

        while elided_width < max_width {
            parts.push(" ");
            elided_width += 1;
        }

        parts.push("…");
    }

    parts.into_iter().collect::<String>()
}

pub fn unicode_aware_pad<'a>(
    left: bool,
    string: &'a str,
    width: usize,
    padding: &str,
    actual_string_width: Option<usize>,
) -> Cow<'a, str> {
    let string_width = actual_string_width.unwrap_or_else(|| string.width());

    if string_width >= width {
        return Cow::Borrowed(string);
    }

    let mut padded = String::with_capacity(width);

    if left {
        for _ in 0..(width - string_width) {
            padded.push_str(padding);
        }

        padded.push_str(string);
    } else {
        padded.push_str(string);

        for _ in 0..(width - string_width) {
            padded.push_str(padding);
        }
    }

    Cow::Owned(padded)
}

pub fn unicode_aware_rpad<'a>(string: &'a str, width: usize, padding: &str) -> Cow<'a, str> {
    unicode_aware_pad(false, string, width, padding, None)
}

// NOTE: adapted from https://docs.rs/is-rtl/0.1.1/src/is_rtl/lib.rs.html#1-30
fn is_rtl(c: char) -> bool {
    matches!(c,
        '\u{600}'..='\u{6FF}'
        | '\u{10840}'..='\u{1085F}'
        | '\u{591}'..='\u{5F4}'
        | '\u{103A0}'..='\u{103D5}'
        | '\u{700}'..='\u{74F}'
    )
}

fn has_rtl(string: &str) -> bool {
    string.chars().any(is_rtl)
}

pub fn unicode_aware_pad_with_ellipsis(
    left: bool,
    string: &str,
    width: usize,
    padding: &str,
) -> String {
    let mut string = unicode_aware_pad(
        left,
        &unicode_aware_ellipsis(string, width),
        width,
        padding,
        None,
    )
    .into_owned();

    // NOTE: we force back to LTR at the end of the string, so it does not destroy
    // table formatting & wrapping.
    if has_rtl(&string) {
        string.push('\u{200E}');
    }

    string
}

pub fn unicode_aware_highlighted_pad_with_ellipsis(
    left: bool,
    string: &str,
    width: usize,
    padding: &str,
    highlight: bool,
) -> String {
    let with_ellipsis = unicode_aware_ellipsis(string, width);

    let mut string = unicode_aware_pad(
        left,
        &(if highlight {
            highlight_problematic_string_features(&with_ellipsis)
        } else {
            with_ellipsis
        }),
        width,
        padding,
        Some(string.width()),
    )
    .into_owned();

    // NOTE: we force back to LTR at the end of the string, so it does not destroy
    // table formatting & wrapping.
    if has_rtl(&string) {
        string.push('\u{200E}');
    }

    string
}

pub fn unicode_aware_rpad_with_ellipsis(string: &str, width: usize, padding: &str) -> String {
    unicode_aware_pad_with_ellipsis(false, string, width, padding)
}

pub fn unicode_aware_lpad_with_ellipsis(string: &str, width: usize, padding: &str) -> String {
    unicode_aware_pad_with_ellipsis(true, string, width, padding)
}

pub fn wrap(string: &str, max_width: usize, indent: usize) -> String {
    let options = textwrap::Options::new(max_width);

    let wrapped = textwrap::fill(string, &options);

    // NOTE: it seems `textwrap` subsequent_indent is not properly implemented
    let mut lines = String::with_capacity(wrapped.len());

    for (i, line) in wrapped.lines().enumerate() {
        if i > 0 {
            for _ in 0..indent {
                lines.push(' ');
            }
        }

        lines.push_str(line);
        lines.push('\n');
    }

    lines.pop();

    lines
}

pub struct EmojiSanitizer {
    pattern: Regex,
}

impl EmojiSanitizer {
    pub fn new() -> Self {
        let mut pattern = String::new();
        pattern.push_str("(?:");

        let mut all_emojis = emojis::iter().collect::<Vec<_>>();
        all_emojis.sort_by_key(|e| std::cmp::Reverse((e.as_bytes().len(), e.as_bytes())));

        for emoji in all_emojis {
            pattern.push_str(&regex::escape(emoji.as_str()));
            pattern.push('|');
        }

        pattern.pop();
        pattern.push(')');

        let pattern = Regex::new(&pattern).unwrap();

        EmojiSanitizer { pattern }
    }

    pub fn sanitize(&self, string: &str) -> String {
        self.pattern
            .replace_all(string, |caps: &Captures| {
                format!(
                    ":{}:",
                    match emojis::get(&caps[0]) {
                        None => "unknown_emoji",
                        Some(emoji) => match emoji.shortcode() {
                            None => "unknown_emoji",
                            Some(shortcode) => shortcode,
                        },
                    }
                )
            })
            .to_string()
    }
}

pub fn str_to_csv_byte_record(target: &str) -> csv::ByteRecord {
    let cursor = io::Cursor::new(target);
    let reader = csv::ReaderBuilder::new()
        .has_headers(false)
        .from_reader(cursor);

    match reader.into_byte_records().next() {
        Some(record) => record.unwrap(),
        None => csv::ByteRecord::new(),
    }
}

pub struct Chunks<I> {
    size: NonZeroUsize,
    inner: I,
}

impl<I> Iterator for Chunks<I>
where
    I: Iterator,
{
    type Item = Vec<I::Item>;

    fn next(&mut self) -> Option<Self::Item> {
        let mut chunk: Vec<I::Item> = Vec::new();

        while chunk.len() < self.size.get() {
            match self.inner.next() {
                None => {
                    if chunk.is_empty() {
                        return None;
                    }

                    return Some(chunk);
                }
                Some(item) => {
                    chunk.push(item);
                }
            }
        }

        Some(chunk)
    }
}

pub trait ChunksIteratorExt: Sized {
    fn chunks(self, size: NonZeroUsize) -> Chunks<Self>;
}

impl<T: Iterator> ChunksIteratorExt for T {
    fn chunks(self, size: NonZeroUsize) -> Chunks<Self> {
        Chunks { size, inner: self }
    }
}

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

    #[test]
    fn test_unicode_aware_ellipsis() {
        assert_eq!(unicode_aware_ellipsis("abcde", 10), "abcde".to_string());
        assert_eq!(unicode_aware_ellipsis("abcde", 5), "abcde".to_string());
        assert_eq!(unicode_aware_ellipsis("abcde", 4), "abc…".to_string());
        assert_eq!(unicode_aware_ellipsis("abcde", 3), "ab…".to_string());
    }

    #[test]
    fn test_emoji_sanitizer() {
        let sanitizer = EmojiSanitizer::new();

        assert_eq!(
            sanitizer.sanitize("👩 hello 👩‍👩‍👧‍👦"),
            ":woman: hello :family_woman_woman_girl_boy:"
        );
    }

    macro_rules! brec {
        () => {
            csv::ByteRecord::new()
        };
        ( $( $x:expr ),* ) => {
            {
                let mut record = csv::ByteRecord::new();
                $(
                    record.push_field($x);
                )*
                record
            }
        };
    }

    #[test]
    fn test_str_to_csv_byte_record() {
        assert_eq!(str_to_csv_byte_record(""), brec![]);
        assert_eq!(str_to_csv_byte_record("test"), brec![b"test"]);
        assert_eq!(str_to_csv_byte_record("test,ok"), brec![b"test", b"ok"]);
        assert_eq!(
            str_to_csv_byte_record("\"test, ok\",\"\"\"John"),
            brec![b"test, ok", b"\"John"]
        );
    }
}
