extern crate log;
extern crate serde;

extern crate csv;
extern crate filetime;
extern crate quickcheck;
extern crate rand;
extern crate stats;

use std::{env, fmt, ops};

use quickcheck::{Arbitrary, Gen, QuickCheck, Testable};
use rand::RngExt;
// use assert_eq;

macro_rules! svec[
    ($($x:expr_2021),*) => (
        vec![$($x),*].into_iter()
                     .map(|s: &'static str| s.to_string())
                     .collect::<Vec<String>>()
    );
    ($($x:expr_2021,)*) => (svec![$($x),*]);
];

macro_rules! rassert_eq {
    ($given:expr_2021, $expected:expr_2021) => {{
        assert_eq!($given, $expected);
        true
    }};
}

mod workdir;

mod test_100;
mod test_alloc_tuning;
#[cfg(feature = "apply")]
mod test_apply;
#[cfg(feature = "datapusher_plus")]
mod test_applydp;
#[cfg(any(feature = "feature_capable", feature = "lite"))]
mod test_behead;
#[cfg(any(feature = "feature_capable", feature = "datapusher_plus"))]
mod test_blake3;
#[cfg(any(feature = "feature_capable", feature = "lite"))]
mod test_cat;
#[cfg(any(feature = "feature_capable", feature = "datapusher_plus"))]
mod test_clean;
#[cfg(feature = "lite")]
mod test_clean_lite;
#[cfg(all(
    feature = "feature_capable",
    any(target_os = "windows", target_os = "macos"),
    feature = "clipboard",
))]
mod test_clipboard;
#[cfg(all(feature = "feature_capable", feature = "color"))]
mod test_color;
mod test_combos;
mod test_comments;
mod test_count;
mod test_datefmt;
mod test_dedup;
#[cfg(feature = "feature_capable")]
mod test_denull;
// describegpt is excluded from qsvlite; only test it where the command exists
#[cfg(all(not(feature = "lite"), not(target_arch = "powerpc64")))]
mod test_describegpt;
mod test_diff;
#[cfg(any(feature = "feature_capable", feature = "lite"))]
mod test_edit;
#[cfg(any(feature = "feature_capable", feature = "lite"))]
mod test_enumerate;
mod test_excel;
mod test_exclude;
#[cfg(any(feature = "feature_capable", feature = "lite"))]
mod test_explode;
mod test_extdedup;
#[cfg(any(feature = "feature_capable", feature = "lite"))]
mod test_extsort;
#[cfg(feature = "fetch")]
mod test_fetch;
#[cfg(any(feature = "feature_capable", feature = "lite"))]
mod test_fill;
#[cfg(feature = "feature_capable")]
mod test_fixedwidth;
#[cfg(any(feature = "feature_capable", feature = "lite"))]
mod test_fixlengths;
#[cfg(any(feature = "feature_capable", feature = "lite"))]
mod test_flatten;
#[cfg(any(feature = "feature_capable", feature = "lite"))]
mod test_fmt;
#[cfg(feature = "foreach")]
mod test_foreach;
mod test_frequency;
#[cfg(feature = "geocode")]
// #[cfg(not(target_arch = "powerpc64"))]
mod test_geocode;
#[cfg(feature = "geocode")]
mod test_geoconvert;
#[cfg(feature = "get")]
mod test_get;
mod test_headers;
#[cfg(any(feature = "feature_capable", feature = "lite"))]
mod test_implode;
mod test_index;
mod test_input;
mod test_join;
#[cfg(feature = "polars")]
mod test_joinp;
#[cfg(not(feature = "datapusher_plus"))]
mod test_json;
#[cfg(any(feature = "feature_capable", feature = "lite"))]
mod test_jsonl;
#[cfg(feature = "mcp")]
mod test_log;
#[cfg(feature = "luau")]
mod test_luau;
mod test_moarstats;
#[cfg(any(feature = "feature_capable", feature = "lite"))]
mod test_partition;
#[cfg(feature = "polars")]
mod test_pivotp;
mod test_pragmastat;
#[cfg(feature = "profile")]
mod test_profile;
#[cfg(feature = "prompt")]
mod test_prompt;
mod test_pseudo;
#[cfg(feature = "python")]
mod test_py;
mod test_rename;
mod test_replace;
mod test_reverse;
mod test_safenames;
mod test_sample;
#[cfg(any(feature = "feature_capable", feature = "lite"))]
mod test_schema;
#[cfg(all(feature = "polars", feature = "feature_capable"))]
mod test_scoresql;
mod test_search;
mod test_searchset;
mod test_select;
mod test_slice;
mod test_snappy;
mod test_sniff;
mod test_sort;
mod test_sortcheck;
#[cfg(any(feature = "feature_capable", feature = "lite"))]
mod test_split;
#[cfg(feature = "polars")]
mod test_sqlp;
mod test_stats;
#[cfg(all(feature = "synthesize", feature = "feature_capable"))]
mod test_synthesize;
#[cfg(any(feature = "feature_capable", feature = "lite"))]
mod test_table;
#[cfg(feature = "feature_capable")]
mod test_template;
#[cfg(all(feature = "to", feature = "feature_capable"))]
mod test_to;
#[cfg(any(feature = "feature_capable", feature = "lite"))]
mod test_tojsonl;
#[cfg(any(feature = "feature_capable", feature = "lite"))]
mod test_transpose;
mod test_validate;
#[cfg(feature = "viz")]
mod test_viz;

fn qcheck<T: Testable>(p: T) {
    // safety: we are in single-threaded code.
    unsafe { env::set_var("QSV_SKIPUTF8_CHECK", "1") };
    QuickCheck::new().rng(Gen::new(5)).quickcheck(p);
    // safety: we are in single-threaded code.
    unsafe { env::set_var("QSV_SKIPUTF8_CHECK", "") };
}

fn qcheck_sized<T: Testable>(p: T, size: usize) {
    // safety: we are in single-threaded code.
    unsafe { env::set_var("QSV_SKIPUTF8_CHECK", "1") };
    QuickCheck::new().rng(Gen::new(size)).quickcheck(p);
    // safety: we are in single-threaded code.
    unsafe { env::set_var("QSV_SKIPUTF8_CHECK", "") };
}

pub type CsvVecs = Vec<Vec<String>>;

pub trait Csv {
    fn to_vecs(self) -> CsvVecs;
    fn from_vecs(_: CsvVecs) -> Self;
}

impl Csv for CsvVecs {
    fn to_vecs(self) -> CsvVecs {
        self
    }

    fn from_vecs(vecs: CsvVecs) -> CsvVecs {
        vecs
    }
}

#[derive(Clone, Eq, Ord, PartialEq, PartialOrd)]
struct CsvRecord(Vec<String>);

impl CsvRecord {
    fn unwrap(self) -> Vec<String> {
        let CsvRecord(v) = self;
        v
    }
}

impl ops::Deref for CsvRecord {
    type Target = [String];

    fn deref(&self) -> &[String] {
        &self.0
    }
}

impl ops::DerefMut for CsvRecord {
    fn deref_mut(&mut self) -> &mut [String] {
        &mut self.0
    }
}

impl fmt::Debug for CsvRecord {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let bytes: Vec<_> = self.iter().map(std::string::String::as_bytes).collect();
        write!(f, "{bytes:?}")
    }
}

impl Arbitrary for CsvRecord {
    fn arbitrary(g: &mut Gen) -> CsvRecord {
        let size = g.size();
        CsvRecord((0..size).map(|_| Arbitrary::arbitrary(g)).collect())
    }

    fn shrink(&self) -> Box<dyn Iterator<Item = CsvRecord> + 'static> {
        Box::new(
            self.clone()
                .unwrap()
                .shrink()
                .filter(|r| !r.is_empty())
                .map(CsvRecord),
        )
    }
}

impl Csv for Vec<CsvRecord> {
    fn to_vecs(self) -> CsvVecs {
        self.into_iter().map(CsvRecord::unwrap).collect()
    }

    fn from_vecs(vecs: CsvVecs) -> Vec<CsvRecord> {
        vecs.into_iter().map(CsvRecord).collect()
    }
}

#[derive(Clone, Debug, Eq, Ord, PartialOrd)]
struct CsvData {
    data: Vec<CsvRecord>,
}

impl CsvData {
    fn unwrap(self) -> Vec<CsvRecord> {
        self.data
    }

    fn len(&self) -> usize {
        (**self).len()
    }

    fn is_empty(&self) -> bool {
        self.len() == 0
    }
}

impl ops::Deref for CsvData {
    type Target = [CsvRecord];

    fn deref(&self) -> &[CsvRecord] {
        &self.data
    }
}

impl Arbitrary for CsvData {
    fn arbitrary(g: &mut Gen) -> CsvData {
        let record_len = g.size();
        let mut rng = rand::rng();

        let num_records: usize = rng.random_range(0..100);
        let mut d = CsvData {
            data: (0..num_records)
                .map(|_| CsvRecord((0..record_len).map(|_| Arbitrary::arbitrary(g)).collect()))
                .collect(),
        };
        // If the CSV data starts with a BOM, strip it, because it wreaks havoc
        // with tests that weren't designed to handle it.
        if !d.data.is_empty()
            && !d.data[0].is_empty()
            && let Some(stripped) = d.data[0][0].strip_prefix('\u{FEFF}')
        {
            d.data[0][0] = stripped.to_string();
        }
        d
    }

    fn shrink(&self) -> Box<dyn Iterator<Item = CsvData> + 'static> {
        let len = if self.is_empty() { 0 } else { self[0].len() };
        let mut rows: Vec<CsvData> = self
            .clone()
            .unwrap()
            .shrink()
            .filter(|rows| rows.iter().all(|r| r.len() == len))
            .map(|rows| CsvData { data: rows })
            .collect();
        // We should also introduce CSV data with fewer columns...
        if len > 1 {
            rows.extend(
                self.clone()
                    .unwrap()
                    .shrink()
                    .filter(|rows| rows.iter().all(|r| r.len() == len - 1))
                    .map(|rows| CsvData { data: rows }),
            );
        }
        Box::new(rows.into_iter())
    }
}

impl Csv for CsvData {
    fn to_vecs(self) -> CsvVecs {
        self.data.into_iter().map(CsvRecord::unwrap).collect()
    }

    fn from_vecs(vecs: CsvVecs) -> CsvData {
        CsvData {
            data: vecs.into_iter().map(CsvRecord).collect(),
        }
    }
}

impl PartialEq for CsvData {
    fn eq(&self, other: &CsvData) -> bool {
        (self.data.is_empty() && other.data.is_empty()) || self.data == other.data
    }
}
