//! Tooling for test manifest and case definitions
//!
//! This module contains tools to read RDF Test Cases
//! manifests and convert them into [libtest_mimic::Trial]s.

use crate::report::*;

use std::env::current_dir;

use futures::StreamExt;
use iref::IriBuf;
use libtest_mimic::Trial;

use taganak_core::prelude::*;
use taganak_core::terms::Iri;
use taganak_framework::{
    graphs::{
        local::{
            memory::simple::SimpleGraph,
            persistent::local_file::{LocalFileSourceGraph, LocalFileSourceGraphConfig},
        },
        ConfigurableGraph as _,
    },
    SELF_TERM,
};
use taganak_orm::{GraphORM, GraphORMMeta};

/// Try to fully parse a graph source
///
/// This iterates over all triples in the source graph, expecting it to yeild
/// an [std::result::Result::Ok] for every triple.
///
/// # Errors
///
/// This function errors on the occurrence of the first [std::result::Result::Err]
/// in the triple stream.
fn run_parse_test(test_graph: impl Graph) -> Result<(), GraphError> {
    let rt = tokio::runtime::Builder::new_current_thread()
        .enable_all()
        .build()
        .unwrap();

    rt.block_on(async move {
        let mut stream = test_graph.view().await.stream().await.unwrap();

        while let Some(res) = stream.next().await {
            if let Err(e) = res {
                return Err(e);
            }
        }

        // All triples yielded successfully
        Ok(())
    })
}

/// Parse two graphs and check isomorphism
///
/// # Errors
///
/// This function passes on errors from [GraphView::isomorphic].
fn run_compare_test(
    test_graph: impl Graph,
    reference_graph: impl Graph,
) -> Result<bool, GraphError> {
    let rt = tokio::runtime::Builder::new_current_thread()
        .enable_all()
        .build()
        .unwrap();

    rt.block_on(async move {
        let view1 = test_graph.view().await;
        let view2 = reference_graph.view().await;

        view1.isomorphic(view2).await
    })
}

/// A manifest containing test entries
#[derive(Debug, PartialEq, GraphORM)]
#[rdf(
    prefix(
        ns = "mf",
        iri = "http://www.w3.org/2001/sw/DataAccess/tests/test-manifest#"
    ),
    default(prefix = "mf", case_conv = true)
)]
pub(crate) struct Manifest {
    name: Option<String>,
    entries: Vec<GraphORMMeta<Test>>,
    include: Option<Vec<Term>>,
    assumed_test_base: Option<Term>,
}

/// A single manifest entry / test case
#[derive(Debug, PartialEq, Eq, GraphORM)]
#[rdf(
    prefix(
        ns = "mf",
        iri = "http://www.w3.org/2001/sw/DataAccess/tests/test-manifest#"
    ),
    prefix(ns = "rdft", iri = "http://www.w3.org/ns/rdftest#"),
    prefix(ns = "rdfs", iri = "http://www.w3.org/2000/01/rdf-schema#"),
    default(prefix = "mf", case_conv = true)
)]
enum Test {
    #[rdf(rdf_type = "rdft:TestNTriplesPositiveSyntax")]
    TestNTriplesPositiveSyntax {
        name: String,
        action: Term,
        assumed_test_base: Option<Term>,
    },
    #[rdf(rdf_type = "rdft:TestNTriplesNegativeSyntax")]
    TestNTriplesNegativeSyntax {
        name: String,
        action: Term,
        assumed_test_base: Option<Term>,
    },
    #[rdf(rdf_type = "rdft:TestTurtlePositiveSyntax")]
    TestTurtlePositiveSyntax {
        name: String,
        action: Term,
        assumed_test_base: Option<Term>,
    },
    #[rdf(rdf_type = "rdft:TestTurtleNegativeSyntax")]
    TestTurtleNegativeSyntax {
        name: String,
        action: Term,
        assumed_test_base: Option<Term>,
    },
    #[rdf(rdf_type = "rdft:TestTurtleEval")]
    TestTurtleEval {
        name: String,
        action: Term,
        result: Term,
        assumed_test_base: Option<Term>,
    },
    #[rdf(rdf_type = "rdft:TestTurtleNegativeEval")]
    TestTurtleNegativeEval {
        name: String,
        action: Term,
        assumed_test_base: Option<Term>,
    },
}

impl Test {
    /// Assume a different base from the IRI the manifest is loaded from
    ///
    /// Some manifests, e.g. eval and canonicalization tests, require several
    /// sources to share the same base for comparison. Thus, a base to be assumed
    /// is specified in the manifest definition.
    fn assume_base(&mut self, base: Term) {
        let assumed_base = match self {
            Test::TestNTriplesPositiveSyntax {
                assumed_test_base, ..
            } => assumed_test_base,
            Test::TestNTriplesNegativeSyntax {
                assumed_test_base, ..
            } => assumed_test_base,
            Test::TestTurtlePositiveSyntax {
                assumed_test_base, ..
            } => assumed_test_base,
            Test::TestTurtleNegativeSyntax {
                assumed_test_base, ..
            } => assumed_test_base,
            Test::TestTurtleEval {
                assumed_test_base, ..
            } => assumed_test_base,
            Test::TestTurtleNegativeEval {
                assumed_test_base, ..
            } => assumed_test_base,
        };

        let _ = assumed_base.insert(base);
    }

    fn into_trial(self, subject: Arc<Term>) -> Trial {
        match self {
            Test::TestNTriplesPositiveSyntax {
                name,
                action,
                assumed_test_base,
            } => Trial::test(name, move || {
                let test_graph =
                    LocalFileSourceGraph::from_config(LocalFileSourceGraphConfig::new(
                        action.into_iri().unwrap(),
                        assumed_test_base.and_then(|t| Some(t.into_iri().unwrap())),
                        Some("application/n-triples".to_string()),
                        false,
                    ))
                    .unwrap();

                let res = run_parse_test(test_graph);

                Assertion {
                    subject: SELF_TERM.clone(),
                    test: subject.clone(),
                    result: TestResult {
                        outcome: if res.is_ok() {
                            OutcomeValue::Passed
                        } else {
                            OutcomeValue::Failed
                        },
                    },
                }
                .record()
                .unwrap();

                Ok(res?)
            })
            .with_kind("NTriplesPositiveSyntax"),
            Test::TestNTriplesNegativeSyntax {
                name,
                action,
                assumed_test_base,
            } => Trial::test(name, move || {
                let test_graph =
                    LocalFileSourceGraph::from_config(LocalFileSourceGraphConfig::new(
                        action.into_iri().unwrap(),
                        assumed_test_base.and_then(|t| Some(t.into_iri().unwrap())),
                        Some("application/n-triples".to_string()),
                        false,
                    ))
                    .unwrap();

                let res = run_parse_test(test_graph);

                Assertion {
                    subject: SELF_TERM.clone(),
                    test: subject.clone(),
                    result: TestResult {
                        outcome: if res.is_err() {
                            OutcomeValue::Passed
                        } else {
                            OutcomeValue::Failed
                        },
                    },
                }
                .record()
                .unwrap();

                match res {
                    Ok(()) => Err("should have failed".into()),
                    Err(_) => Ok(()),
                }
            })
            .with_kind("NTriplesNegativeSyntax"),
            Test::TestTurtlePositiveSyntax {
                name,
                action,
                assumed_test_base,
            } => Trial::test(name, move || {
                let test_graph =
                    LocalFileSourceGraph::from_config(LocalFileSourceGraphConfig::new(
                        action.into_iri().unwrap(),
                        assumed_test_base.and_then(|t| Some(t.into_iri().unwrap())),
                        Some("text/turtle".to_string()),
                        false,
                    ))
                    .unwrap();

                let res = run_parse_test(test_graph);

                Assertion {
                    subject: SELF_TERM.clone(),
                    test: subject.clone(),
                    result: TestResult {
                        outcome: if res.is_ok() {
                            OutcomeValue::Passed
                        } else {
                            OutcomeValue::Failed
                        },
                    },
                }
                .record()
                .unwrap();

                Ok(res?)
            })
            .with_kind("TurtlePositiveSyntax"),
            Test::TestTurtleNegativeSyntax {
                name,
                action,
                assumed_test_base,
            } => Trial::test(name, move || {
                let test_graph =
                    LocalFileSourceGraph::from_config(LocalFileSourceGraphConfig::new(
                        action.into_iri().unwrap(),
                        assumed_test_base.and_then(|t| Some(t.into_iri().unwrap())),
                        Some("text/turtle".to_string()),
                        false,
                    ))
                    .unwrap();

                let res = run_parse_test(test_graph);

                Assertion {
                    subject: SELF_TERM.clone(),
                    test: subject.clone(),
                    result: TestResult {
                        outcome: if res.is_err() {
                            OutcomeValue::Passed
                        } else {
                            OutcomeValue::Failed
                        },
                    },
                }
                .record()
                .unwrap();

                match res {
                    Ok(()) => Err("should have failed".into()),
                    Err(_) => Ok(()),
                }
            })
            .with_kind("TurtleNegativeSyntax"),
            Test::TestTurtleEval {
                name,
                action,
                result,
                assumed_test_base,
            } => Trial::test(name, move || {
                let test_graph =
                    LocalFileSourceGraph::from_config(LocalFileSourceGraphConfig::new(
                        action.into_iri().unwrap(),
                        assumed_test_base
                            .clone()
                            .and_then(|t| Some(t.into_iri().unwrap())),
                        Some("text/turtle".to_string()),
                        false,
                    ))
                    .unwrap();
                let reference_graph =
                    LocalFileSourceGraph::from_config(LocalFileSourceGraphConfig::new(
                        result.into_iri().unwrap(),
                        assumed_test_base.and_then(|t| Some(t.into_iri().unwrap())),
                        Some("application/n-triples".to_string()),
                        false,
                    ))
                    .unwrap();

                let res = run_compare_test(test_graph, reference_graph);

                Assertion {
                    subject: SELF_TERM.clone(),
                    test: subject.clone(),
                    result: TestResult {
                        outcome: if let Ok(true) = res {
                            OutcomeValue::Passed
                        } else {
                            OutcomeValue::Failed
                        },
                    },
                }
                .record()
                .unwrap();

                match res {
                    Ok(true) => Ok(()),
                    Ok(false) => Err("not isomorphic".into()),
                    Err(_) => Err("parser failed".into()),
                }
            })
            .with_kind("TurtleEval"),
            Test::TestTurtleNegativeEval {
                name,
                action,
                assumed_test_base,
            } => Trial::test(name, move || {
                let test_graph =
                    LocalFileSourceGraph::from_config(LocalFileSourceGraphConfig::new(
                        action.into_iri().unwrap(),
                        assumed_test_base.and_then(|t| Some(t.into_iri().unwrap())),
                        Some("text/turtle".to_string()),
                        false,
                    ))
                    .unwrap();

                let res = run_parse_test(test_graph);

                Assertion {
                    subject: SELF_TERM.clone(),
                    test: subject.clone(),
                    result: TestResult {
                        outcome: if res.is_err() {
                            OutcomeValue::Passed
                        } else {
                            OutcomeValue::Failed
                        },
                    },
                }
                .record()
                .unwrap();

                match res {
                    Ok(()) => Err("should have failed".into()),
                    Err(_) => Ok(()),
                }
            })
            .with_kind("TurtleNegativeEval"),
        }
    }
}

impl Manifest {
    /// Load a manifest from a local file relative to the test cases submodule
    ///
    /// This method merely constructs a `file://` URI and calls [Self::from_iri].
    pub(crate) async fn from_file(manifest_file: &str) -> Result<Self, std::io::Error> {
        let mut path = current_dir()?;
        path.push("tests/rdf_test_suites/rdf-tests");
        path.push(manifest_file);

        let iri = Iri::new(format!("file://{}", path.to_str().unwrap())).unwrap();

        Self::from_iri(iri).await
    }

    /// Load a manifest from a source IRI
    ///
    /// # Panics
    ///
    /// This method panics if the source does not exist, is not parsable, or contains no or more than one
    /// manifest.
    async fn from_iri(iri: Iri) -> Result<Self, std::io::Error> {
        let graph = LocalFileSourceGraph::from_config(LocalFileSourceGraphConfig::new(
            iri.clone(),
            None,
            // FIXME remove when LocalFileSourceGraph leanrs how to guess the media type
            Some("text/turtle".to_string()),
            false,
        ))
        .unwrap();

        // FIXME fix parser for blank node stability
        let mut graph2 = SimpleGraph::default();
        graph2.merge(graph.view().await).await.unwrap();

        // RDF 1.2 test manifests don't have stable subjects for the manifest, but the containing files
        // are somewhat promised to only ever contain one mf:Manifest resource
        let subject_term = graph2
            .view()
            .await
            .subject(
                Some(
                    &"<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>"
                        .try_into()
                        .unwrap(),
                ),
                Some(
                    &"<http://www.w3.org/2001/sw/DataAccess/tests/test-manifest#Manifest>"
                        .try_into()
                        .unwrap(),
                ),
            )
            .await
            .unwrap()
            .unwrap();

        let mut manifest = Manifest::deserialize(graph2.view().await, &subject_term)
            .await
            .unwrap()
            .into_deref();

        if let Some(ref base) = manifest.assumed_test_base {
            for test in manifest.entries.iter_mut() {
                test.assume_base(base.clone());
            }
        }

        // Recursively flatten included manifests into this one
        Box::pin(manifest.resolve_include()).await?;

        Ok(manifest)
    }

    /// Merge entries from another manifest into this one
    fn merge(&mut self, mut other: Manifest) {
        self.entries.append(&mut other.entries);

        if let Some(mut includes) = other.include {
            let current = self.include.take();
            if current.is_none() {
                let _ = self.include.insert(includes);
            } else {
                let mut current = current.unwrap();
                current.append(&mut includes);
                let _ = self.include.insert(current);
            }
        }
    }

    /// Load included manifests and [Self::merge] into this one
    async fn resolve_include(&mut self) -> Result<(), std::io::Error> {
        if self.include.is_none() {
            return Ok(());
        }
        let mut includes = self.include.take().unwrap();

        while let Some(include_iri) = includes.pop() {
            let manifest = Manifest::from_iri(
                include_iri
                    .into_iri()
                    .map_err(|_| std::io::Error::other("include is not a valid IRI"))?,
            )
            .await?;
            self.merge(manifest);
        }

        Ok(())
    }
}

impl From<Manifest> for Vec<Trial> {
    fn from(value: Manifest) -> Self {
        value
            .entries
            .into_iter()
            .map(|t| {
                let subject = t.subject().unwrap();
                t.into_deref().into_trial(subject)
            })
            .collect()
    }
}
