//! Property tests for AST to resolved type conversion

use proptest::prelude::*;

use crate::{
    resolve::convert::{
        convert_character,
        convert_file,
    },
    syntax::ast::*,
};

// ===== Generators =====

// Reserved keywords that cannot be used as field names
const RESERVED_KEYWORDS: &[&str] = &[
    "character",
    "template",
    "life_arc",
    "schedule",
    "behavior",
    "institution",
    "relationship",
    "location",
    "species",
    "enum",
    "use",
    "state",
    "on",
    "as",
    "remove",
    "append",
    "strict",
    "include",
    "from",
    "self",
    "other",
    "forall",
    "exists",
    "in",
    "where",
    "and",
    "or",
    "not",
    "is",
    "true",
    "false",
];

fn valid_ident() -> impl Strategy<Value = String> {
    "[a-zA-Z_][a-zA-Z0-9_]{0,15}"
        .prop_filter("not reserved", |s| !RESERVED_KEYWORDS.contains(&s.as_str()))
}

fn valid_value() -> impl Strategy<Value = Value> {
    prop_oneof![
        (-1000i64..1000).prop_map(Value::Number),
        (-1000.0..1000.0)
            .prop_filter("finite", |f: &f64| f.is_finite())
            .prop_map(Value::Decimal),
        any::<bool>().prop_map(Value::Boolean),
        "[a-zA-Z0-9 ]{0,30}".prop_map(Value::Text),
    ]
}

fn valid_field() -> impl Strategy<Value = Field> {
    (valid_ident(), valid_value()).prop_map(|(name, value)| Field {
        name,
        value,
        span: Span::new(0, 10),
    })
}

fn valid_unique_fields() -> impl Strategy<Value = Vec<Field>> {
    prop::collection::vec(valid_field(), 0..10).prop_map(|fields| {
        let mut unique_fields = Vec::new();
        let mut seen_names = std::collections::HashSet::new();

        for field in fields {
            if seen_names.insert(field.name.clone()) {
                unique_fields.push(field);
            }
        }
        unique_fields
    })
}

fn valid_character() -> impl Strategy<Value = Character> {
    (valid_ident(), valid_unique_fields()).prop_map(|(name, fields)| Character {
        name,
        species: None,
        fields,
        template: None,
        uses_behaviors: None,
        uses_schedule: None,
        span: Span::new(0, 100),
    })
}

// ===== Property Tests =====

proptest! {
    #[test]
    fn test_character_name_preserved(character in valid_character()) {
        let original_name = character.name.clone();
        let resolved = convert_character(&character).unwrap();
        assert_eq!(resolved.name, original_name);
    }

    #[test]
    fn test_character_field_count_preserved(character in valid_character()) {
        let original_count = character.fields.len();
        let resolved = convert_character(&character).unwrap();
        let total_count = resolved.fields.len() + resolved.prose_blocks.len();
        assert_eq!(total_count, original_count);
    }

    #[test]
    fn test_character_field_values_preserved(character in valid_character()) {
        let resolved = convert_character(&character).unwrap();

        for field in &character.fields {
            match &field.value {
                | Value::ProseBlock(_) => {
                    assert!(resolved.prose_blocks.contains_key(&field.name));
                },
                | value => {
                    assert_eq!(resolved.fields.get(&field.name), Some(value));
                },
            }
        }
    }


    #[test]
    fn test_convert_file_preserves_declaration_count(
        characters in prop::collection::vec(valid_character(), 0..5)
    ) {
        // Ensure unique names across all declarations to avoid duplicate definition errors
        let mut seen_names = std::collections::HashSet::new();
        let mut declarations = Vec::new();

        for char in characters {
            if seen_names.insert(char.name.clone()) {
                declarations.push(Declaration::Character(char));
            }
        }

        let file = File { declarations: declarations.clone() };
        let resolved = convert_file(&file).unwrap();

        // Should have same count (excluding Use declarations)
        assert_eq!(resolved.len(), declarations.len());
    }

    #[test]
    fn test_duplicate_field_names_rejected(
        name in valid_ident(),
        field_name in valid_ident(),
        val1 in valid_value(),
        val2 in valid_value()
    ) {
        let character = Character {
            name: name.clone(),
            species: None,
            fields: vec![
                Field {
                    name: field_name.clone(),
                    value: val1,
                    span: Span::new(0, 10),
                },
                Field {
                    name: field_name,
                    value: val2,
                    span: Span::new(10, 20),
                },
            ],
            template: None,
        uses_behaviors: None,
        uses_schedule: None,
            span: Span::new(0, 50),
        };

        let result = convert_character(&character);
        assert!(result.is_err(), "Duplicate field names should be rejected");
    }

    #[test]
    fn test_field_lookup_is_efficient(character in valid_character()) {
        let resolved = convert_character(&character).unwrap();

        // All fields should be directly accessible in O(1)
        for field in &character.fields {
            if matches!(field.value, Value::ProseBlock(_)) {
                assert!(
                    resolved.prose_blocks.contains_key(&field.name),
                    "Prose block {} should be in map",
                    field.name
                );
            } else {
                assert!(
                    resolved.fields.contains_key(&field.name),
                    "Field {} should be in map",
                    field.name
                );
            }
        }
    }

    #[test]
    fn test_empty_character_converts(name in valid_ident()) {
        let character = Character {
            name: name.clone(),
            species: None,
            fields: vec![],
            template: None,
        uses_behaviors: None,
        uses_schedule: None,

            span: Span::new(0, 10),
        };

        let resolved = convert_character(&character).unwrap();
        assert_eq!(resolved.name, name);
        assert_eq!(resolved.fields.len(), 0);
        assert_eq!(resolved.prose_blocks.len(), 0);
    }

    #[test]
    fn test_conversion_is_deterministic(character in valid_character()) {
        let resolved1 = convert_character(&character).unwrap();
        let resolved2 = convert_character(&character).unwrap();

        assert_eq!(resolved1.name, resolved2.name);
        assert_eq!(resolved1.fields.len(), resolved2.fields.len());
        assert_eq!(resolved1.prose_blocks.len(), resolved2.prose_blocks.len());

        // All fields should match
        for (key, value) in &resolved1.fields {
            assert_eq!(resolved2.fields.get(key), Some(value));
        }
    }

    #[test]
    fn test_file_with_use_declarations_skips_them(
        char_count in 1usize..5,
        use_count in 0usize..5
    ) {
        let mut declarations = vec![];

        // Add some use declarations
        for i in 0..use_count {
            declarations.push(Declaration::Use(UseDecl {
                path: vec![format!("module{}", i)],
                kind: UseKind::Wildcard,
                span: Span::new(0, 10),
            }));
        }

        // Add characters with unique names to avoid duplicate definition errors
        for i in 0..char_count {
            declarations.push(Declaration::Character(Character {
                name: format!("Char{}", i),
                species: None,
                fields: vec![],
                template: None,
        uses_behaviors: None,
        uses_schedule: None,
                span: Span::new(0, 100),
            }));
        }

        let file = File { declarations };
        let resolved = convert_file(&file).unwrap();

        // Should only have characters, not use declarations
        assert_eq!(resolved.len(), char_count);
    }
}

#[cfg(test)]
mod edge_cases {
    use super::*;

    proptest! {
        #[test]
        fn test_all_value_types_convert(
            int_val in -1000i64..1000,
            float_val in -1000.0..1000.0,
            bool_val in any::<bool>(),
            string_val in "[a-zA-Z0-9 ]{1,30}"
        ) {
            let character = Character {
                name: "Test".to_string(),
            species: None,
                fields: vec![
                    Field {
                        name: "int_field".to_string(),
                        value: Value::Number(int_val),
                        span: Span::new(0, 10),
                    },
                    Field {
                        name: "float_field".to_string(),
                        value: Value::Decimal(float_val),
                        span: Span::new(10, 20),
                    },
                    Field {
                        name: "bool_field".to_string(),
                        value: Value::Boolean(bool_val),
                        span: Span::new(20, 30),
                    },
                    Field {
                        name: "string_field".to_string(),
                        value: Value::Text(string_val.clone()),
                        span: Span::new(30, 40),
                    },
                ],
                template: None,
        uses_behaviors: None,
        uses_schedule: None,
                span: Span::new(0, 50),
            };

            let resolved = convert_character(&character).unwrap();
            assert_eq!(resolved.fields.get("int_field"), Some(&Value::Number(int_val)));
            assert_eq!(resolved.fields.get("float_field"), Some(&Value::Decimal(float_val)));
            assert_eq!(resolved.fields.get("bool_field"), Some(&Value::Boolean(bool_val)));
            assert_eq!(resolved.fields.get("string_field"), Some(&Value::Text(string_val)));
        }

        #[test]
        fn test_unicode_in_names_and_values(
            name in "[a-zA-Z_\u{0080}-\u{00FF}]{1,20}",
            field_name in "[a-zA-Z_\u{0080}-\u{00FF}]{1,20}".prop_filter("not reserved", |s| {
                !RESERVED_KEYWORDS.contains(&s.as_str())
            }),
            string_val in "[a-zA-Z0-9 \u{0080}-\u{00FF}]{0,30}"
        ) {
            let character = Character {
                name: name.clone(),
            species: None,
                fields: vec![Field {
                    name: field_name.clone(),
                    value: Value::Text(string_val.clone()),
                    span: Span::new(0, 10),
                }],
                template: None,
        uses_behaviors: None,
        uses_schedule: None,
                span: Span::new(0, 50),
            };

            let resolved = convert_character(&character).unwrap();
            assert_eq!(resolved.name, name);
            assert_eq!(
                resolved.fields.get(&field_name),
                Some(&Value::Text(string_val))
            );
        }
    }
}
