//! Tests for code actions
#![allow(unused)]

use std::collections::HashMap;

use tower_lsp::lsp_types::{
    CodeActionContext,
    CodeActionParams,
    Diagnostic,
    DiagnosticSeverity,
    Position,
    Range,
    TextDocumentIdentifier,
    Url,
};

use super::{
    code_actions::*,
    document::Document,
};

fn make_diagnostic(message: &str, range: Range) -> Diagnostic {
    Diagnostic {
        range,
        severity: Some(DiagnosticSeverity::ERROR),
        code: None,
        code_description: None,
        source: Some("storybook".to_string()),
        message: message.to_string(),
        related_information: None,
        tags: None,
        data: None,
    }
}

fn make_documents(files: Vec<(&str, &str)>) -> HashMap<Url, Document> {
    files
        .into_iter()
        .map(|(uri_str, content)| {
            let uri = Url::parse(uri_str).unwrap();
            let doc = Document::new(content.to_string());
            (uri, doc)
        })
        .collect()
}

// Phase 1: Create Missing Symbol Tests

#[test]
fn test_create_missing_character() {
    let source = r#"
character Bob {
  friend: Alice
}
"#;
    let uri = Url::parse("file:///test.sb").unwrap();
    let documents = make_documents(vec![("file:///test.sb", source)]);

    let diagnostic = make_diagnostic(
        "name 'Alice' not found",
        Range {
            start: Position {
                line: 2,
                character: 10,
            },
            end: Position {
                line: 2,
                character: 15,
            },
        },
    );

    let params = CodeActionParams {
        text_document: TextDocumentIdentifier { uri: uri.clone() },
        range: diagnostic.range,
        context: CodeActionContext {
            diagnostics: vec![diagnostic.clone()],
            only: None,
            trigger_kind: None,
        },
        work_done_progress_params: Default::default(),
        partial_result_params: Default::default(),
    };

    let result = get_code_actions(&documents, &params);
    assert!(result.is_some());

    let actions = result.unwrap();
    assert!(!actions.is_empty());

    // Should offer to create character or template
    let titles: Vec<String> = actions
        .iter()
        .filter_map(|a| {
            if let tower_lsp::lsp_types::CodeActionOrCommand::CodeAction(action) = a {
                Some(action.title.clone())
            } else {
                None
            }
        })
        .collect();

    assert!(titles
        .iter()
        .any(|t| t.contains("Create character 'Alice'")));
}

#[test]
fn test_create_missing_template() {
    let source = r#"
character Alice from Person {}
"#;
    let uri = Url::parse("file:///test.sb").unwrap();
    let documents = make_documents(vec![("file:///test.sb", source)]);

    let diagnostic = make_diagnostic(
        "name 'Person' not found",
        Range {
            start: Position {
                line: 1,
                character: 21,
            },
            end: Position {
                line: 1,
                character: 27,
            },
        },
    );

    let params = CodeActionParams {
        text_document: TextDocumentIdentifier { uri: uri.clone() },
        range: diagnostic.range,
        context: CodeActionContext {
            diagnostics: vec![diagnostic.clone()],
            only: None,
            trigger_kind: None,
        },
        work_done_progress_params: Default::default(),
        partial_result_params: Default::default(),
    };

    let result = get_code_actions(&documents, &params);
    assert!(result.is_some());

    let actions = result.unwrap();
    let titles: Vec<String> = actions
        .iter()
        .filter_map(|a| {
            if let tower_lsp::lsp_types::CodeActionOrCommand::CodeAction(action) = a {
                Some(action.title.clone())
            } else {
                None
            }
        })
        .collect();

    assert!(titles
        .iter()
        .any(|t| t.contains("Create template 'Person'")));
}

#[test]
fn test_create_missing_species() {
    let source = r#"
character Alice: Human {}
"#;
    let uri = Url::parse("file:///test.sb").unwrap();
    let documents = make_documents(vec![("file:///test.sb", source)]);

    let diagnostic = make_diagnostic(
        "name 'Human' not found",
        Range {
            start: Position {
                line: 1,
                character: 17,
            },
            end: Position {
                line: 1,
                character: 22,
            },
        },
    );

    let params = CodeActionParams {
        text_document: TextDocumentIdentifier { uri: uri.clone() },
        range: diagnostic.range,
        context: CodeActionContext {
            diagnostics: vec![diagnostic.clone()],
            only: None,
            trigger_kind: None,
        },
        work_done_progress_params: Default::default(),
        partial_result_params: Default::default(),
    };

    let result = get_code_actions(&documents, &params);
    assert!(result.is_some());

    let actions = result.unwrap();
    let titles: Vec<String> = actions
        .iter()
        .filter_map(|a| {
            if let tower_lsp::lsp_types::CodeActionOrCommand::CodeAction(action) = a {
                Some(action.title.clone())
            } else {
                None
            }
        })
        .collect();

    assert!(titles.iter().any(|t| t.contains("Create species 'Human'")));
}

// Phase 1: Fix Duplicate Field Tests

#[test]
fn test_fix_duplicate_field() {
    let source = r#"
character Alice {
  age: 25,
  name: "Alice",
  age: 30
}
"#;
    let uri = Url::parse("file:///test.sb").unwrap();
    let documents = make_documents(vec![("file:///test.sb", source)]);

    let diagnostic = make_diagnostic(
        "duplicate field 'age'",
        Range {
            start: Position {
                line: 4,
                character: 2,
            },
            end: Position {
                line: 4,
                character: 5,
            },
        },
    );

    let params = CodeActionParams {
        text_document: TextDocumentIdentifier { uri: uri.clone() },
        range: diagnostic.range,
        context: CodeActionContext {
            diagnostics: vec![diagnostic.clone()],
            only: None,
            trigger_kind: None,
        },
        work_done_progress_params: Default::default(),
        partial_result_params: Default::default(),
    };

    let result = get_code_actions(&documents, &params);
    assert!(result.is_some());

    let actions = result.unwrap();
    let titles: Vec<String> = actions
        .iter()
        .filter_map(|a| {
            if let tower_lsp::lsp_types::CodeActionOrCommand::CodeAction(action) = a {
                Some(action.title.clone())
            } else {
                None
            }
        })
        .collect();

    assert!(titles
        .iter()
        .any(|t| t.contains("Remove duplicate field 'age'")));
}

// Phase 1: Fix Type Mismatch Tests

#[test]
fn test_fix_type_mismatch_enum_string() {
    let source = r#"
enum Emotion { Happy, Sad }

character Alice {
  mood: "Happy"
}
"#;
    let uri = Url::parse("file:///test.sb").unwrap();
    let documents = make_documents(vec![("file:///test.sb", source)]);

    let diagnostic = make_diagnostic(
        "type mismatch: expected Enum, got String",
        Range {
            start: Position {
                line: 4,
                character: 8,
            },
            end: Position {
                line: 4,
                character: 15,
            },
        },
    );

    let params = CodeActionParams {
        text_document: TextDocumentIdentifier { uri: uri.clone() },
        range: diagnostic.range,
        context: CodeActionContext {
            diagnostics: vec![diagnostic.clone()],
            only: None,
            trigger_kind: None,
        },
        work_done_progress_params: Default::default(),
        partial_result_params: Default::default(),
    };

    let result = get_code_actions(&documents, &params);
    assert!(result.is_some());

    let actions = result.unwrap();
    let titles: Vec<String> = actions
        .iter()
        .filter_map(|a| {
            if let tower_lsp::lsp_types::CodeActionOrCommand::CodeAction(action) = a {
                Some(action.title.clone())
            } else {
                None
            }
        })
        .collect();

    assert!(titles
        .iter()
        .any(|t| t.contains("Change to enum value: Happy")));
}

// Phase 1: Fix Duplicate Definition Tests

#[test]
fn test_fix_duplicate_definition() {
    let source = r#"
character Alice {}
character Alice {}
"#;
    let uri = Url::parse("file:///test.sb").unwrap();
    let documents = make_documents(vec![("file:///test.sb", source)]);

    let diagnostic = make_diagnostic(
        "duplicate definition of 'Alice'",
        Range {
            start: Position {
                line: 2,
                character: 10,
            },
            end: Position {
                line: 2,
                character: 15,
            },
        },
    );

    let params = CodeActionParams {
        text_document: TextDocumentIdentifier { uri: uri.clone() },
        range: diagnostic.range,
        context: CodeActionContext {
            diagnostics: vec![diagnostic.clone()],
            only: None,
            trigger_kind: None,
        },
        work_done_progress_params: Default::default(),
        partial_result_params: Default::default(),
    };

    let result = get_code_actions(&documents, &params);
    assert!(result.is_some());

    let actions = result.unwrap();
    let titles: Vec<String> = actions
        .iter()
        .filter_map(|a| {
            if let tower_lsp::lsp_types::CodeActionOrCommand::CodeAction(action) = a {
                Some(action.title.clone())
            } else {
                None
            }
        })
        .collect();

    assert!(titles.iter().any(|t| t.contains("Rename to 'Alice2'")));
    assert!(titles
        .iter()
        .any(|t| t.contains("Remove duplicate definition")));
}

// Phase 1: Remove Unused Symbol Tests

#[test]
fn test_remove_unused_character() {
    let source = r#"
character Alice {}
character Bob {}
character Charlie {
  friend: Alice
}
"#;
    let uri = Url::parse("file:///test.sb").unwrap();
    let documents = make_documents(vec![("file:///test.sb", source)]);

    // Position on Bob declaration
    let range = Range {
        start: Position {
            line: 2,
            character: 10,
        },
        end: Position {
            line: 2,
            character: 13,
        },
    };

    let params = CodeActionParams {
        text_document: TextDocumentIdentifier { uri: uri.clone() },
        range,
        context: CodeActionContext {
            diagnostics: vec![],
            only: None,
            trigger_kind: None,
        },
        work_done_progress_params: Default::default(),
        partial_result_params: Default::default(),
    };

    let result = get_code_actions(&documents, &params);

    if let Some(actions) = result {
        let titles: Vec<String> = actions
            .iter()
            .filter_map(|a| {
                if let tower_lsp::lsp_types::CodeActionOrCommand::CodeAction(action) = a {
                    Some(action.title.clone())
                } else {
                    None
                }
            })
            .collect();

        assert!(titles
            .iter()
            .any(|t| t.contains("Remove unused character 'Bob'")));
    } else {
        // It's okay if no actions are offered - the refactoring action is
        // context-dependent
    }
}

#[test]
fn test_dont_remove_used_character() {
    let source = r#"
character Alice {}
character Bob {
  friend: Alice
}
"#;
    let uri = Url::parse("file:///test.sb").unwrap();
    let documents = make_documents(vec![("file:///test.sb", source)]);

    // Position on Alice declaration
    let range = Range {
        start: Position {
            line: 1,
            character: 10,
        },
        end: Position {
            line: 1,
            character: 15,
        },
    };

    let params = CodeActionParams {
        text_document: TextDocumentIdentifier { uri: uri.clone() },
        range,
        context: CodeActionContext {
            diagnostics: vec![],
            only: None,
            trigger_kind: None,
        },
        work_done_progress_params: Default::default(),
        partial_result_params: Default::default(),
    };

    let result = get_code_actions(&documents, &params);

    // Should not offer to remove Alice since it's used by Bob
    if let Some(actions) = result {
        let titles: Vec<String> = actions
            .iter()
            .filter_map(|a| {
                if let tower_lsp::lsp_types::CodeActionOrCommand::CodeAction(action) = a {
                    Some(action.title.clone())
                } else {
                    None
                }
            })
            .collect();

        assert!(!titles
            .iter()
            .any(|t| t.contains("Remove unused character 'Alice'")));
    }
}

// Helper function tests

#[test]
fn test_extract_quoted_name() {
    use super::code_actions::extract_quoted_name;

    assert_eq!(
        extract_quoted_name("duplicate field 'age'"),
        Some("age".to_string())
    );
    assert_eq!(
        extract_quoted_name("name 'Person' not found"),
        Some("Person".to_string())
    );
    assert_eq!(extract_quoted_name("no quotes here"), None);
}

#[test]
fn test_parse_type_mismatch() {
    use super::code_actions::parse_type_mismatch;

    assert_eq!(
        parse_type_mismatch("type mismatch: expected Enum, got String"),
        Some(("Enum".to_string(), "String".to_string()))
    );
    assert_eq!(
        parse_type_mismatch("type mismatch: expected Number, got String"),
        Some(("Number".to_string(), "String".to_string()))
    );
    assert_eq!(parse_type_mismatch("not a type mismatch"), None);
}

//
// Phase 2 Tests
//

// TODO: Advanced refactoring feature - needs implementation or test fix
// This test expects code actions for adding missing template fields
#[test]
fn test_add_missing_template_fields() {
    let source = r#"
template Person {
  name: "default"
  age: 0
  city: "unknown"
}

character Alice from Person {
  name: "Alice"
  age: 25
}
"#;
    let uri = Url::parse("file:///test.sb").unwrap();
    let documents = make_documents(vec![("file:///test.sb", source)]);

    // Position on Alice character
    let range = Range {
        start: Position {
            line: 8,
            character: 10,
        },
        end: Position {
            line: 8,
            character: 15,
        },
    };

    let params = CodeActionParams {
        text_document: TextDocumentIdentifier { uri: uri.clone() },
        range,
        context: CodeActionContext {
            diagnostics: vec![],
            only: None,
            trigger_kind: None,
        },
        work_done_progress_params: Default::default(),
        partial_result_params: Default::default(),
    };

    let result = get_code_actions(&documents, &params);

    assert!(result.is_some());

    let actions = result.unwrap();
    let titles: Vec<String> = actions
        .iter()
        .filter_map(|a| {
            if let tower_lsp::lsp_types::CodeActionOrCommand::CodeAction(action) = a {
                Some(action.title.clone())
            } else {
                None
            }
        })
        .collect();

    // Should offer to add the missing "city" field
    assert!(titles
        .iter()
        .any(|t| t.contains("Add missing field 'city'") || t.contains("Add 1 missing field")));
}

// TODO: Advanced refactoring feature - template inlining
#[test]
fn test_inline_template() {
    let source = r#"
template Person {
  name: "default"
  age: 0
}

character Alice from Person {
  name: "Alice"
  age: 25
}
"#;
    let uri = Url::parse("file:///test.sb").unwrap();
    let documents = make_documents(vec![("file:///test.sb", source)]);

    // Position on Alice character
    let range = Range {
        start: Position {
            line: 7,
            character: 10,
        },
        end: Position {
            line: 7,
            character: 15,
        },
    };

    let params = CodeActionParams {
        text_document: TextDocumentIdentifier { uri: uri.clone() },
        range,
        context: CodeActionContext {
            diagnostics: vec![],
            only: None,
            trigger_kind: None,
        },
        work_done_progress_params: Default::default(),
        partial_result_params: Default::default(),
    };

    let result = get_code_actions(&documents, &params);

    if let Some(actions) = result {
        let titles: Vec<String> = actions
            .iter()
            .filter_map(|a| {
                if let tower_lsp::lsp_types::CodeActionOrCommand::CodeAction(action) = a {
                    Some(action.title.clone())
                } else {
                    None
                }
            })
            .collect();

        assert!(titles
            .iter()
            .any(|t| t.contains("Inline template 'Person'")));
    }
}

// TODO: Advanced refactoring feature - extract to template
#[test]
fn test_extract_to_template() {
    let source = r#"
character Alice {
  name: "Alice"
  age: 25
  city: "NYC"
}
"#;
    let uri = Url::parse("file:///test.sb").unwrap();
    let documents = make_documents(vec![("file:///test.sb", source)]);

    // Position on Alice character
    let range = Range {
        start: Position {
            line: 2,
            character: 10,
        },
        end: Position {
            line: 2,
            character: 15,
        },
    };

    let params = CodeActionParams {
        text_document: TextDocumentIdentifier { uri: uri.clone() },
        range,
        context: CodeActionContext {
            diagnostics: vec![],
            only: None,
            trigger_kind: None,
        },
        work_done_progress_params: Default::default(),
        partial_result_params: Default::default(),
    };

    let result = get_code_actions(&documents, &params);

    if let Some(actions) = result {
        let titles: Vec<String> = actions
            .iter()
            .filter_map(|a| {
                if let tower_lsp::lsp_types::CodeActionOrCommand::CodeAction(action) = a {
                    Some(action.title.clone())
                } else {
                    None
                }
            })
            .collect();

        assert!(titles.iter().any(|t| t.contains("Extract to template")));
    }
}

#[test]
fn test_no_extract_for_empty_character() {
    let source = r#"
character Alice {}
"#;
    let uri = Url::parse("file:///test.sb").unwrap();
    let documents = make_documents(vec![("file:///test.sb", source)]);

    // Position on Alice character
    let range = Range {
        start: Position {
            line: 1,
            character: 10,
        },
        end: Position {
            line: 1,
            character: 15,
        },
    };

    let params = CodeActionParams {
        text_document: TextDocumentIdentifier { uri: uri.clone() },
        range,
        context: CodeActionContext {
            diagnostics: vec![],
            only: None,
            trigger_kind: None,
        },
        work_done_progress_params: Default::default(),
        partial_result_params: Default::default(),
    };

    let result = get_code_actions(&documents, &params);

    // Should not offer extract to template for empty character
    if let Some(actions) = result {
        let titles: Vec<String> = actions
            .iter()
            .filter_map(|a| {
                if let tower_lsp::lsp_types::CodeActionOrCommand::CodeAction(action) = a {
                    Some(action.title.clone())
                } else {
                    None
                }
            })
            .collect();

        assert!(!titles.iter().any(|t| t.contains("Extract to template")));
    }
}

//
// Phase 3 Tests
//

// TODO: Advanced refactoring feature - character generation from template
#[test]
fn test_generate_character_from_template() {
    let source = r#"
template Person {
  name: "default"
  age: 0
}
"#;
    let uri = Url::parse("file:///test.sb").unwrap();
    let documents = make_documents(vec![("file:///test.sb", source)]);

    // Position on Person template
    let range = Range {
        start: Position {
            line: 2,
            character: 10,
        },
        end: Position {
            line: 2,
            character: 16,
        },
    };

    let params = CodeActionParams {
        text_document: TextDocumentIdentifier { uri: uri.clone() },
        range,
        context: CodeActionContext {
            diagnostics: vec![],
            only: None,
            trigger_kind: None,
        },
        work_done_progress_params: Default::default(),
        partial_result_params: Default::default(),
    };

    let result = get_code_actions(&documents, &params);

    if let Some(actions) = result {
        let titles: Vec<String> = actions
            .iter()
            .filter_map(|a| {
                if let tower_lsp::lsp_types::CodeActionOrCommand::CodeAction(action) = a {
                    Some(action.title.clone())
                } else {
                    None
                }
            })
            .collect();

        assert!(titles
            .iter()
            .any(|t| t.contains("Generate character from template 'Person'")));
    }
}

#[test]
fn test_sort_declarations() {
    let source = r#"
character Zelda {}
character Alice {}
template Zoo {}
template Animal {}
"#;
    let uri = Url::parse("file:///test.sb").unwrap();
    let documents = make_documents(vec![("file:///test.sb", source)]);

    // Position anywhere in the file
    let range = Range {
        start: Position {
            line: 0,
            character: 0,
        },
        end: Position {
            line: 0,
            character: 0,
        },
    };

    let params = CodeActionParams {
        text_document: TextDocumentIdentifier { uri: uri.clone() },
        range,
        context: CodeActionContext {
            diagnostics: vec![],
            only: None,
            trigger_kind: None,
        },
        work_done_progress_params: Default::default(),
        partial_result_params: Default::default(),
    };

    let result = get_code_actions(&documents, &params);

    if let Some(actions) = result {
        let titles: Vec<String> = actions
            .iter()
            .filter_map(|a| {
                if let tower_lsp::lsp_types::CodeActionOrCommand::CodeAction(action) = a {
                    Some(action.title.clone())
                } else {
                    None
                }
            })
            .collect();

        assert!(titles.iter().any(|t| t.contains("Sort declarations")));
    }
}

#[test]
fn test_no_sort_if_already_sorted() {
    let source = r#"
template Animal {}
template Zoo {}

character Alice {}
character Zelda {}
"#;
    let uri = Url::parse("file:///test.sb").unwrap();
    let documents = make_documents(vec![("file:///test.sb", source)]);

    // Position anywhere in the file
    let range = Range {
        start: Position {
            line: 0,
            character: 0,
        },
        end: Position {
            line: 0,
            character: 0,
        },
    };

    let params = CodeActionParams {
        text_document: TextDocumentIdentifier { uri: uri.clone() },
        range,
        context: CodeActionContext {
            diagnostics: vec![],
            only: None,
            trigger_kind: None,
        },
        work_done_progress_params: Default::default(),
        partial_result_params: Default::default(),
    };

    let result = get_code_actions(&documents, &params);

    // Should not offer sort if already sorted
    if let Some(actions) = result {
        let titles: Vec<String> = actions
            .iter()
            .filter_map(|a| {
                if let tower_lsp::lsp_types::CodeActionOrCommand::CodeAction(action) = a {
                    Some(action.title.clone())
                } else {
                    None
                }
            })
            .collect();

        assert!(!titles.iter().any(|t| t.contains("Sort declarations")));
    }
}

// TODO: Advanced refactoring feature - extract common fields to template
#[test]
fn test_extract_common_fields() {
    let source = r#"
character Alice {
  name: "Alice"
  age: 25
  city: "NYC"
}

character Bob {
  name: "Bob"
  age: 30
  country: "USA"
}
"#;
    let uri = Url::parse("file:///test.sb").unwrap();
    let documents = make_documents(vec![("file:///test.sb", source)]);

    // Position on Alice character
    let range = Range {
        start: Position {
            line: 2,
            character: 10,
        },
        end: Position {
            line: 2,
            character: 15,
        },
    };

    let params = CodeActionParams {
        text_document: TextDocumentIdentifier { uri: uri.clone() },
        range,
        context: CodeActionContext {
            diagnostics: vec![],
            only: None,
            trigger_kind: None,
        },
        work_done_progress_params: Default::default(),
        partial_result_params: Default::default(),
    };

    let result = get_code_actions(&documents, &params);

    if let Some(actions) = result {
        let titles: Vec<String> = actions
            .iter()
            .filter_map(|a| {
                if let tower_lsp::lsp_types::CodeActionOrCommand::CodeAction(action) = a {
                    Some(action.title.clone())
                } else {
                    None
                }
            })
            .collect();

        // Should find common fields "name" and "age"
        assert!(titles
            .iter()
            .any(|t| t.contains("Extract 2 common fields to template")));
    }
}

#[test]
fn test_no_common_fields() {
    let source = r#"
character Alice {
  city: "NYC"
}

character Bob {
  country: "USA"
}
"#;
    let uri = Url::parse("file:///test.sb").unwrap();
    let documents = make_documents(vec![("file:///test.sb", source)]);

    // Position on Alice character
    let range = Range {
        start: Position {
            line: 2,
            character: 10,
        },
        end: Position {
            line: 2,
            character: 15,
        },
    };

    let params = CodeActionParams {
        text_document: TextDocumentIdentifier { uri: uri.clone() },
        range,
        context: CodeActionContext {
            diagnostics: vec![],
            only: None,
            trigger_kind: None,
        },
        work_done_progress_params: Default::default(),
        partial_result_params: Default::default(),
    };

    let result = get_code_actions(&documents, &params);

    // Should not offer extract common fields when there are none
    if let Some(actions) = result {
        let titles: Vec<String> = actions
            .iter()
            .filter_map(|a| {
                if let tower_lsp::lsp_types::CodeActionOrCommand::CodeAction(action) = a {
                    Some(action.title.clone())
                } else {
                    None
                }
            })
            .collect();

        assert!(!titles
            .iter()
            .any(|t| t.contains("Extract") && t.contains("common")));
    }
}

// TODO: Advanced refactoring feature - relationship scaffolding
#[test]
fn test_create_relationship_scaffold() {
    let source = r#"
character Alice {}
character Bob {}
"#;
    let uri = Url::parse("file:///test.sb").unwrap();
    let documents = make_documents(vec![("file:///test.sb", source)]);

    // Position on Alice character
    let range = Range {
        start: Position {
            line: 2,
            character: 10,
        },
        end: Position {
            line: 2,
            character: 15,
        },
    };

    let params = CodeActionParams {
        text_document: TextDocumentIdentifier { uri: uri.clone() },
        range,
        context: CodeActionContext {
            diagnostics: vec![],
            only: None,
            trigger_kind: None,
        },
        work_done_progress_params: Default::default(),
        partial_result_params: Default::default(),
    };

    let result = get_code_actions(&documents, &params);

    if let Some(actions) = result {
        let titles: Vec<String> = actions
            .iter()
            .filter_map(|a| {
                if let tower_lsp::lsp_types::CodeActionOrCommand::CodeAction(action) = a {
                    Some(action.title.clone())
                } else {
                    None
                }
            })
            .collect();

        assert!(titles
            .iter()
            .any(|t| t.contains("Create relationship between")));
    }
}

#[test]
fn test_no_relationship_with_single_character() {
    let source = r#"
character Alice {}
"#;
    let uri = Url::parse("file:///test.sb").unwrap();
    let documents = make_documents(vec![("file:///test.sb", source)]);

    // Position on Alice character
    let range = Range {
        start: Position {
            line: 2,
            character: 10,
        },
        end: Position {
            line: 2,
            character: 15,
        },
    };

    let params = CodeActionParams {
        text_document: TextDocumentIdentifier { uri: uri.clone() },
        range,
        context: CodeActionContext {
            diagnostics: vec![],
            only: None,
            trigger_kind: None,
        },
        work_done_progress_params: Default::default(),
        partial_result_params: Default::default(),
    };

    let result = get_code_actions(&documents, &params);

    // Should not offer relationship creation with only one character
    if let Some(actions) = result {
        let titles: Vec<String> = actions
            .iter()
            .filter_map(|a| {
                if let tower_lsp::lsp_types::CodeActionOrCommand::CodeAction(action) = a {
                    Some(action.title.clone())
                } else {
                    None
                }
            })
            .collect();

        assert!(!titles.iter().any(|t| t.contains("Create relationship")));
    }
}

//
// Phase 4 Tests
//

#[test]
fn test_fix_invalid_field_access() {
    let source = r#"
template Person {
  name: String
}

character Alice from Person {
  age: 25
}
"#;
    let uri = Url::parse("file:///test.sb").unwrap();
    let documents = make_documents(vec![("file:///test.sb", source)]);

    let diagnostic = make_diagnostic(
        "field 'age' does not exist on type 'Person'",
        Range {
            start: Position {
                line: 6,
                character: 2,
            },
            end: Position {
                line: 6,
                character: 5,
            },
        },
    );

    let params = CodeActionParams {
        text_document: TextDocumentIdentifier { uri: uri.clone() },
        range: diagnostic.range,
        context: CodeActionContext {
            diagnostics: vec![diagnostic.clone()],
            only: None,
            trigger_kind: None,
        },
        work_done_progress_params: Default::default(),
        partial_result_params: Default::default(),
    };

    let result = get_code_actions(&documents, &params);
    assert!(result.is_some());

    let actions = result.unwrap();
    let titles: Vec<String> = actions
        .iter()
        .filter_map(|a| {
            if let tower_lsp::lsp_types::CodeActionOrCommand::CodeAction(action) = a {
                Some(action.title.clone())
            } else {
                None
            }
        })
        .collect();

    assert!(titles
        .iter()
        .any(|t| t.contains("Remove invalid field 'age'")));
}

#[test]
fn test_fix_trait_out_of_range() {
    let source = r#"
character Alice {
  age: 150
}
"#;
    let uri = Url::parse("file:///test.sb").unwrap();
    let documents = make_documents(vec![("file:///test.sb", source)]);

    let diagnostic = make_diagnostic(
        "trait value 150 out of range [0, 120]",
        Range {
            start: Position {
                line: 2,
                character: 7,
            },
            end: Position {
                line: 2,
                character: 10,
            },
        },
    );

    let params = CodeActionParams {
        text_document: TextDocumentIdentifier { uri: uri.clone() },
        range: diagnostic.range,
        context: CodeActionContext {
            diagnostics: vec![diagnostic.clone()],
            only: None,
            trigger_kind: None,
        },
        work_done_progress_params: Default::default(),
        partial_result_params: Default::default(),
    };

    let result = get_code_actions(&documents, &params);
    assert!(result.is_some());

    let actions = result.unwrap();
    let titles: Vec<String> = actions
        .iter()
        .filter_map(|a| {
            if let tower_lsp::lsp_types::CodeActionOrCommand::CodeAction(action) = a {
                Some(action.title.clone())
            } else {
                None
            }
        })
        .collect();

    assert!(titles
        .iter()
        .any(|t| t.contains("Change to maximum value (120)")));
    assert!(titles
        .iter()
        .any(|t| t.contains("Change to minimum value (0)")));
}

#[test]
fn test_fix_schedule_overlap() {
    let source = r#"
schedule {
  00:00-12:00 Morning
  10:00-14:00 Lunch
}
"#;
    let uri = Url::parse("file:///test.sb").unwrap();
    let documents = make_documents(vec![("file:///test.sb", source)]);

    let diagnostic = make_diagnostic(
        "schedule overlap: Morning and Lunch",
        Range {
            start: Position {
                line: 3,
                character: 2,
            },
            end: Position {
                line: 3,
                character: 17,
            },
        },
    );

    let params = CodeActionParams {
        text_document: TextDocumentIdentifier { uri: uri.clone() },
        range: diagnostic.range,
        context: CodeActionContext {
            diagnostics: vec![diagnostic.clone()],
            only: None,
            trigger_kind: None,
        },
        work_done_progress_params: Default::default(),
        partial_result_params: Default::default(),
    };

    let result = get_code_actions(&documents, &params);
    assert!(result.is_some());

    let actions = result.unwrap();
    let titles: Vec<String> = actions
        .iter()
        .filter_map(|a| {
            if let tower_lsp::lsp_types::CodeActionOrCommand::CodeAction(action) = a {
                Some(action.title.clone())
            } else {
                None
            }
        })
        .collect();

    assert!(titles
        .iter()
        .any(|t| t.contains("Remove overlapping schedule entry")));
}

// TODO: Advanced refactoring feature - convert species to template
#[test]
fn test_convert_species_to_template() {
    let source = r#"
character Alice: Human {}
"#;
    let uri = Url::parse("file:///test.sb").unwrap();
    let documents = make_documents(vec![("file:///test.sb", source)]);

    // Position on Alice character
    let range = Range {
        start: Position {
            line: 1,
            character: 10,
        },
        end: Position {
            line: 1,
            character: 15,
        },
    };

    let params = CodeActionParams {
        text_document: TextDocumentIdentifier { uri: uri.clone() },
        range,
        context: CodeActionContext {
            diagnostics: vec![],
            only: None,
            trigger_kind: None,
        },
        work_done_progress_params: Default::default(),
        partial_result_params: Default::default(),
    };

    let result = get_code_actions(&documents, &params);

    if let Some(actions) = result {
        let titles: Vec<String> = actions
            .iter()
            .filter_map(|a| {
                if let tower_lsp::lsp_types::CodeActionOrCommand::CodeAction(action) = a {
                    Some(action.title.clone())
                } else {
                    None
                }
            })
            .collect();

        assert!(titles
            .iter()
            .any(|t| t.contains("Convert species to template")));
    }
}

// TODO: Advanced refactoring feature - convert template to species
#[test]
fn test_convert_template_to_species() {
    let source = r#"
character Alice from Person {}
"#;
    let uri = Url::parse("file:///test.sb").unwrap();
    let documents = make_documents(vec![("file:///test.sb", source)]);

    // Position on Alice character
    let range = Range {
        start: Position {
            line: 1,
            character: 10,
        },
        end: Position {
            line: 1,
            character: 15,
        },
    };

    let params = CodeActionParams {
        text_document: TextDocumentIdentifier { uri: uri.clone() },
        range,
        context: CodeActionContext {
            diagnostics: vec![],
            only: None,
            trigger_kind: None,
        },
        work_done_progress_params: Default::default(),
        partial_result_params: Default::default(),
    };

    let result = get_code_actions(&documents, &params);

    if let Some(actions) = result {
        let titles: Vec<String> = actions
            .iter()
            .filter_map(|a| {
                if let tower_lsp::lsp_types::CodeActionOrCommand::CodeAction(action) = a {
                    Some(action.title.clone())
                } else {
                    None
                }
            })
            .collect();

        eprintln!("DEBUG convert_template_to_species: {:?}", titles);

        assert!(titles
            .iter()
            .any(|t| t.contains("Convert template to species")));
    }
}
