use crate::{
    factory,
    store::{Store, VariableStore},
    RatError, TestResult,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

#[derive(Serialize, Deserialize, Debug)]
pub struct Config {
    pub environment: HashMap<String, serde_json::Value>,
    pub tests: Vec<TestConfig>,
}

#[derive(Serialize, Deserialize, Debug)]
pub struct TestConfig {
    #[serde(flatten)]
    pub base: TestBaseConfiguration,
    #[serde(flatten)]
    pub multi_step: MultiStepTestConfig,
    #[serde(flatten)]
    pub endpoint: TestEndpoint,
}

impl TestConfig {
    pub fn log<F>(&self, log: F)
    where
        F: Fn() -> (),
    {
        if Some(true) == self.base.verbose {
            log()
        }
    }

    pub fn parse(self) -> TestConfiguration {
        (self.base, self.endpoint, self.multi_step)
    }
}

#[derive(Serialize, Deserialize, Debug)]
pub struct TestBaseConfiguration {
    pub name: String,
    pub verbose: Option<bool>,
    pub assertions: Vec<String>,
}

#[derive(Serialize, Deserialize, Debug)]
pub struct TestEndpoint {
    pub method: String,
    pub url: String,
}

#[derive(Serialize, Deserialize, Debug)]
pub struct MultiStepTestConfig {
    pub outputs: HashMap<String, String>,
}

impl TestBaseConfiguration {
    pub fn assert<S: Store>(self, store: &S) -> TestResult {
        let mut test = true;

        let results: Vec<String> = self
            .assertions
            .iter()
            .map(|t| {
                let assertion = factory::match_and_replace(store, t.as_ref());
                let test_result = evalexpr::eval(&assertion) == Ok(evalexpr::Value::Boolean(true));
                test &= test_result;
                format!("{} {}", TestResult::report_test(test_result), assertion)
            })
            .collect();

        TestResult {
            name: self.name,
            result: test,
            assertions: results,
        }
    }
}

pub type TestConfiguration = (TestBaseConfiguration, TestEndpoint, MultiStepTestConfig);

impl Config {
    pub fn read() -> Result<Self, RatError> {
        let contents = std::fs::read("./config.json")?;
        Self::new(contents)
    }

    pub fn new<T: AsRef<[u8]>>(contents: T) -> Result<Self, RatError> {
        //let contents = std::fs::read("./config.json")?;
        let config = serde_json::from_slice(contents.as_ref())?;
        Ok(config)
    }

    pub fn load() -> Result<(VariableStore, Vec<TestConfiguration>), RatError> {
        Self::init_logging();

        let config = Self::read()?;

        let store: VariableStore = config.environment.into();
        let tests = config.tests.into_iter().map(|t| t.parse()).collect();

        Ok((store, tests))
    }

    pub fn init_logging() {
        use chrono::{DateTime, Utc};
        use simplelog::{
            ColorChoice, CombinedLogger, ConfigBuilder, TermLogger, TerminalMode, WriteLogger,
        };

        let datetime: DateTime<Utc> = chrono::offset::Utc::now();

        CombinedLogger::init(vec![
            TermLogger::new(
                log::LevelFilter::Info,
                ConfigBuilder::default().build(),
                TerminalMode::Mixed,
                ColorChoice::Auto,
            ),
            WriteLogger::new(
                log::LevelFilter::Debug,
                ConfigBuilder::default()
                    //.add_filter_ignore_str("yup_oauth2")
                    .build(),
                std::fs::File::create(format!("{}.log", datetime.format("%Y-%m-%dT%H"))).unwrap(),
            ),
        ])
        .unwrap();
    }
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn test() {
        let config = Config::new(multi_step());
        assert!(config.is_ok());
        let config = config.unwrap();
        assert_eq!(config.tests[0].base.name, "get first todo");
        assert_eq!(config.tests[0].endpoint.url, "{{base}}/todos/1");
        assert_eq!(config.tests[0].multi_step.outputs.len(), 2);
        assert_eq!(
            config.tests[0]
                .multi_step
                .outputs
                .get("title")
                .and_then(|s| Some(s.as_str())),
            Some("{{r.body.title}}")
        );
    }

    fn multi_step() -> &'static str {
        r#"{
            "environment": {
              "base": "https://jsonplaceholder.typicode.com"
            },
            "tests": [
              {
                "name": "get first todo",
                "method": "GET",
                "url": "{{base}}/todos/1",
                "verbose": true,
                "assertions": [
                  "{{r.status}} == 200",
                  "{{r.headers.content-length}} > 0",
                  "\"{{r.headers.content-type}}\" == \"application/json; charset=utf-8\"",
                  "\"{{r.body.title}}\" == \"delectus aut autem\""
                ],
                "outputs": {
                  "title": "{{r.body.title}}",
                  "userId": "{{r.body.userId}}"
                }
              },
              {
                "name": "get posts using last steps userId from last step",
                "method": "GET",
                "url": "{{base}}/posts?userId={{userId}}",
                "verbose": true,
                "assertions": [
                  "{{userId}} == 1",
                  "\"{{title}}\" == \"delectus aut autem\"",
                  "{{r.status}} == 200",
                  "\"{{r.headers.content-type}}\" == \"application/json; charset=utf-8\"",
                  "\"{{r.body.[0].title}}\" == \"sunt aut facere repellat provident occaecati excepturi optio reprehenderit\""
                ],
                "outputs": {}
              }
            ]
          }
          "#
    }
}