mod document_output;
mod element_output;
mod item_output;
mod options;
mod response;
mod result;

pub use self::options::{RenderFormat, RenderOptions};
use self::{document_output::RenderedDocumentOutput, result::RenderedResult};
use crate::{DocumentOutput, error::Error};
use colored::Colorize;
use core::pin::pin;
use tokio::io::{AsyncWrite, AsyncWriteExt};

/// Renders a result of document validation.
pub async fn render_document(
    document: &DocumentOutput,
    options: &RenderOptions,
    writer: impl AsyncWrite,
) -> Result<(), Error> {
    let mut document = RenderedDocumentOutput::from(document);
    let mut writer = pin!(writer);

    if !options.verbose() {
        document.retain_error();
    }

    if !options.verbose()
        && document
            .elements()
            .all(|element| element.results().all(RenderedResult::is_ok))
    {
        return Ok(());
    }

    if options.format() == RenderFormat::Json {
        return render_json_document(&document, &mut writer).await;
    }

    render_line(
        &format!("{}", document.url().to_string().yellow()),
        &mut writer,
    )
    .await?;

    for output in document.elements() {
        render_line(
            &format!(
                "\t{} {}",
                output.element().name(),
                output
                    .element()
                    .attributes()
                    .iter()
                    .map(|(key, value)| format!("{key}=\"{value}\""))
                    .collect::<Vec<_>>()
                    .join(" "),
            ),
            &mut writer,
        )
        .await?;

        for result in output.results() {
            match result.result() {
                Ok(success) => {
                    render_line(
                        &success.response().map_or_else(
                            || "\t\tvalid URL".into(),
                            |response| {
                                format!(
                                    "\t\t{}\t{}\t{}",
                                    response.status().to_string().green(),
                                    response.url(),
                                    format!("{} ms", response.duration()).yellow()
                                )
                            },
                        ),
                        &mut writer,
                    )
                    .await?
                }
                Err(error) => {
                    render_line(&format!("\t\t{}\t{error}", "ERROR".red()), &mut writer).await?
                }
            }
        }
    }

    Ok(())
}

pub async fn render_json_document(
    document: &RenderedDocumentOutput<'_>,
    writer: &mut (impl AsyncWrite + Unpin),
) -> Result<(), Error> {
    render_line(&serde_json::to_string(&document)?, writer).await
}

async fn render_line(string: &str, writer: &mut (impl AsyncWrite + Unpin)) -> Result<(), Error> {
    writer.write_all(string.as_bytes()).await?;
    writer.write_all(b"\n").await?;

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{
        element::Element, element_output::ElementOutput, error::ItemError, item_output::ItemOutput,
        response::Response,
    };
    use core::str;
    use insta::assert_snapshot;
    use url::Url;

    fn mixed_document_output() -> DocumentOutput {
        DocumentOutput::new(
            Url::parse("https://foo.com").unwrap(),
            vec![ElementOutput::new(
                Element::new("foo".into(), vec![]),
                vec![
                    Ok(ItemOutput::default().with_response(
                        Response::new(
                            Url::parse("https://foo.com").unwrap(),
                            Default::default(),
                            Default::default(),
                            Default::default(),
                            Default::default(),
                        )
                        .into(),
                    )),
                    Err(ItemError::HtmlValidation(
                        muffy_validation::MarkupError::UnknownTag("foo".into()),
                    )),
                ],
            )],
        )
    }

    fn successful_document_output() -> DocumentOutput {
        DocumentOutput::new(
            Url::parse("https://foo.com").unwrap(),
            vec![ElementOutput::new(
                Element::new("a".into(), vec![]),
                vec![Ok(ItemOutput::default().with_response(
                    Response::new(
                        Url::parse("https://foo.com").unwrap(),
                        Default::default(),
                        Default::default(),
                        Default::default(),
                        Default::default(),
                    )
                    .into(),
                ))],
            )],
        )
    }

    mod json {
        use super::*;

        #[tokio::test]
        async fn render_error() {
            let mut string = vec![];

            render_document(
                &mixed_document_output(),
                &RenderOptions::default().set_format(RenderFormat::Json),
                &mut string,
            )
            .await
            .unwrap();

            assert_snapshot!(str::from_utf8(&string).unwrap());
        }

        #[tokio::test]
        async fn render_success_and_error_with_verbose_option() {
            let mut string = vec![];

            render_document(
                &mixed_document_output(),
                &RenderOptions::default()
                    .set_format(RenderFormat::Json)
                    .set_verbose(true),
                &mut string,
            )
            .await
            .unwrap();

            assert_snapshot!(str::from_utf8(&string).unwrap());
        }

        #[tokio::test]
        async fn render_success() {
            let mut string = vec![];

            render_document(
                &successful_document_output(),
                &RenderOptions::default().set_format(RenderFormat::Json),
                &mut string,
            )
            .await
            .unwrap();

            assert_snapshot!(str::from_utf8(&string).unwrap());
        }

        #[tokio::test]
        async fn render_success_with_verbose_option() {
            let mut string = vec![];

            render_document(
                &successful_document_output(),
                &RenderOptions::default()
                    .set_format(RenderFormat::Json)
                    .set_verbose(true),
                &mut string,
            )
            .await
            .unwrap();

            assert_snapshot!(str::from_utf8(&string).unwrap());
        }
    }

    mod text {
        use super::*;

        #[tokio::test]
        async fn render_error() {
            colored::control::set_override(false);
            let mut string = vec![];

            render_document(
                &mixed_document_output(),
                &RenderOptions::default(),
                &mut string,
            )
            .await
            .unwrap();

            assert_snapshot!(str::from_utf8(&string).unwrap());
        }

        #[tokio::test]
        async fn render_success_and_error_with_verbose_option() {
            colored::control::set_override(false);
            let mut string = vec![];

            render_document(
                &mixed_document_output(),
                &RenderOptions::default().set_verbose(true),
                &mut string,
            )
            .await
            .unwrap();

            assert_snapshot!(str::from_utf8(&string).unwrap());
        }

        #[tokio::test]
        async fn render_success() {
            colored::control::set_override(false);
            let mut string = vec![];

            render_document(
                &successful_document_output(),
                &RenderOptions::default(),
                &mut string,
            )
            .await
            .unwrap();

            assert_snapshot!(str::from_utf8(&string).unwrap());
        }

        #[tokio::test]
        async fn render_success_with_verbose_option() {
            colored::control::set_override(false);
            let mut string = vec![];

            render_document(
                &successful_document_output(),
                &RenderOptions::default().set_verbose(true),
                &mut string,
            )
            .await
            .unwrap();

            assert_snapshot!(str::from_utf8(&string).unwrap());
        }
    }
}
