//! Property-based tests for the resolution engine

use proptest::prelude::*;

use crate::{
    resolve::names::{
        DeclKind,
        NameTable,
    },
    syntax::ast::*,
};

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

fn valid_ident() -> impl Strategy<Value = String> {
    "[a-zA-Z_][a-zA-Z0-9_]{0,20}".prop_filter("not a keyword", |s| {
        !matches!(
            s.as_str(),
            "use" |
                "character" |
                "template" |
                "life_arc" |
                "schedule" |
                "behavior" |
                "institution" |
                "relationship" |
                "location" |
                "species" |
                "enum" |
                "state" |
                "on" |
                "as" |
                "self" |
                "other" |
                "remove" |
                "append" |
                "forall" |
                "exists" |
                "in" |
                "where" |
                "and" |
                "or" |
                "not" |
                "is" |
                "true" |
                "false"
        )
    })
}

fn valid_character_decl() -> impl Strategy<Value = (String, Declaration)> {
    valid_ident().prop_map(|name| {
        let decl = Declaration::Character(Character {
            name: name.clone(),
            species: None,
            fields: vec![],
            template: None,
            uses_behaviors: None,
            uses_schedule: None,

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

fn valid_template_decl() -> impl Strategy<Value = (String, Declaration)> {
    valid_ident().prop_map(|name| {
        let decl = Declaration::Template(Template {
            name: name.clone(),
            species_base: None,
            fields: vec![],
            strict: false,
            includes: vec![],
            uses_behaviors: None,
            uses_schedule: None,
            span: Span::new(0, 10),
        });
        (name, decl)
    })
}

fn valid_use_single() -> impl Strategy<Value = Declaration> {
    (valid_ident(), valid_ident()).prop_map(|(module, name)| {
        Declaration::Use(UseDecl {
            path: vec![module, name],
            kind: UseKind::Single,
            span: Span::new(0, 10),
        })
    })
}

fn valid_use_grouped() -> impl Strategy<Value = Declaration> {
    (valid_ident(), prop::collection::vec(valid_ident(), 1..5)).prop_map(|(module, names)| {
        Declaration::Use(UseDecl {
            path: vec![module],
            kind: UseKind::Grouped(names),
            span: Span::new(0, 10),
        })
    })
}

fn valid_use_wildcard() -> impl Strategy<Value = Declaration> {
    valid_ident().prop_map(|module| {
        Declaration::Use(UseDecl {
            path: vec![module],
            kind: UseKind::Wildcard,
            span: Span::new(0, 10),
        })
    })
}

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

proptest! {
    #[test]
    fn test_name_table_registers_all_declarations(
        chars in prop::collection::vec(valid_character_decl(), 0..10)
    ) {
        let declarations: Vec<_> = chars.iter().map(|(_, decl)| decl.clone()).collect();
        let file = File { declarations };

        let result = NameTable::from_file(&file);

        if chars.is_empty() {
            // Empty file should succeed
            assert!(result.is_ok());
        } else {
            // Check if there are duplicates
            let mut seen = std::collections::HashSet::new();
            let has_duplicates = chars.iter().any(|(name, _)| !seen.insert(name));

            if has_duplicates {
                // Should fail with duplicate error
                assert!(result.is_err());
            } else {
                // Should succeed and all names should be registered
                let table = result.unwrap();
                for (name, _) in &chars {
                    assert!(table.lookup(std::slice::from_ref(name)).is_some(),
                        "Name '{}' should be registered", name);
                }
            }
        }
    }

    #[test]
    fn test_duplicate_detection_always_fails(
        name in valid_ident(),
        count in 2usize..5
    ) {
        let declarations: Vec<_> = (0..count).map(|i| {
            Declaration::Character(Character {
                name: name.clone(),
            species: None,
                fields: vec![],
                template: None,
        uses_behaviors: None,
        uses_schedule: None,

                span: Span::new(i * 10, i * 10 + 10),
            })
        }).collect();

        let file = File { declarations };
        let result = NameTable::from_file(&file);

        // Should always fail with duplicate error
        assert!(result.is_err());
    }

    #[test]
    fn test_lookup_is_case_sensitive(name in valid_ident()) {
        let file = File {
            declarations: vec![
                Declaration::Character(Character {
                    name: name.clone(),
            species: None,
                    fields: vec![],
                    template: None,
        uses_behaviors: None,
        uses_schedule: None,

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

        let table = NameTable::from_file(&file).unwrap();

        // Original name should be found
        assert!(table.lookup(std::slice::from_ref(&name)).is_some());

        // Different case should not be found
        let uppercase = name.to_uppercase();
        if uppercase != name {
            assert!(table.lookup(&[uppercase]).is_none());
        }
    }

    #[test]
    fn test_kind_filtering_works(
        chars in prop::collection::vec(valid_character_decl(), 0..5),
        templates in prop::collection::vec(valid_template_decl(), 0..5)
    ) {
        let mut declarations = vec![];
        declarations.extend(chars.iter().map(|(_, d)| d.clone()));
        declarations.extend(templates.iter().map(|(_, d)| d.clone()));

        let file = File { declarations };

        // Only proceed if no duplicates
        let mut seen = std::collections::HashSet::new();
        let has_duplicates = chars.iter().any(|(name, _)| !seen.insert(name))
            || templates.iter().any(|(name, _)| !seen.insert(name));

        if !has_duplicates {
            let table = NameTable::from_file(&file).unwrap();

            let char_count = table.entries_of_kind(DeclKind::Character).count();
            let template_count = table.entries_of_kind(DeclKind::Template).count();

            assert_eq!(char_count, chars.len());
            assert_eq!(template_count, templates.len());
        }
    }

    #[test]
    fn test_use_statements_are_collected(
        uses in prop::collection::vec(
            prop_oneof![
                valid_use_single(),
                valid_use_grouped(),
                valid_use_wildcard(),
            ],
            0..10
        )
    ) {
        let file = File { declarations: uses.clone() };
        let table = NameTable::from_file(&file).unwrap();

        assert_eq!(table.imports().len(), uses.len());
    }

    #[test]
    fn test_fuzzy_matching_finds_close_names(
        name in valid_ident().prop_filter("long enough", |s| s.len() > 3)
    ) {
        let file = File {
            declarations: vec![
                Declaration::Character(Character {
                    name: name.clone(),
            species: None,
                    fields: vec![],
                    template: None,
        uses_behaviors: None,
        uses_schedule: None,

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

        let table = NameTable::from_file(&file).unwrap();

        // Create a typo by swapping two adjacent characters
        if name.len() >= 2 {
            let mut chars: Vec<char> = name.chars().collect();
            chars.swap(0, 1);
            let typo: String = chars.into_iter().collect();

            // Should suggest the original name
            if let Some(suggestion) = table.find_suggestion(&typo) {
                assert_eq!(suggestion, name);
            }
        }
    }

    #[test]
    fn test_mixed_declarations_and_imports(
        chars in prop::collection::vec(valid_character_decl(), 1..5),
        uses in prop::collection::vec(valid_use_single(), 0..3)
    ) {
        let mut declarations = uses;
        declarations.extend(chars.iter().map(|(_, d)| d.clone()));

        let file = File { declarations };

        // Check for duplicates
        let mut seen = std::collections::HashSet::new();
        let has_duplicates = chars.iter().any(|(name, _)| !seen.insert(name));

        if !has_duplicates {
            let table = NameTable::from_file(&file).unwrap();

            // All characters should be registered
            for (name, _) in &chars {
                assert!(table.lookup(std::slice::from_ref(name)).is_some());
            }
        }
    }
}
