//! Tests for navigation features (go-to-definition and find-references)

#[cfg(test)]
mod tests {
    use tower_lsp::lsp_types::{
        GotoDefinitionParams,
        Position,
        ReferenceContext,
        ReferenceParams,
        TextDocumentIdentifier,
        TextDocumentPositionParams,
        Url,
    };

    use crate::lsp::{
        definition,
        document::Document,
        references,
    };

    fn make_uri() -> Url {
        Url::parse("file:///test.sb").unwrap()
    }

    #[test]
    fn test_goto_definition_character() {
        let source = "character Alice { age: 7 }";
        let doc = Document::new(source.to_string());
        let uri = make_uri();

        // Position on "Alice" (at character 10)
        let params = GotoDefinitionParams {
            text_document_position_params: TextDocumentPositionParams {
                text_document: TextDocumentIdentifier { uri: uri.clone() },
                position: Position {
                    line: 0,
                    character: 10,
                },
            },
            work_done_progress_params: Default::default(),
            partial_result_params: Default::default(),
        };

        let result = definition::get_definition(&doc, &params, &uri);
        assert!(result.is_some(), "Should find definition for Alice");

        // Verify it points to the character declaration
        if let Some(response) = result {
            match response {
                | tower_lsp::lsp_types::GotoDefinitionResponse::Scalar(location) => {
                    assert_eq!(location.uri, uri);
                    // Should span the whole character declaration
                    assert_eq!(location.range.start.line, 0);
                },
                | _ => panic!("Expected scalar location"),
            }
        }
    }

    #[test]
    fn test_goto_definition_not_found() {
        let source = "character Alice { age: 7 }";
        let doc = Document::new(source.to_string());
        let uri = make_uri();

        // Position on whitespace
        let params = GotoDefinitionParams {
            text_document_position_params: TextDocumentPositionParams {
                text_document: TextDocumentIdentifier { uri: uri.clone() },
                position: Position {
                    line: 0,
                    character: 0,
                },
            },
            work_done_progress_params: Default::default(),
            partial_result_params: Default::default(),
        };

        let result = definition::get_definition(&doc, &params, &uri);
        assert!(result.is_none(), "Should not find definition on whitespace");
    }

    #[test]
    fn test_goto_definition_template() {
        let source = r#"
template Child {
    age: number
}

character Alice: Child {}
"#;
        let doc = Document::new(source.to_string());
        let uri = make_uri();

        // Position on "Child" in template declaration (line 1)
        let params = GotoDefinitionParams {
            text_document_position_params: TextDocumentPositionParams {
                text_document: TextDocumentIdentifier { uri: uri.clone() },
                position: Position {
                    line: 1,
                    character: 9,
                },
            },
            work_done_progress_params: Default::default(),
            partial_result_params: Default::default(),
        };

        let result = definition::get_definition(&doc, &params, &uri);
        assert!(
            result.is_some(),
            "Should find definition for Child template"
        );
    }

    #[test]
    fn test_find_references_character() {
        let source = r#"
character Alice { age: 7 }
character Bob { friend: Alice }
"#;
        let doc = Document::new(source.to_string());
        let uri = make_uri();

        // Position on "Alice" in first line
        let params = ReferenceParams {
            text_document_position: TextDocumentPositionParams {
                text_document: TextDocumentIdentifier { uri: uri.clone() },
                position: Position {
                    line: 1,
                    character: 10,
                },
            },
            work_done_progress_params: Default::default(),
            partial_result_params: Default::default(),
            context: ReferenceContext {
                include_declaration: true,
            },
        };

        let result = references::find_references(&doc, &params, &uri);
        assert!(result.is_some(), "Should find references to Alice");

        if let Some(locations) = result {
            // Should find at least 2 references: the declaration and the use in Bob's
            // friend field
            assert!(
                locations.len() >= 2,
                "Should find multiple references to Alice, found {}",
                locations.len()
            );
        }
    }

    #[test]
    fn test_find_references_single_occurrence() {
        let source = "character Bob { age: 5 }";
        let doc = Document::new(source.to_string());
        let uri = make_uri();

        // Position on "Bob"
        let params = ReferenceParams {
            text_document_position: TextDocumentPositionParams {
                text_document: TextDocumentIdentifier { uri: uri.clone() },
                position: Position {
                    line: 0,
                    character: 10,
                },
            },
            work_done_progress_params: Default::default(),
            partial_result_params: Default::default(),
            context: ReferenceContext {
                include_declaration: true,
            },
        };

        let result = references::find_references(&doc, &params, &uri);
        assert!(result.is_some(), "Should find reference to Bob");

        if let Some(locations) = result {
            assert_eq!(locations.len(), 1, "Should find exactly one reference");
        }
    }

    #[test]
    fn test_find_references_not_found() {
        let source = "character Alice { age: 7 }";
        let doc = Document::new(source.to_string());
        let uri = make_uri();

        // Position on punctuation character (the opening brace)
        let params = ReferenceParams {
            text_document_position: TextDocumentPositionParams {
                text_document: TextDocumentIdentifier { uri: uri.clone() },
                position: Position {
                    line: 0,
                    character: 16,
                }, // Position on '{'
            },
            work_done_progress_params: Default::default(),
            partial_result_params: Default::default(),
            context: ReferenceContext {
                include_declaration: true,
            },
        };

        let result = references::find_references(&doc, &params, &uri);
        // It's okay if this finds something or nothing - the important thing is it
        // doesn't crash If word_at_offset returns None, this will be None
        // If it returns the nearby word, that's also acceptable behavior
        let _ = result; // Just verify it doesn't panic
    }

    #[test]
    fn test_find_references_word_boundaries() {
        let source = r#"
character Alice { age: 7 }
character Alison { age: 8 }
character Ali { age: 6 }
"#;
        let doc = Document::new(source.to_string());
        let uri = make_uri();

        // Position on "Alice"
        let params = ReferenceParams {
            text_document_position: TextDocumentPositionParams {
                text_document: TextDocumentIdentifier { uri: uri.clone() },
                position: Position {
                    line: 1,
                    character: 10,
                },
            },
            work_done_progress_params: Default::default(),
            partial_result_params: Default::default(),
            context: ReferenceContext {
                include_declaration: true,
            },
        };

        let result = references::find_references(&doc, &params, &uri);
        assert!(result.is_some(), "Should find references to Alice");

        if let Some(locations) = result {
            // Should only find "Alice", not "Alison" or "Ali"
            assert_eq!(locations.len(), 1, "Should only find exact match for Alice");
        }
    }

    #[test]
    fn test_find_references_multiple_files_same_name() {
        let source = r#"
template Child { age: number }
character Alice: Child { age: 7 }
character Bob: Child { age: 5 }
"#;
        let doc = Document::new(source.to_string());
        let uri = make_uri();

        // Position on "Child" in template declaration
        let params = ReferenceParams {
            text_document_position: TextDocumentPositionParams {
                text_document: TextDocumentIdentifier { uri: uri.clone() },
                position: Position {
                    line: 1,
                    character: 9,
                },
            },
            work_done_progress_params: Default::default(),
            partial_result_params: Default::default(),
            context: ReferenceContext {
                include_declaration: true,
            },
        };

        let result = references::find_references(&doc, &params, &uri);
        assert!(result.is_some(), "Should find references to Child template");

        if let Some(locations) = result {
            // Should find declaration + 2 uses (Alice: Child, Bob: Child)
            assert!(
                locations.len() >= 3,
                "Should find template declaration and uses, found {}",
                locations.len()
            );
        }
    }

    #[test]
    fn test_goto_definition_behavior() {
        let source = r#"
behavior WalkAround {
    patrol
}
"#;
        let doc = Document::new(source.to_string());
        let uri = make_uri();

        // Position on "WalkAround"
        let params = GotoDefinitionParams {
            text_document_position_params: TextDocumentPositionParams {
                text_document: TextDocumentIdentifier { uri: uri.clone() },
                position: Position {
                    line: 1,
                    character: 9,
                },
            },
            work_done_progress_params: Default::default(),
            partial_result_params: Default::default(),
        };

        let result = definition::get_definition(&doc, &params, &uri);
        assert!(
            result.is_some(),
            "Should find definition for WalkAround behavior"
        );
    }

    #[test]
    fn test_find_references_species() {
        let source = r#"
species Human {}
character Alice: Human {}
character Bob: Human {}
"#;
        let doc = Document::new(source.to_string());
        let uri = make_uri();

        // Position on "Human" in species declaration
        let params = ReferenceParams {
            text_document_position: TextDocumentPositionParams {
                text_document: TextDocumentIdentifier { uri: uri.clone() },
                position: Position {
                    line: 1,
                    character: 8,
                },
            },
            work_done_progress_params: Default::default(),
            partial_result_params: Default::default(),
            context: ReferenceContext {
                include_declaration: true,
            },
        };

        let result = references::find_references(&doc, &params, &uri);
        assert!(result.is_some(), "Should find references to Human species");

        if let Some(locations) = result {
            // Should find declaration + 2 uses
            assert!(
                locations.len() >= 3,
                "Should find species declaration and character uses, found {}",
                locations.len()
            );
        }
    }

    #[test]
    fn test_goto_definition_multiline() {
        // Don't start with newline to make line numbers clearer
        let source = "character Alice {\n    age: 7\n}\n\ncharacter Bob {\n    friend: Alice\n}";
        let doc = Document::new(source.to_string());
        let uri = make_uri();

        // Position on "Alice" in Bob's friend field (line 5)
        let params = GotoDefinitionParams {
            text_document_position_params: TextDocumentPositionParams {
                text_document: TextDocumentIdentifier { uri: uri.clone() },
                position: Position {
                    line: 5,
                    character: 12,
                },
            },
            work_done_progress_params: Default::default(),
            partial_result_params: Default::default(),
        };

        let result = definition::get_definition(&doc, &params, &uri);
        assert!(result.is_some(), "Should find definition for Alice");

        if let Some(response) = result {
            match response {
                | tower_lsp::lsp_types::GotoDefinitionResponse::Scalar(location) => {
                    // Should point to Alice's declaration on line 0
                    assert_eq!(location.range.start.line, 0);
                },
                | _ => panic!("Expected scalar location"),
            }
        }
    }
}
