// Copyright Materialize, Inc. All rights reserved.
//
// Use of this software is governed by the Business Source License
// included in the LICENSE file.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0.

//! Port of https://github.com/apache/avro/blob/master/lang/py/test/test_io.py
use std::io::Cursor;

use avro::schema::resolve_schemas;
use avro::{
    from_avro_datum, to_avro_datum, types::Value, Schema, SchemaResolutionError, ValidationError,
};
use lazy_static::lazy_static;

lazy_static! {
    static ref SCHEMAS_TO_VALIDATE: Vec<(&'static str, Value)> = vec![
        (r#""null""#, Value::Null),
        (r#""boolean""#, Value::Boolean(true)),
        (r#""string""#, Value::String("adsfasdf09809dsf-=adsf".to_string())),
        (r#""bytes""#, Value::Bytes("12345abcd".to_string().into_bytes())),
        (r#""int""#, Value::Int(1234)),
        (r#""long""#, Value::Long(1234)),
        (r#""float""#, Value::Float(1234.0)),
        (r#""double""#, Value::Double(1234.0)),
        (r#"{"type": "fixed", "name": "Test", "size": 1}"#, Value::Fixed(1, vec![b'B'])),
        (r#"{"type": "enum", "name": "Test", "symbols": ["A", "B"]}"#, Value::Enum(1, "B".to_string())),
        (r#"{"type": "array", "items": "long"}"#, Value::Array(vec![Value::Long(1), Value::Long(3), Value::Long(2)])),
        (r#"{"type": "map", "values": "long"}"#, Value::Map([("a".to_string(), Value::Long(1i64)), ("b".to_string(), Value::Long(3i64)), ("c".to_string(), Value::Long(2i64))].iter().cloned().collect())),
        (r#"["string", "null", "long"]"#, Value::Union(1, Box::new(Value::Null))),
        (r#"{"type": "record", "name": "Test", "fields": [{"name": "f", "type": "long"}]}"#, Value::Record(vec![("f".to_string(), Value::Long(1))]))
    ];

    static ref BINARY_ENCODINGS: Vec<(i64, Vec<u8>)> = vec![
        (0, vec![0x00]),
        (-1, vec![0x01]),
        (1, vec![0x02]),
        (-2, vec![0x03]),
        (2, vec![0x04]),
        (-64, vec![0x7f]),
        (64, vec![0x80, 0x01]),
        (8192, vec![0x80, 0x80, 0x01]),
        (-8193, vec![0x81, 0x80, 0x01]),
    ];

    static ref DEFAULT_VALUE_EXAMPLES: Vec<(&'static str, &'static str, Value)> = vec![
        (r#""null""#, "null", Value::Null),
        (r#""boolean""#, "true", Value::Boolean(true)),
        (r#""string""#, r#""foo""#, Value::String("foo".to_string())),
        //(r#""bytes""#, r#""\u00FF\u00FF""#, Value::Bytes(vec![0xff, 0xff])),
        (r#""int""#, "5", Value::Int(5)),
        (r#""long""#, "5", Value::Long(5)),
        (r#""float""#, "1.1", Value::Float(1.1)),
        (r#""double""#, "1.1", Value::Double(1.1)),
        //(r#"{"type": "fixed", "name": "F", "size": 2}"#, r#""\u00FF\u00FF""#, Value::Bytes(vec![0xff, 0xff])),
        //(r#"{"type": "enum", "name": "F", "symbols": ["FOO", "BAR"]}"#, r#""FOO""#, Value::Enum(0, "FOO".to_string())),
        (r#"{"type": "array", "items": "int"}"#, "[1, 2, 3]", Value::Array(vec![Value::Int(1), Value::Int(2), Value::Int(3)])),
        (r#"{"type": "map", "values": "int"}"#, r#"{"a": 1, "b": 2}"#, Value::Map([("a".to_string(), Value::Int(1)), ("b".to_string(), Value::Int(2))].iter().cloned().collect())),
        //(r#"["int", "null"]"#, "5", Value::Union(Box::new(Value::Int(5)))),
        (r#"{"type": "record", "name": "F", "fields": [{"name": "A", "type": "int"}]}"#, r#"{"A": 5}"#,Value::Record(vec![("A".to_string(), Value::Int(5))])),
    ];

    static ref LONG_RECORD_SCHEMA: Schema = Schema::parse_str(r#"
    {
        "type": "record",
        "name": "Test",
        "fields": [
            {"name": "A", "type": "int"},
            {"name": "B", "type": "int"},
            {"name": "C", "type": "int"},
            {"name": "D", "type": "int"},
            {"name": "E", "type": "int"},
            {"name": "F", "type": "int"},
            {"name": "G", "type": "int"}
        ]
    }
    "#).unwrap();

    static ref LONG_RECORD_DATUM: Value = Value::Record(vec![
        ("A".to_string(), Value::Int(1)),
        ("B".to_string(), Value::Int(2)),
        ("C".to_string(), Value::Int(3)),
        ("D".to_string(), Value::Int(4)),
        ("E".to_string(), Value::Int(5)),
        ("F".to_string(), Value::Int(6)),
        ("G".to_string(), Value::Int(7)),
    ]);
}

#[test]
fn test_validate() {
    for (raw_schema, value) in SCHEMAS_TO_VALIDATE.iter() {
        let schema = Schema::parse_str(raw_schema).unwrap();
        assert!(
            value.validate(schema.top_node()),
            format!("value {:?} does not validate schema: {}", value, raw_schema)
        );
    }
}

#[test]
fn test_round_trip() {
    for (raw_schema, value) in SCHEMAS_TO_VALIDATE.iter() {
        let schema = Schema::parse_str(raw_schema).unwrap();
        let encoded = to_avro_datum(&schema, value.clone()).unwrap();
        let decoded = from_avro_datum(&schema, &mut Cursor::new(encoded)).unwrap();
        assert_eq!(value, &decoded);
    }
}

#[test]
fn test_binary_int_encoding() {
    for (number, hex_encoding) in BINARY_ENCODINGS.iter() {
        let encoded = to_avro_datum(
            &Schema::parse_str("\"int\"").unwrap(),
            Value::Int(*number as i32),
        )
        .unwrap();
        assert_eq!(&encoded, hex_encoding);
    }
}

#[test]
fn test_binary_long_encoding() {
    for (number, hex_encoding) in BINARY_ENCODINGS.iter() {
        let encoded = to_avro_datum(
            &Schema::parse_str("\"long\"").unwrap(),
            Value::Long(*number as i64),
        )
        .unwrap();
        assert_eq!(&encoded, hex_encoding);
    }
}

#[test]
fn test_schema_promotion() {
    // Each schema is present in order of promotion (int -> long, long -> float, float -> double)
    // Each value represents the expected decoded value when promoting a value previously encoded with a promotable schema
    let promotable_schemas = vec![r#""int""#, r#""long""#, r#""float""#, r#""double""#];
    let promotable_values = vec![
        Value::Int(219),
        Value::Long(219),
        Value::Float(219.0),
        Value::Double(219.0),
    ];
    for (i, writer_raw_schema) in promotable_schemas.iter().enumerate() {
        let writer_schema = Schema::parse_str(writer_raw_schema).unwrap();
        let original_value = &promotable_values[i];
        for (j, reader_raw_schema) in promotable_schemas.iter().enumerate().skip(i + 1) {
            let reader_schema = Schema::parse_str(reader_raw_schema).unwrap();
            let encoded = to_avro_datum(&writer_schema, original_value.clone()).unwrap();
            let resolved_schema = resolve_schemas(&writer_schema, &reader_schema).unwrap();
            let decoded = from_avro_datum(&resolved_schema, &mut Cursor::new(encoded))
                .unwrap_or_else(|_| {
                    panic!(
                        "failed to decode {:?} with schema: {:?}",
                        original_value, reader_raw_schema
                    )
                });
            assert_eq!(decoded, promotable_values[j]);
        }
    }
}

#[test]
fn test_unknown_symbol() {
    let writer_schema =
        Schema::parse_str(r#"{"type": "enum", "name": "Test", "symbols": ["FOO", "BAR"]}"#)
            .unwrap();
    let reader_schema =
        Schema::parse_str(r#"{"type": "enum", "name": "Test", "symbols": ["BAR", "BAZ"]}"#)
            .unwrap();
    let original_value = Value::Enum(0, "FOO".to_string());
    let encoded = to_avro_datum(&writer_schema, original_value).unwrap();
    let resolved_schema = resolve_schemas(&writer_schema, &reader_schema).unwrap();
    let decoded = from_avro_datum(&resolved_schema, &mut Cursor::new(encoded));
    assert!(decoded.is_err());
}

#[test]
fn test_default_value() {
    for (field_type, default_json, default_datum) in DEFAULT_VALUE_EXAMPLES.iter() {
        let reader_schema = Schema::parse_str(&format!(
            r#"{{
                "type": "record",
                "name": "Test",
                "fields": [
                    {{"name": "H", "type": {}, "default": {}}}
                ]
            }}"#,
            field_type, default_json
        ))
        .unwrap();
        let datum_to_read = Value::Record(vec![("H".to_string(), default_datum.clone())]);
        let encoded = to_avro_datum(&LONG_RECORD_SCHEMA, LONG_RECORD_DATUM.clone()).unwrap();
        let resolved_schema = resolve_schemas(&LONG_RECORD_SCHEMA, &reader_schema).unwrap();
        let datum_read = from_avro_datum(&resolved_schema, &mut Cursor::new(encoded)).unwrap();
        assert_eq!(
            datum_read, datum_to_read,
            "{} -> {}",
            *field_type, *default_json
        );
    }
}

#[test]
fn test_no_default_value() -> Result<(), String> {
    let reader_schema = Schema::parse_str(
        r#"{
            "type": "record",
            "name": "Test",
            "fields": [
                {"name": "H", "type": "int"}
            ]
        }"#,
    )
    .unwrap();
    let resolved_schema = resolve_schemas(&LONG_RECORD_SCHEMA, &reader_schema);
    match resolved_schema {
        Ok(_) => Err(String::from("Expected SchemaResolutionError, got Ok")),
        Err(ref e) => match e.downcast_ref::<SchemaResolutionError>() {
            Some(_) => Ok(()),
            None => Err(format!("Expected SchemaResolutionError, got {}", e)),
        },
    }
}

#[test]
fn test_union_default() {
    let reader_schema = Schema::parse_str(
        r#"{
            "type": "record",
            "name": "Test",
            "fields": [
                {"name": "f1", "type": ["int", "null"], "default": 42},
                {"name": "f2", "type": "long"}
            ]
        }"#,
    )
    .unwrap();
    let writer_schema = Schema::parse_str(
        r#"{
            "type": "record",
            "name": "Test",
            "fields": [
                {"name": "f2", "type": "long"}
            ]
        }"#,
    )
    .unwrap();
    resolve_schemas(&writer_schema, &reader_schema).unwrap();
}

#[test]
fn test_projection() {
    let reader_schema = Schema::parse_str(
        r#"
        {
            "type": "record",
            "name": "Test",
            "fields": [
                {"name": "E", "type": "int"},
                {"name": "F", "type": "int"}
            ]
        }
    "#,
    )
    .unwrap();
    let datum_to_read = Value::Record(vec![
        ("E".to_string(), Value::Int(5)),
        ("F".to_string(), Value::Int(6)),
    ]);
    let encoded = to_avro_datum(&LONG_RECORD_SCHEMA, LONG_RECORD_DATUM.clone()).unwrap();
    let resolved_schema = resolve_schemas(&LONG_RECORD_SCHEMA, &reader_schema).unwrap();
    let datum_read = from_avro_datum(&resolved_schema, &mut Cursor::new(encoded)).unwrap();
    assert_eq!(datum_to_read, datum_read);
}

#[test]
fn test_field_order() {
    let reader_schema = Schema::parse_str(
        r#"
        {
            "type": "record",
            "name": "Test",
            "fields": [
                {"name": "F", "type": "int"},
                {"name": "E", "type": "int"}
            ]
        }
    "#,
    )
    .unwrap();
    let datum_to_read = Value::Record(vec![
        ("F".to_string(), Value::Int(6)),
        ("E".to_string(), Value::Int(5)),
    ]);
    let encoded = to_avro_datum(&LONG_RECORD_SCHEMA, LONG_RECORD_DATUM.clone()).unwrap();
    let resolved_schema = resolve_schemas(&LONG_RECORD_SCHEMA, &reader_schema).unwrap();
    println!("Resolved: {:#?}", resolved_schema);
    let datum_read = from_avro_datum(&resolved_schema, &mut Cursor::new(encoded)).unwrap();
    assert_eq!(datum_to_read, datum_read);
}

#[test]
fn test_type_exception() -> Result<(), String> {
    let writer_schema = Schema::parse_str(
        r#"
        {
             "type": "record",
             "name": "Test",
             "fields": [
                {"name": "F", "type": "int"},
                {"name": "E", "type": "int"}
             ]
        }
    "#,
    )
    .unwrap();
    let datum_to_write = Value::Record(vec![
        ("E".to_string(), Value::Int(5)),
        ("F".to_string(), Value::String(String::from("Bad"))),
    ]);
    let encoded = to_avro_datum(&writer_schema, datum_to_write);
    match encoded {
        Ok(_) => Err(String::from("Expected ValidationError, got Ok")),
        Err(ref e) => match e.downcast_ref::<ValidationError>() {
            Some(_) => Ok(()),
            None => Err(format!("Expected ValidationError, got {}", e)),
        },
    }
}

#[test]
fn test_namespaces() {
    let schema = r#"
    {
        "type": "record",
        "name": "some_record",
        "namespace": "io.materialize",
        "fields": [
            {
                "name": "link",
                "type": [
                    "null",
                    "io.materialize.some_record"
                ]
            }
        ]
    }"#;
    let schema = Schema::parse_str(schema).unwrap();
    let datum_to_write = Value::Record(vec![(
        "link".to_owned(),
        Value::Union(
            1,
            Box::new(Value::Record(vec![(
                "link".to_owned(),
                Value::Union(0, Box::new(Value::Null)),
            )])),
        ),
    )]);
    let encoded = to_avro_datum(&schema, datum_to_write.clone()).unwrap();
    let datum_read = from_avro_datum(&schema, &mut Cursor::new(encoded)).unwrap();
    assert_eq!(datum_to_write, datum_read);
}

#[test]
fn test_self_referential_schema() {
    let schema = r#"
        {
            "name": "some_record",
            "type": "record",
            "fields": [
                {
                    "name": "f1",
                    "type": {
                        "type": "array",
                        "items": {
                            "name": "some_item",
                            "type": "record",
                            "fields": [
                                {
                                    "name": "f3",
                                    "type": "string"
                                },
                                {
                                    "name": "f4",
                                    "type": "string"
                                },
                                {
                                    "name": "f5",
                                    "type": [
                                        "null",
                                        "double"
                                    ],
                                    "default": null
                                }
                            ]
                        }
                    }
                },
                {
                    "name": "f2",
                    "type": {
                        "type": "array",
                        "items": "some_item"
                    }
                }
            ]
        }
        "#;
    let schema = Schema::parse_str(schema).unwrap();
    let datum_to_write = Value::Record(vec![
        (
            "f1".to_owned(),
            Value::Array(vec![
                Value::Record(vec![
                    ("f3".to_owned(), Value::String("s1".to_owned())),
                    ("f4".to_owned(), Value::String("s2".to_owned())),
                    (
                        "f5".to_owned(),
                        Value::Union(1, Box::new(Value::Double(1.234))),
                    ),
                ]),
                Value::Record(vec![
                    ("f3".to_owned(), Value::String("s3".to_owned())),
                    ("f4".to_owned(), Value::String("s4".to_owned())),
                    (
                        "f5".to_owned(),
                        Value::Union(1, Box::new(Value::Double(2.468))),
                    ),
                ]),
            ]),
        ),
        (
            "f2".to_owned(),
            Value::Array(vec![Value::Record(vec![
                ("f3".to_owned(), Value::String("s5".to_owned())),
                ("f4".to_owned(), Value::String("s6".to_owned())),
                ("f5".to_owned(), Value::Union(0, Box::new(Value::Null))),
            ])]),
        ),
    ]);
    let encoded = to_avro_datum(&schema, datum_to_write.clone()).unwrap();
    let datum_read = from_avro_datum(&schema, &mut Cursor::new(encoded)).unwrap();
    assert_eq!(datum_to_write, datum_read);
}

#[test]
fn test_complex_resolutions() {
    // Attempt to exercise many of the hard parts of schema resolution:
    // Reordering fields in "some_record", field "f0" missing from writer, field "f3" missing
    // from reader, reordering and different set of symbols in enum in "f2",
    // "union to concrete" resolution in "f4", "concrete to union" in f5, "union to union"
    // with reordered fields in "f1".
    let writer_schema = r#"
        {
            "name": "some_record",
            "type": "record",
            "fields": [
                {
                    "name": "f5",
                    "type": "long"
                },
                {
                    "name": "f4",
                    "type": [
                        "double",
                        {
                            "name": "variant1",
                            "type": "fixed",
                            "size": 1
                        },
                        "null",
                        {
                            "name": "variant2",
                            "type": "fixed",
                            "size": 2
                        }
                    ]
                },
                {
                    "name": "f3",
                    "type": "double"
                },
                {
                    "name": "f2",
                    "type": {
                        "type": "enum",
                        "symbols": ["Clubs", "Diamonds", "Hearts", "Spades", "Jokers"],
                        "name": "Suit"
                    }
                },
                {
                    "name": "f1",
                    "type": [
                        {
                            "name": "variant3",
                            "type": "fixed",
                            "size": 3
                        },
                        "double",
                        {
                            "name": "variant4",
                            "type": "fixed",
                            "size": 4
                        }
                    ]
                }
            ]
        }"#;
    let datum_to_write = Value::Record(vec![
        ("f5".to_owned(), Value::Long(1234)),
        (
            "f4".to_owned(),
            Value::Union(1, Box::new(Value::Fixed(1, vec![0]))),
        ),
        ("f3".to_owned(), Value::Double(1.234)),
        ("f2".to_owned(), Value::Enum(1, "Diamonds".to_owned())),
        (
            "f1".to_owned(),
            Value::Union(2, Box::new(Value::Fixed(4, vec![0, 1, 2, 3]))),
        ),
    ]);
    let expected_read = Value::Record(vec![
        (
            "f0".to_owned(),
            Value::Record(vec![
                ("f0_0".to_owned(), Value::Double(1.234)),
                (
                    "f0_1".to_owned(),
                    Value::Union(0, Box::new(Value::Enum(1, "bar".to_owned()))),
                ),
            ]),
        ),
        (
            "f1".to_owned(),
            Value::Union(0, Box::new(Value::Fixed(4, vec![0, 1, 2, 3]))),
        ),
        ("f2".to_owned(), Value::Enum(1, "Diamonds".to_owned())), // TODO - should be the index in the reader
        ("f4".to_owned(), Value::Fixed(1, vec![0])),
        (
            "f5".to_owned(),
            Value::Union(1, Box::new(Value::Long(1234))),
        ),
    ]);
    let reader_schema = r#"
        {
            "name": "some_record",
            "type": "record",
            "fields": [
                {
                    "name": "f0",
                    "type": {
                        "type": "record",
                        "name": "f0_value",
                        "fields": [
                            {
                                "name": "f0_0",
                                "type": "double"
                            },
                            {
                                "name": "f0_1",
                                "type": [
                                    {
                                        "type": "enum",
                                        "symbols": ["foo", "bar", "blah"],
                                        "name": "some_enum"
                                    },
                                    "null"
                                ]
                            }
                        ]
                    },
                    "default": {"f0_1": "bar", "f0_0": 1.234}
                },
                {
                    "name": "f1",
                    "type": [
                        {
                            "name": "variant4",
                            "type": "fixed",
                            "size": 4
                        },
                        {
                            "name": "variant3",
                            "type": "fixed",
                            "size": 3
                        },
                        "double"
                    ]
                },
                {
                    "name": "f2",
                    "type": {
                        "type": "enum",
                        "symbols": ["Hearts", "Spades", "Diamonds", "Clubs"],
                        "name": "Suit"
                    }
                },
                {
                    "name": "f4",
                    "type": {
                        "name": "variant1",
                        "type": "fixed",
                        "size": 1
                    }
                },
                {
                    "name": "f5",
                    "type": [
                        {
                            "name": "extra_variant",
                            "type": "fixed",
                            "size": 10
                        },
                        "long"
                     ]
                }
            ]
        }
    "#;
    let writer_schema = Schema::parse_str(writer_schema).unwrap();
    let reader_schema = Schema::parse_str(reader_schema).unwrap();
    let resolved_schema = resolve_schemas(&writer_schema, &reader_schema).unwrap();
    let encoded = to_avro_datum(&writer_schema, datum_to_write).unwrap();
    let datum_read = from_avro_datum(&resolved_schema, &mut Cursor::new(encoded)).unwrap();
    assert_eq!(expected_read, datum_read);
}
