//! Comprehensive LSP tests organized by LSP protocol categories.
//!
//! Test structure follows patterns from rust-analyzer and sqls:
//! - Unit tests per handler (fast, fixture-based)
//! - Edge cases from mature LSP implementations
//! - Cursor position markers via (line, col) tuples

// ═══════════════════════════════════════════════════════════════════════
// 1. DOCUMENT SYNCHRONIZATION
// ═══════════════════════════════════════════════════════════════════════

#[cfg(test)]
mod document_sync {
	use crate::document::DocumentStore;
	use tower_lsp::lsp_types::Url;

	fn uri(s: &str) -> Url {
		Url::parse(s).unwrap()
	}

	#[test]
	fn open_stores_content() {
		let store = DocumentStore::new();
		store.open(uri("file:///a.surql"), "SELECT * FROM user".into());
		assert_eq!(
			store.get(&uri("file:///a.surql")).unwrap(),
			"SELECT * FROM user"
		);
	}

	#[test]
	fn update_replaces_content() {
		let store = DocumentStore::new();
		store.open(uri("file:///a.surql"), "old".into());
		store.update(&uri("file:///a.surql"), "new".into());
		assert_eq!(store.get(&uri("file:///a.surql")).unwrap(), "new");
	}

	#[test]
	fn close_removes_document() {
		let store = DocumentStore::new();
		store.open(uri("file:///a.surql"), "content".into());
		store.close(&uri("file:///a.surql"));
		assert!(store.get(&uri("file:///a.surql")).is_none());
	}

	#[test]
	fn get_nonexistent_returns_none() {
		let store = DocumentStore::new();
		assert!(store.get(&uri("file:///missing.surql")).is_none());
	}

	#[test]
	fn multiple_documents_isolated() {
		let store = DocumentStore::new();
		store.open(uri("file:///a.surql"), "aaa".into());
		store.open(uri("file:///b.surql"), "bbb".into());
		store.update(&uri("file:///a.surql"), "updated".into());
		assert_eq!(store.get(&uri("file:///a.surql")).unwrap(), "updated");
		assert_eq!(store.get(&uri("file:///b.surql")).unwrap(), "bbb");
	}

	#[test]
	fn update_nonexistent_creates_it() {
		let store = DocumentStore::new();
		store.update(&uri("file:///new.surql"), "created".into());
		assert_eq!(store.get(&uri("file:///new.surql")).unwrap(), "created");
	}

	#[test]
	fn close_nonexistent_no_panic() {
		let store = DocumentStore::new();
		store.close(&uri("file:///never_opened.surql")); // must not panic
	}

	#[test]
	fn empty_content() {
		let store = DocumentStore::new();
		store.open(uri("file:///empty.surql"), String::new());
		assert_eq!(store.get(&uri("file:///empty.surql")).unwrap(), "");
	}
}

// ═══════════════════════════════════════════════════════════════════════
// 2. DIAGNOSTICS
// ═══════════════════════════════════════════════════════════════════════

#[cfg(test)]
mod diagnostics {
	use crate::diagnostics;
	use tower_lsp::lsp_types::DiagnosticSeverity;

	// ─── Valid inputs produce no diagnostics ───

	#[test]
	fn empty_input() {
		assert!(diagnostics::compute("").is_empty());
	}

	#[test]
	fn comment_only() {
		assert!(diagnostics::compute("-- comment").is_empty());
	}

	#[test]
	fn block_comment_only() {
		assert!(diagnostics::compute("/* block */").is_empty());
	}

	#[test]
	fn whitespace_only() {
		assert!(diagnostics::compute("   \n\t\n  ").is_empty());
	}

	#[test]
	fn single_statement() {
		assert!(diagnostics::compute("SELECT * FROM user").is_empty());
	}

	#[test]
	fn multiple_statements() {
		assert!(diagnostics::compute("SELECT * FROM a; SELECT * FROM b").is_empty());
	}

	#[test]
	fn define_table() {
		assert!(diagnostics::compute("DEFINE TABLE user SCHEMAFULL").is_empty());
	}

	#[test]
	fn define_function() {
		assert!(
			diagnostics::compute("DEFINE FUNCTION fn::greet($name: string) { RETURN 'Hello'; }")
				.is_empty()
		);
	}

	#[test]
	fn relate() {
		assert!(
			diagnostics::compute("RELATE user:tobie->follows->user:jaime SET since = time::now()")
				.is_empty()
		);
	}

	#[test]
	fn graph_traversal() {
		assert!(
			diagnostics::compute("SELECT ->follows->user.name AS friends FROM user:tobie")
				.is_empty()
		);
	}

	#[test]
	fn complex_query() {
		assert!(
			diagnostics::compute(
				"SELECT count() AS total, country FROM user GROUP BY country ORDER BY total DESC"
			)
			.is_empty()
		);
	}

	// ─── Invalid inputs produce diagnostics ───

	#[test]
	fn syntax_error_returns_diagnostic() {
		let diags = diagnostics::compute("SELEC * FROM user");
		assert!(!diags.is_empty());
	}

	#[test]
	fn severity_is_error() {
		let diags = diagnostics::compute("SELEC * FROM user");
		assert_eq!(diags[0].severity, Some(DiagnosticSeverity::ERROR));
	}

	#[test]
	fn source_is_surql() {
		let diags = diagnostics::compute("SELEC * FROM user");
		assert_eq!(diags[0].source.as_deref(), Some("surql"));
	}

	#[test]
	fn position_is_zero_indexed() {
		let diags = diagnostics::compute("SELEC * FROM user");
		assert_eq!(diags[0].range.start.line, 0);
	}

	#[test]
	fn unclosed_string() {
		let diags = diagnostics::compute("SELECT * FROM user WHERE name = 'oops");
		assert!(!diags.is_empty());
	}

	#[test]
	fn unclosed_paren() {
		let diags = diagnostics::compute("SELECT * FROM (SELECT name FROM user");
		assert!(!diags.is_empty());
	}

	#[test]
	fn unclosed_brace() {
		let diags = diagnostics::compute("DEFINE FUNCTION fn::test() {");
		assert!(!diags.is_empty());
	}

	#[test]
	fn multiline_error_on_correct_line() {
		let diags = diagnostics::compute("SELECT * FROM user;\nSELEC * FROM post");
		assert!(!diags.is_empty());
		assert_eq!(diags[0].range.start.line, 1); // second line, 0-indexed
	}

	#[test]
	fn error_in_second_statement() {
		let diags = diagnostics::compute("SELECT * FROM user; SELEC * FROM post");
		assert!(!diags.is_empty());
	}

	#[test]
	fn message_is_descriptive() {
		let diags = diagnostics::compute("SELEC * FROM user");
		assert!(!diags[0].message.is_empty());
	}

	#[test]
	fn invalid_type_annotation() {
		// "strng" is treated as an identifier but recovery parser may produce
		// diagnostics depending on trailing context. Just verify we get a result
		// (empty or not) without panicking.
		let diags = diagnostics::compute("DEFINE FIELD name ON user TYPE strng");
		// The parser may or may not flag this — the important thing is no crash
		// and if there are diagnostics, they have proper positions
		for d in &diags {
			assert!(d.range.start.line == 0);
			assert!(!d.message.is_empty());
		}
	}
}

// ═══════════════════════════════════════════════════════════════════════
// 3. COMPLETION — Context Detection
// ═══════════════════════════════════════════════════════════════════════

#[cfg(test)]
mod completion_context {
	use crate::completion::{Context, detect_context};
	use tower_lsp::lsp_types::Position;

	fn pos(line: u32, col: u32) -> Position {
		Position {
			line,
			character: col,
		}
	}

	// ─── General context ───

	#[test]
	fn empty_document() {
		assert_eq!(detect_context("", pos(0, 0)), Context::General);
	}

	#[test]
	fn start_of_line() {
		assert_eq!(detect_context("SELECT", pos(0, 0)), Context::General);
	}

	#[test]
	fn after_select() {
		assert_eq!(detect_context("SELECT ", pos(0, 7)), Context::General);
	}

	#[test]
	fn after_where() {
		assert_eq!(
			detect_context("SELECT * FROM user WHERE ", pos(0, 25)),
			Context::General
		);
	}

	#[test]
	fn after_equals() {
		assert_eq!(detect_context("SET name = ", pos(0, 11)), Context::General);
	}

	// ─── Table name context ───

	#[test]
	fn after_from() {
		assert_eq!(
			detect_context("SELECT * FROM ", pos(0, 14)),
			Context::TableName
		);
	}

	#[test]
	fn after_into() {
		assert_eq!(
			detect_context("INSERT INTO ", pos(0, 12)),
			Context::TableName
		);
	}

	#[test]
	fn after_on() {
		assert_eq!(
			detect_context("DEFINE FIELD name ON ", pos(0, 21)),
			Context::TableName
		);
	}

	#[test]
	fn after_table() {
		assert_eq!(
			detect_context("DEFINE TABLE ", pos(0, 13)),
			Context::TableName
		);
	}

	#[test]
	fn from_case_insensitive() {
		assert_eq!(
			detect_context("select * from ", pos(0, 14)),
			Context::TableName
		);
	}

	#[test]
	fn from_on_second_line() {
		assert_eq!(
			detect_context("SELECT *\nFROM ", pos(1, 5)),
			Context::TableName
		);
	}

	// ─── Function name context ───

	#[test]
	fn after_fn_colons() {
		assert_eq!(
			detect_context("SELECT fn::", pos(0, 11)),
			Context::FunctionName
		);
	}

	#[test]
	fn after_fn_single_colon() {
		// `fn:` (single colon) is incomplete — not yet a function context
		// This is correct: the token-based approach requires `fn::` (PathSeperator)
		assert_eq!(detect_context("fn:", pos(0, 3)), Context::General);
	}

	// ─── Param name context ───

	#[test]
	fn after_dollar_sign() {
		assert_eq!(
			detect_context("WHERE age > $", pos(0, 13)),
			Context::ParamName
		);
	}

	#[test]
	fn dollar_at_line_start() {
		assert_eq!(detect_context("$", pos(0, 1)), Context::ParamName);
	}

	#[test]
	fn dollar_after_equals() {
		assert_eq!(
			detect_context("SET name = $", pos(0, 12)),
			Context::ParamName
		);
	}

	// ─── Token-based correctness (the text heuristic got these wrong) ───

	#[test]
	fn from_inside_string_is_not_table_context() {
		// "SELECT * FROM user WHERE name = 'FROM '" — cursor after string
		// The old text heuristic would incorrectly detect "FROM " and return TableName
		assert_eq!(
			detect_context("SELECT * FROM user WHERE name = 'FROM '", pos(0, 39)),
			Context::General
		);
	}

	#[test]
	fn from_in_comment_is_not_table_context() {
		assert_eq!(
			detect_context("-- FROM \nSELECT * FROM user", pos(0, 8)),
			Context::General
		);
	}

	#[test]
	fn dollar_inside_string_is_not_param() {
		assert_eq!(
			detect_context("SELECT * FROM user WHERE name = '$x'", pos(0, 35)),
			Context::General
		);
	}
}

// ═══════════════════════════════════════════════════════════════════════
// 4. COMPLETION — Result Items
// ═══════════════════════════════════════════════════════════════════════

#[cfg(test)]
mod completion_items {
	use crate::completion;
	use surql_parser::SchemaGraph;
	use tower_lsp::lsp_types::{CompletionItemKind, Position};

	fn pos(line: u32, col: u32) -> Position {
		Position {
			line,
			character: col,
		}
	}

	fn schema() -> SchemaGraph {
		SchemaGraph::from_source(
			"
			DEFINE TABLE user SCHEMAFULL;
			DEFINE FIELD name ON user TYPE string;
			DEFINE FIELD age ON user TYPE int;
			DEFINE TABLE post SCHEMAFULL;
			DEFINE FIELD title ON post TYPE string;
			DEFINE FIELD author ON post TYPE record<user>;
			DEFINE FUNCTION fn::greet($name: string) -> string { RETURN 'Hello, ' + $name; };
			DEFINE FUNCTION fn::add($a: int, $b: int) -> int { RETURN $a + $b; };
		",
		)
		.unwrap()
	}

	// ─── Without schema ───

	#[test]
	fn no_schema_returns_keywords() {
		let items = completion::complete("", pos(0, 0), None);
		assert!(!items.is_empty());
		assert!(items.iter().any(|i| i.label == "SELECT"));
	}

	#[test]
	fn no_schema_after_from_still_returns_keywords() {
		let items = completion::complete("SELECT * FROM ", pos(0, 14), None);
		assert!(
			items
				.iter()
				.any(|i| i.kind == Some(CompletionItemKind::KEYWORD))
		);
	}

	// ─── With schema: table completions ───

	#[test]
	fn tables_after_from() {
		let sg = schema();
		let items = completion::complete("SELECT * FROM ", pos(0, 14), Some(&sg));
		let tables: Vec<_> = items
			.iter()
			.filter(|i| i.kind == Some(CompletionItemKind::CLASS))
			.collect();
		assert!(tables.iter().any(|i| i.label == "user"));
		assert!(tables.iter().any(|i| i.label == "post"));
	}

	#[test]
	fn tables_after_into() {
		let sg = schema();
		let items = completion::complete("INSERT INTO ", pos(0, 12), Some(&sg));
		assert!(items.iter().any(|i| i.label == "user"));
	}

	// ─── With schema: function completions ───

	#[test]
	fn functions_after_fn_prefix() {
		let sg = schema();
		let items = completion::complete("SELECT fn::", pos(0, 11), Some(&sg));
		let fns: Vec<_> = items
			.iter()
			.filter(|i| i.kind == Some(CompletionItemKind::FUNCTION))
			.collect();
		assert!(fns.iter().any(|i| i.label == "fn::greet"));
		assert!(fns.iter().any(|i| i.label == "fn::add"));
	}

	#[test]
	fn function_detail_shows_signature() {
		let sg = schema();
		let items = completion::complete("SELECT fn::", pos(0, 11), Some(&sg));
		let greet = items.iter().find(|i| i.label == "fn::greet").unwrap();
		let detail = greet.detail.as_deref().unwrap();
		assert!(detail.contains("$name: string"));
		assert!(detail.contains("-> string"));
	}

	#[test]
	fn function_add_has_two_params() {
		let sg = schema();
		let items = completion::complete("SELECT fn::", pos(0, 11), Some(&sg));
		let add = items.iter().find(|i| i.label == "fn::add").unwrap();
		let detail = add.detail.as_deref().unwrap();
		assert!(detail.contains("$a: int"));
		assert!(detail.contains("$b: int"));
	}

	// ─── General context with schema ───

	#[test]
	fn general_includes_tables_keywords_functions() {
		let sg = schema();
		let items = completion::complete("", pos(0, 0), Some(&sg));
		assert!(
			items
				.iter()
				.any(|i| i.kind == Some(CompletionItemKind::CLASS))
		);
		assert!(
			items
				.iter()
				.any(|i| i.kind == Some(CompletionItemKind::KEYWORD))
		);
		assert!(
			items
				.iter()
				.any(|i| i.kind == Some(CompletionItemKind::FUNCTION))
		);
	}

	// ─── Keyword quality ───

	#[test]
	fn common_keywords_present() {
		let items = completion::complete("", pos(0, 0), None);
		for kw in &[
			"SELECT", "FROM", "WHERE", "CREATE", "UPDATE", "DELETE", "DEFINE",
		] {
			assert!(
				items.iter().any(|i| i.label == *kw),
				"missing keyword: {kw}"
			);
		}
	}

	#[test]
	fn type_keywords_present() {
		let items = completion::complete("", pos(0, 0), None);
		for kw in &["STRING", "INT", "FLOAT", "BOOL", "DATETIME", "RECORD"] {
			assert!(
				items.iter().any(|i| i.label == *kw),
				"missing type keyword: {kw}"
			);
		}
	}
}

// ═══════════════════════════════════════════════════════════════════════
// 5. HOVER
// ═══════════════════════════════════════════════════════════════════════

#[cfg(test)]
mod hover {
	use crate::server::word_at_position;
	use tower_lsp::lsp_types::Position;

	fn pos(line: u32, col: u32) -> Position {
		Position {
			line,
			character: col,
		}
	}

	// ─── Word extraction ───

	#[test]
	fn simple_word() {
		assert_eq!(word_at_position("SELECT * FROM user", pos(0, 15)), "user");
	}

	#[test]
	fn word_at_start() {
		assert_eq!(word_at_position("SELECT * FROM user", pos(0, 3)), "SELECT");
	}

	#[test]
	fn word_with_colons() {
		assert_eq!(
			word_at_position("SELECT fn::greet()", pos(0, 12)),
			"fn::greet"
		);
	}

	#[test]
	fn word_with_underscores() {
		assert_eq!(
			word_at_position("SELECT user_name FROM t", pos(0, 10)),
			"user_name"
		);
	}

	#[test]
	fn empty_at_space() {
		assert_eq!(word_at_position("SELECT * FROM user", pos(0, 7)), "");
	}

	#[test]
	fn empty_at_paren() {
		// pos 5 is '(' which is not alphanumeric
		assert_eq!(word_at_position("count()", pos(0, 5)), "count");
	}

	#[test]
	fn empty_at_operator() {
		assert_eq!(word_at_position("a + b", pos(0, 2)), "");
	}

	#[test]
	fn empty_source() {
		assert_eq!(word_at_position("", pos(0, 0)), "");
	}

	#[test]
	fn multiline_second_line() {
		assert_eq!(word_at_position("SELECT *\nFROM user", pos(1, 6)), "user");
	}

	#[test]
	fn out_of_bounds_line() {
		assert_eq!(word_at_position("SELECT", pos(5, 0)), "");
	}

	#[test]
	fn cursor_at_word_boundary_start() {
		assert_eq!(word_at_position("SELECT * FROM user", pos(0, 14)), "user");
	}

	#[test]
	fn cursor_at_word_boundary_end() {
		assert_eq!(word_at_position("SELECT * FROM user", pos(0, 18)), "user");
	}

	#[test]
	fn word_at_eol() {
		assert_eq!(word_at_position("SELECT * FROM user\n", pos(0, 18)), "user");
	}

	#[test]
	fn unicode_returns_empty_at_non_ascii_boundary() {
		// Non-ASCII characters aren't matched by is_ascii_alphanumeric
		// so word extraction stops at the boundary
		let word = word_at_position("SELECT * FROM abc", pos(0, 16));
		assert_eq!(word, "abc");
	}

	#[test]
	fn should_resolve_record_id_table() {
		let source = "DEFINE TABLE user SCHEMAFULL;\nDELETE user:alice;";
		let schema = surql_parser::SchemaGraph::from_source(source).unwrap();
		let word = word_at_position(source, pos(1, 9));
		assert_eq!(word, "user:alice");
		let table_name = word.split(':').next().unwrap();
		assert_eq!(table_name, "user");
		assert!(schema.table(table_name).is_some());
	}

	#[test]
	fn should_not_resolve_record_id_when_no_colon() {
		let word = word_at_position("SELECT * FROM user", pos(0, 16));
		assert_eq!(word, "user");
		let table_name = word.split(':').next().unwrap();
		assert_eq!(table_name, "user");
		assert_eq!(table_name, word);
	}

	#[test]
	fn should_extract_table_comment_in_hover() {
		let source = "DEFINE TABLE user SCHEMAFULL COMMENT 'Main user table';";
		let schema = surql_parser::SchemaGraph::from_source(source).unwrap();
		let table = schema.table("user").unwrap();
		assert_eq!(table.comment.as_deref(), Some("Main user table"));
	}
}

// ═══════════════════════════════════════════════════════════════════════
// 6. FORMATTING
// ═══════════════════════════════════════════════════════════════════════

#[cfg(test)]
mod formatting {
	use crate::formatting::format_document_for_test as format_document;

	#[test]
	fn formats_lowercase_keywords() {
		assert!(format_document("select  *   from   user").is_some());
	}

	#[test]
	fn returns_none_when_all_keywords_uppercase() {
		assert!(format_document("SELECT * FROM user").is_none());
	}

	#[test]
	fn uppercases_lowercase_keywords() {
		let edits = format_document("select * from user").unwrap();
		let formatted = &edits[0].new_text;
		assert!(formatted.contains("SELECT"), "should uppercase SELECT");
		assert!(formatted.contains("FROM"), "should uppercase FROM");
	}

	#[test]
	fn formats_all_keywords_in_query() {
		let edits = format_document("select * from user where age > 18").unwrap();
		let formatted = &edits[0].new_text;
		assert!(formatted.contains("SELECT"));
		assert!(formatted.contains("FROM"));
		assert!(formatted.contains("WHERE"));
	}

	#[test]
	fn preserves_already_uppercase() {
		assert!(format_document("SELECT * FROM user WHERE active = true").is_none());
	}

	#[test]
	fn empty_input_returns_none() {
		// Empty parses as valid empty AST → format produces empty → no edit needed
		assert!(format_document("").is_none());
	}

	#[test]
	fn preserves_semicolons() {
		if let Some(edits) = format_document("SELECT * FROM a ; SELECT * FROM b") {
			assert!(edits[0].new_text.contains(';'));
		}
	}

	#[test]
	fn define_table_lowercase_keywords() {
		let result = format_document("define  table  user  schemafull");
		assert!(result.is_some(), "should uppercase keywords");
	}
}

// ═══════════════════════════════════════════════════════════════════════
// 7. all() LIST QUALITY
// ═══════════════════════════════════════════════════════════════════════

#[cfg(test)]
mod keywords {
	use crate::keywords;

	fn all() -> &'static [&'static str] {
		keywords::all_keywords()
	}

	#[test]
	fn not_empty() {
		assert!(
			all().len() > 100,
			"Expected 100+ keywords, got {}",
			all().len()
		);
	}

	#[test]
	fn contains_dml_keywords() {
		for kw in &[
			"SELECT", "CREATE", "UPDATE", "DELETE", "INSERT", "UPSERT", "RELATE",
		] {
			assert!(all().contains(kw), "Missing DML keyword: {kw}");
		}
	}

	#[test]
	fn contains_ddl_keywords() {
		for kw in &[
			"DEFINE", "REMOVE", "ALTER", "TABLE", "FIELD", "INDEX", "FUNCTION",
		] {
			assert!(all().contains(kw), "Missing DDL keyword: {kw}");
		}
	}

	#[test]
	fn contains_type_keywords() {
		for kw in &[
			"STRING", "INT", "FLOAT", "BOOL", "DATETIME", "RECORD", "ARRAY", "OBJECT",
		] {
			assert!(all().contains(kw), "Missing type keyword: {kw}");
		}
	}

	#[test]
	fn contains_control_flow() {
		for kw in &["IF", "ELSE", "FOR", "BEGIN", "COMMIT", "CANCEL"] {
			assert!(all().contains(kw), "Missing control keyword: {kw}");
		}
	}

	#[test]
	fn contains_live_query() {
		assert!(all().contains(&"LIVE"));
		assert!(all().contains(&"KILL"));
	}

	#[test]
	fn no_empty_strings() {
		assert!(all().iter().all(|kw| !kw.is_empty()));
	}

	#[test]
	fn no_duplicates() {
		let mut seen = std::collections::HashSet::new();
		let mut dupes = Vec::new();
		for kw in all() {
			if !seen.insert(kw) {
				dupes.push(kw);
			}
		}
		assert!(dupes.is_empty(), "Duplicate keywords in list: {:?}", dupes);
	}
}

// ═══════════════════════════════════════════════════════════════════════
// 8. EDGE CASES — graceful degradation
// ═══════════════════════════════════════════════════════════════════════

#[cfg(test)]
mod edge_cases {
	use crate::completion;
	use crate::diagnostics;
	use crate::formatting::format_document_for_test as format_document;
	use crate::server::word_at_position;
	use tower_lsp::lsp_types::Position;

	fn pos(line: u32, col: u32) -> Position {
		Position {
			line,
			character: col,
		}
	}

	// ─── Very large documents ───

	#[test]
	fn large_valid_document() {
		let mut doc = String::new();
		for i in 0..100 {
			doc.push_str(&format!("SELECT * FROM table_{i};\n"));
		}
		assert!(diagnostics::compute(&doc).is_empty());
	}

	#[test]
	fn many_define_statements() {
		let mut doc = String::new();
		for i in 0..50 {
			doc.push_str(&format!("DEFINE TABLE t{i} SCHEMALESS;\n"));
		}
		assert!(diagnostics::compute(&doc).is_empty());
	}

	// ─── Unicode handling ───

	#[test]
	fn unicode_in_strings() {
		assert!(diagnostics::compute("SELECT * FROM user WHERE name = '日本語'").is_empty());
	}

	#[test]
	fn emoji_in_strings() {
		assert!(diagnostics::compute("CREATE post SET title = '🚀 Launch!'").is_empty());
	}

	#[test]
	fn completion_with_unicode() {
		// Unicode source should return keyword completions, not crash
		let items = completion::complete("SELECT * FROM юзер WHERE ", pos(0, 25), None);
		assert!(!items.is_empty(), "should return keyword completions");
	}

	#[test]
	fn word_at_position_unicode() {
		// ASCII chars before unicode should still extract correctly
		let word = word_at_position("SELECT * FROM table1", pos(0, 17));
		assert_eq!(word, "table1");
	}

	// ─── Cursor at boundaries ───

	#[test]
	fn completion_at_col_zero() {
		// At position 0, should get general completions (keywords)
		let items = completion::complete("SELECT", pos(0, 0), None);
		assert!(!items.is_empty());
	}

	#[test]
	fn completion_past_end_of_line() {
		// Past end should get general context, not crash
		let items = completion::complete("SELECT", pos(0, 100), None);
		assert!(!items.is_empty());
	}

	#[test]
	fn completion_past_end_of_document() {
		// Past document end should return general completions
		let items = completion::complete("SELECT", pos(100, 0), None);
		assert!(!items.is_empty());
	}

	// ─── Empty/whitespace inputs ───

	#[test]
	fn format_whitespace_returns_none_or_edit() {
		// Whitespace-only may parse as empty or fail — either way, no crash
		let result = format_document("   \n\t\n  ");
		// Whitespace parses as valid empty AST → format produces empty string
		// So result is either None (already "formatted") or Some with replacement
		assert!(result.is_none() || result.unwrap().len() == 1);
	}

	#[test]
	fn completion_whitespace_returns_keywords() {
		let items = completion::complete("   ", pos(0, 3), None);
		assert!(
			!items.is_empty(),
			"whitespace should still give keyword completions"
		);
	}

	#[test]
	fn diagnostics_single_semicolon_no_errors() {
		// Semicolons alone are valid (empty statements)
		let diags = diagnostics::compute(";");
		assert!(diags.is_empty());
	}

	// ─── Deeply nested queries ───

	#[test]
	fn nested_subquery() {
		assert!(
			diagnostics::compute("SELECT * FROM (SELECT * FROM (SELECT * FROM user))").is_empty()
		);
	}

	#[test]
	fn nested_if_else() {
		assert!(
			diagnostics::compute("IF true { IF true { IF true { RETURN 1 } ELSE { RETURN 2 } } }")
				.is_empty()
		);
	}
}

// ═══════════════════════════════════════════════════════════════════════
// 9. FIELD COMPLETIONS (after `table.`)
// ═══════════════════════════════════════════════════════════════════════

#[cfg(test)]
mod field_completions {
	use crate::completion;
	use crate::completion::{Context, detect_context};
	use surql_parser::SchemaGraph;
	use tower_lsp::lsp_types::{CompletionItemKind, Position};

	fn pos(line: u32, col: u32) -> Position {
		Position {
			line,
			character: col,
		}
	}

	fn schema() -> SchemaGraph {
		SchemaGraph::from_source(
			"
			DEFINE TABLE user SCHEMAFULL;
			DEFINE FIELD name ON user TYPE string;
			DEFINE FIELD age ON user TYPE int;
			DEFINE FIELD email ON user TYPE string;
			DEFINE TABLE post SCHEMAFULL;
			DEFINE FIELD title ON post TYPE string;
			DEFINE FIELD author ON post TYPE record<user>;
		",
		)
		.unwrap()
	}

	#[test]
	fn context_after_table_dot() {
		assert_eq!(
			detect_context("user.", pos(0, 5)),
			Context::FieldName("user".into())
		);
	}

	#[test]
	fn context_after_table_dot_in_select() {
		assert_eq!(
			detect_context("SELECT user.", pos(0, 12)),
			Context::FieldName("user".into())
		);
	}

	#[test]
	fn context_after_table_dot_in_where() {
		// col 30 = after the dot (len of "SELECT * FROM user WHERE user.")
		assert_eq!(
			detect_context("SELECT * FROM user WHERE user.", pos(0, 30)),
			Context::FieldName("user".into())
		);
	}

	#[test]
	fn fields_returned_for_known_table() {
		let sg = schema();
		let items = completion::complete("SELECT user.", pos(0, 12), Some(&sg));
		let fields: Vec<_> = items
			.iter()
			.filter(|i| i.kind == Some(CompletionItemKind::FIELD))
			.collect();
		assert!(fields.iter().any(|i| i.label == "name"));
		assert!(fields.iter().any(|i| i.label == "age"));
		assert!(fields.iter().any(|i| i.label == "email"));
	}

	#[test]
	fn field_detail_shows_type() {
		let sg = schema();
		let items = completion::complete("SELECT user.", pos(0, 12), Some(&sg));
		let name = items.iter().find(|i| i.label == "name").unwrap();
		assert_eq!(name.detail.as_deref(), Some("string"));
	}

	#[test]
	fn post_fields_different_from_user() {
		let sg = schema();
		let items = completion::complete("SELECT post.", pos(0, 12), Some(&sg));
		let fields: Vec<_> = items
			.iter()
			.filter(|i| i.kind == Some(CompletionItemKind::FIELD))
			.collect();
		assert!(fields.iter().any(|i| i.label == "title"));
		assert!(fields.iter().any(|i| i.label == "author"));
		assert!(!fields.iter().any(|i| i.label == "name"));
	}

	#[test]
	fn unknown_table_returns_no_fields() {
		let sg = schema();
		let items = completion::complete("SELECT unknown.", pos(0, 15), Some(&sg));
		let fields: Vec<_> = items
			.iter()
			.filter(|i| i.kind == Some(CompletionItemKind::FIELD))
			.collect();
		assert!(fields.is_empty());
	}

	#[test]
	fn graph_traversal_operators_included() {
		let sg = schema();
		let items = completion::complete("SELECT user.", pos(0, 12), Some(&sg));
		assert!(items.iter().any(|i| i.label == "->"));
		assert!(items.iter().any(|i| i.label == "<-"));
	}

	#[test]
	fn no_schema_no_fields() {
		let items = completion::complete("SELECT user.", pos(0, 12), None);
		let fields: Vec<_> = items
			.iter()
			.filter(|i| i.kind == Some(CompletionItemKind::FIELD))
			.collect();
		assert!(fields.is_empty());
	}
}

// ═══════════════════════════════════════════════════════════════════════
// 10. SIGNATURE HELP
// ═══════════════════════════════════════════════════════════════════════

#[cfg(test)]
mod signature_help {
	use crate::signature;
	use surql_parser::SchemaGraph;
	use tower_lsp::lsp_types::Position;

	fn pos(line: u32, col: u32) -> Position {
		Position {
			line,
			character: col,
		}
	}

	fn schema() -> SchemaGraph {
		SchemaGraph::from_source(
			"
			DEFINE FUNCTION fn::greet($name: string) -> string { RETURN 'Hello, ' + $name; };
			DEFINE FUNCTION fn::add($a: int, $b: int) -> int { RETURN $a + $b; };
			DEFINE FUNCTION fn::noargs() -> string { RETURN 'hi'; };
		",
		)
		.unwrap()
	}

	#[test]
	fn signature_at_opening_paren() {
		let sg = schema();
		let help = signature::signature_help("fn::greet(", pos(0, 10), Some(&sg));
		assert!(help.is_some());
		let help = help.unwrap();
		assert_eq!(help.signatures.len(), 1);
		assert!(help.signatures[0].label.contains("fn::greet"));
		assert_eq!(help.active_parameter, Some(0));
	}

	#[test]
	fn signature_first_param() {
		let sg = schema();
		let help = signature::signature_help("fn::add(1", pos(0, 9), Some(&sg));
		assert!(help.is_some());
		assert_eq!(help.unwrap().active_parameter, Some(0));
	}

	#[test]
	fn signature_second_param() {
		let sg = schema();
		let help = signature::signature_help("fn::add(1, ", pos(0, 11), Some(&sg));
		assert!(help.is_some());
		assert_eq!(help.unwrap().active_parameter, Some(1));
	}

	#[test]
	fn signature_params_info() {
		let sg = schema();
		let help = signature::signature_help("fn::add(", pos(0, 8), Some(&sg)).unwrap();
		let params = help.signatures[0].parameters.as_ref().unwrap();
		assert_eq!(params.len(), 2);
	}

	#[test]
	fn no_signature_outside_parens() {
		let sg = schema();
		let help = signature::signature_help("fn::greet", pos(0, 9), Some(&sg));
		assert!(help.is_none());
	}

	#[test]
	fn no_signature_for_unknown_function() {
		let sg = schema();
		let help = signature::signature_help("fn::unknown(", pos(0, 12), Some(&sg));
		assert!(help.is_none());
	}

	#[test]
	fn no_signature_without_schema() {
		let help = signature::signature_help("fn::greet(", pos(0, 10), None);
		assert!(help.is_none());
	}

	#[test]
	fn signature_noargs_function() {
		let sg = schema();
		let help = signature::signature_help("fn::noargs(", pos(0, 11), Some(&sg)).unwrap();
		let params = help.signatures[0].parameters.as_ref().unwrap();
		assert!(params.is_empty());
		assert_eq!(help.active_parameter, Some(0));
	}

	#[test]
	fn signature_in_nested_context() {
		let sg = schema();
		let help = signature::signature_help("SELECT fn::greet(", pos(0, 17), Some(&sg));
		assert!(help.is_some());
	}

	#[test]
	fn signature_with_nested_parens() {
		let sg = schema();
		// fn::add(count(), | — cursor after the comma, nested parens
		let help = signature::signature_help("fn::add(count(), ", pos(0, 17), Some(&sg));
		assert!(help.is_some());
		assert_eq!(help.unwrap().active_parameter, Some(1));
	}

	#[test]
	fn signature_label_includes_return_type() {
		let sg = schema();
		let help = signature::signature_help("fn::greet(", pos(0, 10), Some(&sg)).unwrap();
		assert!(help.signatures[0].label.contains("-> string"));
	}
}

// ═══════════════════════════════════════════════════════════════════════
// 11. BYTE OFFSET TO POSITION CONVERSION
// ═══════════════════════════════════════════════════════════════════════

#[cfg(test)]
mod position_conversion {
	use crate::server::byte_offset_to_position;
	use tower_lsp::lsp_types::Position;

	#[test]
	fn start_of_file() {
		let pos = byte_offset_to_position("hello\nworld", 0);
		assert_eq!(
			pos,
			Position {
				line: 0,
				character: 0
			}
		);
	}

	#[test]
	fn middle_of_first_line() {
		let pos = byte_offset_to_position("hello\nworld", 3);
		assert_eq!(
			pos,
			Position {
				line: 0,
				character: 3
			}
		);
	}

	#[test]
	fn start_of_second_line() {
		let pos = byte_offset_to_position("hello\nworld", 6);
		assert_eq!(
			pos,
			Position {
				line: 1,
				character: 0
			}
		);
	}

	#[test]
	fn middle_of_second_line() {
		let pos = byte_offset_to_position("hello\nworld", 9);
		assert_eq!(
			pos,
			Position {
				line: 1,
				character: 3
			}
		);
	}

	#[test]
	fn end_of_file() {
		let pos = byte_offset_to_position("hello\nworld", 11);
		assert_eq!(
			pos,
			Position {
				line: 1,
				character: 5
			}
		);
	}

	#[test]
	fn past_end_of_file() {
		let pos = byte_offset_to_position("hello", 100);
		assert_eq!(
			pos,
			Position {
				line: 0,
				character: 5
			}
		);
	}

	#[test]
	fn empty_file() {
		let pos = byte_offset_to_position("", 0);
		assert_eq!(
			pos,
			Position {
				line: 0,
				character: 0
			}
		);
	}

	#[test]
	fn three_lines() {
		let pos = byte_offset_to_position("a\nb\nc", 4);
		assert_eq!(
			pos,
			Position {
				line: 2,
				character: 0
			}
		);
	}
}

// ═══════════════════════════════════════════════════════════════════════
// 12. BUILT-IN FUNCTION NAMESPACES
// ═══════════════════════════════════════════════════════════════════════

#[cfg(test)]
mod builtin_completions {
	use crate::completion;
	use surql_parser::SchemaGraph;
	use tower_lsp::lsp_types::{CompletionItemKind, Position};

	fn pos(line: u32, col: u32) -> Position {
		Position {
			line,
			character: col,
		}
	}

	#[test]
	fn builtin_namespaces_after_fn() {
		let sg = SchemaGraph::default();
		let items = completion::complete("SELECT fn::", pos(0, 11), Some(&sg));
		let modules: Vec<_> = items
			.iter()
			.filter(|i| i.kind == Some(CompletionItemKind::MODULE))
			.collect();
		assert!(modules.iter().any(|i| i.label == "array::"));
		assert!(modules.iter().any(|i| i.label == "string::"));
		assert!(modules.iter().any(|i| i.label == "time::"));
		assert!(modules.iter().any(|i| i.label == "math::"));
		assert!(modules.iter().any(|i| i.label == "crypto::"));
	}

	#[test]
	fn builtin_namespaces_count() {
		let sg = SchemaGraph::default();
		let items = completion::complete("SELECT fn::", pos(0, 11), Some(&sg));
		let modules: Vec<_> = items
			.iter()
			.filter(|i| i.kind == Some(CompletionItemKind::MODULE))
			.collect();
		assert!(
			modules.len() >= 15,
			"Expected 15+ built-in namespaces, got {}",
			modules.len()
		);
	}
}

// ═══════════════════════════════════════════════════════════════════════
// 13. COMPLETION EXCLUSIONS — verify wrong items are NOT returned
// ═══════════════════════════════════════════════════════════════════════

#[cfg(test)]
mod completion_exclusions {
	use crate::completion;
	use surql_parser::SchemaGraph;
	use tower_lsp::lsp_types::{CompletionItemKind, Position};

	fn pos(line: u32, col: u32) -> Position {
		Position {
			line,
			character: col,
		}
	}

	fn schema() -> SchemaGraph {
		SchemaGraph::from_source(
			"
			DEFINE TABLE user SCHEMAFULL;
			DEFINE FIELD name ON user TYPE string;
			DEFINE TABLE post SCHEMAFULL;
			DEFINE FIELD title ON post TYPE string;
		",
		)
		.unwrap()
	}

	#[test]
	fn field_completion_excludes_wrong_table_fields() {
		let sg = schema();
		let items = completion::complete("SELECT user.", pos(0, 12), Some(&sg));
		let fields: Vec<_> = items
			.iter()
			.filter(|i| i.kind == Some(CompletionItemKind::FIELD))
			.collect();
		// user.name YES, user.title NO
		assert!(fields.iter().any(|i| i.label == "name"));
		assert!(
			!fields.iter().any(|i| i.label == "title"),
			"post.title should NOT appear in user.* completions"
		);
	}

	#[test]
	fn fn_context_excludes_table_names() {
		let sg = schema();
		let items = completion::complete("SELECT fn::", pos(0, 11), Some(&sg));
		// Should NOT include table names
		assert!(
			!items
				.iter()
				.any(|i| i.kind == Some(CompletionItemKind::CLASS)),
			"table names should NOT appear in fn:: context"
		);
	}

	#[test]
	fn param_context_excludes_keywords() {
		let sg = schema();
		let items = completion::complete("WHERE $", pos(0, 7), Some(&sg));
		// Should NOT include keywords
		assert!(
			!items
				.iter()
				.any(|i| i.kind == Some(CompletionItemKind::KEYWORD)),
			"keywords should NOT appear in $ context"
		);
	}
}

// ═══════════════════════════════════════════════════════════════════════
// 14. ERROR RECOVERY IN LSP — diagnostics from partial documents
// ═══════════════════════════════════════════════════════════════════════

#[cfg(test)]
mod recovery_diagnostics {
	use crate::diagnostics;

	#[test]
	fn multiple_errors_all_reported() {
		let source = "SELEC broken; UPDAET also broken; SELECT * FROM user";
		let diags = diagnostics::compute(source);
		// Should report errors for both broken statements, not just the first
		assert!(
			diags.len() >= 2,
			"expected 2+ diagnostics, got {}",
			diags.len()
		);
	}

	#[test]
	fn valid_statements_dont_generate_errors() {
		let source = "SELECT * FROM user; SELEC broken; SELECT * FROM post";
		let diags = diagnostics::compute(source);
		// Only 1 error (the middle statement)
		assert_eq!(diags.len(), 1, "expected exactly 1 diagnostic");
	}

	#[test]
	fn error_after_define_reported() {
		let source =
			"DEFINE TABLE user SCHEMAFULL; SELEC broken; DEFINE FIELD name ON user TYPE string";
		let diags = diagnostics::compute(source);
		assert_eq!(diags.len(), 1);
	}
}

// ═══════════════════════════════════════════════════════════════════════
// 15. KEYWORD LIST QUALITY (from parser enum)
// ═══════════════════════════════════════════════════════════════════════

#[cfg(test)]
mod keyword_quality {
	use crate::keywords;

	#[test]
	fn count_matches_parser_enum() {
		// The parser has 176+ keywords. Our list should match exactly.
		let kws = keywords::all_keywords();
		assert!(
			kws.len() >= 175,
			"Expected 175+ keywords from parser enum, got {}",
			kws.len()
		);
	}

	#[test]
	fn no_empty_strings() {
		assert!(keywords::all_keywords().iter().all(|k| !k.is_empty()));
	}

	#[test]
	fn select_present() {
		assert!(keywords::all_keywords().contains(&"SELECT"));
	}

	#[test]
	fn surrealql_specific_keywords() {
		let kws = keywords::all_keywords();
		// SurrealQL-specific keywords that generic SQL doesn't have
		assert!(kws.contains(&"SCHEMAFULL"));
		assert!(kws.contains(&"SCHEMALESS"));
		assert!(kws.contains(&"CHANGEFEED"));
		assert!(kws.contains(&"RELATE"));
		assert!(kws.contains(&"LIVE"));
		assert!(kws.contains(&"GRAPHQL"));
	}
}

// ═══════════════════════════════════════════════════════════════════════
// 16. BUILTIN FUNCTION LOOKUP
// ═══════════════════════════════════════════════════════════════════════

#[cfg(test)]
mod builtin_lookup {
	#[test]
	fn should_find_string_len() {
		let builtin = surql_parser::builtin_function("string::len");
		assert!(builtin.is_some());
		let b = builtin.unwrap();
		assert_eq!(b.name, "string::len");
		assert!(!b.description.is_empty());
		assert!(!b.signatures.is_empty());
	}

	#[test]
	fn should_find_array_add() {
		let builtin = surql_parser::builtin_function("array::add");
		assert!(builtin.is_some());
		assert!(builtin.unwrap().signatures[0].contains("array"));
	}

	#[test]
	fn should_find_time_now() {
		let builtin = surql_parser::builtin_function("time::now");
		assert!(builtin.is_some());
		assert!(builtin.unwrap().signatures[0].contains("datetime"));
	}

	#[test]
	fn should_find_math_mean() {
		let builtin = surql_parser::builtin_function("math::mean");
		assert!(builtin.is_some());
	}

	#[test]
	fn should_return_none_for_unknown() {
		assert!(surql_parser::builtin_function("nonexistent::func").is_none());
	}

	#[test]
	fn should_return_none_for_user_functions() {
		assert!(surql_parser::builtin_function("fn::my_func").is_none());
	}

	#[test]
	fn should_list_string_namespace() {
		let fns = surql_parser::builtins_in_namespace("string");
		assert!(
			fns.len() > 10,
			"Expected 10+ string functions, got {}",
			fns.len()
		);
		assert!(fns.iter().any(|f| f.name == "string::len"));
		assert!(fns.iter().any(|f| f.name == "string::lowercase"));
		assert!(fns.iter().any(|f| f.name == "string::uppercase"));
	}

	#[test]
	fn should_list_array_namespace() {
		let fns = surql_parser::builtins_in_namespace("array");
		assert!(
			fns.len() > 10,
			"Expected 10+ array functions, got {}",
			fns.len()
		);
		assert!(fns.iter().any(|f| f.name == "array::add"));
		assert!(fns.iter().any(|f| f.name == "array::len"));
	}

	#[test]
	fn should_return_empty_for_unknown_namespace() {
		let fns = surql_parser::builtins_in_namespace("nonexistent");
		assert!(fns.is_empty());
	}

	#[test]
	fn should_return_empty_for_empty_namespace() {
		let fns = surql_parser::builtins_in_namespace("");
		assert!(fns.is_empty());
	}

	#[test]
	fn builtins_count_is_reasonable() {
		assert!(
			surql_parser::builtins_generated::BUILTINS.len() > 200,
			"Expected 200+ builtins, got {}",
			surql_parser::builtins_generated::BUILTINS.len()
		);
	}

	#[test]
	fn namespaces_count_is_reasonable() {
		assert!(
			surql_parser::builtins_generated::BUILTIN_NAMESPACES.len() > 15,
			"Expected 15+ namespaces, got {}",
			surql_parser::builtins_generated::BUILTIN_NAMESPACES.len()
		);
	}
}

// ═══════════════════════════════════════════════════════════════════════
// 17. BUILTIN NAMESPACE COMPLETIONS
// ═══════════════════════════════════════════════════════════════════════

#[cfg(test)]
mod builtin_namespace_completions {
	use crate::completion;
	use crate::completion::{Context, detect_context};
	use tower_lsp::lsp_types::{CompletionItemKind, Position};

	fn pos(line: u32, col: u32) -> Position {
		Position {
			line,
			character: col,
		}
	}

	#[test]
	fn should_detect_string_namespace_context() {
		let ctx = detect_context("string::", pos(0, 8));
		assert_eq!(ctx, Context::BuiltinNamespace("string".into()));
	}

	#[test]
	fn should_detect_array_namespace_context() {
		let ctx = detect_context("array::", pos(0, 7));
		assert_eq!(ctx, Context::BuiltinNamespace("array".into()));
	}

	#[test]
	fn should_detect_namespace_in_expression() {
		let ctx = detect_context("SELECT string::", pos(0, 15));
		assert_eq!(ctx, Context::BuiltinNamespace("string".into()));
	}

	#[test]
	fn should_complete_string_functions() {
		let items = completion::complete("string::", pos(0, 8), None);
		let fns: Vec<_> = items
			.iter()
			.filter(|i| i.kind == Some(CompletionItemKind::FUNCTION))
			.collect();
		assert!(
			fns.iter().any(|i| i.label == "string::len"),
			"Expected string::len in completions"
		);
		assert!(
			fns.iter().any(|i| i.label == "string::lowercase"),
			"Expected string::lowercase in completions"
		);
	}

	#[test]
	fn should_complete_array_functions() {
		let items = completion::complete("array::", pos(0, 7), None);
		let fns: Vec<_> = items
			.iter()
			.filter(|i| i.kind == Some(CompletionItemKind::FUNCTION))
			.collect();
		assert!(
			fns.iter().any(|i| i.label == "array::add"),
			"Expected array::add in completions"
		);
		assert!(
			fns.iter().any(|i| i.label == "array::len"),
			"Expected array::len in completions"
		);
	}

	#[test]
	fn should_show_sub_namespaces() {
		let items = completion::complete("string::", pos(0, 8), None);
		let modules: Vec<_> = items
			.iter()
			.filter(|i| i.kind == Some(CompletionItemKind::MODULE))
			.collect();
		assert!(
			modules.iter().any(|i| i.label.starts_with("string::")),
			"Expected sub-namespaces like string::semver::"
		);
	}

	#[test]
	fn should_have_function_detail_with_signature() {
		let items = completion::complete("string::", pos(0, 8), None);
		let len = items.iter().find(|i| i.label == "string::len");
		assert!(len.is_some(), "string::len should be in completions");
		let detail = len.unwrap().detail.as_deref().unwrap_or("");
		assert!(
			detail.contains("string"),
			"Detail should contain parameter type, got: {detail}"
		);
	}

	#[test]
	fn should_include_namespaces_in_general_context() {
		let items = completion::complete("", pos(0, 0), None);
		let modules: Vec<_> = items
			.iter()
			.filter(|i| i.kind == Some(CompletionItemKind::MODULE))
			.collect();
		assert!(
			modules.iter().any(|i| i.label == "string::"),
			"General context should include builtin namespaces"
		);
		assert!(
			modules.iter().any(|i| i.label == "array::"),
			"General context should include builtin namespaces"
		);
	}
}

// ═══════════════════════════════════════════════════════════════════════
// 18. BUILTIN SIGNATURE HELP
// ═══════════════════════════════════════════════════════════════════════

#[cfg(test)]
mod builtin_signature_help {
	use crate::signature;
	use surql_parser::SchemaGraph;
	use tower_lsp::lsp_types::Position;

	fn pos(line: u32, col: u32) -> Position {
		Position {
			line,
			character: col,
		}
	}

	#[test]
	fn should_show_signature_for_string_len() {
		let help = signature::signature_help("string::len(", pos(0, 12), None);
		assert!(help.is_some(), "Expected signature help for string::len");
		let help = help.unwrap();
		assert!(!help.signatures.is_empty());
		assert!(help.signatures[0].label.contains("string::len"));
	}

	#[test]
	fn should_show_signature_for_array_add() {
		let help = signature::signature_help("array::add(", pos(0, 11), None);
		assert!(help.is_some(), "Expected signature help for array::add");
		let help = help.unwrap();
		assert!(help.signatures[0].label.contains("array::add"));
	}

	#[test]
	fn should_show_signature_for_time_now() {
		let help = signature::signature_help("time::now(", pos(0, 10), None);
		assert!(help.is_some(), "Expected signature help for time::now");
	}

	#[test]
	fn should_track_active_param_for_builtins() {
		let help = signature::signature_help("array::add(arr, ", pos(0, 16), None);
		assert!(help.is_some());
		assert_eq!(help.unwrap().active_parameter, Some(1));
	}

	#[test]
	fn should_show_description_for_builtins() {
		let help = signature::signature_help("string::len(", pos(0, 12), None).unwrap();
		let doc = &help.signatures[0].documentation;
		assert!(doc.is_some(), "Expected documentation for builtin");
	}

	#[test]
	fn should_prefer_user_fn_over_builtin() {
		let sg = SchemaGraph::from_source(
			"DEFINE FUNCTION fn::greet($name: string) -> string { RETURN 'Hello, ' + $name; };",
		)
		.unwrap();
		let help = signature::signature_help("fn::greet(", pos(0, 10), Some(&sg));
		assert!(help.is_some());
		let label = &help.unwrap().signatures[0].label;
		assert!(
			label.starts_with("fn::greet"),
			"User fn:: should take priority"
		);
	}

	#[test]
	fn should_not_show_signature_for_unknown_builtin() {
		let help = signature::signature_help("nonexistent::func(", pos(0, 18), None);
		assert!(help.is_none());
	}

	#[test]
	fn should_work_in_nested_expression() {
		let help = signature::signature_help("SELECT string::len(", pos(0, 19), None);
		assert!(help.is_some());
	}
}

// ═══════════════════════════════════════════════════════════════════════
// 19. DOCUMENT SCHEMA OVERLAY
// ═══════════════════════════════════════════════════════════════════════

#[cfg(test)]
mod document_schema {
	use surql_parser::SchemaGraph;

	#[test]
	fn should_build_schema_from_definitions() {
		let defs = surql_parser::extract_definitions(
			"DEFINE TABLE user SCHEMAFULL; DEFINE FIELD name ON user TYPE string;",
		)
		.unwrap();
		let graph = SchemaGraph::from_definitions(&defs);
		assert!(graph.table("user").is_some());
		assert_eq!(graph.fields_of("user").len(), 1);
	}

	#[test]
	fn should_merge_schemas() {
		let mut base = SchemaGraph::from_source(
			"DEFINE TABLE user SCHEMAFULL; DEFINE FIELD name ON user TYPE string;",
		)
		.unwrap();
		let overlay = SchemaGraph::from_source(
			"DEFINE TABLE post SCHEMAFULL; DEFINE FIELD title ON post TYPE string;",
		)
		.unwrap();
		base.merge(overlay);
		assert!(base.table("user").is_some());
		assert!(base.table("post").is_some());
	}

	#[test]
	fn should_preserve_schemafull_across_merge() {
		let mut base = SchemaGraph::from_source(
			"DEFINE TABLE user SCHEMAFULL; DEFINE FIELD name ON user TYPE string;",
		)
		.unwrap();
		let overlay = SchemaGraph::from_source("DEFINE FIELD bio ON user TYPE string;").unwrap();
		base.merge(overlay);

		let table = base.table("user").unwrap();
		assert!(
			table.full,
			"SCHEMAFULL should survive merge with SCHEMALESS overlay"
		);
	}

	#[test]
	fn should_deduplicate_fields_across_merge() {
		let mut base = SchemaGraph::from_source(
			"DEFINE TABLE user SCHEMAFULL; DEFINE FIELD name ON user TYPE string;",
		)
		.unwrap();
		let overlay = SchemaGraph::from_source("DEFINE FIELD name ON user TYPE string;").unwrap();
		base.merge(overlay);

		let name_count = base
			.fields_of("user")
			.iter()
			.filter(|f| f.name == "name")
			.count();
		assert_eq!(name_count, 1, "field 'name' should not be duplicated");
	}

	#[test]
	fn should_merge_schemafull_regardless_of_order() {
		let mut base = SchemaGraph::from_source("DEFINE FIELD bio ON user TYPE string;").unwrap();
		let overlay = SchemaGraph::from_source(
			"DEFINE TABLE user SCHEMAFULL; DEFINE FIELD name ON user TYPE string;",
		)
		.unwrap();
		base.merge(overlay);

		let table = base.table("user").unwrap();
		assert!(
			table.full,
			"SCHEMAFULL should win regardless of merge order"
		);
		assert!(
			base.fields_of("user").len() >= 2,
			"Should have fields from both files"
		);
	}

	#[test]
	fn should_extract_from_recovered_ast() {
		let (statements, _diagnostics) = surql_parser::parse_with_recovery(
			"DEFINE TABLE user SCHEMAFULL; SELEC broken; DEFINE FIELD name ON user TYPE string;",
		);
		let defs = surql_parser::extract_definitions_from_ast(&statements).unwrap();
		assert_eq!(defs.tables.len(), 1);
		assert_eq!(defs.fields.len(), 1);
	}

	#[test]
	fn should_attach_source_locations_to_document_schema() {
		let source = "DEFINE TABLE user SCHEMAFULL;\nDEFINE FIELD name ON user TYPE string;\nDEFINE FUNCTION fn::greet($name: string) -> string { RETURN 'Hello'; };";
		let defs = surql_parser::extract_definitions(source).unwrap();
		let mut graph = SchemaGraph::from_definitions(&defs);
		let tmp = tempfile::NamedTempFile::with_suffix(".surql").unwrap();
		std::fs::write(tmp.path(), source).unwrap();
		graph.attach_source_locations(source, tmp.path());

		let table = graph.table("user").unwrap();
		assert!(table.source.is_some(), "table should have source location");

		let func = graph.function("greet").unwrap();
		assert!(
			func.source.is_some(),
			"function should have source location"
		);
	}
}

// ═══════════════════════════════════════════════════════════════════════
// 20. KEYWORD DOCUMENTATION HOVER
// ═══════════════════════════════════════════════════════════════════════

#[cfg(test)]
mod keyword_hover {
	use crate::server::keyword_documentation;

	#[test]
	fn should_return_docs_for_select() {
		let doc = keyword_documentation("SELECT");
		assert!(doc.is_some());
		assert!(doc.unwrap().contains("SELECT"));
		assert!(doc.unwrap().contains("FROM"));
	}

	#[test]
	fn should_return_docs_for_define() {
		let doc = keyword_documentation("DEFINE");
		assert!(doc.is_some());
		assert!(doc.unwrap().contains("DEFINE TABLE"));
		assert!(doc.unwrap().contains("DEFINE FIELD"));
		assert!(doc.unwrap().contains("DEFINE FUNCTION"));
	}

	#[test]
	fn should_return_docs_for_let() {
		let doc = keyword_documentation("LET");
		assert!(doc.is_some());
		assert!(doc.unwrap().contains("LET"));
	}

	#[test]
	fn should_return_docs_for_insert() {
		let doc = keyword_documentation("INSERT");
		assert!(doc.is_some());
		assert!(doc.unwrap().contains("INSERT INTO"));
	}

	#[test]
	fn should_return_docs_for_relate() {
		let doc = keyword_documentation("RELATE");
		assert!(doc.is_some());
		assert!(doc.unwrap().contains("graph edge"));
	}

	#[test]
	fn should_be_case_insensitive() {
		assert!(keyword_documentation("select").is_some());
		assert!(keyword_documentation("Select").is_some());
		assert!(keyword_documentation("SELECT").is_some());
	}

	#[test]
	fn should_return_none_for_non_keywords() {
		assert!(keyword_documentation("foo").is_none());
		assert!(keyword_documentation("myvar").is_none());
	}

	#[test]
	fn should_include_docs_links() {
		let doc = keyword_documentation("SELECT").unwrap();
		assert!(doc.contains("surrealdb.com"), "should have docs link");
	}

	#[test]
	fn should_have_docs_for_schema_keywords() {
		assert!(keyword_documentation("SCHEMAFULL").is_some());
		assert!(keyword_documentation("SCHEMALESS").is_some());
		assert!(keyword_documentation("CHANGEFEED").is_some());
		assert!(keyword_documentation("PERMISSIONS").is_some());
	}

	#[test]
	fn should_have_docs_for_transaction_keywords() {
		assert!(keyword_documentation("BEGIN").is_some());
		assert!(keyword_documentation("COMMIT").is_some());
		assert!(keyword_documentation("CANCEL").is_some());
	}

	#[test]
	fn should_have_docs_for_live_queries() {
		assert!(keyword_documentation("LIVE").is_some());
		assert!(keyword_documentation("KILL").is_some());
	}
}

// ═══════════════════════════════════════════════════════════════════════
// 20b. KEYWORD HOVER COVERAGE
// ═══════════════════════════════════════════════════════════════════════

#[cfg(test)]
mod keyword_coverage {
	use crate::server::keyword_documentation;

	#[test]
	fn coverage_report() {
		let all = surql_parser::all_keywords();
		let mut covered = Vec::new();
		let mut missing = Vec::new();

		for kw in all {
			if keyword_documentation(kw).is_some() {
				covered.push(*kw);
			} else {
				missing.push(*kw);
			}
		}

		let pct = covered.len() * 100 / all.len().max(1);
		eprintln!(
			"\n=== Keyword hover coverage: {}/{} ({pct}%) ===",
			covered.len(),
			all.len()
		);
		if !missing.is_empty() {
			eprintln!("Missing ({}):", missing.len());
			for chunk in missing.chunks(10) {
				eprintln!("  {}", chunk.join(", "));
			}
		}

		// Enforce minimum coverage — increase as we add more
		assert!(
			pct >= 25,
			"Keyword hover coverage dropped below 25%: {pct}% ({}/{} keywords)",
			covered.len(),
			all.len()
		);
	}

	#[test]
	fn all_dml_keywords_have_docs() {
		let required = [
			"SELECT", "CREATE", "UPDATE", "DELETE", "INSERT", "UPSERT", "RELATE", "DEFINE",
			"REMOVE", "LET", "RETURN", "IF", "FOR", "BEGIN", "COMMIT", "CANCEL", "LIVE", "KILL",
			"USE", "INFO", "SLEEP", "THROW",
		];
		for kw in &required {
			assert!(
				keyword_documentation(kw).is_some(),
				"DML keyword '{kw}' must have hover documentation"
			);
		}
	}

	#[test]
	fn all_define_sub_keywords_have_docs() {
		let required = [
			"TABLE",
			"FIELD",
			"INDEX",
			"FUNCTION",
			"EVENT",
			"ANALYZER",
			"PARAM",
			"ACCESS",
			"NAMESPACE",
			"DATABASE",
		];
		for kw in &required {
			assert!(
				keyword_documentation(kw).is_some(),
				"DEFINE sub-keyword '{kw}' must have hover documentation"
			);
		}
	}

	#[test]
	fn all_clause_keywords_have_docs() {
		let required = ["WHERE", "ORDER", "GROUP", "LIMIT", "FROM", "SPLIT", "FETCH"];
		for kw in &required {
			assert!(
				keyword_documentation(kw).is_some(),
				"Clause keyword '{kw}' must have hover documentation"
			);
		}
	}
}

// ═══════════════════════════════════════════════════════════════════════
// 21. HOVER AND GOTO_DEF WITH SCHEMA CONTEXT
// ═══════════════════════════════════════════════════════════════════════

#[cfg(test)]
mod hover_with_schema {
	use crate::server::word_at_position;
	use surql_parser::SchemaGraph;
	use tower_lsp::lsp_types::Position;

	fn pos(line: u32, col: u32) -> Position {
		Position {
			line,
			character: col,
		}
	}

	#[test]
	fn should_extract_fn_greet_in_let_context() {
		let source = "LET $greeting = fn::greet('World');";
		let word = word_at_position(source, pos(0, 20));
		assert_eq!(word, "fn::greet");
	}

	#[test]
	fn should_extract_table_in_insert_context() {
		let source = "INSERT INTO user { name: 'Charlie', age: 28 };";
		let word = word_at_position(source, pos(0, 14));
		assert_eq!(word, "user");
	}

	#[test]
	fn should_find_fn_greet_in_schema() {
		let source = concat!(
			"DEFINE FUNCTION fn::greet($name: string) -> string { RETURN 'Hello'; };\n",
			"LET $greeting = fn::greet('World');\n",
		);
		let schema = SchemaGraph::from_source(source).unwrap();
		let func = schema.function("greet");
		assert!(func.is_some(), "fn::greet should be in schema");
		let func = func.unwrap();
		assert_eq!(func.args.len(), 1);
		assert_eq!(func.args[0].0, "$name");
	}

	#[test]
	fn should_find_table_user_in_schema() {
		let source = concat!(
			"DEFINE TABLE user SCHEMAFULL;\n",
			"DEFINE FIELD name ON user TYPE string;\n",
			"INSERT INTO user { name: 'Charlie' };\n",
		);
		let schema = SchemaGraph::from_source(source).unwrap();
		assert!(schema.table("user").is_some());
		assert_eq!(schema.fields_of("user").len(), 1);
	}

	#[test]
	fn should_have_source_location_after_attach() {
		let source = concat!(
			"DEFINE TABLE user SCHEMAFULL;\n",
			"DEFINE FUNCTION fn::greet($name: string) -> string { RETURN 'Hello'; };\n",
		);
		let defs = surql_parser::extract_definitions(source).unwrap();
		let mut graph = SchemaGraph::from_definitions(&defs);

		let tmp = tempfile::NamedTempFile::with_suffix(".surql").unwrap();
		std::fs::write(tmp.path(), source).unwrap();
		graph.attach_source_locations(source, tmp.path());

		let table = graph.table("user").unwrap();
		assert!(table.source.is_some());
		let loc = table.source.as_ref().unwrap();
		assert_eq!(loc.offset, 0, "DEFINE TABLE should be at start of file");

		let func = graph.function("greet").unwrap();
		assert!(func.source.is_some());
		let loc = func.source.as_ref().unwrap();
		assert!(
			loc.offset > 0,
			"DEFINE FUNCTION should be after DEFINE TABLE"
		);
	}

	#[test]
	fn should_extract_word_at_various_cursor_positions() {
		//                 0123456789012345678901234567890123456789012
		let source = "SELECT name, age FROM user WHERE age > 18;";
		assert_eq!(word_at_position(source, pos(0, 3)), "SELECT");
		assert_eq!(word_at_position(source, pos(0, 9)), "name");
		assert_eq!(word_at_position(source, pos(0, 14)), "age");
		assert_eq!(word_at_position(source, pos(0, 23)), "user");
		assert_eq!(word_at_position(source, pos(0, 28)), "WHERE");
	}

	#[test]
	fn should_find_table_ref_in_define_field() {
		// User hovers on "audit" in: DEFINE FIELD action ON audit TYPE string;
		let source = concat!(
			"DEFINE TABLE audit SCHEMAFULL DROP;\n",
			"DEFINE FIELD action ON audit TYPE string;\n",
		);
		let schema = SchemaGraph::from_source(source).unwrap();
		// "audit" on line 1, col ~23
		let word = word_at_position(source, pos(1, 23));
		assert_eq!(word, "audit");
		assert!(
			schema.table("audit").is_some(),
			"table 'audit' should be in schema"
		);
		assert!(
			schema.table("audit").unwrap().full,
			"audit should be SCHEMAFULL"
		);
	}

	#[test]
	fn should_have_goto_def_location_for_table_in_same_file() {
		let source = concat!(
			"DEFINE TABLE audit SCHEMAFULL DROP;\n",
			"DEFINE FIELD action ON audit TYPE string;\n",
		);
		let defs = surql_parser::extract_definitions(source).unwrap();
		let mut graph = SchemaGraph::from_definitions(&defs);
		let tmp = tempfile::NamedTempFile::with_suffix(".surql").unwrap();
		std::fs::write(tmp.path(), source).unwrap();
		graph.attach_source_locations(source, tmp.path());

		let table = graph.table("audit").unwrap();
		assert!(
			table.source.is_some(),
			"table 'audit' must have a source location for goto_def"
		);
	}
}

// ═══════════════════════════════════════════════════════════════════════
// 29. FIND REFERENCES
// ═══════════════════════════════════════════════════════════════════════

#[cfg(test)]
mod find_references {
	use crate::server::find_word_occurrences;
	use tower_lsp::lsp_types::Url;

	#[test]
	fn should_find_table_occurrences() {
		let source = "DEFINE TABLE user SCHEMAFULL;\nDEFINE FIELD name ON user TYPE string;\nSELECT * FROM user;";
		let uri = Url::parse("file:///test.surql").unwrap();
		let mut locs = Vec::new();
		find_word_occurrences(source, "user", &uri, &mut locs);
		assert_eq!(locs.len(), 3, "user appears 3 times");
	}

	#[test]
	fn should_not_match_partial_words() {
		let source = "SELECT username FROM user;";
		let uri = Url::parse("file:///test.surql").unwrap();
		let mut locs = Vec::new();
		find_word_occurrences(source, "user", &uri, &mut locs);
		assert_eq!(locs.len(), 1, "should match 'user' but not 'username'");
	}

	#[test]
	fn should_find_function_references() {
		let source = "DEFINE FUNCTION fn::greet($name: string) -> string { RETURN 'Hello'; };\nLET $g = fn::greet('World');\nLET $h = fn::greet('Test');";
		let uri = Url::parse("file:///test.surql").unwrap();
		let mut locs = Vec::new();
		find_word_occurrences(source, "fn::greet", &uri, &mut locs);
		assert_eq!(locs.len(), 3, "fn::greet appears 3 times");
	}

	#[test]
	fn should_return_correct_positions() {
		let source = "SELECT * FROM user;";
		let uri = Url::parse("file:///test.surql").unwrap();
		let mut locs = Vec::new();
		find_word_occurrences(source, "user", &uri, &mut locs);
		assert_eq!(locs.len(), 1);
		assert_eq!(locs[0].range.start.line, 0);
		assert_eq!(locs[0].range.start.character, 14);
		assert_eq!(locs[0].range.end.character, 18);
	}

	#[test]
	fn should_handle_multiline() {
		let source = "DEFINE TABLE post SCHEMAFULL;\nDEFINE FIELD title ON post TYPE string;\nINSERT INTO post { title: 'hi' };";
		let uri = Url::parse("file:///test.surql").unwrap();
		let mut locs = Vec::new();
		find_word_occurrences(source, "post", &uri, &mut locs);
		assert_eq!(locs.len(), 3);
		assert_eq!(locs[0].range.start.line, 0);
		assert_eq!(locs[1].range.start.line, 1);
		assert_eq!(locs[2].range.start.line, 2);
	}

	#[test]
	fn should_return_empty_for_no_matches() {
		let source = "SELECT * FROM user;";
		let uri = Url::parse("file:///test.surql").unwrap();
		let mut locs = Vec::new();
		find_word_occurrences(source, "nonexistent", &uri, &mut locs);
		assert!(locs.is_empty());
	}
}

// ═══════════════════════════════════════════════════════════════════════
// 29b. SCHEMA-AWARE FIND REFERENCES
// ═══════════════════════════════════════════════════════════════════════

#[cfg(test)]
mod schema_aware_references {
	use crate::context::{
		SymbolKind, classify_symbol_at_position, extract_all_table_occurrences,
		find_field_references, find_function_references_in,
	};
	use tower_lsp::lsp_types::Position;

	fn pos(line: u32, character: u32) -> Position {
		Position { line, character }
	}

	// ─── classify_symbol_at_position ───

	#[test]
	fn should_classify_table_after_from() {
		let source = "SELECT * FROM user;";
		let sym = classify_symbol_at_position(source, pos(0, 16));
		assert_eq!(sym, Some(SymbolKind::Table("user".into())));
	}

	#[test]
	fn should_classify_table_in_define_table() {
		let source = "DEFINE TABLE user SCHEMAFULL;";
		let sym = classify_symbol_at_position(source, pos(0, 15));
		assert_eq!(sym, Some(SymbolKind::Table("user".into())));
	}

	#[test]
	fn should_classify_field_in_define_field() {
		let source = "DEFINE FIELD name ON user TYPE string;";
		let sym = classify_symbol_at_position(source, pos(0, 14));
		assert_eq!(
			sym,
			Some(SymbolKind::Field {
				table: "user".into(),
				field: "name".into(),
			})
		);
	}

	#[test]
	fn should_classify_table_after_on_in_define_field() {
		let source = "DEFINE FIELD name ON user TYPE string;";
		let sym = classify_symbol_at_position(source, pos(0, 22));
		assert_eq!(sym, Some(SymbolKind::Table("user".into())));
	}

	#[test]
	fn should_classify_field_in_where_clause() {
		let source = "SELECT * FROM user WHERE name = 'Alice';";
		let sym = classify_symbol_at_position(source, pos(0, 27));
		assert_eq!(
			sym,
			Some(SymbolKind::Field {
				table: "user".into(),
				field: "name".into(),
			})
		);
	}

	#[test]
	fn should_classify_function_with_fn_prefix() {
		let source = "LET $g = fn::greet('World');";
		let sym = classify_symbol_at_position(source, pos(0, 14));
		assert_eq!(sym, Some(SymbolKind::Function("fn::greet".into())));
	}

	#[test]
	fn should_classify_table_after_update() {
		let source = "UPDATE user SET name = 'Bob';";
		let sym = classify_symbol_at_position(source, pos(0, 9));
		assert_eq!(sym, Some(SymbolKind::Table("user".into())));
	}

	#[test]
	fn should_classify_field_after_set_in_update() {
		let source = "UPDATE user SET name = 'Bob';";
		let sym = classify_symbol_at_position(source, pos(0, 18));
		assert_eq!(
			sym,
			Some(SymbolKind::Field {
				table: "user".into(),
				field: "name".into(),
			})
		);
	}

	// ─── find_field_references ───

	#[test]
	fn should_find_field_in_define_and_where() {
		let source =
			"DEFINE FIELD name ON user TYPE string;\nSELECT * FROM user WHERE name = 'Alice';";
		let refs = find_field_references(source, "user", "name");
		assert_eq!(
			refs.len(),
			2,
			"field 'name' in DEFINE FIELD and WHERE clause"
		);
		assert_eq!(refs[0].line, 0);
		assert_eq!(refs[1].line, 1);
	}

	#[test]
	fn should_find_dot_access_field_refs() {
		let source = "SELECT user.name FROM user;";
		let refs = find_field_references(source, "user", "name");
		assert_eq!(refs.len(), 1, "user.name dot access");
		assert_eq!(refs[0].col, 12);
	}

	#[test]
	fn should_not_find_field_on_wrong_table() {
		let source = "DEFINE FIELD name ON post TYPE string;\nSELECT * FROM user WHERE name = 'x';";
		let refs = find_field_references(source, "post", "name");
		assert_eq!(
			refs.len(),
			1,
			"only the DEFINE FIELD on post, not the WHERE on user"
		);
		assert_eq!(refs[0].line, 0);
	}

	#[test]
	fn should_find_field_in_update_set() {
		let source = "UPDATE user SET name = 'New';";
		let refs = find_field_references(source, "user", "name");
		assert_eq!(refs.len(), 1);
	}

	#[test]
	fn should_find_field_in_insert_into() {
		let source = "INSERT INTO user { name: 'Alice' };";
		let refs = find_field_references(source, "user", "name");
		assert_eq!(refs.len(), 1);
	}

	#[test]
	fn should_return_empty_for_no_field_refs() {
		let source = "SELECT * FROM user;";
		let refs = find_field_references(source, "user", "email");
		assert!(refs.is_empty());
	}

	// ─── extract_all_table_occurrences ───

	#[test]
	fn should_find_table_in_define_and_dml() {
		let source =
			"DEFINE TABLE user SCHEMAFULL;\nSELECT * FROM user;\nUPDATE user SET name = 'x';";
		let refs = extract_all_table_occurrences(source, "user");
		assert_eq!(refs.len(), 3, "DEFINE TABLE + FROM + UPDATE");
	}

	#[test]
	fn should_find_table_in_record_type() {
		let source = "DEFINE FIELD author ON post TYPE record<user>;";
		let refs = extract_all_table_occurrences(source, "user");
		assert_eq!(refs.len(), 1);
	}

	#[test]
	fn should_find_table_in_record_id_separate() {
		// Record ID like user:john is tokenized as a single token by the lexer,
		// so we can't extract the table prefix from it without string-level parsing.
		// This is a known limitation — record IDs in LET context won't be found.
		let source = "LET $id = user:john;";
		let refs = extract_all_table_occurrences(source, "user");
		// May be 0 (lexer treats user:john as single token) or 1 (if split)
		assert!(refs.len() <= 1, "record ID prefix detection is best-effort");
	}

	#[test]
	fn should_find_table_after_from_with_record_id() {
		let source = "SELECT * FROM user:john;";
		let refs = extract_all_table_occurrences(source, "user");
		assert!(
			!refs.is_empty(),
			"should find at least 1 reference for user in FROM user:john"
		);
	}

	#[test]
	fn should_find_table_in_define_field_on() {
		let source = "DEFINE FIELD name ON user TYPE string;";
		let refs = extract_all_table_occurrences(source, "user");
		assert_eq!(refs.len(), 1);
	}

	#[test]
	fn should_not_match_wrong_table() {
		let source = "SELECT * FROM post;\nUPDATE post SET title = 'x';";
		let refs = extract_all_table_occurrences(source, "user");
		assert!(refs.is_empty());
	}

	// ─── find_function_references_in ───

	#[test]
	fn should_find_function_definition_and_calls() {
		let source = "DEFINE FUNCTION fn::greet($n: string) -> string { RETURN 'hi'; };\nLET $x = fn::greet('World');\nLET $y = fn::greet('Test');";
		let refs = find_function_references_in(source, "fn::greet");
		assert_eq!(refs.len(), 3);
	}

	#[test]
	fn should_not_match_fn_prefix_of_longer_name() {
		let source = "LET $x = fn::greet_all('World');";
		let refs = find_function_references_in(source, "fn::greet");
		assert!(refs.is_empty(), "fn::greet should not match fn::greet_all");
	}

	#[test]
	fn should_find_function_at_correct_positions() {
		let source = "fn::greet('hi')";
		let refs = find_function_references_in(source, "fn::greet");
		assert_eq!(refs.len(), 1);
		assert_eq!(refs[0].col, 0);
		assert_eq!(refs[0].len, 9);
	}

	#[test]
	fn should_return_empty_for_no_function_refs() {
		let source = "SELECT * FROM user;";
		let refs = find_function_references_in(source, "fn::greet");
		assert!(refs.is_empty());
	}
}

// ═══════════════════════════════════════════════════════════════════════
// 30. TREE-SITTER QUERY VALIDATION (replaces Python test)
// ═══════════════════════════════════════════════════════════════════════

#[cfg(test)]
mod tree_sitter_query_validation {
	use std::collections::HashSet;
	use std::path::Path;

	fn load_node_types(path: &Path) -> HashSet<String> {
		let content = std::fs::read_to_string(path).unwrap();
		let types: Vec<serde_json::Value> = serde_json::from_str(&content).unwrap();
		let mut names = HashSet::new();
		for nt in &types {
			if let Some(name) = nt["type"].as_str() {
				names.insert(name.to_string());
			}
			if let Some(children) = nt["children"].as_object() {
				if let Some(child_types) = children["types"].as_array() {
					for ct in child_types {
						if let Some(name) = ct["type"].as_str() {
							names.insert(name.to_string());
						}
					}
				}
			}
			if let Some(fields) = nt["fields"].as_object() {
				for field_info in fields.values() {
					if let Some(field_types) = field_info["types"].as_array() {
						for ft in field_types {
							if let Some(name) = ft["type"].as_str() {
								names.insert(name.to_string());
							}
						}
					}
				}
			}
		}
		names
	}

	fn extract_scm_refs(path: &Path) -> HashSet<String> {
		let content = std::fs::read_to_string(path).unwrap();
		let re = regex::Regex::new(r"\(([a-z_][a-z0-9_]*)\)").unwrap();
		let mut refs: HashSet<String> = re
			.captures_iter(&content)
			.map(|c| c[1].to_string())
			.collect();
		refs.remove("ERROR");
		refs
	}

	#[test]
	fn highlights_scm_references_only_valid_nodes() {
		let grammar_dir = Path::new(env!("CARGO_MANIFEST_DIR"))
			.parent()
			.unwrap()
			.join("tree-sitter-surrealql");
		let node_types = grammar_dir.join("src/node-types.json");
		let highlights = grammar_dir.join("queries/highlights.scm");

		if !node_types.exists() || !highlights.exists() {
			return; // skip if tree-sitter-surrealql not present
		}

		let valid = load_node_types(&node_types);
		let refs = extract_scm_refs(&highlights);
		let invalid: Vec<_> = refs.difference(&valid).collect();

		assert!(
			invalid.is_empty(),
			"highlights.scm references invalid node types: {invalid:?}"
		);
	}

	#[test]
	fn folds_scm_references_only_valid_nodes() {
		let grammar_dir = Path::new(env!("CARGO_MANIFEST_DIR"))
			.parent()
			.unwrap()
			.join("tree-sitter-surrealql");
		let node_types = grammar_dir.join("src/node-types.json");
		let folds = grammar_dir.join("queries/folds.scm");

		if !node_types.exists() || !folds.exists() {
			return;
		}

		let valid = load_node_types(&node_types);
		let refs = extract_scm_refs(&folds);
		let invalid: Vec<_> = refs.difference(&valid).collect();

		assert!(
			invalid.is_empty(),
			"folds.scm references invalid node types: {invalid:?}"
		);
	}
}

// ═══════════════════════════════════════════════════════════════════════
// 22. HOVER IN DML CONTEXTS — table/function hover in real queries
// ═══════════════════════════════════════════════════════════════════════

#[cfg(test)]
mod hover_in_dml {
	use crate::server::word_at_position;
	use surql_parser::SchemaGraph;
	use tower_lsp::lsp_types::Position;

	fn pos(line: u32, col: u32) -> Position {
		Position {
			line,
			character: col,
		}
	}

	fn schema() -> SchemaGraph {
		SchemaGraph::from_source(concat!(
			"DEFINE TABLE user SCHEMAFULL;\n",
			"DEFINE FIELD name ON user TYPE string;\n",
			"DEFINE FIELD email ON user TYPE string;\n",
			"DEFINE FIELD age ON user TYPE int DEFAULT 0;\n",
			"DEFINE TABLE post SCHEMAFULL;\n",
			"DEFINE FIELD title ON post TYPE string;\n",
			"DEFINE FIELD author ON post TYPE record<user>;\n",
			"DEFINE FUNCTION fn::greet($name: string) -> string { RETURN 'Hello, ' + $name; };\n",
			"DEFINE FUNCTION fn::user::create($name: string, $email: string) -> object { RETURN CREATE user SET name = $name, email = $email; };\n",
		))
		.unwrap()
	}

	#[test]
	fn should_resolve_table_in_select_from() {
		let sg = schema();
		let source = "SELECT * FROM user WHERE age > 18;";
		let word = word_at_position(source, pos(0, 16));
		assert_eq!(word, "user");
		assert!(sg.table("user").is_some());
		assert!(sg.table("user").unwrap().full);
	}

	#[test]
	fn should_resolve_table_in_insert_into() {
		let sg = schema();
		let source = "INSERT INTO user { name: 'Alice' };";
		let word = word_at_position(source, pos(0, 14));
		assert_eq!(word, "user");
		assert!(sg.table("user").is_some());
	}

	#[test]
	fn should_resolve_table_in_update() {
		let sg = schema();
		let source = "UPDATE user SET age = 31 WHERE name = 'Alice';";
		let word = word_at_position(source, pos(0, 9));
		assert_eq!(word, "user");
		assert!(sg.table("user").is_some());
	}

	#[test]
	fn should_resolve_table_in_delete() {
		let sg = schema();
		let source = "DELETE user WHERE active = false;";
		let word = word_at_position(source, pos(0, 9));
		assert_eq!(word, "user");
		assert!(sg.table("user").is_some());
	}

	#[test]
	fn should_resolve_table_in_upsert() {
		let sg = schema();
		let source = "UPSERT user SET name = $name;";
		let word = word_at_position(source, pos(0, 9));
		assert_eq!(word, "user");
		assert!(sg.table("user").is_some());
	}

	#[test]
	fn should_resolve_table_in_define_field_on() {
		let sg = schema();
		let source = "DEFINE FIELD status ON user TYPE string;";
		let word = word_at_position(source, pos(0, 24));
		assert_eq!(word, "user");
		assert!(sg.table("user").is_some());
	}

	#[test]
	fn should_resolve_fn_greet_in_let() {
		let sg = schema();
		let source = "LET $greeting = fn::greet('World');";
		let word = word_at_position(source, pos(0, 22));
		assert_eq!(word, "fn::greet");
		let fn_name = word.strip_prefix("fn::").unwrap();
		assert!(sg.function(fn_name).is_some());
	}

	#[test]
	fn should_resolve_nested_fn_in_select() {
		let sg = schema();
		let source = "SELECT fn::greet(name) FROM user;";
		let word = word_at_position(source, pos(0, 12));
		assert_eq!(word, "fn::greet");
		assert!(sg.function("greet").is_some());
	}

	#[test]
	fn should_resolve_namespaced_fn() {
		let sg = schema();
		let source = "LET $new = fn::user::create('Bob', 'bob@test.com');";
		let word = word_at_position(source, pos(0, 18));
		assert_eq!(word, "fn::user::create");
		assert!(sg.function("user::create").is_some());
	}

	#[test]
	fn should_show_correct_field_count_for_user() {
		let sg = schema();
		let fields = sg.fields_of("user");
		assert_eq!(fields.len(), 3, "user should have name, email, age");
	}

	#[test]
	fn should_show_correct_field_count_for_post() {
		let sg = schema();
		let fields = sg.fields_of("post");
		assert_eq!(fields.len(), 2, "post should have title, author");
	}

	#[test]
	fn should_detect_record_link_in_field() {
		let sg = schema();
		let author = sg
			.fields_of("post")
			.iter()
			.find(|f| f.name == "author")
			.unwrap();
		assert!(
			author.record_links.contains(&"user".to_string()),
			"author field should link to user"
		);
	}
}

// ═══════════════════════════════════════════════════════════════════════
// 23. BUILTIN HOVER IN EXPRESSIONS — string::len inside SELECT etc.
// ═══════════════════════════════════════════════════════════════════════

#[cfg(test)]
mod builtin_hover_in_expressions {
	use crate::server::word_at_position;
	use tower_lsp::lsp_types::Position;

	fn pos(line: u32, col: u32) -> Position {
		Position {
			line,
			character: col,
		}
	}

	#[test]
	fn should_resolve_string_len_in_select() {
		let source = "SELECT string::len(name) FROM user;";
		let word = word_at_position(source, pos(0, 12));
		assert_eq!(word, "string::len");
		assert!(surql_parser::builtin_function("string::len").is_some());
	}

	#[test]
	fn should_resolve_math_round_in_expression() {
		let source = "SELECT math::round(price * 1.1, 2) AS with_tax FROM product;";
		let word = word_at_position(source, pos(0, 12));
		assert_eq!(word, "math::round");
		assert!(surql_parser::builtin_function("math::round").is_some());
	}

	#[test]
	fn should_resolve_time_now_in_default() {
		let source = "DEFINE FIELD created_at ON user TYPE datetime DEFAULT time::now();";
		let word = word_at_position(source, pos(0, 55));
		assert_eq!(word, "time::now");
		assert!(surql_parser::builtin_function("time::now").is_some());
	}

	#[test]
	fn should_resolve_array_len_in_where() {
		let source = "SELECT * FROM post WHERE array::len(tags) > 3;";
		let word = word_at_position(source, pos(0, 33));
		assert_eq!(word, "array::len");
		assert!(surql_parser::builtin_function("array::len").is_some());
	}

	#[test]
	fn should_resolve_crypto_argon2_generate() {
		let source = "SELECT crypto::argon2::generate(password) FROM input;";
		let word = word_at_position(source, pos(0, 18));
		assert_eq!(word, "crypto::argon2::generate");
		assert!(surql_parser::builtin_function("crypto::argon2::generate").is_some());
	}

	#[test]
	fn should_resolve_rand_uuid_v4() {
		let source = "SELECT rand::uuid::v4() AS id FROM ONLY {};";
		let word = word_at_position(source, pos(0, 14));
		assert_eq!(word, "rand::uuid::v4");
		// rand::uuid::v4 may or may not be in docs (some are rand::uuid)
		// just verify word extraction works
	}

	#[test]
	fn should_resolve_type_is_string() {
		let source = "SELECT type::is::string(name) FROM user;";
		let word = word_at_position(source, pos(0, 16));
		assert_eq!(word, "type::is::string");
	}

	#[test]
	fn should_not_resolve_inside_string_literal() {
		// Word extraction at a position inside a string literal
		// returns the raw text (not a function name)
		let source = "SELECT 'string::len' FROM user;";
		let word = word_at_position(source, pos(0, 14));
		// Inside quotes, word_at_position still extracts text
		// but it won't be a valid builtin because of the quotes context
		assert!(word.contains("string"));
	}
}

// ═══════════════════════════════════════════════════════════════════════
// 24. COMPLETION ACROSS NAMESPACES — verify all major namespaces work
// ═══════════════════════════════════════════════════════════════════════

#[cfg(test)]
mod completion_across_namespaces {
	use crate::completion;
	use crate::completion::{Context, detect_context};
	use tower_lsp::lsp_types::{CompletionItemKind, Position};

	fn pos(line: u32, col: u32) -> Position {
		Position {
			line,
			character: col,
		}
	}

	#[test]
	fn should_detect_math_namespace() {
		assert_eq!(
			detect_context("math::", pos(0, 6)),
			Context::BuiltinNamespace("math".into())
		);
	}

	#[test]
	fn should_detect_time_namespace() {
		assert_eq!(
			detect_context("time::", pos(0, 6)),
			Context::BuiltinNamespace("time".into())
		);
	}

	#[test]
	fn should_detect_crypto_namespace() {
		assert_eq!(
			detect_context("crypto::", pos(0, 8)),
			Context::BuiltinNamespace("crypto".into())
		);
	}

	#[test]
	fn should_detect_type_namespace() {
		assert_eq!(
			detect_context("type::", pos(0, 6)),
			Context::BuiltinNamespace("type".into())
		);
	}

	#[test]
	fn should_detect_geo_namespace() {
		assert_eq!(
			detect_context("geo::", pos(0, 5)),
			Context::BuiltinNamespace("geo".into())
		);
	}

	#[test]
	fn should_detect_http_namespace() {
		assert_eq!(
			detect_context("http::", pos(0, 6)),
			Context::BuiltinNamespace("http".into())
		);
	}

	#[test]
	fn should_complete_math_functions() {
		let items = completion::complete("math::", pos(0, 6), None);
		let fns: Vec<_> = items
			.iter()
			.filter(|i| i.kind == Some(CompletionItemKind::FUNCTION))
			.collect();
		assert!(fns.len() > 10, "math should have 10+ functions");
		assert!(fns.iter().any(|i| i.label == "math::mean"));
		assert!(fns.iter().any(|i| i.label == "math::sum"));
	}

	#[test]
	fn should_complete_time_functions() {
		let items = completion::complete("time::", pos(0, 6), None);
		let fns: Vec<_> = items
			.iter()
			.filter(|i| i.kind == Some(CompletionItemKind::FUNCTION))
			.collect();
		assert!(fns.len() > 5, "time should have 5+ functions");
		assert!(fns.iter().any(|i| i.label == "time::now"));
	}

	#[test]
	fn should_complete_crypto_sub_namespaces() {
		let items = completion::complete("crypto::", pos(0, 8), None);
		let modules: Vec<_> = items
			.iter()
			.filter(|i| i.kind == Some(CompletionItemKind::MODULE))
			.collect();
		assert!(
			modules
				.iter()
				.any(|i| i.label.starts_with("crypto::argon2")),
			"crypto:: should suggest argon2 sub-namespace"
		);
	}

	#[test]
	fn should_complete_string_semver_sub_namespace() {
		let items = completion::complete("string::", pos(0, 8), None);
		let modules: Vec<_> = items
			.iter()
			.filter(|i| i.kind == Some(CompletionItemKind::MODULE))
			.collect();
		assert!(
			modules
				.iter()
				.any(|i| i.label.starts_with("string::semver")),
			"string:: should suggest semver sub-namespace"
		);
	}

	#[test]
	fn should_detect_multi_level_namespace() {
		assert_eq!(
			detect_context("crypto::argon2::", pos(0, 16)),
			Context::BuiltinNamespace("crypto::argon2".into())
		);
	}

	#[test]
	fn should_complete_argon2_functions() {
		let items = completion::complete("crypto::argon2::", pos(0, 16), None);
		let fns: Vec<_> = items
			.iter()
			.filter(|i| i.kind == Some(CompletionItemKind::FUNCTION))
			.collect();
		assert!(
			fns.iter().any(|i| i.label == "crypto::argon2::generate"),
			"crypto::argon2:: should suggest generate"
		);
	}

	#[test]
	fn should_not_suggest_wrong_namespace_functions() {
		let items = completion::complete("string::", pos(0, 8), None);
		let fns: Vec<_> = items
			.iter()
			.filter(|i| i.kind == Some(CompletionItemKind::FUNCTION))
			.collect();
		assert!(
			!fns.iter().any(|i| i.label.starts_with("math::")),
			"string:: should not suggest math:: functions"
		);
		assert!(
			!fns.iter().any(|i| i.label.starts_with("array::")),
			"string:: should not suggest array:: functions"
		);
	}
}

// ═══════════════════════════════════════════════════════════════════════
// 25. SIGNATURE HELP — real-world expressions
// ═══════════════════════════════════════════════════════════════════════

#[cfg(test)]
mod signature_help_real_world {
	use crate::signature;
	use surql_parser::SchemaGraph;
	use tower_lsp::lsp_types::Position;

	fn pos(line: u32, col: u32) -> Position {
		Position {
			line,
			character: col,
		}
	}

	#[test]
	fn should_show_sig_for_string_uppercase() {
		let help = signature::signature_help("string::uppercase(", pos(0, 18), None);
		assert!(help.is_some());
		let sig = &help.unwrap().signatures[0];
		assert!(sig.label.contains("string::uppercase"));
	}

	#[test]
	fn should_show_sig_for_math_round() {
		let help = signature::signature_help("math::round(", pos(0, 12), None);
		assert!(help.is_some());
	}

	#[test]
	fn should_show_sig_for_array_flatten() {
		let help = signature::signature_help("array::flatten(", pos(0, 15), None);
		assert!(help.is_some());
	}

	#[test]
	fn should_track_param_in_builtin_with_multiple_args() {
		let help = signature::signature_help("string::concat(a, ", pos(0, 19), None);
		assert!(help.is_some());
		assert_eq!(help.unwrap().active_parameter, Some(1));
	}

	#[test]
	fn should_show_sig_in_select_expression() {
		let help = signature::signature_help("SELECT string::len(", pos(0, 19), None);
		assert!(help.is_some());
		assert!(help.unwrap().signatures[0].label.contains("string::len"));
	}

	#[test]
	fn should_show_sig_for_user_fn_with_schema() {
		let sg = SchemaGraph::from_source(concat!(
			"DEFINE FUNCTION fn::calculate($x: int, $y: int) -> int { RETURN $x + $y; };\n",
		))
		.unwrap();
		let help =
			signature::signature_help("LET $result = fn::calculate(10, ", pos(0, 32), Some(&sg));
		assert!(help.is_some());
		let h = help.unwrap();
		assert_eq!(h.active_parameter, Some(1), "second param should be active");
		assert!(h.signatures[0].label.contains("fn::calculate"));
	}

	#[test]
	fn should_not_show_sig_outside_parens() {
		let help = signature::signature_help("string::len", pos(0, 11), None);
		assert!(help.is_none());
	}

	#[test]
	fn should_not_show_sig_after_closing_paren() {
		let help = signature::signature_help("string::len(x) ", pos(0, 15), None);
		assert!(help.is_none());
	}
}

// ═══════════════════════════════════════════════════════════════════════
// 26. MULTI-FILE SCHEMA MERGE — migration-style workflow
// ═══════════════════════════════════════════════════════════════════════

#[cfg(test)]
mod multi_file_schema_merge {
	use surql_parser::SchemaGraph;

	#[test]
	fn should_accumulate_fields_from_multiple_migrations() {
		let mut sg = SchemaGraph::from_source(
			"DEFINE TABLE user SCHEMAFULL; DEFINE FIELD name ON user TYPE string;",
		)
		.unwrap();
		let m2 = SchemaGraph::from_source(
			"DEFINE FIELD email ON user TYPE string; DEFINE FIELD age ON user TYPE int;",
		)
		.unwrap();
		let m3 = SchemaGraph::from_source("DEFINE FIELD bio ON user TYPE option<string>;").unwrap();
		sg.merge(m2);
		sg.merge(m3);

		assert_eq!(sg.fields_of("user").len(), 4, "name + email + age + bio");
		assert!(sg.table("user").unwrap().full, "should stay SCHEMAFULL");
	}

	#[test]
	fn should_merge_tables_from_different_files() {
		let mut sg = SchemaGraph::from_source("DEFINE TABLE user SCHEMAFULL;").unwrap();
		let m2 = SchemaGraph::from_source("DEFINE TABLE post SCHEMAFULL;").unwrap();
		let m3 = SchemaGraph::from_source("DEFINE TABLE comment SCHEMAFULL;").unwrap();
		sg.merge(m2);
		sg.merge(m3);

		assert!(sg.table("user").is_some());
		assert!(sg.table("post").is_some());
		assert!(sg.table("comment").is_some());
	}

	#[test]
	fn should_merge_functions_from_different_files() {
		let mut sg = SchemaGraph::from_source(
			"DEFINE FUNCTION fn::greet($name: string) -> string { RETURN $name; };",
		)
		.unwrap();
		let m2 = SchemaGraph::from_source(
			"DEFINE FUNCTION fn::add($a: int, $b: int) -> int { RETURN $a + $b; };",
		)
		.unwrap();
		sg.merge(m2);

		assert!(sg.function("greet").is_some());
		assert!(sg.function("add").is_some());
	}

	#[test]
	fn should_not_lose_indexes_on_merge() {
		let mut sg = SchemaGraph::from_source(concat!(
			"DEFINE TABLE user SCHEMAFULL;\n",
			"DEFINE INDEX email_idx ON user FIELDS email UNIQUE;\n",
		))
		.unwrap();
		let m2 = SchemaGraph::from_source("DEFINE FIELD name ON user TYPE string;").unwrap();
		sg.merge(m2);

		assert_eq!(
			sg.indexes_of("user").len(),
			1,
			"email_idx should survive merge"
		);
	}

	#[test]
	fn should_not_duplicate_indexes_on_merge() {
		let mut sg = SchemaGraph::from_source(concat!(
			"DEFINE TABLE user SCHEMAFULL;\n",
			"DEFINE INDEX email_idx ON user FIELDS email UNIQUE;\n",
		))
		.unwrap();
		let m2 = SchemaGraph::from_source("DEFINE INDEX email_idx ON user FIELDS email UNIQUE;")
			.unwrap();
		sg.merge(m2);

		assert_eq!(
			sg.indexes_of("user").len(),
			1,
			"email_idx should not be duplicated"
		);
	}

	#[test]
	fn should_handle_empty_merge() {
		let mut sg = SchemaGraph::from_source("DEFINE TABLE user SCHEMAFULL;").unwrap();
		let empty = SchemaGraph::default();
		sg.merge(empty);
		assert!(sg.table("user").is_some());
	}

	#[test]
	fn should_handle_merge_into_empty() {
		let mut sg = SchemaGraph::default();
		let filled = SchemaGraph::from_source("DEFINE TABLE user SCHEMAFULL;").unwrap();
		sg.merge(filled);
		assert!(sg.table("user").is_some());
	}
}

// ═══════════════════════════════════════════════════════════════════════
// 27. COMPLETION CONTEXT DETECTION — edge cases
// ═══════════════════════════════════════════════════════════════════════

#[cfg(test)]
mod context_detection_edge_cases {
	use crate::completion::{Context, detect_context};
	use tower_lsp::lsp_types::Position;

	fn pos(line: u32, col: u32) -> Position {
		Position {
			line,
			character: col,
		}
	}

	#[test]
	fn should_detect_table_after_from_with_space() {
		assert_eq!(
			detect_context("SELECT * FROM ", pos(0, 14)),
			Context::TableName
		);
	}

	#[test]
	fn should_detect_table_after_into() {
		assert_eq!(
			detect_context("INSERT INTO ", pos(0, 12)),
			Context::TableName
		);
	}

	#[test]
	fn should_detect_table_after_on() {
		assert_eq!(
			detect_context("DEFINE FIELD name ON ", pos(0, 21)),
			Context::TableName
		);
	}

	#[test]
	fn should_detect_fn_context_after_fn_colon_colon() {
		assert_eq!(
			detect_context("SELECT fn::", pos(0, 11)),
			Context::FunctionName
		);
	}

	#[test]
	fn should_detect_param_after_dollar() {
		assert_eq!(detect_context("$", pos(0, 1)), Context::ParamName);
	}

	#[test]
	fn should_detect_general_at_empty_input() {
		assert_eq!(detect_context("", pos(0, 0)), Context::General);
	}

	#[test]
	fn should_detect_field_after_dot() {
		assert_eq!(
			detect_context("user.", pos(0, 5)),
			Context::FieldName("user".into())
		);
	}

	#[test]
	fn should_detect_general_after_keyword() {
		// After SELECT (but no FROM yet), general context
		let ctx = detect_context("SELECT ", pos(0, 7));
		// Should be General since SELECT is not FROM/INTO/ON/TABLE
		assert_eq!(ctx, Context::General);
	}

	#[test]
	fn should_detect_namespace_in_set_clause() {
		assert_eq!(
			detect_context("UPDATE user SET name = string::", pos(0, 31)),
			Context::BuiltinNamespace("string".into())
		);
	}

	#[test]
	fn should_detect_fn_in_where_clause() {
		assert_eq!(
			detect_context("SELECT * FROM user WHERE fn::", pos(0, 29)),
			Context::FunctionName
		);
	}
}

// ═══════════════════════════════════════════════════════════════════════
// 28. SCHEMA GRAPH FROM REAL-WORLD QUERIES
// ═══════════════════════════════════════════════════════════════════════

#[cfg(test)]
mod schema_graph_real_world {
	use surql_parser::SchemaGraph;

	#[test]
	fn should_parse_full_app_schema() {
		let sg = SchemaGraph::from_source(concat!(
			"DEFINE NAMESPACE production;\n",
			"DEFINE DATABASE main;\n",
			"DEFINE TABLE user SCHEMAFULL;\n",
			"DEFINE TABLE post SCHEMAFULL;\n",
			"DEFINE TABLE comment SCHEMAFULL;\n",
			"DEFINE TABLE audit SCHEMAFULL DROP;\n",
			"DEFINE FIELD name ON user TYPE string;\n",
			"DEFINE FIELD email ON user TYPE string;\n",
			"DEFINE FIELD age ON user TYPE int DEFAULT 0;\n",
			"DEFINE FIELD active ON user TYPE bool DEFAULT true;\n",
			"DEFINE FIELD title ON post TYPE string;\n",
			"DEFINE FIELD body ON post TYPE string;\n",
			"DEFINE FIELD author ON post TYPE record<user>;\n",
			"DEFINE FIELD tags ON post TYPE array<string> DEFAULT [];\n",
			"DEFINE FIELD text ON comment TYPE string;\n",
			"DEFINE FIELD post ON comment TYPE record<post>;\n",
			"DEFINE INDEX email_idx ON user FIELDS email UNIQUE;\n",
			"DEFINE INDEX post_author ON post FIELDS author;\n",
			"DEFINE EVENT user_created ON user WHEN $event = 'CREATE' THEN {\n",
			"  CREATE audit SET action = 'user_created', target = $after.id;\n",
			"};\n",
			"DEFINE FUNCTION fn::greet($name: string) -> string { RETURN 'Hello, ' + $name; };\n",
			"DEFINE FUNCTION fn::user::list() -> array { RETURN SELECT * FROM user; };\n",
		))
		.unwrap();

		assert_eq!(sg.table_names().count(), 4);
		assert_eq!(sg.function_names().count(), 2);
		assert_eq!(sg.fields_of("user").len(), 4);
		assert_eq!(sg.fields_of("post").len(), 4);
		assert_eq!(sg.fields_of("comment").len(), 2);
		assert_eq!(sg.indexes_of("user").len(), 1);
		assert_eq!(sg.indexes_of("post").len(), 1);
		assert_eq!(sg.events_of("user").len(), 1);
	}

	#[test]
	fn should_extract_record_links() {
		let sg = SchemaGraph::from_source(concat!(
			"DEFINE TABLE post SCHEMAFULL;\n",
			"DEFINE FIELD author ON post TYPE record<user>;\n",
			"DEFINE FIELD reviewers ON post TYPE array<record<user>>;\n",
		))
		.unwrap();

		let author = sg
			.fields_of("post")
			.iter()
			.find(|f| f.name == "author")
			.unwrap();
		assert!(author.record_links.contains(&"user".to_string()));
	}

	#[test]
	fn should_handle_recovered_ast_with_errors() {
		// Mix of valid and invalid statements
		let (stmts, diags) = surql_parser::parse_with_recovery(concat!(
			"DEFINE TABLE user SCHEMAFULL;\n",
			"SELCT broken syntax;\n",
			"DEFINE FIELD name ON user TYPE string;\n",
			"UPDAET also broken;\n",
			"DEFINE FUNCTION fn::hello() -> string { RETURN 'hi'; };\n",
		));
		assert!(!diags.is_empty(), "should have parse errors");

		let defs = surql_parser::extract_definitions_from_ast(&stmts).unwrap();
		assert_eq!(
			defs.tables.len(),
			1,
			"user table should be extracted despite errors"
		);
		assert_eq!(
			defs.fields.len(),
			1,
			"name field should be extracted despite errors"
		);
		assert_eq!(
			defs.functions.len(),
			1,
			"fn::hello should be extracted despite errors"
		);
	}

	#[test]
	fn should_preserve_default_values() {
		let sg = SchemaGraph::from_source(concat!(
			"DEFINE TABLE user SCHEMAFULL;\n",
			"DEFINE FIELD age ON user TYPE int DEFAULT 0;\n",
			"DEFINE FIELD active ON user TYPE bool DEFAULT true;\n",
		))
		.unwrap();

		let age = sg
			.fields_of("user")
			.iter()
			.find(|f| f.name == "age")
			.unwrap();
		assert!(age.default.is_some());

		let active = sg
			.fields_of("user")
			.iter()
			.find(|f| f.name == "active")
			.unwrap();
		assert!(active.default.is_some());
	}

	#[test]
	fn should_detect_readonly_fields() {
		let sg = SchemaGraph::from_source(concat!(
			"DEFINE TABLE user SCHEMAFULL;\n",
			"DEFINE FIELD password ON user TYPE string READONLY;\n",
			"DEFINE FIELD name ON user TYPE string;\n",
		))
		.unwrap();

		let password = sg
			.fields_of("user")
			.iter()
			.find(|f| f.name == "password")
			.unwrap();
		assert!(password.readonly, "password field should be readonly");

		let name = sg
			.fields_of("user")
			.iter()
			.find(|f| f.name == "name")
			.unwrap();
		assert!(!name.readonly, "name field should not be readonly");
	}
}

// ═══════════════════════════════════════════════════════════════════════
// CONTEXT DETECTION
// ═══════════════════════════════════════════════════════════════════════

#[cfg(test)]
mod context_detection {
	use crate::context::table_context_at_position;
	use tower_lsp::lsp_types::Position;

	fn pos(line: u32, character: u32) -> Position {
		Position { line, character }
	}

	#[test]
	fn should_detect_table_from_select_from() {
		let src = "SELECT age FROM user WHERE age > 18";
		// Cursor on "age" after WHERE (line 0, col 29 = inside "age")
		let result = table_context_at_position(src, pos(0, 29));
		assert_eq!(result.as_deref(), Some("user"));
	}

	#[test]
	fn should_detect_table_from_update() {
		let src = "UPDATE user SET age = 31";
		// Cursor on "age" (line 0, col 16)
		let result = table_context_at_position(src, pos(0, 16));
		assert_eq!(result.as_deref(), Some("user"));
	}

	#[test]
	fn should_detect_table_from_insert_into() {
		let src = "INSERT INTO product { name: 'Widget' }";
		// Cursor on "name" (line 0, col 24)
		let result = table_context_at_position(src, pos(0, 24));
		assert_eq!(result.as_deref(), Some("product"));
	}

	#[test]
	fn should_detect_table_from_create() {
		let src = "CREATE user SET name = 'Alice'";
		// Cursor on "name" (line 0, col 16)
		let result = table_context_at_position(src, pos(0, 16));
		assert_eq!(result.as_deref(), Some("user"));
	}

	#[test]
	fn should_detect_table_from_delete() {
		let src = "DELETE user WHERE active = false";
		// Cursor on "active" (line 0, col 19)
		let result = table_context_at_position(src, pos(0, 19));
		assert_eq!(result.as_deref(), Some("user"));
	}

	#[test]
	fn should_detect_table_from_upsert() {
		let src = "UPSERT user SET name = 'Bob'";
		// Cursor on "name" (line 0, col 16)
		let result = table_context_at_position(src, pos(0, 16));
		assert_eq!(result.as_deref(), Some("user"));
	}

	#[test]
	fn should_detect_table_from_define_field_on() {
		let src = "DEFINE FIELD email ON user TYPE string";
		// Cursor on the end of "string" (line 0, col 37)
		let result = table_context_at_position(src, pos(0, 37));
		assert_eq!(result.as_deref(), Some("user"));
	}

	#[test]
	fn should_detect_table_from_define_index_on() {
		let src = "DEFINE INDEX idx_email ON user FIELDS email UNIQUE";
		// Cursor on "email" after FIELDS (line 0, col 38)
		let result = table_context_at_position(src, pos(0, 38));
		assert_eq!(result.as_deref(), Some("user"));
	}

	#[test]
	fn should_return_none_when_no_context() {
		let src = "LET $x = 42";
		let result = table_context_at_position(src, pos(0, 10));
		assert_eq!(result, None);
	}

	#[test]
	fn should_return_none_for_empty_source() {
		let result = table_context_at_position("", pos(0, 0));
		assert_eq!(result, None);
	}

	#[test]
	fn should_detect_table_in_multiline_query() {
		let src = "SELECT\n  name,\n  age\nFROM user\nWHERE age > 18";
		// Cursor on "age" in the WHERE clause (line 4, col 6)
		let result = table_context_at_position(src, pos(4, 6));
		assert_eq!(result.as_deref(), Some("user"));
	}

	#[test]
	fn should_not_cross_semicolon_boundary() {
		let src = "SELECT * FROM old_table;\nUPDATE user SET age = 31";
		// Cursor on "age" in the UPDATE statement (line 1, col 16)
		let result = table_context_at_position(src, pos(1, 16));
		assert_eq!(result.as_deref(), Some("user"));
	}

	#[test]
	fn should_detect_table_from_select_from_at_field_position() {
		let src = "SELECT name, email FROM customer";
		// Cursor on "name" (line 0, col 7) — before FROM, so
		// no FROM token yet in the prefix. This should return None
		// because the cursor is before any table-establishing keyword.
		let result = table_context_at_position(src, pos(0, 7));
		assert_eq!(result, None);
	}
}

// ═══════════════════════════════════════════════════════════════════════
// 31. DOCUMENT SYMBOLS
// ═══════════════════════════════════════════════════════════════════════

#[cfg(test)]
mod document_symbols {
	use surql_parser::SchemaGraph;
	use tower_lsp::lsp_types::*;

	use crate::server::build_document_symbols;

	#[test]
	fn should_produce_symbols_for_table_and_fields() {
		let source = concat!(
			"DEFINE TABLE user SCHEMAFULL;\n",
			"DEFINE FIELD name ON user TYPE string;\n",
			"DEFINE FIELD email ON user TYPE string;\n",
			"DEFINE INDEX email_idx ON user FIELDS email UNIQUE;\n",
		);
		let schema = SchemaGraph::from_source(source).unwrap();
		let response = build_document_symbols(source, &schema);
		assert!(response.is_some(), "should produce document symbols");

		let symbols = match response.unwrap() {
			DocumentSymbolResponse::Nested(s) => s,
			_ => panic!("expected nested symbols"),
		};

		assert_eq!(symbols.len(), 1, "should have 1 table symbol");
		let table_sym = &symbols[0];
		assert_eq!(table_sym.name, "user");
		assert_eq!(table_sym.kind, SymbolKind::CLASS);
		assert_eq!(table_sym.detail.as_deref(), Some("SCHEMAFULL"));

		let children = table_sym
			.children
			.as_ref()
			.expect("table should have children");
		assert_eq!(children.len(), 3, "2 fields + 1 index");

		let name_field = children.iter().find(|c| c.name == "name").unwrap();
		assert_eq!(name_field.kind, SymbolKind::FIELD);

		let email_field = children.iter().find(|c| c.name == "email").unwrap();
		assert_eq!(email_field.kind, SymbolKind::FIELD);

		let index_sym = children.iter().find(|c| c.name == "email_idx").unwrap();
		assert_eq!(index_sym.kind, SymbolKind::KEY);
		assert_eq!(index_sym.detail.as_deref(), Some("UNIQUE"));
	}

	#[test]
	fn should_produce_symbols_for_functions() {
		let source = concat!(
			"DEFINE FUNCTION fn::greet($name: string) -> string {\n",
			"  RETURN 'Hello, ' + $name;\n",
			"};\n",
		);
		let schema = SchemaGraph::from_source(source).unwrap();
		let response = build_document_symbols(source, &schema);
		assert!(response.is_some());

		let symbols = match response.unwrap() {
			DocumentSymbolResponse::Nested(s) => s,
			_ => panic!("expected nested symbols"),
		};

		let fn_sym = symbols.iter().find(|s| s.name == "fn::greet").unwrap();
		assert_eq!(fn_sym.kind, SymbolKind::FUNCTION);
		assert!(
			fn_sym.detail.as_ref().unwrap().contains("$name: string"),
			"detail should include signature, got: {:?}",
			fn_sym.detail
		);
	}

	#[test]
	fn should_nest_fields_under_tables() {
		let source = concat!(
			"DEFINE TABLE post SCHEMALESS;\n",
			"DEFINE FIELD title ON post TYPE string;\n",
			"DEFINE FIELD body ON post TYPE string;\n",
			"DEFINE EVENT on_create ON post WHEN $event = 'CREATE' THEN {};\n",
		);
		let schema = SchemaGraph::from_source(source).unwrap();
		let response = build_document_symbols(source, &schema);
		assert!(response.is_some());

		let symbols = match response.unwrap() {
			DocumentSymbolResponse::Nested(s) => s,
			_ => panic!("expected nested symbols"),
		};

		let table_sym = symbols.iter().find(|s| s.name == "post").unwrap();
		assert_eq!(table_sym.kind, SymbolKind::CLASS);
		assert_eq!(table_sym.detail.as_deref(), Some("SCHEMALESS"));

		let children = table_sym
			.children
			.as_ref()
			.expect("post should have children");
		assert_eq!(children.len(), 3, "2 fields + 1 event");

		let event_sym = children.iter().find(|c| c.name == "on_create").unwrap();
		assert_eq!(event_sym.kind, SymbolKind::EVENT);
	}

	#[test]
	fn should_return_none_for_empty_file() {
		let source = "";
		let schema = SchemaGraph::default();
		let response = build_document_symbols(source, &schema);
		assert!(response.is_none(), "empty file should produce no symbols");
	}

	#[test]
	fn should_produce_symbols_for_params() {
		let source = "DEFINE PARAM $base_url VALUE 42;\n";
		let schema = SchemaGraph::from_source(source).unwrap();
		let param_names: Vec<&str> = schema.param_names().collect();
		assert!(
			!param_names.is_empty(),
			"schema should contain at least one param"
		);

		let response = build_document_symbols(source, &schema);
		assert!(response.is_some());

		let symbols = match response.unwrap() {
			DocumentSymbolResponse::Nested(s) => s,
			_ => panic!("expected nested symbols"),
		};

		let param_sym = symbols
			.iter()
			.find(|s| s.kind == SymbolKind::VARIABLE)
			.expect("should have a VARIABLE symbol for the param");
		assert!(
			param_sym.name.contains("base_url"),
			"param symbol name should contain 'base_url', got: {}",
			param_sym.name
		);
		assert_eq!(param_sym.kind, SymbolKind::VARIABLE);
	}

	#[test]
	fn should_produce_correct_ranges_for_table_definitions() {
		let source = concat!(
			"DEFINE TABLE user SCHEMAFULL;\n",
			"DEFINE FIELD name ON user TYPE string;\n",
		);
		let schema = SchemaGraph::from_source(source).unwrap();
		let response = build_document_symbols(source, &schema);
		assert!(response.is_some());

		let symbols = match response.unwrap() {
			DocumentSymbolResponse::Nested(s) => s,
			_ => panic!("expected nested symbols"),
		};

		let table_sym = &symbols[0];
		assert_eq!(table_sym.range.start.line, 0, "table should be on line 0");
		assert_eq!(
			table_sym.range.start.character, 0,
			"table should start at col 0"
		);

		let children = table_sym.children.as_ref().unwrap();
		let field_sym = &children[0];
		assert_eq!(field_sym.range.start.line, 1, "field should be on line 1");
	}
}

// ═══════════════════════════════════════════════════════════════════════
// 32. CODE LENS
// ═══════════════════════════════════════════════════════════════════════

#[cfg(test)]
mod code_lens {
	use surql_parser::SchemaGraph;

	use crate::server::build_code_lenses;

	#[test]
	fn should_produce_lens_with_field_and_index_counts() {
		let source = concat!(
			"DEFINE TABLE user SCHEMAFULL;\n",
			"DEFINE FIELD name ON user TYPE string;\n",
			"DEFINE FIELD email ON user TYPE string;\n",
			"DEFINE FIELD age ON user TYPE int;\n",
			"DEFINE INDEX email_idx ON user FIELDS email UNIQUE;\n",
			"DEFINE INDEX name_idx ON user FIELDS name;\n",
		);
		let schema = SchemaGraph::from_source(source).unwrap();
		let lenses = build_code_lenses(source, &schema);
		assert!(lenses.is_some());

		let lenses = lenses.unwrap();
		assert_eq!(lenses.len(), 1, "should have 1 code lens for 1 table");

		let lens = &lenses[0];
		let title = &lens.command.as_ref().unwrap().title;
		assert!(
			title.contains("3 fields"),
			"should show 3 fields, got: {title}"
		);
		assert!(
			title.contains("2 indexes"),
			"should show 2 indexes, got: {title}"
		);
	}

	#[test]
	fn should_produce_lens_with_record_links() {
		let source = concat!(
			"DEFINE TABLE user SCHEMAFULL;\n",
			"DEFINE FIELD name ON user TYPE string;\n",
			"DEFINE TABLE post SCHEMAFULL;\n",
			"DEFINE FIELD title ON post TYPE string;\n",
			"DEFINE FIELD author ON post TYPE record<user>;\n",
		);
		let schema = SchemaGraph::from_source(source).unwrap();
		let lenses = build_code_lenses(source, &schema);
		assert!(lenses.is_some());

		let lenses = lenses.unwrap();
		let post_lens = lenses
			.iter()
			.find(|l| l.range.start.line == 2)
			.expect("should have lens for post table at line 2");
		let title = &post_lens.command.as_ref().unwrap().title;
		assert!(
			title.contains("\u{2192}user"),
			"should show outgoing link to user, got: {title}"
		);

		let user_lens = lenses
			.iter()
			.find(|l| l.range.start.line == 0)
			.expect("should have lens for user table at line 0");
		let user_title = &user_lens.command.as_ref().unwrap().title;
		assert!(
			user_title.contains("\u{2190}post"),
			"should show incoming link from post, got: {user_title}"
		);
	}

	#[test]
	fn should_produce_lens_with_events() {
		let source = concat!(
			"DEFINE TABLE audit SCHEMALESS;\n",
			"DEFINE FIELD action ON audit TYPE string;\n",
			"DEFINE EVENT log_action ON audit WHEN $event = 'CREATE' THEN {};\n",
		);
		let schema = SchemaGraph::from_source(source).unwrap();
		let lenses = build_code_lenses(source, &schema);
		assert!(lenses.is_some());

		let lenses = lenses.unwrap();
		let title = &lenses[0].command.as_ref().unwrap().title;
		assert!(
			title.contains("1 field"),
			"should show 1 field, got: {title}"
		);
		assert!(
			title.contains("1 event"),
			"should show 1 event, got: {title}"
		);
	}

	#[test]
	fn should_return_none_for_empty_schema() {
		let source = "SELECT * FROM user;";
		let schema = SchemaGraph::default();
		let lenses = build_code_lenses(source, &schema);
		assert!(lenses.is_none(), "no tables means no code lenses");
	}

	#[test]
	fn should_use_singular_forms_for_single_items() {
		let source = concat!(
			"DEFINE TABLE item SCHEMAFULL;\n",
			"DEFINE FIELD name ON item TYPE string;\n",
			"DEFINE INDEX name_idx ON item FIELDS name UNIQUE;\n",
		);
		let schema = SchemaGraph::from_source(source).unwrap();
		let lenses = build_code_lenses(source, &schema).unwrap();
		let title = &lenses[0].command.as_ref().unwrap().title;
		assert!(
			title.contains("1 field"),
			"should use singular 'field', got: {title}"
		);
		assert!(
			!title.contains("1 fields"),
			"should not use plural 'fields' for 1, got: {title}"
		);
		assert!(
			title.contains("1 index"),
			"should use singular 'index', got: {title}"
		);
		assert!(
			!title.contains("1 indexs"),
			"should not have wrong plural, got: {title}"
		);
	}
}

// ═══════════════════════════════════════════════════════════════════════
// TABLE REFERENCE EXTRACTION
// ═══════════════════════════════════════════════════════════════════════

#[cfg(test)]
mod table_references {
	use crate::context::extract_table_references;

	#[test]
	fn should_extract_select_from_table() {
		let refs = extract_table_references("SELECT * FROM user;");
		assert_eq!(refs.len(), 1);
		assert_eq!(refs[0].name, "user");
	}

	#[test]
	fn should_extract_multiple_references() {
		let source = "SELECT * FROM user;\nSELECT * FROM post;";
		let refs = extract_table_references(source);
		let names: Vec<&str> = refs.iter().map(|r| r.name.as_str()).collect();
		assert!(names.contains(&"user"));
		assert!(names.contains(&"post"));
	}

	#[test]
	fn should_extract_update_table() {
		let refs = extract_table_references("UPDATE user SET name = 'Alice';");
		assert_eq!(refs.len(), 1);
		assert_eq!(refs[0].name, "user");
	}

	#[test]
	fn should_extract_create_table() {
		let refs = extract_table_references("CREATE post SET title = 'hi';");
		assert_eq!(refs.len(), 1);
		assert_eq!(refs[0].name, "post");
	}

	#[test]
	fn should_extract_delete_table() {
		let refs = extract_table_references("DELETE user WHERE id = 1;");
		assert_eq!(refs.len(), 1);
		assert_eq!(refs[0].name, "user");
	}

	#[test]
	fn should_extract_insert_into_table() {
		let refs = extract_table_references("INSERT INTO user { name: 'Bob' };");
		assert_eq!(refs.len(), 1);
		assert_eq!(refs[0].name, "user");
	}

	#[test]
	fn should_skip_define_statements() {
		let source = "DEFINE TABLE user SCHEMAFULL;\nSELECT * FROM user;";
		let refs = extract_table_references(source);
		assert_eq!(refs.len(), 1, "DEFINE TABLE should not be a reference");
		assert_eq!(refs[0].name, "user");
	}

	#[test]
	fn should_return_empty_for_no_dml() {
		let refs = extract_table_references("DEFINE TABLE user SCHEMAFULL;");
		assert!(refs.is_empty());
	}

	#[test]
	fn should_return_empty_for_empty_input() {
		assert!(extract_table_references("").is_empty());
	}

	#[test]
	fn should_track_correct_line_and_col() {
		let source = "-- comment\nSELECT * FROM user;";
		let refs = extract_table_references(source);
		assert_eq!(refs.len(), 1);
		assert_eq!(refs[0].line, 1);
		assert_eq!(refs[0].name, "user");
	}

	#[test]
	fn should_skip_dml_inside_define_event_body() {
		let source = "\
DEFINE EVENT user_created ON user WHEN $event = 'CREATE' THEN {
    CREATE audit SET
        action = 'user_created',
        target = $after.id,
        timestamp = time::now();
};";
		let refs = extract_table_references(source);
		let names: Vec<&str> = refs.iter().map(|r| r.name.as_str()).collect();
		assert!(
			!names.contains(&"audit"),
			"CREATE inside DEFINE EVENT should not be a table reference, got: {names:?}"
		);
	}

	#[test]
	fn should_handle_upsert() {
		let refs = extract_table_references("UPSERT user SET name = 'Alice';");
		assert_eq!(refs.len(), 1);
		assert_eq!(refs[0].name, "user");
	}
}

// ═══════════════════════════════════════════════════════════════════════
// CROSS-FILE RECORD LINK DIAGNOSTICS
// ═══════════════════════════════════════════════════════════════════════

#[cfg(test)]
mod cross_file_record_links {
	use crate::server::find_record_link_range;

	#[test]
	fn should_find_record_link_range() {
		let source = "DEFINE FIELD author ON post TYPE record<user>;";
		let range = find_record_link_range(source, "user").expect("should find range");
		assert_eq!(range.start.line, 0);
		assert_eq!(
			range.start.character,
			"DEFINE FIELD author ON post TYPE record<".len() as u32
		);
		assert_eq!(range.end.character, range.start.character + 4); // "user".len()
	}

	#[test]
	fn should_find_record_link_case_insensitive() {
		let source = "DEFINE FIELD author ON post TYPE RECORD<User>;";
		let range = find_record_link_range(source, "User").expect("should find range");
		assert!(range.start.character > 0, "should find the link");
	}

	#[test]
	fn should_return_none_for_missing() {
		let source = "DEFINE TABLE user SCHEMAFULL;";
		let range = find_record_link_range(source, "nonexistent");
		assert!(
			range.is_none(),
			"should return None when record link not found"
		);
	}
}

// ═══════════════════════════════════════════════════════════════════════
// INLINE SUPPRESS COMMENTS
// ═══════════════════════════════════════════════════════════════════════

#[cfg(test)]
mod inline_suppress {
	use crate::server::line_has_suppress;

	#[test]
	fn should_suppress_with_double_slash_comment() {
		let source = "SELECT * FROM metric // surql-allow: undefined-table";
		assert!(line_has_suppress(source, 0, "undefined-table"));
	}

	#[test]
	fn should_suppress_with_double_dash_comment() {
		let source = "SELECT * FROM metric -- surql-allow: undefined-table";
		assert!(line_has_suppress(source, 0, "undefined-table"));
	}

	#[test]
	fn should_not_suppress_without_comment() {
		let source = "SELECT * FROM metric";
		assert!(!line_has_suppress(source, 0, "undefined-table"));
	}

	#[test]
	fn should_not_suppress_wrong_code() {
		let source = "SELECT * FROM metric // surql-allow: other-code";
		assert!(!line_has_suppress(source, 0, "undefined-table"));
	}

	#[test]
	fn should_suppress_case_insensitive() {
		let source = "SELECT * FROM metric // SURQL-ALLOW: UNDEFINED-TABLE";
		assert!(line_has_suppress(source, 0, "undefined-table"));
	}

	#[test]
	fn should_suppress_on_correct_line() {
		let source = "SELECT * FROM user;\nSELECT * FROM metric // surql-allow: undefined-table";
		assert!(!line_has_suppress(source, 0, "undefined-table"));
		assert!(line_has_suppress(source, 1, "undefined-table"));
	}

	#[test]
	fn should_not_suppress_out_of_bounds_line() {
		let source = "SELECT * FROM metric";
		assert!(!line_has_suppress(source, 99, "undefined-table"));
	}

	#[test]
	fn should_suppress_with_extra_whitespace() {
		let source = "SELECT * FROM metric //   surql-allow:   undefined-table  ";
		assert!(line_has_suppress(source, 0, "undefined-table"));
	}
}

// ═══════════════════════════════════════════════════════════════════════
// RENAME REFACTORING
// ═══════════════════════════════════════════════════════════════════════

#[cfg(test)]
mod rename {
	use crate::context::extract_all_table_occurrences;
	use crate::server::{word_and_range_at_position, word_at_position};
	use tower_lsp::lsp_types::Position;

	#[test]
	fn should_find_table_in_define_table() {
		let source = "DEFINE TABLE user SCHEMAFULL;";
		let refs = extract_all_table_occurrences(source, "user");
		assert_eq!(refs.len(), 1);
		assert_eq!(refs[0].name, "user");
		assert_eq!(refs[0].line, 0);
	}

	#[test]
	fn should_find_table_in_select_from() {
		let source = "SELECT * FROM user;";
		let refs = extract_all_table_occurrences(source, "user");
		assert_eq!(refs.len(), 1);
		assert_eq!(refs[0].name, "user");
	}

	#[test]
	fn should_find_table_in_update() {
		let source = "UPDATE user SET name = 'Alice';";
		let refs = extract_all_table_occurrences(source, "user");
		assert_eq!(refs.len(), 1);
		assert_eq!(refs[0].name, "user");
	}

	#[test]
	fn should_find_table_in_create() {
		let source = "CREATE post SET title = 'hi';";
		let refs = extract_all_table_occurrences(source, "post");
		assert_eq!(refs.len(), 1);
		assert_eq!(refs[0].name, "post");
	}

	#[test]
	fn should_find_table_in_delete() {
		let source = "DELETE user WHERE id = 1;";
		let refs = extract_all_table_occurrences(source, "user");
		assert_eq!(refs.len(), 1);
		assert_eq!(refs[0].name, "user");
	}

	#[test]
	fn should_find_table_in_upsert() {
		let source = "UPSERT user SET name = 'Bob';";
		let refs = extract_all_table_occurrences(source, "user");
		assert_eq!(refs.len(), 1);
		assert_eq!(refs[0].name, "user");
	}

	#[test]
	fn should_find_table_in_insert_into() {
		let source = "INSERT INTO user { name: 'Bob' };";
		let refs = extract_all_table_occurrences(source, "user");
		assert_eq!(refs.len(), 1);
		assert_eq!(refs[0].name, "user");
	}

	#[test]
	fn should_find_table_in_define_field_on() {
		let source = "DEFINE FIELD name ON user TYPE string;";
		let refs = extract_all_table_occurrences(source, "user");
		assert_eq!(refs.len(), 1);
		assert_eq!(refs[0].name, "user");
	}

	#[test]
	fn should_find_table_in_define_field_on_table() {
		let source = "DEFINE FIELD name ON TABLE user TYPE string;";
		let refs = extract_all_table_occurrences(source, "user");
		assert_eq!(refs.len(), 1);
		assert_eq!(refs[0].name, "user");
	}

	#[test]
	fn should_find_table_in_define_index_on() {
		let source = "DEFINE INDEX idx_name ON user FIELDS name;";
		let refs = extract_all_table_occurrences(source, "user");
		assert_eq!(refs.len(), 1);
		assert_eq!(refs[0].name, "user");
	}

	#[test]
	fn should_find_table_in_define_event_on() {
		let source = "DEFINE EVENT evt ON user WHEN $before != $after THEN {};";
		let refs = extract_all_table_occurrences(source, "user");
		assert_eq!(refs.len(), 1);
		assert_eq!(refs[0].name, "user");
	}

	#[test]
	fn should_find_table_in_record_type() {
		let source = "DEFINE FIELD author ON post TYPE record<user>;";
		let refs = extract_all_table_occurrences(source, "user");
		assert_eq!(refs.len(), 1);
		assert_eq!(refs[0].name, "user");
	}

	#[test]
	fn should_find_table_in_record_id() {
		let source = "SELECT * FROM user:tobie;";
		let refs = extract_all_table_occurrences(source, "user");
		// Should find both FROM user:tobie (via record ID pattern) and possibly FROM keyword
		assert!(!refs.is_empty());
		assert!(refs.iter().all(|r| r.name == "user"));
	}

	#[test]
	fn should_find_all_occurrences_across_statements() {
		let source = "\
DEFINE TABLE user SCHEMAFULL;
DEFINE FIELD name ON user TYPE string;
DEFINE FIELD email ON user TYPE string;
SELECT * FROM user;
UPDATE user SET name = 'Alice';";
		let refs = extract_all_table_occurrences(source, "user");
		// DEFINE TABLE user + ON user + ON user + FROM user + UPDATE user = 5
		assert_eq!(refs.len(), 5, "expected 5 occurrences, got: {refs:?}");
	}

	#[test]
	fn should_not_find_unrelated_table() {
		let source = "SELECT * FROM user;\nCREATE post SET title = 'hi';";
		let refs = extract_all_table_occurrences(source, "account");
		assert!(refs.is_empty());
	}

	#[test]
	fn should_be_case_insensitive_for_table_name() {
		let source = "SELECT * FROM User;";
		let refs = extract_all_table_occurrences(source, "user");
		assert_eq!(refs.len(), 1);
	}

	#[test]
	fn should_return_empty_for_empty_source() {
		let refs = extract_all_table_occurrences("", "user");
		assert!(refs.is_empty());
	}

	#[test]
	fn should_return_correct_line_and_col() {
		let source = "DEFINE TABLE user SCHEMAFULL;\nSELECT * FROM user;";
		let refs = extract_all_table_occurrences(source, "user");
		assert_eq!(refs.len(), 2);
		// First occurrence: DEFINE TABLE user — line 0
		assert_eq!(refs[0].line, 0);
		// Second occurrence: FROM user — line 1
		assert_eq!(refs[1].line, 1);
	}

	#[test]
	fn should_extract_word_at_cursor_on_table_name() {
		let source = "SELECT * FROM user;";
		// Cursor on 'u' of 'user' (col 14)
		let word = word_at_position(
			source,
			Position {
				line: 0,
				character: 14,
			},
		);
		assert_eq!(word, "user");
	}

	#[test]
	fn should_extract_word_and_range_at_cursor() {
		let source = "SELECT * FROM user;";
		let (word, range) = word_and_range_at_position(
			source,
			Position {
				line: 0,
				character: 15,
			},
		);
		assert_eq!(word, "user");
		let range = range.unwrap();
		assert_eq!(range.start.character, 14);
		assert_eq!(range.end.character, 18);
	}

	#[test]
	fn should_deduplicate_overlapping_matches() {
		// A token like `user:tobie` could match both FROM and record-id patterns.
		// Ensure deduplication by (line, col).
		let source = "CREATE user:tobie SET name = 'Tobie';";
		let refs = extract_all_table_occurrences(source, "user");
		// CREATE user:tobie matches both CREATE keyword and record-id — deduped
		let unique_positions: std::collections::HashSet<(u32, u32)> =
			refs.iter().map(|r| (r.line, r.col)).collect();
		assert_eq!(
			refs.len(),
			unique_positions.len(),
			"duplicates found: {refs:?}"
		);
	}

	#[test]
	fn should_handle_from_only_modifier() {
		let source = "DELETE ONLY user WHERE id = 1;";
		let refs = extract_all_table_occurrences(source, "user");
		assert_eq!(refs.len(), 1);
		assert_eq!(refs[0].name, "user");
	}
}

// ═══════════════════════════════════════════════════════════════════════
// DOTENV LOADING
// ═══════════════════════════════════════════════════════════════════════

#[cfg(test)]
mod dotenv_loading {
	use crate::server::load_dotenv;
	use tempfile::TempDir;

	#[test]
	fn should_extract_ns_and_db_from_env() {
		let dir = TempDir::new().unwrap();
		std::fs::write(
			dir.path().join(".env"),
			"SURREALDB_URL=ws://localhost:8000\nSURREALDB_NS=test_ns\nSURREALDB_DB=test_db\n",
		)
		.unwrap();
		let result = load_dotenv(dir.path());
		assert!(result.is_some());
		let (url, ns, db) = result.unwrap();
		assert_eq!(url, "ws://localhost:8000");
		assert_eq!(ns, "test_ns");
		assert_eq!(db, "test_db");
	}

	#[test]
	fn should_accept_surreal_prefix_variant() {
		let dir = TempDir::new().unwrap();
		std::fs::write(
			dir.path().join(".env"),
			"SURREAL_NS=alt_ns\nSURREAL_DB=alt_db\n",
		)
		.unwrap();
		let result = load_dotenv(dir.path());
		assert!(result.is_some());
		let (url, ns, db) = result.unwrap();
		assert_eq!(url, "");
		assert_eq!(ns, "alt_ns");
		assert_eq!(db, "alt_db");
	}

	#[test]
	fn should_return_none_when_ns_missing() {
		let dir = TempDir::new().unwrap();
		std::fs::write(
			dir.path().join(".env"),
			"SURREALDB_URL=ws://localhost:8000\nSURREALDB_DB=test_db\n",
		)
		.unwrap();
		assert!(load_dotenv(dir.path()).is_none());
	}

	#[test]
	fn should_return_none_when_db_missing() {
		let dir = TempDir::new().unwrap();
		std::fs::write(dir.path().join(".env"), "SURREALDB_NS=test_ns\n").unwrap();
		assert!(load_dotenv(dir.path()).is_none());
	}

	#[test]
	fn should_return_none_when_no_env_file() {
		let dir = TempDir::new().unwrap();
		assert!(load_dotenv(dir.path()).is_none());
	}

	#[test]
	fn should_skip_comments_and_blank_lines() {
		let dir = TempDir::new().unwrap();
		std::fs::write(
			dir.path().join(".env"),
			"# Database config\n\nSURREALDB_NS=prod\n\n# DB name\nSURREALDB_DB=myapp\n",
		)
		.unwrap();
		let result = load_dotenv(dir.path());
		assert!(result.is_some());
		let (_, ns, db) = result.unwrap();
		assert_eq!(ns, "prod");
		assert_eq!(db, "myapp");
	}

	#[test]
	fn should_strip_quotes_from_values() {
		let dir = TempDir::new().unwrap();
		std::fs::write(
			dir.path().join(".env"),
			"SURREALDB_NS=\"quoted_ns\"\nSURREALDB_DB='quoted_db'\n",
		)
		.unwrap();
		let result = load_dotenv(dir.path());
		assert!(result.is_some());
		let (_, ns, db) = result.unwrap();
		assert_eq!(ns, "quoted_ns");
		assert_eq!(db, "quoted_db");
	}

	#[test]
	fn should_handle_whitespace_around_equals() {
		let dir = TempDir::new().unwrap();
		std::fs::write(
			dir.path().join(".env"),
			"SURREALDB_NS = spaced_ns \nSURREALDB_DB = spaced_db \n",
		)
		.unwrap();
		let result = load_dotenv(dir.path());
		assert!(result.is_some());
		let (_, ns, db) = result.unwrap();
		assert_eq!(ns, "spaced_ns");
		assert_eq!(db, "spaced_db");
	}

	#[test]
	fn should_ignore_unrelated_vars() {
		let dir = TempDir::new().unwrap();
		std::fs::write(
			dir.path().join(".env"),
			"DATABASE_URL=postgres://...\nSURREALDB_NS=ns1\nSURREALDB_DB=db1\nPORT=3000\n",
		)
		.unwrap();
		let result = load_dotenv(dir.path());
		assert!(result.is_some());
		let (url, ns, db) = result.unwrap();
		assert_eq!(url, "");
		assert_eq!(ns, "ns1");
		assert_eq!(db, "db1");
	}
}

// ═══════════════════════════════════════════════════════════════════════
// MONOREPO DETECTION
// ═══════════════════════════════════════════════════════════════════════

#[cfg(test)]
mod monorepo_detection {
	use crate::server::detect_monorepo_projects;
	use tempfile::TempDir;

	#[test]
	fn should_detect_single_project_at_root() {
		let dir = TempDir::new().unwrap();
		std::fs::write(
			dir.path().join("manifest.toml"),
			"[meta]\nns = \"test\"\ndb = \"app\"\n",
		)
		.unwrap();
		let projects = detect_monorepo_projects(dir.path());
		assert_eq!(projects.len(), 1);
		assert_eq!(projects[0], dir.path());
	}

	#[test]
	fn should_detect_multiple_projects_in_subdirectories() {
		let dir = TempDir::new().unwrap();
		let auth_dir = dir.path().join("auth");
		let billing_dir = dir.path().join("billing");
		std::fs::create_dir_all(&auth_dir).unwrap();
		std::fs::create_dir_all(&billing_dir).unwrap();
		std::fs::write(
			auth_dir.join("manifest.toml"),
			"[meta]\nns = \"test\"\ndb = \"auth\"\n",
		)
		.unwrap();
		std::fs::write(
			billing_dir.join("manifest.toml"),
			"[meta]\nns = \"test\"\ndb = \"billing\"\n",
		)
		.unwrap();
		let projects = detect_monorepo_projects(dir.path());
		assert_eq!(projects.len(), 2);
	}

	#[test]
	fn should_detect_nested_projects_under_services() {
		let dir = TempDir::new().unwrap();
		let svc_auth = dir.path().join("services").join("auth");
		let svc_billing = dir.path().join("services").join("billing");
		std::fs::create_dir_all(&svc_auth).unwrap();
		std::fs::create_dir_all(&svc_billing).unwrap();
		std::fs::write(
			svc_auth.join("manifest.toml"),
			"[meta]\nns = \"test\"\ndb = \"auth\"\n",
		)
		.unwrap();
		std::fs::write(
			svc_billing.join("manifest.toml"),
			"[meta]\nns = \"test\"\ndb = \"billing\"\n",
		)
		.unwrap();
		let projects = detect_monorepo_projects(dir.path());
		assert_eq!(projects.len(), 2);
	}

	#[test]
	fn should_return_empty_for_no_markers() {
		let dir = TempDir::new().unwrap();
		let projects = detect_monorepo_projects(dir.path());
		assert!(projects.is_empty());
	}

	#[test]
	fn should_detect_env_based_project() {
		let dir = TempDir::new().unwrap();
		let sub = dir.path().join("api");
		std::fs::create_dir_all(&sub).unwrap();
		std::fs::write(sub.join(".env"), "SURREALDB_NS=test\nSURREALDB_DB=api\n").unwrap();
		let projects = detect_monorepo_projects(dir.path());
		assert_eq!(projects.len(), 1);
		assert_eq!(projects[0], sub);
	}

	#[test]
	fn should_skip_hidden_and_target_directories() {
		let dir = TempDir::new().unwrap();
		let hidden = dir.path().join(".hidden");
		let target = dir.path().join("target");
		std::fs::create_dir_all(&hidden).unwrap();
		std::fs::create_dir_all(&target).unwrap();
		std::fs::write(
			hidden.join("manifest.toml"),
			"[meta]\nns = \"test\"\ndb = \"hidden\"\n",
		)
		.unwrap();
		std::fs::write(
			target.join("manifest.toml"),
			"[meta]\nns = \"test\"\ndb = \"target\"\n",
		)
		.unwrap();
		let projects = detect_monorepo_projects(dir.path());
		assert!(projects.is_empty());
	}

	#[test]
	fn should_detect_surql_subdirectory_manifest() {
		let dir = TempDir::new().unwrap();
		let sub = dir.path().join("myproject");
		std::fs::create_dir_all(sub.join("surql")).unwrap();
		std::fs::write(
			sub.join("surql").join("manifest.toml"),
			"[meta]\nns = \"test\"\ndb = \"myproject\"\n",
		)
		.unwrap();
		let projects = detect_monorepo_projects(dir.path());
		assert_eq!(projects.len(), 1);
		assert_eq!(projects[0], sub);
	}
}

// ═══════════════════════════════════════════════════════════════════════
// NESTED OBJECT FIELD HOVER
// ═══════════════════════════════════════════════════════════════════════

#[cfg(test)]
mod nested_field_hover {
	use crate::server::dotted_path_at_position;
	use surql_parser::SchemaGraph;
	use tower_lsp::lsp_types::Position;

	fn pos(line: u32, col: u32) -> Position {
		Position {
			line,
			character: col,
		}
	}

	#[test]
	fn should_extract_dotted_path_on_last_segment() {
		//                 0123456789012345678901234567890
		let source = "SELECT settings.theme FROM project";
		let path = dotted_path_at_position(source, pos(0, 18));
		assert_eq!(path, "settings.theme");
	}

	#[test]
	fn should_extract_just_first_segment_when_cursor_on_it() {
		//                 0123456789012345678901234567890
		let source = "SELECT settings.theme FROM project";
		let path = dotted_path_at_position(source, pos(0, 10));
		assert_eq!(path, "settings");
	}

	#[test]
	fn should_extract_triple_dotted_path() {
		//                 0123456789012345678901234567890123456789
		let source = "SELECT a.b.c FROM t";
		let path = dotted_path_at_position(source, pos(0, 11));
		assert_eq!(path, "a.b.c");
	}

	#[test]
	fn should_extract_middle_of_triple_path() {
		//                 0123456789012345678901234567890123456789
		let source = "SELECT a.b.c FROM t";
		let path = dotted_path_at_position(source, pos(0, 9));
		assert_eq!(path, "a.b");
	}

	#[test]
	fn should_extract_plain_word_without_dots() {
		let source = "SELECT name FROM user";
		let path = dotted_path_at_position(source, pos(0, 9));
		assert_eq!(path, "name");
	}

	#[test]
	fn should_resolve_nested_field_through_schema() {
		let source = concat!(
			"DEFINE TABLE project SCHEMAFULL;\n",
			"DEFINE FIELD settings ON project TYPE object;\n",
			"DEFINE FIELD settings.theme ON project TYPE string;\n",
			"SELECT settings.theme FROM project;\n",
		);
		let schema = SchemaGraph::from_source(source).unwrap();
		// Cursor on "theme" in "settings.theme" on the SELECT line (line 3)
		let dotted = dotted_path_at_position(source, pos(3, 18));
		assert_eq!(dotted, "settings.theme");

		let field = schema.field_on("project", &dotted);
		assert!(
			field.is_some(),
			"settings.theme should be a field on project"
		);
		assert_eq!(field.unwrap().kind.as_deref(), Some("string"));
	}

	#[test]
	fn should_not_resolve_nonexistent_nested_field() {
		let source = concat!(
			"DEFINE TABLE project SCHEMAFULL;\n",
			"DEFINE FIELD settings ON project TYPE object;\n",
		);
		let schema = SchemaGraph::from_source(source).unwrap();
		let field = schema.field_on("project", "settings.nonexistent");
		assert!(field.is_none());
	}

	#[test]
	fn should_handle_empty_source() {
		let path = dotted_path_at_position("", pos(0, 0));
		assert_eq!(path, "");
	}

	#[test]
	fn should_handle_cursor_on_dot_itself() {
		//                 0123456789012345
		let source = "SELECT a.b FROM t";
		// pos(0, 8) is on the '.' character - should return empty since dot isn't an ident
		let path = dotted_path_at_position(source, pos(0, 8));
		assert_eq!(path, "");
	}
}

// ═══════════════════════════════════════════════════════════════════════
// GRAPH PATH HOVER
// ═══════════════════════════════════════════════════════════════════════

#[cfg(test)]
mod graph_path_hover {
	use crate::server::{GraphContext, graph_context_at_position};
	use surql_parser::SchemaGraph;
	use tower_lsp::lsp_types::Position;

	fn pos(line: u32, col: u32) -> Position {
		Position {
			line,
			character: col,
		}
	}

	#[test]
	fn should_detect_edge_table_between_arrows() {
		//                 0         1         2         3
		//                 0123456789012345678901234567890123456789
		let source = "SELECT ->manages->project FROM user";
		// "manages" starts at col 9, ends at col 16, followed by ->
		let ctx = graph_context_at_position(source, pos(0, 12));
		assert_eq!(ctx, Some(GraphContext::EdgeTable("manages".to_string())));
	}

	#[test]
	fn should_detect_target_table_after_last_arrow() {
		//                 0         1         2         3
		//                 0123456789012345678901234567890123456789
		let source = "SELECT ->manages->project FROM user";
		// "project" starts at col 19
		let ctx = graph_context_at_position(source, pos(0, 22));
		assert_eq!(ctx, Some(GraphContext::TargetTable("project".to_string())));
	}

	#[test]
	fn should_detect_field_on_target_table() {
		//                 0         1         2         3
		//                 0123456789012345678901234567890123456789012
		let source = "SELECT ->manages->project.name FROM user";
		// "name" starts at col 26
		let ctx = graph_context_at_position(source, pos(0, 28));
		assert_eq!(
			ctx,
			Some(GraphContext::FieldOnTarget {
				table: "project".to_string(),
				field: "name".to_string(),
			})
		);
	}

	#[test]
	fn should_detect_reverse_edge_table() {
		//                 0123456789012345678901234567890
		let source = "SELECT <-likes<-user FROM post";
		// "likes" starts at col 9
		let ctx = graph_context_at_position(source, pos(0, 11));
		assert_eq!(ctx, Some(GraphContext::EdgeTable("likes".to_string())));
	}

	#[test]
	fn should_detect_reverse_target_table() {
		//                 0123456789012345678901234567890
		let source = "SELECT <-likes<-user FROM post";
		// "user" starts at col 16
		let ctx = graph_context_at_position(source, pos(0, 18));
		assert_eq!(ctx, Some(GraphContext::TargetTable("user".to_string())));
	}

	#[test]
	fn should_return_none_for_normal_word() {
		let source = "SELECT name FROM user";
		let ctx = graph_context_at_position(source, pos(0, 9));
		assert!(ctx.is_none());
	}

	#[test]
	fn should_resolve_edge_table_in_schema() {
		let source = concat!(
			"DEFINE TABLE manages SCHEMAFULL;\n",
			"DEFINE FIELD in ON manages TYPE record<user>;\n",
			"DEFINE FIELD out ON manages TYPE record<project>;\n",
		);
		let schema = SchemaGraph::from_source(source).unwrap();
		assert!(
			schema.table("manages").is_some(),
			"edge table 'manages' should be in schema"
		);
	}

	#[test]
	fn should_resolve_field_on_graph_target_in_schema() {
		let source = concat!(
			"DEFINE TABLE project SCHEMAFULL;\n",
			"DEFINE FIELD name ON project TYPE string;\n",
			"DEFINE TABLE manages SCHEMAFULL;\n",
		);
		let schema = SchemaGraph::from_source(source).unwrap();
		let field = schema.field_on("project", "name");
		assert!(field.is_some());
		assert_eq!(field.unwrap().kind.as_deref(), Some("string"));
	}

	#[test]
	fn should_return_none_for_empty_source() {
		let ctx = graph_context_at_position("", pos(0, 0));
		assert!(ctx.is_none());
	}

	#[test]
	fn should_not_detect_minus_sign_as_arrow() {
		let source = "SELECT a - b FROM t";
		let ctx = graph_context_at_position(source, pos(0, 12));
		assert!(ctx.is_none());
	}
}
