#![allow(clippy::await_holding_lock)]

use std::{io::Write, mem::MaybeUninit, process::Stdio, sync::Mutex, time::Duration};

use fantoccini::{Client, ClientBuilder, Locator};
use heck::ToTitleCase;
use inventory::collect;
use libtest_mimic::{Arguments, Conclusion, Failed, Trial};
use pulldown_latex::{config::RenderConfig, mathml::push_mathml, Parser, Storage};
use tokio::process::Command;

#[allow(clippy::type_complexity)]
pub static RENDERED: Mutex<Vec<(&str, Vec<(&str, String)>)>> = Mutex::new(Vec::new());
pub const OUTPUT_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/out");

pub struct TestCase {
    pub name: &'static str,
    pub test: fn() -> Result<(), Failed>,
}
collect!(TestCase);

pub fn test() -> Conclusion {
    let args = Arguments::from_args();
    let tests = inventory::iter::<TestCase>
        .into_iter()
        .map(|TestCase { name, test }| Trial::test(*name, test))
        .collect::<Vec<_>>();
    libtest_mimic::run(&args, tests)
}

pub fn round_trip(
    fn_name: &'static str,
    inputs: &[&'static str],
    config: RenderConfig,
) -> Result<(), Failed> {
    let mut storage = Storage::new();
    let rendered: Vec<_> = inputs
        .iter()
        .map(|input| -> Result<_, Failed> {
            show_errors(Parser::new(input, &storage))?;
            let parser = Parser::new(input, &storage);
            let mut output = String::new();
            push_mathml(&mut output, parser, config)?;
            storage.reset();
            Ok((*input, output))
        })
        .collect::<Result<_, Failed>>()?;

    RENDERED.lock()?.push((fn_name, rendered));

    Ok(())
}

pub fn show_errors(parser: Parser) -> Result<(), usize> {
    let mut error_count = 0;

    parser.for_each(|event| {
        if let Err(e) = event {
            eprintln!("{:?}", e);
            error_count += 1;
        }
    });

    if error_count > 0 {
        Err(error_count)
    } else {
        Ok(())
    }
}

pub fn tabled(file: &mut std::fs::File) -> anyhow::Result<()> {
    file.write_all(br#"<table style="max-width: 60vw; margin: auto;">"#)?;
    let mut rendered = RENDERED.lock().unwrap();
    rendered.sort();
    rendered
        .iter()
        .try_for_each(|(table_name, rows)| -> std::io::Result<()> {
            file.write_fmt(format_args!(
                r#"<tr><th colspan="2">{table_name}</th></tr>"#,
                table_name = table_name.to_title_case()
            ))?;

            rows.iter().try_for_each(|(input, output)| {
                file.write_fmt(format_args!(
                    r#"<tr><td>{input}</td><td style="position: relative">{output}</td></tr>"#,
                    input = input,
                    output = output
                ))
            })
        })?;
    file.write_all(b"</table>")?;
    Ok(())
}

pub fn html_template(
    file: &mut std::fs::File,
    title: &str,
    stylesheet: Option<&str>,
    render: impl FnOnce(&mut std::fs::File) -> anyhow::Result<()>,
) -> anyhow::Result<()> {
    let styles = match stylesheet {
        Some(stylesheet) => {
            format!(r#"<link rel="stylesheet" type="text/css" href="{OUTPUT_DIR}/{stylesheet}">"#,)
        }
        None => "".to_string(),
    };

    file.write_fmt(format_args!(
        r#"<!DOCTYPE html>
<html>
<head>
<title>{title}</title>
<link rel="stylesheet" type="text/css" href="{}/styles.css">
<meta charset="UTF-8">
{styles}</head>
<body>
"#,
        env!("CARGO_MANIFEST_DIR")
    ))?;

    render(file)?;

    file.write_fmt(format_args!(
        r#"</body>
</html>"#
    ))?;
    Ok(())
}

pub async fn cross_browser() -> anyhow::Result<()> {
    let mut tmp = tempfile::Builder::new().suffix(".html").tempfile()?;
    let path = format!("file://{}", tmp.path().to_str().unwrap());
    let driver_processes = [
        {
            let port = 4444;
            let process = Command::new("chromedriver")
                .arg(format!("--port={port}"))
                .stdout(Stdio::null())
                .stderr(Stdio::null())
                .kill_on_drop(true)
                .spawn()?;
            (process, "chrome", port)
        },
        {
            let port = 4445;
            let process = Command::new("geckodriver")
                .args(["--port", port.to_string().as_str()])
                .stdout(Stdio::null())
                .stderr(Stdio::null())
                .kill_on_drop(true)
                .spawn()?;
            (process, "firefox", 4445)
        },
        {
            let port = 4446;
            let process = Command::new("safaridriver")
                .args(["--port", port.to_string().as_str()])
                .stdout(Stdio::null())
                .stderr(Stdio::null())
                .kill_on_drop(true)
                .spawn()?;
            (process, "safari", 4446)
        },
    ];
    // Wait for processes to start, otherwise the clients will fail to connect
    tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
    // Safety: This is safe because all elements of the array do not need to be initialized.
    let mut clients: [MaybeUninit<Client>; 3] = unsafe { MaybeUninit::uninit().assume_init() };
    for (i, (_, _, port)) in driver_processes.iter().enumerate() {
        let client = ClientBuilder::native()
            .connect(&format!("http://localhost:{}", port))
            .await?;
        clients[i].write(client);
    }
    // Safety: The clients are initialized
    let clients = unsafe { std::mem::transmute::<[MaybeUninit<Client>; 3], [Client; 3]>(clients) };

    let rendered = &*RENDERED.lock().unwrap();
    for (table_name, rows) in rendered {
        for (_input, output) in rows {
            html_template(
                tmp.as_file_mut(),
                "",
                Some("cross-browser-render.css"),
                |file: &mut std::fs::File| -> anyhow::Result<()> {
                    file.write_all(output.as_bytes())?;
                    Ok(())
                },
            )?;

            for (name, client) in driver_processes.iter().map(|t| t.1).zip(&clients) {
                client.goto(&path).await?;
                let elem = client
                    .wait()
                    .at_most(Duration::from_secs(10))
                    .for_element(Locator::XPath("/html/body"))
                    .await?;

                let screenshot = elem.screenshot().await?;

                tokio::fs::write(
                    format!("{OUTPUT_DIR}/screenshots/{name}/{table_name}.png"),
                    screenshot,
                )
                .await?;
            }

            tmp.as_file_mut().set_len(0)?;
        }
    }

    for (mut process, _, _) in driver_processes {
        process.kill().await?;
    }
    for client in clients {
        client.close().await?;
    }

    Ok(())
}

pub fn cross_browser_tabled(file: &mut std::fs::File) -> anyhow::Result<()> {
    file.write_all(br#"<table style="margin: auto;">"#)?;
    file.write_all(br#"<tr><th>Input</th><th>Chrome</th><th>Firefox</th><th>Safari</th></tr>"#)?;
    let mut rendered = RENDERED.lock().unwrap();
    rendered.sort();
    rendered
        .iter()
        .try_for_each(|(table_name, rows)| -> std::io::Result<()> {
            file.write_fmt(format_args!(
                r#"<tr><th colspan="4">{table_name}</th></tr>"#,
                table_name = table_name.to_title_case()
            ))?;

            file.write_all(br#"<tr><td class="input">"#)?;
            rows.iter()
                .try_for_each(|(input, _)| -> std::io::Result<()> {
                    file.write_all(input.as_bytes())?;
                    file.write_all(b"\n")?;
                    Ok(())
                })?;
            file.write_all(b"</td>")?;
            for browser in ["chrome", "firefox", "safari"] {
                file.write_fmt(
                    format_args!(r#"<td class="image-container"><img class="{browser}-img" src="{OUTPUT_DIR}/screenshots/{browser}/{table_name}.png"></td>"#)
                )?;
            }
            file.write_all(b"</tr>")?;
            Ok(())
        })?;
    file.write_all(b"</table>")?;
    Ok(())
}

#[macro_export]
macro_rules! round_trip_display {
    ($name:ident, $($input:literal),+ $(,)?) => {
        $crate::round_trip!(
            $name,
            $($input),+,
            display_mode = pulldown_latex::config::DisplayMode::Block
        );
    };
    (should_panic, $name:ident, $($input:literal),+ $(,)?) => {
        $crate::round_trip!(
            should_panic,
            $name,
            $($input),+
        );
    }
}

#[macro_export]
macro_rules! round_trip {
    (should_panic, $name:ident, $($input:literal),+ $(,)?) => {
        pub fn $name() -> Result<(), libtest_mimic::Failed> {
            let inputs = &[$($input),*];
            let mut storage = pulldown_latex::Storage::new();
            for input in inputs {
                let mut parser = pulldown_latex::parser::Parser::new(input, &storage);
                if !parser.all(|event| event.is_err()) {
                    return Err(libtest_mimic::Failed::without_message());
                };
                storage.reset();
            }
            Ok(())
        }

        inventory::submit! {
            $crate::common::TestCase {
                name: stringify!($name),
                test: $name
            }
        }
    };
    ($name:ident, $($input:literal),+ $(, $field:ident = $value:expr)* $(,)?) => {
        pub fn $name() -> Result<(), libtest_mimic::Failed> {
            let inputs = &[$($input),*];
            $crate::common::round_trip(stringify!($name), inputs, pulldown_latex::config::RenderConfig {
                $($field: $value,)*
                ..Default::default()
            })
        }

        inventory::submit! {
            $crate::common::TestCase {
                name: stringify!($name),
                test: $name
            }
        }
    };
}
