//! MD018: No space after hash on atx style heading
//!
//! This rule checks for missing space after hash characters in ATX style headings.

use mdbook_lint_core::error::Result;
use mdbook_lint_core::rule::{Rule, RuleCategory, RuleMetadata};
use mdbook_lint_core::{
    Document,
    violation::{Fix, Position, Severity, Violation},
};

/// Rule to check for missing space after hash on ATX style headings
pub struct MD018;

impl Rule for MD018 {
    fn id(&self) -> &'static str {
        "MD018"
    }

    fn name(&self) -> &'static str {
        "no-missing-space-atx"
    }

    fn description(&self) -> &'static str {
        "No space after hash on atx style heading"
    }

    fn metadata(&self) -> RuleMetadata {
        RuleMetadata::stable(RuleCategory::Structure).introduced_in("markdownlint v0.1.0")
    }

    fn can_fix(&self) -> bool {
        true
    }

    fn check_with_ast<'a>(
        &self,
        document: &Document,
        _ast: Option<&'a comrak::nodes::AstNode<'a>>,
    ) -> Result<Vec<Violation>> {
        let mut violations = Vec::new();
        let mut in_fenced_code_block = false;

        for (line_number, line) in document.lines.iter().enumerate() {
            let line_num = line_number + 1; // Convert to 1-based line numbers
            let trimmed = line.trim_start();

            // Track fenced code blocks to skip content inside them
            if trimmed.starts_with("```") || trimmed.starts_with("~~~") {
                in_fenced_code_block = !in_fenced_code_block;
                continue;
            }

            // Skip content inside code blocks (e.g., Rust attributes like #[no_mangle])
            if in_fenced_code_block {
                continue;
            }

            // Check if this is an ATX-style heading (starts with #)
            // Skip shebang lines (#!/...)
            if trimmed.starts_with('#') && !trimmed.starts_with("#!") {
                // Find where the heading content starts
                let hash_count = trimmed.chars().take_while(|&c| c == '#').count();

                // Check if there's content after the hashes
                if trimmed.len() > hash_count {
                    let after_hashes = &trimmed[hash_count..];

                    // If there's content but no space, it's a violation
                    if !after_hashes.is_empty() && !after_hashes.starts_with(' ') {
                        let column = line.len() - line.trim_start().len() + hash_count + 1;

                        // Create fixed line by adding a space after the hashes
                        let indent = &line[..line.len() - trimmed.len()];
                        let hashes = &trimmed[..hash_count];
                        let fixed_line = format!("{}{} {}\n", indent, hashes, after_hashes.trim());

                        let fix = Fix {
                            description: "Add space after hash on atx style heading".to_string(),
                            replacement: Some(fixed_line),
                            start: Position {
                                line: line_num,
                                column: 1,
                            },
                            end: Position {
                                line: line_num,
                                column: line.len() + 1,
                            },
                        };

                        violations.push(self.create_violation_with_fix(
                            "No space after hash on atx style heading".to_string(),
                            line_num,
                            column,
                            Severity::Warning,
                            fix,
                        ));
                    }
                }
            }
        }

        Ok(violations)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use mdbook_lint_core::rule::Rule;
    use std::path::PathBuf;

    fn create_test_document(content: &str) -> Document {
        Document::new(content.to_string(), PathBuf::from("test.md")).unwrap()
    }

    #[test]
    fn test_md018_valid_headings() {
        let content = "# Heading 1\n## Heading 2\n### Heading 3";
        let document = create_test_document(content);
        let rule = MD018;
        let violations = rule.check(&document).unwrap();

        assert_eq!(violations.len(), 0);
    }

    #[test]
    fn test_md018_no_space_after_hash() {
        let content = "#Heading without space";
        let document = create_test_document(content);
        let rule = MD018;
        let violations = rule.check(&document).unwrap();

        assert_eq!(violations.len(), 1);
        assert_eq!(violations[0].rule_id, "MD018");
        assert_eq!(violations[0].line, 1);
        assert_eq!(violations[0].column, 2);
        assert!(violations[0].message.contains("No space after hash"));
    }

    #[test]
    fn test_md018_multiple_violations() {
        let content = "#Heading 1\n##Heading 2\n### Valid heading\n####Another violation";
        let document = create_test_document(content);
        let rule = MD018;
        let violations = rule.check(&document).unwrap();

        assert_eq!(violations.len(), 3);
        assert_eq!(violations[0].line, 1);
        assert_eq!(violations[1].line, 2);
        assert_eq!(violations[2].line, 4);
    }

    #[test]
    fn test_md018_indented_heading() {
        let content = "  #Indented heading without space";
        let document = create_test_document(content);
        let rule = MD018;
        let violations = rule.check(&document).unwrap();

        assert_eq!(violations.len(), 1);
        assert_eq!(violations[0].column, 4); // After the hash
    }

    #[test]
    fn test_md018_empty_heading() {
        let content = "#\n##\n###";
        let document = create_test_document(content);
        let rule = MD018;
        let violations = rule.check(&document).unwrap();

        // Empty headings (just hashes) should not trigger violations
        assert_eq!(violations.len(), 0);
    }

    #[test]
    fn test_md018_closed_atx_style() {
        let content = "#Heading#\n##Another#Heading##";
        let document = create_test_document(content);
        let rule = MD018;
        let violations = rule.check(&document).unwrap();

        assert_eq!(violations.len(), 2);
        assert_eq!(violations[0].line, 1);
        assert_eq!(violations[1].line, 2);
    }

    #[test]
    fn test_md018_setext_headings_ignored() {
        let content = "Setext Heading\n==============\n\nAnother Setext\n--------------";
        let document = create_test_document(content);
        let rule = MD018;
        let violations = rule.check(&document).unwrap();

        // Setext headings should not trigger this rule
        assert_eq!(violations.len(), 0);
    }

    #[test]
    fn test_md018_mixed_valid_invalid() {
        let content = "# Valid heading\n#Invalid heading\n## Another valid\n###Invalid again";
        let document = create_test_document(content);
        let rule = MD018;
        let violations = rule.check(&document).unwrap();

        assert_eq!(violations.len(), 2);
        assert_eq!(violations[0].line, 2);
        assert_eq!(violations[1].line, 4);
    }

    #[test]
    fn test_md018_code_blocks_ignored() {
        // Issue #274: Rust attributes like #[no_mangle] inside code blocks should NOT be flagged
        let content = r#"# Valid Heading

```rust
#[no_mangle]
pub extern "C" fn call_from_c() {
    println!("Just called a Rust function from C!");
}

#[unsafe(no_mangle)]
fn another_function() {}
```

## Another Valid Heading

~~~python
# This is a comment in Python
def foo():
    pass
~~~

#This should be flagged
"#;
        let document = create_test_document(content);
        let rule = MD018;
        let violations = rule.check(&document).unwrap();

        // Only the last line "#This should be flagged" should trigger a violation
        assert_eq!(violations.len(), 1);
        assert!(violations[0].message.contains("No space after hash"));
        assert_eq!(violations[0].line, 21);
    }

    #[test]
    fn test_md018_shebang_lines_ignored() {
        let content = "#!/bin/bash\n#This should trigger\n#!/usr/bin/env python3\n# This is valid";
        let document = create_test_document(content);
        let rule = MD018;
        let violations = rule.check(&document).unwrap();

        // Only the actual malformed heading should trigger, not shebangs
        assert_eq!(violations.len(), 1);
        assert_eq!(violations[0].line, 2);
        assert!(violations[0].message.contains("No space after hash"));
    }

    #[test]
    fn test_md018_fix_single_hash() {
        let content = "#Heading";
        let document = create_test_document(content);
        let rule = MD018;
        let violations = rule.check(&document).unwrap();

        assert_eq!(violations.len(), 1);
        assert!(violations[0].fix.is_some());
        let fix = violations[0].fix.as_ref().unwrap();
        assert_eq!(fix.replacement.as_ref().unwrap(), "# Heading\n");
        assert_eq!(fix.description, "Add space after hash on atx style heading");
    }

    #[test]
    fn test_md018_fix_multiple_hashes() {
        let content = "##Heading Two\n###Heading Three";
        let document = create_test_document(content);
        let rule = MD018;
        let violations = rule.check(&document).unwrap();

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

        // First heading
        let fix1 = violations[0].fix.as_ref().unwrap();
        assert_eq!(fix1.replacement.as_ref().unwrap(), "## Heading Two\n");

        // Second heading
        let fix2 = violations[1].fix.as_ref().unwrap();
        assert_eq!(fix2.replacement.as_ref().unwrap(), "### Heading Three\n");
    }

    #[test]
    fn test_md018_fix_preserves_indentation() {
        let content = "  ##Indented Heading";
        let document = create_test_document(content);
        let rule = MD018;
        let violations = rule.check(&document).unwrap();

        assert_eq!(violations.len(), 1);
        let fix = violations[0].fix.as_ref().unwrap();
        assert_eq!(fix.replacement.as_ref().unwrap(), "  ## Indented Heading\n");
    }

    #[test]
    fn test_md018_fix_closed_atx() {
        let content = "##Closed Heading##";
        let document = create_test_document(content);
        let rule = MD018;
        let violations = rule.check(&document).unwrap();

        assert_eq!(violations.len(), 1);
        let fix = violations[0].fix.as_ref().unwrap();
        assert_eq!(fix.replacement.as_ref().unwrap(), "## Closed Heading##\n");
    }

    #[test]
    fn test_md018_fix_position_accuracy() {
        let content = "###No Space";
        let document = create_test_document(content);
        let rule = MD018;
        let violations = rule.check(&document).unwrap();

        assert_eq!(violations.len(), 1);
        let fix = violations[0].fix.as_ref().unwrap();
        assert_eq!(fix.start.line, 1);
        assert_eq!(fix.start.column, 1);
        assert_eq!(fix.end.line, 1);
        assert_eq!(fix.end.column, content.len() + 1);
    }

    #[test]
    fn test_md018_fix_with_trailing_whitespace() {
        let content = "#Heading with spaces   ";
        let document = create_test_document(content);
        let rule = MD018;
        let violations = rule.check(&document).unwrap();

        assert_eq!(violations.len(), 1);
        let fix = violations[0].fix.as_ref().unwrap();
        assert_eq!(fix.replacement.as_ref().unwrap(), "# Heading with spaces\n");
    }

    #[test]
    fn test_md018_fix_all_heading_levels() {
        let content = "#H1\n##H2\n###H3\n####H4\n#####H5\n######H6";
        let document = create_test_document(content);
        let rule = MD018;
        let violations = rule.check(&document).unwrap();

        assert_eq!(violations.len(), 6);
        assert_eq!(
            violations[0]
                .fix
                .as_ref()
                .unwrap()
                .replacement
                .as_ref()
                .unwrap(),
            "# H1\n"
        );
        assert_eq!(
            violations[1]
                .fix
                .as_ref()
                .unwrap()
                .replacement
                .as_ref()
                .unwrap(),
            "## H2\n"
        );
        assert_eq!(
            violations[2]
                .fix
                .as_ref()
                .unwrap()
                .replacement
                .as_ref()
                .unwrap(),
            "### H3\n"
        );
        assert_eq!(
            violations[3]
                .fix
                .as_ref()
                .unwrap()
                .replacement
                .as_ref()
                .unwrap(),
            "#### H4\n"
        );
        assert_eq!(
            violations[4]
                .fix
                .as_ref()
                .unwrap()
                .replacement
                .as_ref()
                .unwrap(),
            "##### H5\n"
        );
        assert_eq!(
            violations[5]
                .fix
                .as_ref()
                .unwrap()
                .replacement
                .as_ref()
                .unwrap(),
            "###### H6\n"
        );
    }
}
