use serde_json::{Map, Value};

use super::{
    types::{AnyType, FlatModel, Model, ModelType},
    JsonSchemaExtractOptions, ModelContainer,
};
use crate::{error::Error, resolver::SchemaResolver, scope::SchemaScope};

pub fn from_pattern_properties(
    schema: &Map<String, Value>,
    container: &mut ModelContainer,
    scope: &mut SchemaScope,
    resolver: &SchemaResolver,
    options: &JsonSchemaExtractOptions,
) -> Result<Model, Error> {
    let name = super::title::extract_title(schema, scope, options)?;

    match schema.get("patternProperties") {
        Some(Value::Object(map)) => {
            scope.form("patternProperties");
            let types = {
                let types = map
                    .iter()
                    .map(|(pattern, value)| {
                        scope.form(pattern);
                        let result =
                            super::extract_type(value, container, scope, resolver, options)
                                .and_then(|m| m.flatten(container, scope));
                        scope.pop();

                        result
                    })
                    .collect::<Result<Vec<_>, Error>>();
                scope.pop();
                types
            }?;

            let model = {
                let first_type = types
                    .first()
                    .map(|s| s.type_.clone())
                    .unwrap_or_else(|| "string".to_string());
                let filtered = types.iter().filter(|f| f.type_ == first_type).count();

                if filtered != types.len() {
                    log::warn!("{}: patternProperties is mixed", scope);
                    AnyType::model(map, scope).flatten(container, scope)?
                } else {
                    types.first().unwrap().clone()
                }
            };

            Ok(Model::new(ModelType::FlatModel(FlatModel {
                name: Some(name),
                type_: "map".to_string(),
                model: Some(Box::new(model)),
                ..FlatModel::default()
            })))
        }
        _ => Err(Error::SchemaInvalidProperty(
            "patternProperties".to_string(),
        )),
    }
}

#[cfg(test)]
mod tests {
    use crate::codegen::jsonschema::types::Model;

    use super::*;
    use serde_json::json;

    #[test]
    fn test_should_convert_to_map() {
        let schema = json!({"patternProperties": {
            "[A-z]+": { "type": "string"},
            "[0-9]+": { "type": "string"}
        }});

        let mut container = ModelContainer::default();
        let mut scope = SchemaScope::default();
        let resolver = SchemaResolver::empty();
        let options = JsonSchemaExtractOptions::default();

        scope.entity("TestName");
        let result = from_pattern_properties(
            schema.as_object().unwrap(),
            &mut container,
            &mut scope,
            &resolver,
            &options,
        );

        assert_eq!(
            result.unwrap(),
            Model::new(ModelType::FlatModel(FlatModel {
                name: Some("TestName".to_string()),
                type_: "map".to_string(),
                model: Some(Box::new(FlatModel {
                    name: Some("TestName".to_string()),
                    type_: "string".to_string(),
                    ..FlatModel::default()
                })),
                ..FlatModel::default()
            }))
        );
    }

    #[test]
    fn test_should_convert_to_any_on_mixed() {
        let schema = json!({"patternProperties": {
            "[A-z]+": { "type": "string"},
            "[0-9]+": { "type": "number"}
        }});

        let mut container = ModelContainer::default();
        let mut scope = SchemaScope::default();
        let resolver = SchemaResolver::empty();
        let options = JsonSchemaExtractOptions::default();

        scope.entity("TestName");
        let result = from_pattern_properties(
            schema.as_object().unwrap(),
            &mut container,
            &mut scope,
            &resolver,
            &options,
        );

        assert_eq!(
            result.unwrap(),
            Model::new(ModelType::FlatModel(FlatModel {
                name: Some("TestName".to_string()),
                type_: "map".to_string(),
                model: Some(Box::new(FlatModel {
                    name: None,
                    type_: "any".to_string(),
                    ..FlatModel::default()
                })),
                ..FlatModel::default()
            }))
        );
    }
}
