use crate::model;
use crate::model::Function;
use crate::model::GlobalTypeProperty;
use crate::model::GlobalTypeValue;
use crate::model::InputProperty;
use crate::model::NumberEnumElement;
use crate::model::OutputProperty;
use crate::model::Ref;
use crate::model::StringEnumElement;
use crate::model::*;
use crate::model::{ElementId, Resource};
use itertools::Itertools;
use proptest::arbitrary::any;
use proptest::prelude::Just;
use proptest::prelude::{Arbitrary, prop};
use proptest::prelude::{BoxedStrategy, Strategy};
use proptest::prop_oneof;
use std::collections::BTreeMap;

pub fn string_strategy() -> impl Strategy<Value = String> {
    prop::collection::vec(
        prop::sample::select(
            "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
                .chars()
                .collect_vec(),
        ),
        1..=5,
    )
    .prop_map(|chars| chars.into_iter().collect::<String>())
}

pub fn vec_strategy<T: Arbitrary>() -> impl Strategy<Value = Vec<T>> {
    prop::collection::vec(any::<T>(), 0..=5)
}

impl Arbitrary for model::Package {
    type Parameters = ();
    type Strategy = BoxedStrategy<Self>;

    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
        (
            string_strategy(),
            prop::option::of(string_strategy()),
            prop::option::of(string_strategy()),
            string_strategy(),
            any::<Provider>(),
            vec_strategy::<Resource>(),
            vec_strategy::<Function>(),
            vec_strategy::<GlobalType>(),
        )
            .prop_map(
                |(
                    name,
                    display_name,
                    plugin_download_url,
                    version,
                    provider,
                    resources,
                    functions,
                    types,
                )| {
                    let mut resource_name_map = BTreeMap::new();
                    for resource in resources {
                        resource_name_map.insert(resource.element_id.clone(), resource);
                    }

                    let mut function_name_map = BTreeMap::new();
                    for function in functions {
                        function_name_map.insert(function.element_id.clone(), function);
                    }

                    let mut types_map = BTreeMap::new();
                    for global_type in types {
                        types_map.insert(global_type.element_id.clone(), global_type);
                    }

                    model::Package::new(
                        name,
                        display_name,
                        plugin_download_url,
                        version,
                        provider,
                        resource_name_map,
                        function_name_map,
                        types_map,
                    )
                },
            )
            .boxed()
    }
}

impl Arbitrary for Provider {
    type Parameters = ();
    type Strategy = BoxedStrategy<Self>;

    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
        (
            prop::option::of(string_strategy()),
            vec_strategy::<InputProperty>(),
            vec_strategy::<OutputProperty>(),
        )
            .prop_map(
                |(description, input_properties, output_properties)| Provider {
                    description,
                    input_properties,
                    output_properties,
                },
            )
            .boxed()
    }
}

impl Arbitrary for Type {
    type Parameters = ();
    type Strategy = BoxedStrategy<Self>;

    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
        let leaf = prop_oneof![
            Just(Type::Boolean),
            Just(Type::Integer),
            Just(Type::Number),
            Just(Type::String),
            string_strategy().prop_map(Type::ConstString),
            any::<Ref>().prop_map(Type::Ref)
        ];

        leaf.prop_recursive(4, 20, 10, |inner| {
            prop_oneof![
                inner.clone().prop_map(|t| Type::Array(Box::new(t))),
                inner.clone().prop_map(|t| Type::Object(Box::new(t))),
                inner.clone().prop_map(|t| Type::Option(Box::new(t))),
                prop::collection::vec(inner, 1..5).prop_map(Type::DiscriminatedUnion)
            ]
        })
        .boxed()
    }
}

impl Arbitrary for InputProperty {
    type Parameters = ();
    type Strategy = BoxedStrategy<Self>;

    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
        (
            string_strategy(),
            any::<Type>(),
            prop::option::of(string_strategy()),
        )
            .prop_map(|(name, r#type, description)| InputProperty {
                name,
                r#type,
                description,
            })
            .boxed()
    }
}

impl Arbitrary for OutputProperty {
    type Parameters = ();
    type Strategy = BoxedStrategy<Self>;

    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
        (
            string_strategy(),
            any::<Type>(),
            prop::option::of(string_strategy()),
        )
            .prop_map(|(name, r#type, description)| OutputProperty {
                name,
                r#type,
                description,
            })
            .boxed()
    }
}

impl Arbitrary for GlobalTypeProperty {
    type Parameters = ();
    type Strategy = BoxedStrategy<Self>;

    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
        (
            string_strategy(),
            any::<Type>(),
            prop::option::of(string_strategy()),
        )
            .prop_map(|(name, r#type, description)| GlobalTypeProperty {
                name,
                r#type,
                description,
            })
            .boxed()
    }
}

impl Arbitrary for ElementId {
    type Parameters = ();
    type Strategy = BoxedStrategy<Self>;

    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
        (
            string_strategy(),
            prop::collection::vec(string_strategy(), 0..=3),
            string_strategy(),
        )
            .prop_map(|(raw, namespace, name)| ElementId {
                raw,
                namespace,
                name,
            })
            .boxed()
    }
}

impl Arbitrary for Ref {
    type Parameters = ();
    type Strategy = BoxedStrategy<Self>;

    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
        prop_oneof![
            Just(Ref::Archive),
            Just(Ref::Asset),
            Just(Ref::Any),
            any::<ElementId>().prop_map(Ref::Type)
        ]
        .boxed()
    }
}

impl Arbitrary for GlobalTypeValue {
    type Parameters = ();
    type Strategy = BoxedStrategy<Self>;

    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
        prop_oneof![
            (
                prop::option::of(string_strategy()),
                vec_strategy::<GlobalTypeProperty>()
            )
                .prop_map(|(desc, props)| GlobalTypeValue::Object(desc, props)),
            (
                prop::option::of(string_strategy()),
                vec_strategy::<StringEnumElement>()
            )
                .prop_map(|(desc, elements)| GlobalTypeValue::StringEnum(desc, elements)),
            (
                prop::option::of(string_strategy()),
                vec_strategy::<NumberEnumElement>()
            )
                .prop_map(|(desc, elements)| GlobalTypeValue::NumberEnum(desc, elements)),
            (
                prop::option::of(string_strategy()),
                vec_strategy::<IntegerEnumElement>()
            )
                .prop_map(|(desc, elements)| GlobalTypeValue::IntegerEnum(desc, elements))
        ]
        .boxed()
    }
}

impl Arbitrary for GlobalType {
    type Parameters = ();
    type Strategy = BoxedStrategy<Self>;

    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
        (any::<ElementId>(), any::<GlobalTypeValue>())
            .prop_map(|(element_id, value)| GlobalType { element_id, value })
            .boxed()
    }
}

impl Arbitrary for StringEnumElement {
    type Parameters = ();
    type Strategy = BoxedStrategy<Self>;

    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
        (
            string_strategy(),
            string_strategy(),
            prop::option::of(string_strategy()),
        )
            .prop_map(|(name, value, description)| StringEnumElement {
                name,
                value,
                description,
            })
            .boxed()
    }
}

impl Arbitrary for NumberEnumElement {
    type Parameters = ();
    type Strategy = BoxedStrategy<Self>;

    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
        (
            string_strategy(),
            prop::num::f64::NORMAL,
            prop::option::of(string_strategy()),
        )
            .prop_map(|(name, value, description)| NumberEnumElement {
                name,
                value,
                description,
            })
            .boxed()
    }
}

impl Arbitrary for IntegerEnumElement {
    type Parameters = ();
    type Strategy = BoxedStrategy<Self>;

    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
        (
            string_strategy(),
            prop::num::i64::ANY,
            prop::option::of(string_strategy()),
        )
            .prop_map(|(name, value, description)| IntegerEnumElement {
                name,
                value,
                description,
            })
            .boxed()
    }
}

impl Arbitrary for Resource {
    type Parameters = ();
    type Strategy = BoxedStrategy<Self>;

    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
        (
            any::<ElementId>(),
            prop::option::of(string_strategy()),
            vec_strategy::<InputProperty>(),
            vec_strategy::<OutputProperty>(),
        )
            .prop_map(
                |(element_id, description, input_properties, output_properties)| Resource {
                    element_id,
                    description,
                    input_properties,
                    output_properties,
                },
            )
            .boxed()
    }
}

impl Arbitrary for Function {
    type Parameters = ();
    type Strategy = BoxedStrategy<Self>;

    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
        (
            any::<ElementId>(),
            prop::option::of(string_strategy()),
            vec_strategy::<InputProperty>(),
            vec_strategy::<OutputProperty>(),
        )
            .prop_map(
                |(element_id, description, input_properties, output_properties)| Function {
                    element_id,
                    description,
                    input_properties,
                    output_properties,
                },
            )
            .boxed()
    }
}
