//! MD047: Files should end with a single newline character
//!
//! This rule checks that files end with exactly one newline character.
//!
//! ## Why This Rule Exists
//!
//! POSIX defines a line as "a sequence of zero or more non-newline characters plus a terminating newline character".
//! Benefits of ending files with a newline:
//! - Many Unix tools expect this and may behave unexpectedly without it
//! - Git shows "No newline at end of file" warnings
//! - Consistent file endings improve diff readability
//! - Some editors automatically add newlines, causing unnecessary changes
//!
//! ## Examples
//!
//! ### ❌ Incorrect (violates rule)
//!
//! ```text
//! # Document
//!
//! Content here⟨EOF⟩
//! ```
//!
//! Or with multiple newlines:
//!
//! ```text
//! # Document
//!
//! Content here
//!
//!
//! ⟨EOF⟩
//! ```
//!
//! ### ✅ Correct
//!
//! ```text
//! # Document
//!
//! Content here
//! ⟨EOF⟩
//! ```
//!
//! (Where ⟨EOF⟩ represents end of file with exactly one newline before it)
//!
//! ## Configuration
//!
//! This rule has no configuration options. Files must end with exactly one newline.
//!
//! ## Automatic Fix
//!
//! This rule supports automatic fixing. The fix will:
//! - Add a newline if the file doesn't end with one
//! - Remove extra newlines if there are multiple at the end
//! - Ensure exactly one newline at the end of the file
//!
//! ## When to Disable
//!
//! Consider disabling this rule if:
//! - You're working with generated files that don't follow this convention
//! - Your project specifically requires no trailing newline
//! - You're documenting file formats that shouldn't have trailing newlines

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 that files end with a single newline
pub struct MD047;

impl MD047 {
    /// Check the ending of the file content
    fn check_file_ending(&self, content: &str) -> Option<String> {
        if content.is_empty() {
            return Some("File is missing a trailing newline".to_string());
        }

        let ends_with_newline = content.ends_with('\n');

        if !ends_with_newline {
            Some("File is missing a trailing newline".to_string())
        } else {
            // Count trailing newlines
            let trailing_newlines = content.chars().rev().take_while(|&c| c == '\n').count();

            if trailing_newlines > 1 {
                Some(format!(
                    "File has {} trailing newlines, expected 1",
                    trailing_newlines
                ))
            } else {
                None
            }
        }
    }
}

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

    fn name(&self) -> &'static str {
        "single-trailing-newline"
    }

    fn description(&self) -> &'static str {
        "Files should end with a single newline character"
    }

    fn metadata(&self) -> RuleMetadata {
        RuleMetadata::stable(RuleCategory::Formatting).introduced_in("mdbook-lint v0.1.0")
    }

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

        if let Some(message) = self.check_file_ending(&document.content) {
            let line_count = document.lines.len();
            let line_number = if line_count == 0 { 1 } else { line_count };

            // Create fix based on the specific issue
            let fix = if document.content.is_empty() {
                // Empty file: add a single newline
                Fix {
                    description: "Add newline at end of file".to_string(),
                    replacement: Some("\n".to_string()),
                    start: Position { line: 1, column: 1 },
                    end: Position { line: 1, column: 1 },
                }
            } else if !document.content.ends_with('\n') {
                // No trailing newline: add one
                let last_line_len = document.lines.last().map(|l| l.len()).unwrap_or(0) + 1;
                Fix {
                    description: "Add newline at end of file".to_string(),
                    replacement: Some("\n".to_string()),
                    start: Position {
                        line: line_number,
                        column: last_line_len,
                    },
                    end: Position {
                        line: line_number,
                        column: last_line_len,
                    },
                }
            } else {
                // Multiple trailing newlines: remove extras
                let trailing_newlines = document
                    .content
                    .chars()
                    .rev()
                    .take_while(|&c| c == '\n')
                    .count();
                let start_line = line_count - trailing_newlines + 2;
                Fix {
                    description: "Remove extra trailing newlines".to_string(),
                    replacement: Some("\n".to_string()),
                    start: Position {
                        line: start_line,
                        column: 1,
                    },
                    end: Position {
                        line: line_count + 1,
                        column: 1,
                    },
                }
            };

            violations.push(self.create_violation_with_fix(
                message,
                line_number,
                1,
                Severity::Warning,
                fix,
            ));
        }

        Ok(violations)
    }

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

#[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_md047_single_newline_valid() {
        let content = "# Heading\n\nSome content here.\n";
        let document = create_test_document(content);
        let rule = MD047;
        let violations = rule.check(&document).unwrap();

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

    #[test]
    fn test_md047_no_newline_invalid() {
        let content = "# Heading\n\nSome content here.";
        let document = create_test_document(content);
        let rule = MD047;
        let violations = rule.check(&document).unwrap();

        assert_eq!(violations.len(), 1);
        assert_eq!(violations[0].rule_id, "MD047");
        assert_eq!(violations[0].message, "File is missing a trailing newline");

        // Check fix is present
        assert!(violations[0].fix.is_some());
        let fix = violations[0].fix.as_ref().unwrap();
        assert_eq!(fix.description, "Add newline at end of file");
        assert_eq!(fix.replacement, Some("\n".to_string()));
    }

    #[test]
    fn test_md047_multiple_newlines_invalid() {
        let content = "# Heading\n\nSome content here.\n\n";
        let document = create_test_document(content);
        let rule = MD047;
        let violations = rule.check(&document).unwrap();

        assert_eq!(violations.len(), 1);
        assert_eq!(violations[0].rule_id, "MD047");
        assert_eq!(
            violations[0].message,
            "File has 2 trailing newlines, expected 1"
        );
    }

    #[test]
    fn test_md047_three_newlines_invalid() {
        let content = "# Heading\n\nSome content here.\n\n\n";
        let document = create_test_document(content);
        let rule = MD047;
        let violations = rule.check(&document).unwrap();

        assert_eq!(violations.len(), 1);
        assert_eq!(
            violations[0].message,
            "File has 3 trailing newlines, expected 1"
        );
    }

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

        assert_eq!(violations.len(), 1);
        assert_eq!(violations[0].message, "File is missing a trailing newline");
    }

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

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

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

        assert_eq!(violations.len(), 1);
        assert_eq!(
            violations[0].message,
            "File has 2 trailing newlines, expected 1"
        );
    }

    #[test]
    fn test_md047_content_with_final_newline_valid() {
        let content = "Line 1\nLine 2\nLine 3\n";
        let document = create_test_document(content);
        let rule = MD047;
        let violations = rule.check(&document).unwrap();

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

    #[test]
    fn test_md047_content_without_final_newline_invalid() {
        let content = "Line 1\nLine 2\nLine 3";
        let document = create_test_document(content);
        let rule = MD047;
        let violations = rule.check(&document).unwrap();

        assert_eq!(violations.len(), 1);
        assert_eq!(violations[0].line, 3); // Should report on last line
    }

    #[test]
    fn test_md047_mixed_line_endings_with_newline_valid() {
        let content = "# Title\r\n\r\nContent here.\n";
        let document = create_test_document(content);
        let rule = MD047;
        let violations = rule.check(&document).unwrap();

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

    #[test]
    fn test_md047_single_line_with_newline_valid() {
        let content = "Single line\n";
        let document = create_test_document(content);
        let rule = MD047;
        let violations = rule.check(&document).unwrap();

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

    #[test]
    fn test_md047_single_line_without_newline_invalid() {
        let content = "Single line";
        let document = create_test_document(content);
        let rule = MD047;
        let violations = rule.check(&document).unwrap();

        assert_eq!(violations.len(), 1);
        assert_eq!(violations[0].line, 1);
        assert_eq!(violations[0].message, "File is missing a trailing newline");
    }

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

        assert_eq!(violations.len(), 1);
        assert_eq!(
            violations[0].message,
            "File has 5 trailing newlines, expected 1"
        );
    }

    #[test]
    fn test_md047_whitespace_before_newline() {
        // File ends with spaces then newline - this is valid for MD047
        let content = "Content here   \n";
        let document = create_test_document(content);
        let rule = MD047;
        let violations = rule.check(&document).unwrap();

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

    #[test]
    fn test_md047_tabs_and_newlines() {
        // File ends with tab then multiple newlines
        let content = "Content\t\n\n";
        let document = create_test_document(content);
        let rule = MD047;
        let violations = rule.check(&document).unwrap();

        assert_eq!(violations.len(), 1);
        assert_eq!(
            violations[0].message,
            "File has 2 trailing newlines, expected 1"
        );
    }

    #[test]
    fn test_md047_fix_missing_newline() {
        let content = "# Heading\n\nContent without newline";
        let document = create_test_document(content);
        let rule = MD047;
        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.description, "Add newline at end of file");
        assert_eq!(fix.replacement, Some("\n".to_string()));
        assert_eq!(fix.start.line, 3);
        assert_eq!(fix.start.column, 24); // After "newline"
    }

    #[test]
    fn test_md047_fix_multiple_newlines() {
        let content = "# Heading\n\nContent\n\n\n";
        let document = create_test_document(content);
        let rule = MD047;
        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.description, "Remove extra trailing newlines");
        // The fix replaces the extra newlines with a single newline
        assert_eq!(fix.replacement, Some("\n".to_string()));
        assert_eq!(fix.start.line, 4);
        assert_eq!(fix.start.column, 1);
    }

    #[test]
    fn test_md047_fix_many_newlines() {
        let content = "Content\n\n\n\n\n";
        let document = create_test_document(content);
        let rule = MD047;
        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.description, "Remove extra trailing newlines");
        // The fix replaces extra newlines with a single newline
        assert_eq!(fix.replacement, Some("\n".to_string()));
        // Should be at the position after the first trailing newline
        assert_eq!(fix.start.line, 2);
        assert_eq!(fix.start.column, 1);
    }

    #[test]
    fn test_md047_fix_empty_file() {
        let content = "";
        let document = create_test_document(content);
        let rule = MD047;
        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.description, "Add newline at end of file");
        assert_eq!(fix.replacement, Some("\n".to_string()));
        assert_eq!(fix.start.line, 1);
        assert_eq!(fix.start.column, 1);
    }

    #[test]
    fn test_md047_fix_single_line_no_newline() {
        let content = "Single line";
        let document = create_test_document(content);
        let rule = MD047;
        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.description, "Add newline at end of file");
        assert_eq!(fix.replacement, Some("\n".to_string()));
        assert_eq!(fix.start.line, 1);
        assert_eq!(fix.start.column, 12); // After "Single line"
    }

    #[test]
    fn test_md047_fix_preserves_content() {
        let content = "# Document\n\n- Item 1\n- Item 2";
        let document = create_test_document(content);
        let rule = MD047;
        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.description, "Add newline at end of file");
        assert_eq!(fix.replacement, Some("\n".to_string()));
        // Should add after last character
        assert_eq!(fix.start.line, 4);
    }

    #[test]
    fn test_md047_fix_with_trailing_spaces() {
        let content = "Content   ";
        let document = create_test_document(content);
        let rule = MD047;
        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.description, "Add newline at end of file");
        assert_eq!(fix.replacement, Some("\n".to_string()));
        assert_eq!(fix.start.line, 1);
        assert_eq!(fix.start.column, 11); // After all content including spaces
    }

    #[test]
    fn test_md047_fix_exactly_two_newlines() {
        let content = "Content\n\n";
        let document = create_test_document(content);
        let rule = MD047;
        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.description, "Remove extra trailing newlines");
        // The fix replaces extra newlines with a single newline
        assert_eq!(fix.replacement, Some("\n".to_string()));
        assert_eq!(fix.start.line, 2);
        assert_eq!(fix.start.column, 1);
    }

    #[test]
    fn test_md047_fix_complex_document() {
        let content = "# Title\n\n## Section\n\nParagraph\n\n- List item\n\n> Quote";
        let document = create_test_document(content);
        let rule = MD047;
        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.description, "Add newline at end of file");
        assert_eq!(fix.replacement, Some("\n".to_string()));
        assert_eq!(fix.start.line, 9);
    }
}
