//! Integration tests for the ToHeroScript and FromHeroScript derive macros

#![allow(dead_code)]

use herolib_core::heroscript::{FromHeroScript, HeroScriptCollection, ToHeroScript};
use herolib_toschema::{FromHeroScript, ToHeroScript};

// Basic struct with primitive types
#[derive(ToHeroScript, FromHeroScript, Default, Debug, PartialEq)]
struct Person {
    name: String,
    age: u32,
    active: bool,
}

// Struct with optional fields
#[derive(ToHeroScript, FromHeroScript, Default, Debug, PartialEq)]
struct Config {
    host: String,
    port: u32,
    debug: Option<bool>,
    timeout: Option<u32>,
}

// Struct with collections
#[derive(ToHeroScript, FromHeroScript, Default, Debug, PartialEq)]
struct Tags {
    name: String,
    items: Vec<String>,
}

// Struct with float types
#[derive(ToHeroScript, FromHeroScript, Default, Debug, PartialEq)]
struct Metrics {
    name: String,
    cpu_usage: f64,
    memory_ratio: f32,
}

#[test]
fn test_heroscript_simple() {
    // Simplest usage - just call .heroscript()
    let person = Person {
        name: "John".to_string(),
        age: 30,
        active: true,
    };

    // Uses struct name "Person" -> "person" as actor, "define" as action
    let hs = person.heroscript();
    println!("Simple .heroscript():\n{}", hs);

    assert!(hs.contains("!!person.define"));
    assert!(hs.contains("name:John"));
    assert!(hs.contains("age:30"));
    assert!(hs.contains("active:true"));
}

#[test]
fn test_heroscript_oneline_simple() {
    let person = Person {
        name: "Jane".to_string(),
        age: 25,
        active: false,
    };

    // One-line version using defaults
    let hs = person.heroscript_oneline();
    println!("Simple .heroscript_oneline(): {}", hs);

    assert!(hs.starts_with("!!person.define"));
    assert!(hs.contains("name:Jane"));
}

#[test]
fn test_to_heroscript_custom_actor() {
    // Custom actor/action when needed
    let person = Person {
        name: "John".to_string(),
        age: 30,
        active: true,
    };

    let hs = person.to_heroscript("user", "create");
    println!("Custom actor/action:\n{}", hs);

    assert!(hs.contains("!!user.create"));
    assert!(hs.contains("name:John"));
}

#[test]
fn test_to_heroscript_oneline() {
    let person = Person {
        name: "Jane".to_string(),
        age: 25,
        active: false,
    };

    let hs = person.to_heroscript_oneline("person", "define");
    println!("Generated HeroScript (oneline): {}", hs);

    assert!(hs.starts_with("!!person.define"));
    assert!(!hs.contains('\n') || hs.trim().lines().count() == 1);
    assert!(hs.contains("name:Jane"));
}

#[test]
fn test_from_heroscript_basic() {
    let script = "!!person.define name:John age:30 active:true";
    let person = Person::from_heroscript(script).unwrap();

    assert_eq!(person.name, "John");
    assert_eq!(person.age, 30);
    assert!(person.active);
}

#[test]
fn test_from_heroscript_multiline() {
    let script = r#"
!!person.define
    name: 'Jane Doe'
    age: 25
    active: false
"#;
    let person = Person::from_heroscript(script).unwrap();

    assert_eq!(person.name, "Jane Doe");
    assert_eq!(person.age, 25);
    assert!(!person.active);
}

#[test]
fn test_roundtrip() {
    let original = Person {
        name: "Alice".to_string(),
        age: 28,
        active: true,
    };

    // Serialize
    let hs = original.to_heroscript("person", "define");
    println!("Serialized:\n{}", hs);

    // Deserialize
    let parsed = Person::from_heroscript(&hs).unwrap();

    assert_eq!(original.name, parsed.name);
    assert_eq!(original.age, parsed.age);
    assert_eq!(original.active, parsed.active);
}

#[test]
fn test_optional_fields_some() {
    let config = Config {
        host: "localhost".to_string(),
        port: 8080,
        debug: Some(true),
        timeout: Some(30),
    };

    let hs = config.to_heroscript("config", "set");
    println!("Config with optionals:\n{}", hs);

    assert!(hs.contains("host:localhost"));
    assert!(hs.contains("port:8080"));
    assert!(hs.contains("debug:true"));
    assert!(hs.contains("timeout:30"));
}

#[test]
fn test_optional_fields_none() {
    let config = Config {
        host: "localhost".to_string(),
        port: 8080,
        debug: None,
        timeout: None,
    };

    let hs = config.to_heroscript("config", "set");
    println!("Config without optionals:\n{}", hs);

    assert!(hs.contains("host:localhost"));
    assert!(hs.contains("port:8080"));
    // Optional fields with None should not be present
    assert!(!hs.contains("debug:"));
    assert!(!hs.contains("timeout:"));
}

#[test]
fn test_vec_fields() {
    let tags = Tags {
        name: "project".to_string(),
        items: vec!["rust".to_string(), "web".to_string(), "api".to_string()],
    };

    let hs = tags.to_heroscript("tags", "define");
    println!("Tags with vec:\n{}", hs);

    assert!(hs.contains("name:project"));
    assert!(hs.contains("items:rust,web,api"));
}

#[test]
fn test_vec_fields_parse() {
    let script = "!!tags.define name:project items:rust,web,api";
    let tags = Tags::from_heroscript(script).unwrap();

    assert_eq!(tags.name, "project");
    assert_eq!(tags.items, vec!["rust", "web", "api"]);
}

#[test]
fn test_float_fields() {
    let metrics = Metrics {
        name: "server1".to_string(),
        cpu_usage: 45.5,
        memory_ratio: 0.75,
    };

    let hs = metrics.to_heroscript("metrics", "record");
    println!("Metrics:\n{}", hs);

    assert!(hs.contains("name:server1"));
    assert!(hs.contains("cpu_usage:"));
    assert!(hs.contains("memory_ratio:"));
}

#[test]
fn test_all_from_heroscript() {
    let script = r#"
!!person.define name:John age:30 active:true
!!person.define name:Jane age:25 active:false
!!other.action value:123
"#;

    let people = Person::all_from_heroscript(script).unwrap();

    assert_eq!(people.len(), 2);
    assert_eq!(people[0].name, "John");
    assert_eq!(people[1].name, "Jane");
}

#[test]
fn test_source_tracking() {
    let script = r#"
!!person.define
    name: 'John'
    age: 30
    active: true
"#;

    let (person, source) = Person::from_heroscript_with_source(script, None).unwrap();

    assert_eq!(person.name, "John");
    assert_eq!(source.actor, "person");
    assert_eq!(source.action, "define");
    assert_eq!(source.start_line, 2);
}

#[test]
fn test_collection() {
    let script = r#"
!!person.define name:John age:30 active:true
!!person.define name:Jane age:25 active:false
"#;

    let collection = HeroScriptCollection::<Person>::load_from_text(script, None).unwrap();

    assert_eq!(collection.len(), 2);

    let hs = collection.to_heroscript();
    println!("Collection exported:\n{}", hs);

    assert!(hs.contains("name:John"));
    assert!(hs.contains("name:Jane"));
}

#[test]
fn test_default_actor_action() {
    // The default actor should be the struct name lowercased
    assert_eq!(Person::default_actor(), "person");
    assert_eq!(Person::default_action(), "define");

    assert_eq!(Config::default_actor(), "config");
    assert_eq!(Tags::default_actor(), "tags");
}

#[test]
fn test_empty_string_not_serialized() {
    let person = Person {
        name: String::new(), // Empty
        age: 30,
        active: true,
    };

    let hs = person.to_heroscript("person", "define");
    println!("Person with empty name:\n{}", hs);

    // Empty strings should be skipped
    assert!(!hs.contains("name:"));
    assert!(hs.contains("age:30"));
}

#[test]
fn test_empty_vec_not_serialized() {
    let tags = Tags {
        name: "project".to_string(),
        items: Vec::new(), // Empty
    };

    let hs = tags.to_heroscript("tags", "define");
    println!("Tags with empty vec:\n{}", hs);

    assert!(hs.contains("name:project"));
    // Empty vecs should be skipped
    assert!(!hs.contains("items:"));
}

#[test]
fn test_write_back_to_file() {
    use std::fs;

    // Create a temporary file with HeroScript content
    let temp_dir = std::env::temp_dir();
    let temp_file = temp_dir.join("test_heroscript_writeback.hero");

    let original_content = r#"// Header comment
!!person.define
    name: 'John'
    age: 30
    active: true

// Another person
!!person.define
    name: 'Jane'
    age: 25
    active: false
"#;

    fs::write(&temp_file, original_content).unwrap();

    // Load with source tracking
    let content = fs::read_to_string(&temp_file).unwrap();
    let results = Person::all_from_heroscript_with_source(&content, Some(&temp_file)).unwrap();

    assert_eq!(results.len(), 2);

    // Modify the first person
    let (mut john, source) = results.into_iter().next().unwrap();
    assert_eq!(john.name, "John");
    assert_eq!(john.age, 30);

    // Update age
    john.age = 31;

    // Write back
    source.write_back(&john).unwrap();

    // Read back and verify
    let updated_content = fs::read_to_string(&temp_file).unwrap();
    println!("Updated file content:\n{}", updated_content);

    // Verify the update was applied
    assert!(updated_content.contains("age:31"));

    // Verify other content is preserved
    assert!(updated_content.contains("// Header comment"));
    assert!(updated_content.contains("Jane"));

    // Clean up
    fs::remove_file(&temp_file).ok();
}

#[test]
fn test_full_workflow_load_modify_save() {
    use std::fs;

    // Create a temp file
    let temp_dir = std::env::temp_dir();
    let temp_file = temp_dir.join("test_heroscript_workflow.hero");

    // Initial content with multiple objects
    let initial = r#"!!config.define host:localhost port:8080

!!config.define host:production port:443
"#;

    fs::write(&temp_file, initial).unwrap();

    // Load all configs
    let mut collection = HeroScriptCollection::<Config>::load_from_file(&temp_file).unwrap();
    assert_eq!(collection.len(), 2);

    // Modify the first config
    if let Some((config, source)) = collection.items_mut().first_mut() {
        config.port = 9090;
        config.debug = Some(true);
        // Update source actor/action for serialization
        source.actor = "config".to_string();
        source.action = "define".to_string();
    }

    // Save all back to a new file
    let output_file = temp_dir.join("test_heroscript_workflow_output.hero");
    collection.save_to_file(&output_file).unwrap();

    // Verify
    let saved = fs::read_to_string(&output_file).unwrap();
    println!("Saved content:\n{}", saved);

    assert!(saved.contains("port:9090"));
    assert!(saved.contains("debug:true"));
    assert!(saved.contains("port:443")); // Second config unchanged

    // Clean up
    fs::remove_file(&temp_file).ok();
    fs::remove_file(&output_file).ok();
}

#[test]
fn test_quoted_string_values() {
    let script = "!!person.define name:'John Doe with spaces' age:30 active:true";
    let person = Person::from_heroscript(script).unwrap();

    assert_eq!(person.name, "John Doe with spaces");
    assert_eq!(person.age, 30);
}

#[test]
fn test_multiple_action_types_filtering() {
    // Test that we only get Person objects, not other action types
    let script = r#"
!!person.define name:John age:30 active:true
!!server.configure host:localhost port:8080
!!person.define name:Jane age:25 active:false
!!database.connect url:postgres://localhost
"#;

    let people = Person::all_from_heroscript(script).unwrap();

    // Should only get the 2 person.define actions
    assert_eq!(people.len(), 2);
    assert_eq!(people[0].name, "John");
    assert_eq!(people[1].name, "Jane");
}

#[test]
fn test_source_line_tracking_accuracy() {
    let script = r#"// Comment line 1
// Comment line 2

!!person.define
    name: 'First'
    age: 20
    active: true

// Middle comment

!!person.define
    name: 'Second'
    age: 30
    active: false
"#;

    let results = Person::all_from_heroscript_with_source(script, None).unwrap();

    assert_eq!(results.len(), 2);

    let (first, first_source) = &results[0];
    assert_eq!(first.name, "First");
    assert_eq!(first_source.start_line, 4); // Line 4 is where !!person.define starts

    let (second, second_source) = &results[1];
    assert_eq!(second.name, "Second");
    assert_eq!(second_source.start_line, 11); // Line 11 for second action
}
