use crate::nodes::{AstNode, NodeValue, Sourcepos};
use crate::*;
use std::collections::HashMap;
use std::io::{self, Write};
use std::panic;

mod api;
mod autolink;
mod commonmark;
mod core;
mod description_lists;
mod footnotes;
mod fuzz;
mod header_ids;
mod options;
mod pathological;
mod plugins;
mod propfuzz;
mod regressions;
mod shortcodes;
mod strikethrough;
mod superscript;
mod table;
mod tagfilter;
mod tasklist;
mod xml;

#[track_caller]
fn compare_strs(output: &str, expected: &str, kind: &str) {
    if output != expected {
        println!("Running {} test", kind);
        println!("Got:");
        println!("==============================");
        println!("{}", output);
        println!("==============================");
        println!();
        println!("Expected:");
        println!("==============================");
        println!("{}", expected);
        println!("==============================");
        println!();
    }
    assert_eq!(output, expected);
}

#[track_caller]
fn commonmark(input: &str, expected: &str, opts: Option<&Options>) {
    let arena = Arena::new();
    let defaults = Options::default();
    let options = opts.unwrap_or(&defaults);

    let root = parse_document(&arena, input, options);
    let mut output = vec![];
    cm::format_document(root, options, &mut output).unwrap();
    compare_strs(&String::from_utf8(output).unwrap(), expected, "regular");
}

#[track_caller]
pub fn html(input: &str, expected: &str) {
    html_opts_i(input, expected, |_| ());
}

#[track_caller]
fn html_opts_i<F>(input: &str, expected: &str, opts: F)
where
    F: Fn(&mut Options),
{
    let mut options = Options::default();
    opts(&mut options);

    html_opts_w(input, expected, &options);
}

#[track_caller]
fn html_opts_w(input: &str, expected: &str, options: &Options) {
    let arena = Arena::new();

    let root = parse_document(&arena, input, &options);
    let mut output = vec![];
    html::format_document(root, &options, &mut output).unwrap();
    compare_strs(&String::from_utf8(output).unwrap(), expected, "regular");

    if options.render.sourcepos {
        return;
    }

    let mut md = vec![];
    cm::format_document(root, &options, &mut md).unwrap();
    let root = parse_document(&arena, &String::from_utf8(md).unwrap(), &options);
    let mut output_from_rt = vec![];
    html::format_document(root, &options, &mut output_from_rt).unwrap();
    compare_strs(
        &String::from_utf8(output_from_rt).unwrap(),
        expected,
        "roundtrip",
    );
}

macro_rules! html_opts {
    ([$($optclass:ident.$optname:ident),*], $lhs:expr, $rhs:expr,) => {
        html_opts!([$($optclass.$optname),*], $lhs, $rhs)
    };
    ([$($optclass:ident.$optname:ident),*], $lhs:expr, $rhs:expr) => {
        $crate::tests::html_opts_i($lhs, $rhs, |opts| {
            $(opts.$optclass.$optname = true;)*
        });
    };
    ([all], $lhs:expr, $rhs:expr) => {
        $crate::tests::html_opts_w($lhs, $rhs, &$crate::Options {
            extension: $crate::ExtensionOptions {
                strikethrough: true,
                tagfilter: true,
                table: true,
                autolink: true,
                tasklist: true,
                superscript: true,
                header_ids: Some("user-content-".to_string()),
                footnotes: true,
                description_lists: true,
                front_matter_delimiter: Some("---".to_string()),
                shortcodes: true,
            },
            parse: $crate::ParseOptions {
                smart: true,
                default_info_string: Some("rust".to_string()),
                relaxed_tasklist_matching: true,
                relaxed_autolinks: true,
            },
            render: $crate::RenderOptions {
                hardbreaks: true,
                github_pre_lang: true,
                full_info_string: true,
                width: 80,
                unsafe_: true,
                escape: true,
                list_style: $crate::ListStyleType::Star,
                sourcepos: true,
            },
        });
    }
}

pub(crate) use html_opts;

#[track_caller]
fn html_plugins(input: &str, expected: &str, plugins: &Plugins) {
    let arena = Arena::new();
    let options = Options::default();

    let root = parse_document(&arena, input, &options);
    let mut output = vec![];
    html::format_document_with_plugins(root, &options, &mut output, plugins).unwrap();
    compare_strs(&String::from_utf8(output).unwrap(), expected, "regular");

    if options.render.sourcepos {
        return;
    }

    let mut md = vec![];
    cm::format_document(root, &options, &mut md).unwrap();
    let root = parse_document(&arena, &String::from_utf8(md).unwrap(), &options);
    let mut output_from_rt = vec![];
    html::format_document_with_plugins(root, &options, &mut output_from_rt, plugins).unwrap();
    compare_strs(
        &String::from_utf8(output_from_rt).unwrap(),
        expected,
        "roundtrip",
    );
}

#[track_caller]
fn xml(input: &str, expected: &str) {
    xml_opts(input, expected, |_| ());
}

#[track_caller]
fn xml_opts<F>(input: &str, expected: &str, opts: F)
where
    F: Fn(&mut Options),
{
    let arena = Arena::new();
    let mut options = Options::default();
    opts(&mut options);

    let root = parse_document(&arena, input, &options);
    let mut output = vec![];
    crate::xml::format_document(root, &options, &mut output).unwrap();
    compare_strs(&String::from_utf8(output).unwrap(), expected, "regular");

    if options.render.sourcepos {
        return;
    }

    let mut md = vec![];
    cm::format_document(root, &options, &mut md).unwrap();
    let root = parse_document(&arena, &String::from_utf8(md).unwrap(), &options);
    let mut output_from_rt = vec![];
    crate::xml::format_document(root, &options, &mut output_from_rt).unwrap();
    compare_strs(
        &String::from_utf8(output_from_rt).unwrap(),
        expected,
        "roundtrip",
    );
}

fn asssert_node_eq<'a>(node: &'a AstNode<'a>, location: &[usize], expected: &NodeValue) {
    let node = location
        .iter()
        .fold(node, |node, &n| node.children().nth(n).unwrap());

    let data = node.data.borrow();
    let actual = format!("{:?}", data.value);
    let expected = format!("{:?}", expected);

    compare_strs(&actual, &expected, "ast comparison");
}

macro_rules! sourcepos {
    (($spsl:literal:$spsc:literal-$spel:literal:$spec:literal)) => {
        ($spsl, $spsc, $spel, $spec).into()
    };
    ((XXX)) => {
        (0, 1, 0, 1).into()
    };
}

pub(crate) use sourcepos;

macro_rules! ast {
    (($name:tt $sp:tt)) => {
        ast!(($name $sp []))
    };
    (($name:tt $sp:tt $content:tt)) => {
        AstMatchTree {
            name: stringify!($name).to_string(),
            sourcepos: sourcepos!($sp),
            content: ast!($content),
        }
    };

    ($text:literal) => {AstMatchContent::Text($text.to_string())};
    ([ $( $children:tt )* ]) => {
        AstMatchContent::Children(vec![ $( ast!($children), )* ])
    };
}

pub(crate) use ast;

#[track_caller]
fn assert_ast_match_i<F>(md: &str, amt: AstMatchTree, opts: F)
where
    F: Fn(&mut Options),
{
    let mut options = Options::default();
    options.render.sourcepos = true;
    opts(&mut options);

    let result = panic::catch_unwind(|| {
        let arena = Arena::new();
        let root = parse_document(&arena, md, &options);

        amt.assert_match(root);
    });

    if let Err(err) = result {
        let arena = Arena::new();
        let root = parse_document(&arena, md, &options);

        let mut output = vec![];
        format_xml(root, &options, &mut output).unwrap();
        eprintln!("{}", std::str::from_utf8(&output).unwrap());

        panic::resume_unwind(err)
    }
}

macro_rules! assert_ast_match {
    ([ $( $optclass:ident.$optname:ident ),* ], $( $md:literal )+, $amt:tt,) => {
        assert_ast_match!(
            [ $( $optclass.$optname ),* ],
            $( $md )+,
            $amt
        )
    };
    ([ $( $optclass:ident.$optname:ident ),* ], $( $md:literal )+, $amt:tt) => {
        crate::tests::assert_ast_match_i(
            concat!( $( $md ),+ ),
            ast!($amt),
            |#[allow(unused_variables)] opts| {$(opts.$optclass.$optname = true;)*},
        );
    };
}

pub(crate) use assert_ast_match;

struct AstMatchTree {
    name: String,
    sourcepos: Sourcepos,
    content: AstMatchContent,
}

enum AstMatchContent {
    Text(String),
    Children(Vec<AstMatchTree>),
}

impl AstMatchTree {
    #[track_caller]
    fn assert_match<'a>(&self, node: &'a AstNode<'a>) {
        let ast = node.data.borrow();
        assert_eq!(self.name, ast.value.xml_node_name(), "node type matches");
        assert_eq!(self.sourcepos, ast.sourcepos, "sourcepos are equal");

        match &self.content {
            AstMatchContent::Text(text) => {
                assert_eq!(
                    0,
                    node.children().count(),
                    "text node should have no children"
                );
                assert_eq!(
                    text,
                    ast.value.text().unwrap(),
                    "text node content should match"
                );
            }
            AstMatchContent::Children(children) => {
                assert_eq!(
                    children.len(),
                    node.children().count(),
                    "children count should match"
                );
                for (e, a) in children.iter().zip(node.children()) {
                    e.assert_match(a);
                }
            }
        }
    }
}
