use crate::prelude::*;
use pretty_assertions::assert_eq;
use schemars::generate::SchemaSettings;
use std::collections::BTreeMap;

#[derive(JsonSchema, Deserialize, Serialize)]
struct UnitStruct;

#[derive(JsonSchema, Deserialize, Serialize, Default)]
struct Struct {
    foo: i32,
    bar: bool,
}

#[derive(JsonSchema, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
enum External {
    UnitOne,
    StringMap(BTreeMap<String, String>),
    UnitStructNewType(UnitStruct),
    StructNewType(Struct),
    Struct {
        foo: i32,
        bar: bool,
    },
    EmptyTuple(),
    Tuple(i32, bool),
    UnitTwo,
    #[serde(with = "unit_variant_as_u64")]
    #[schemars(with = "u64")]
    UnitAsInt,
    #[serde(with = "tuple_variant_as_str")]
    #[schemars(schema_with = "tuple_variant_as_str::json_schema")]
    TupleAsStr(i32, bool),
}

impl External {
    fn values() -> impl IntoIterator<Item = Self> {
        [
            Self::UnitOne,
            Self::StringMap(
                [("hello".to_owned(), "world".to_owned())]
                    .into_iter()
                    .collect(),
            ),
            Self::UnitStructNewType(UnitStruct),
            Self::StructNewType(Struct {
                foo: 123,
                bar: true,
            }),
            Self::Struct {
                foo: 123,
                bar: true,
            },
            // Ignored due to https://github.com/serde-rs/json/issues/1084
            // Self::EmptyTuple(),
            Self::Tuple(456, false),
            Self::UnitTwo,
            Self::UnitAsInt,
            Self::TupleAsStr(789, true),
        ]
    }
}

#[derive(JsonSchema, Deserialize, Serialize)]
#[serde(tag = "tag")]
enum Internal {
    UnitOne,
    StringMap(BTreeMap<String, String>),
    UnitStructNewType(UnitStruct),
    StructNewType(Struct),
    Struct { foo: i32, bar: bool },
    // Internally-tagged enums don't support tuple variants
    //  EmptyTuple(),
    //  Tuple(i32, bool),
    UnitTwo,
    // Internally-tagged enum variants don't support non-object "payloads"
    //  #[serde(with = "unit_variant_as_u64")]
    //  #[schemars(with = "u64")]
    //  UnitAsInt,
    // Internally-tagged enums don't support tuple variants
    //  #[serde(with = "tuple_variant_as_str")]
    //  #[schemars(schema_with = "tuple_variant_as_str::json_schema")]
    //  TupleAsStr(i32, bool),
}

impl Internal {
    fn values() -> impl IntoIterator<Item = Self> {
        [
            Self::UnitOne,
            Self::StringMap(
                [("hello".to_owned(), "world".to_owned())]
                    .into_iter()
                    .collect(),
            ),
            Self::UnitStructNewType(UnitStruct),
            Self::StructNewType(Struct {
                foo: 123,
                bar: true,
            }),
            Self::Struct {
                foo: 123,
                bar: true,
            },
            // Self::EmptyTuple(),
            // Self::Tuple(456, false),
            Self::UnitTwo,
            // Self::UnitAsInt,
            // Self::TupleAsStr(789, true),
        ]
    }
}

#[derive(JsonSchema, Deserialize, Serialize)]
#[serde(tag = "tag", content = "content")]
enum Adjacent {
    UnitOne,
    StringMap(BTreeMap<String, String>),
    UnitStructNewType(UnitStruct),
    StructNewType(Struct),
    Struct {
        foo: i32,
        bar: bool,
    },
    EmptyTuple(),
    Tuple(i32, bool),
    UnitTwo,
    #[serde(with = "unit_variant_as_u64")]
    #[schemars(with = "u64")]
    UnitAsInt,
    #[serde(with = "tuple_variant_as_str")]
    #[schemars(schema_with = "tuple_variant_as_str::json_schema")]
    TupleAsStr(i32, bool),
}

impl Adjacent {
    fn values() -> impl IntoIterator<Item = Self> {
        [
            Self::UnitOne,
            Self::StringMap(
                [("hello".to_owned(), "world".to_owned())]
                    .into_iter()
                    .collect(),
            ),
            Self::UnitStructNewType(UnitStruct),
            Self::StructNewType(Struct {
                foo: 123,
                bar: true,
            }),
            Self::Struct {
                foo: 123,
                bar: true,
            },
            Self::EmptyTuple(),
            Self::Tuple(456, false),
            Self::UnitTwo,
            Self::UnitAsInt,
            Self::TupleAsStr(789, true),
        ]
    }
}

#[derive(JsonSchema, Deserialize, Serialize)]
#[serde(untagged)]
enum Untagged {
    UnitOne,
    StringMap(BTreeMap<String, String>),
    UnitStructNewType(UnitStruct),
    StructNewType(Struct),
    Struct {
        foo: i32,
        bar: bool,
    },
    EmptyTuple(),
    Tuple(i32, bool),
    UnitTwo,
    #[serde(with = "unit_variant_as_u64")]
    #[schemars(with = "u64")]
    UnitAsInt,
    #[serde(with = "tuple_variant_as_str")]
    #[schemars(schema_with = "tuple_variant_as_str::json_schema")]
    TupleAsStr(i32, bool),
}

impl Untagged {
    fn values() -> impl IntoIterator<Item = Self> {
        [
            Self::UnitOne,
            Self::StringMap(
                [("hello".to_owned(), "world".to_owned())]
                    .into_iter()
                    .collect(),
            ),
            Self::UnitStructNewType(UnitStruct),
            Self::StructNewType(Struct {
                foo: 123,
                bar: true,
            }),
            Self::Struct {
                foo: 123,
                bar: true,
            },
            Self::EmptyTuple(),
            Self::Tuple(456, false),
            Self::UnitTwo,
            Self::UnitAsInt,
            Self::TupleAsStr(789, true),
        ]
    }
}

mod unit_variant_as_u64 {
    pub(super) fn serialize<S>(ser: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        ser.serialize_u64(42)
    }

    pub(super) fn deserialize<'de, D>(deser: D) -> Result<(), D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        use serde::de::Deserialize;

        u64::deserialize(deser).map(|_| ())
    }
}

mod tuple_variant_as_str {
    pub(super) fn serialize<S>(i: &i32, b: &bool, ser: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        ser.collect_str(&format_args!("{i} {b}"))
    }

    pub(super) fn deserialize<'de, D>(deser: D) -> Result<(i32, bool), D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        use serde::de::{Deserialize, Error};
        let error = || Error::custom("invalid string");

        let (i, b) = <&str>::deserialize(deser)?
            .split_once(' ')
            .ok_or_else(error)?;

        Ok((
            i.parse().map_err(|_| error())?,
            b.parse().map_err(|_| error())?,
        ))
    }

    pub(super) fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
        schemars::json_schema!({
            "type": "string",
            "pattern": r"^\d+ (true|false)$"
        })
    }
}

#[test]
fn externally_tagged_enum() {
    test!(External)
        .assert_snapshot()
        .assert_allows_ser_roundtrip(External::values())
        .assert_matches_de_roundtrip(arbitrary_values());
}

#[test]
fn internally_tagged_enum() {
    test!(Internal)
        .assert_snapshot()
        .assert_allows_ser_roundtrip(Internal::values())
        .assert_matches_de_roundtrip(arbitrary_values());
}

#[test]
fn adjacently_tagged_enum() {
    test!(Adjacent)
        .assert_snapshot()
        .assert_allows_ser_roundtrip(Adjacent::values())
        .assert_matches_de_roundtrip(arbitrary_values());
}

#[test]
fn untagged_enum() {
    test!(Untagged)
        .assert_snapshot()
        .assert_allows_ser_roundtrip(Untagged::values())
        .assert_matches_de_roundtrip(arbitrary_values());
}

#[test]
fn untagged_enum_with_titles() {
    test!(
        Untagged,
        SchemaSettings::default().with(|s| s.untagged_enum_variant_titles = true)
    )
    .custom(|s, _| {
        assert_eq!(
            s.pointer("/anyOf/0/title").and_then(Value::as_str),
            Some("UnitOne")
        );
        for variant in s.pointer("/anyOf").and_then(Value::as_array).unwrap() {
            assert!(variant.get("title").is_some_and(Value::is_string));
        }
    })
    .assert_snapshot()
    .assert_allows_ser_roundtrip(Untagged::values())
    .assert_matches_de_roundtrip(arbitrary_values());
}

#[derive(JsonSchema, Serialize, Deserialize)]
enum NoVariants {}

#[test]
fn no_variants() {
    test!(NoVariants)
        .assert_snapshot()
        .assert_rejects_de(arbitrary_values());
}

#[derive(JsonSchema, Serialize, Deserialize)]
#[serde(rename_all_fields = "UPPERCASE", rename_all = "snake_case")]
enum Renamed {
    StructVariant {
        field: String,
    },
    #[serde(rename = "custom name variant")]
    RenamedStructVariant {
        #[serde(rename = "custom name field")]
        field: String,
    },
}

#[test]
fn renamed() {
    test!(Renamed)
        .assert_snapshot()
        .assert_allows_ser_roundtrip([
            Renamed::StructVariant {
                field: "foo".to_owned(),
            },
            Renamed::RenamedStructVariant {
                field: "bar".to_owned(),
            },
        ])
        .assert_rejects_de(arbitrary_values());
}

#[allow(dead_code)]
#[derive(JsonSchema, Deserialize, Serialize)]
enum SoundOfMusic {
    /// # A deer
    ///
    /// A female deer
    Do,
    /// A drop of golden sun
    Re,
    /// A name I call myself
    Mi,
}

#[test]
fn unit_variants_with_doc_comments() {
    test!(SoundOfMusic)
        .assert_snapshot()
        .assert_allows_ser_roundtrip([SoundOfMusic::Do, SoundOfMusic::Re, SoundOfMusic::Mi])
        .assert_rejects_de(arbitrary_values())
        .custom(|schema, _| {
            assert_eq!(
                schema.as_value().pointer("/oneOf/0/title"),
                Some(&("A deer".into())),
            );
            assert_eq!(
                schema.as_value().pointer("/oneOf/0/description"),
                Some(&("A female deer".into())),
            );
            assert_eq!(
                schema.as_value().pointer("/oneOf/1/description"),
                Some(&("A drop of golden sun".into())),
            );
            assert_eq!(
                schema.as_value().pointer("/oneOf/2/description"),
                Some(&("A name I call myself".into())),
            );
        });
}
