use bson::{Bson, Document};
use serde_json::{self, Map, Value};
use std::fs::File;

use super::arguments::Arguments;
use super::outcome::Outcome;

pub struct Test {
    pub operation: Arguments,
    pub outcome: Outcome,
}

impl Test {
    fn from_json(object: &Map<String, Value>) -> Result<Test, String> {
        macro_rules! res_or_err {
            ($exp:expr) => { match $exp {
                Ok(a) => a,
                Err(s) => return Err(s)
            }};
        }

        let op = val_or_err!(object.get("operation"),
                             Some(&Value::Object(ref obj)) => obj.clone(),
                             "`operation` must be an object");

        let args_obj = val_or_err!(op.get("arguments"),
                                   Some(&Value::Object(ref obj)) => obj.clone(),
                                   "`arguments` must be an object");

        let name = val_or_err!(op.get("name"),
                               Some(&Value::String(ref s)) => s,
                               "`name` must be a string");

        let args = match name.as_ref() {
            "aggregate" => res_or_err!(Arguments::aggregate_from_json(&args_obj)),
            "count" => Arguments::count_from_json(&args_obj),
            "deleteMany" => res_or_err!(Arguments::delete_from_json(&args_obj, true)),
            "deleteOne" => res_or_err!(Arguments::delete_from_json(&args_obj, false)),
            "distinct" => res_or_err!(Arguments::distinct_from_json(&args_obj)),
            "find" => Arguments::find_from_json(&args_obj),
            "findOneAndDelete" => res_or_err!(Arguments::find_one_and_delete_from_json(&args_obj)),
            "findOneAndReplace" => {
                res_or_err!(Arguments::find_one_and_replace_from_json(&args_obj))
            }
            "findOneAndUpdate" => res_or_err!(Arguments::find_one_and_update_from_json(&args_obj)),
            "insertMany" => res_or_err!(Arguments::insert_many_from_json(&args_obj)),
            "insertOne" => res_or_err!(Arguments::insert_one_from_json(&args_obj)),
            "replaceOne" => res_or_err!(Arguments::replace_one_from_json(&args_obj)),
            "updateMany" => res_or_err!(Arguments::update_from_json(&args_obj, true)),
            "updateOne" => res_or_err!(Arguments::update_from_json(&args_obj, false)),
            _ => return Err(String::from("Invalid operation name")),
        };


        let outcome_obj = val_or_err!(object.get("outcome"),
                                      Some(&Value::Object(ref obj)) => obj.clone(),
                                      "`outcome` must be an object");

        let outcome = match Outcome::from_json(&outcome_obj) {
            Ok(outcome) => outcome,
            Err(s) => return Err(s),
        };

        Ok(Test {
            operation: args,
            outcome: outcome,
        })
    }
}

pub struct Suite {
    pub data: Vec<Document>,
    pub tests: Vec<Test>,
}

fn get_data(object: &Map<String, Value>) -> Result<Vec<Document>, String> {
    let array = val_or_err!(object.get("data"),
                            Some(&Value::Array(ref arr)) => arr.clone(),
                            "No `data` array found");
    let mut data = vec![];

    for json in array {
        match Bson::from(json) {
            Bson::Document(doc) => data.push(doc),
            _ => return Err(String::from("`data` array must contain only objects")),
        }
    }

    Ok(data)
}

fn get_tests(object: &Map<String, Value>) -> Result<Vec<Test>, String> {
    let array = val_or_err!(object.get("tests"),
                            Some(&Value::Array(ref array)) => array.clone(),
                            "No `tests` array found");

    let mut tests = vec![];

    for json in array {
        let obj = val_or_err!(json,
                              Value::Object(ref obj) => obj.clone(),
                              "`tests` array must only contain objects");

        let test = match Test::from_json(&obj) {
            Ok(test) => test,
            Err(s) => return Err(s),
        };

        tests.push(test);
    }

    Ok(tests)
}

pub trait SuiteContainer: Sized {
    fn from_file(path: &str) -> Result<Self, String>;
    fn get_suite(&self) -> Result<Suite, String>;
}

impl SuiteContainer for Value {
    fn from_file(path: &str) -> Result<Value, String> {
        let mut file = File::open(path).expect(&format!("Unable to open file: {}", path));
        Ok(serde_json::from_reader(&mut file).expect(
            &format!("Invalid JSON file: {}", path),
        ))
    }

    fn get_suite(&self) -> Result<Suite, String> {
        let object = val_or_err!(*self,
                                 Value::Object(ref object) => object.clone(),
                                 "`get_suite` requires a JSON object");

        let data = try!(get_data(&object));
        let tests = try!(get_tests(&object));

        Ok(Suite {
            data: data,
            tests: tests,
        })
    }
}