use rumdl_lib::lint_context::LintContext;
use rumdl_lib::rule::Rule;
use rumdl_lib::rules::MD014CommandsShowOutput;

#[test]
fn test_valid_command() {
    let rule = MD014CommandsShowOutput::new();
    let content = "```bash\n$ ls -l\nfile1 file2\n```";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();
    assert!(result.is_empty());
}

#[test]
fn test_invalid_command() {
    let rule = MD014CommandsShowOutput::new();
    let content = "```bash\n$ ls -l\n```";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();
    assert_eq!(result.len(), 1);
    assert_eq!(result[0].line, 2); // Command is on line 2, not line 1 (fence)
    assert_eq!(result[0].column, 1); // Highlights the $ character
    assert_eq!(result[0].end_line, 2);
    assert_eq!(result[0].end_column, 2); // End of $ character
}

#[test]
fn test_multiple_commands() {
    let rule = MD014CommandsShowOutput::new();
    let content = "```bash\n$ ls -l\nfile1 file2\n$ pwd\n/home\n```";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();
    assert!(result.is_empty());
}

#[test]
fn test_fix_command() {
    let rule = MD014CommandsShowOutput::new();
    let content = "```bash\n$ ls -l\n```";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.fix(&ctx).unwrap();
    assert_eq!(result, "```bash\nls -l\n```"); // No trailing newline in input, so none in output
}

#[test]
fn test_no_output_commands() {
    let rule = MD014CommandsShowOutput::new();
    let content = "```bash\n$ cd /home\n$ mkdir test\n$ touch file.txt\n```";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();
    assert!(result.is_empty()); // These commands don't require output
}

#[test]
fn test_mixed_commands() {
    let rule = MD014CommandsShowOutput::new();
    let content = "```bash\n$ cd /home\n$ ls -l\nfile1 file2\n$ touch test.txt\n```";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();
    assert!(result.is_empty());
}

#[test]
fn test_shell_prompt_variations() {
    let rule = MD014CommandsShowOutput::new();
    let content = "```console\n$ ls -l\nfile1 file2\n> pwd\n/home\n```";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();
    assert!(result.is_empty());
}

#[test]
fn test_non_shell_code_blocks() {
    let rule = MD014CommandsShowOutput::new();
    let content = "```python\n$ print('hello')\n```";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();
    assert!(result.is_empty()); // Should ignore non-shell code blocks
}

#[test]
fn test_shell_language_variations() {
    let rule = MD014CommandsShowOutput::new();
    let content = "```sh\n$ ls -l\nfile1 file2\n```\n```shell\n$ pwd\n/home\n```\n```console\n$ echo hello\nworld\n```";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();
    assert!(result.is_empty());
}

#[test]
fn test_disabled_output_check() {
    let rule = MD014CommandsShowOutput::with_show_output(false);
    let content = "```bash\n$ ls -l\n```";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();
    assert!(result.is_empty()); // Should not check for output when disabled
}

#[test]
fn test_comments_in_commands() {
    let rule = MD014CommandsShowOutput::new();
    let content = "```bash\n$ ls -l  # List files\nfile1 file2\n```";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();
    assert!(result.is_empty());
}

#[test]
fn test_indented_commands() {
    let rule = MD014CommandsShowOutput::new();
    let content = "```bash\n    $ ls -l\n    file1 file2\n```";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();
    assert!(result.is_empty());
}

#[test]
fn test_fix_multiple_commands() {
    let rule = MD014CommandsShowOutput::new();
    let content = "```bash\n$ ls -l\n$ pwd\n$ echo hello\n```";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.fix(&ctx).unwrap();
    assert_eq!(result, "```bash\nls -l\npwd\necho hello\n```"); // No trailing newline in input
}

#[test]
fn test_fix_indented_commands() {
    let rule = MD014CommandsShowOutput::new();
    let content = "```bash\n    $ ls -l\n    $ pwd\n```";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.fix(&ctx).unwrap();
    assert_eq!(result, "```bash\n    ls -l\n    pwd\n```"); // No trailing newline in input
}

#[test]
fn test_empty_lines_not_output() {
    let rule = MD014CommandsShowOutput::new();
    let content = "```bash\n$ echo test\n\n```";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();
    assert_eq!(result.len(), 1, "Empty lines should not count as output");
}

#[test]
fn test_greater_than_prompt() {
    let rule = MD014CommandsShowOutput::new();
    let content = "```bash\n> echo test\n```";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();
    assert_eq!(
        result.len(),
        1,
        "Commands with > prompt should be flagged without output"
    );
}

#[test]
fn test_case_insensitive_languages() {
    let rule = MD014CommandsShowOutput::new();
    let content1 = "```BASH\n$ echo test\n```";
    let ctx1 = LintContext::new(content1, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result1 = rule.check(&ctx1).unwrap();
    assert_eq!(result1.len(), 1, "BASH (uppercase) should be recognized");

    let content2 = "```Shell\n$ echo test\n```";
    let ctx2 = LintContext::new(content2, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result2 = rule.check(&ctx2).unwrap();
    assert_eq!(result2.len(), 1, "Shell (mixed case) should be recognized");

    let content3 = "```CONSOLE\n$ echo test\n```";
    let ctx3 = LintContext::new(content3, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result3 = rule.check(&ctx3).unwrap();
    assert_eq!(result3.len(), 1, "CONSOLE (uppercase) should be recognized");

    let content4 = "```Terminal\n$ echo test\n```";
    let ctx4 = LintContext::new(content4, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result4 = rule.check(&ctx4).unwrap();
    assert_eq!(result4.len(), 1, "Terminal (mixed case) should be recognized");
}

#[test]
fn test_message_includes_command() {
    let rule = MD014CommandsShowOutput::new();
    let content = "```bash\n$ git status\n```";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();
    assert_eq!(result.len(), 1);
    assert!(
        result[0].message.contains("git status"),
        "Error message should include the actual command"
    );
}

#[test]
fn test_no_output_commands_case_insensitive() {
    let rule = MD014CommandsShowOutput::new();
    let content = "```bash\n$ CD /home\n$ MKDIR test\n$ Touch file.txt\n$ RM file.txt\n$ MV old new\n$ CP src dst\n$ EXPORT VAR=val\n$ SET -e\n```";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();
    assert!(result.is_empty(), "No-output commands should work case-insensitively");
}

#[test]
fn test_commands_only_without_other_content() {
    let rule = MD014CommandsShowOutput::new();
    let content = "```bash\n$ pwd\n$ ls\n$ echo test\n```";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();
    assert_eq!(
        result.len(),
        3,
        "Each command without output should produce a separate warning"
    );
}

#[test]
fn test_fix_preserves_trailing_newline() {
    let rule = MD014CommandsShowOutput::new();
    let content_with_newline = "```bash\n$ echo test\n```\n";
    let ctx1 = LintContext::new(content_with_newline, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let fixed1 = rule.fix(&ctx1).unwrap();
    assert!(fixed1.ends_with('\n'), "Should preserve trailing newline");

    let content_without_newline = "```bash\n$ echo test\n```";
    let ctx2 = LintContext::new(
        content_without_newline,
        rumdl_lib::config::MarkdownFlavor::Standard,
        None,
    );
    let fixed2 = rule.fix(&ctx2).unwrap();
    assert!(!fixed2.ends_with('\n'), "Should not add trailing newline");
}

#[test]
fn test_complex_commands() {
    let rule = MD014CommandsShowOutput::new();
    let content = "```bash\n$ echo test | grep test\ntest\n$ find . -name '*.rs' | head -5\n```";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();
    assert!(result.is_empty(), "Block has output so should not be flagged");
}

#[test]
fn test_only_commands_no_output() {
    let rule = MD014CommandsShowOutput::new();
    let content = "```bash\n$ ls\n$ pwd\n```";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();
    assert_eq!(
        result.len(),
        2,
        "Each command without output should produce a separate warning"
    );
}

#[test]
fn test_config_from_toml() {
    let mut config = rumdl_lib::config::Config::default();
    let mut rule_config = rumdl_lib::config::RuleConfig::default();
    rule_config
        .values
        .insert("show-output".to_string(), toml::Value::Boolean(false)); // kebab-case
    config.rules.insert("MD014".to_string(), rule_config);

    let rule = MD014CommandsShowOutput::from_config(&config);
    let content = "```bash\n$ echo test\n```";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();
    assert!(result.is_empty(), "Rule should respect show_output=false from config");
}

#[test]
fn test_fix_range_calculation() {
    let rule = MD014CommandsShowOutput::new();
    let content = "# Header\n\n```bash\n$ echo test\n```\n\nMore content";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();
    assert_eq!(result.len(), 1);

    // Verify the fix range is correct
    if let Some(_fix) = &result[0].fix {
        let fixed_content = rule.fix(&ctx).unwrap();
        assert!(
            fixed_content.contains("echo test\n```"),
            "Fix should produce correct output"
        );
    }
}

#[test]
fn test_multiple_blocks_in_document() {
    let rule = MD014CommandsShowOutput::new();
    let content = r#"# Doc

```bash
$ echo "has output"
has output
```

Some text

```bash
$ echo "no output"
```

More text

```python
print("not shell")
```

```console
$ pwd
/home
```
"#;
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();
    assert_eq!(result.len(), 1, "Only the bash block without output should be flagged");
    assert_eq!(result[0].line, 11, "Should flag line 11 (the command without output)");
}

#[test]
fn test_comments_not_treated_as_output() {
    let rule = MD014CommandsShowOutput::new();
    let content = "```bash\n$ ls -la\n# This is a comment\n# Another comment\n```";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();
    assert_eq!(result.len(), 1, "Comments starting with # should not count as output");
}

#[test]
fn test_whitespace_only_lines_not_output() {
    let rule = MD014CommandsShowOutput::new();
    let content = "```bash\n$ echo test\n   \n\t\n```";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();
    assert_eq!(result.len(), 1, "Whitespace-only lines should not count as output");
}

#[test]
fn test_language_with_attributes() {
    let rule = MD014CommandsShowOutput::new();
    let content = "```bash {.line-numbers}\n$ echo test\n```";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();
    assert_eq!(result.len(), 1, "Should still recognize bash even with attributes");
}

#[test]
fn test_default_config() {
    let rule = MD014CommandsShowOutput::new();
    // By default, show_output should be true
    let content = "```bash\n$ echo test\n```";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();
    assert_eq!(result.len(), 1, "Default config should have show_output=true");
}

#[test]
fn test_multiple_violations_have_correct_lines() {
    let rule = MD014CommandsShowOutput::new();
    // Exact case from issue #519: two commands without output
    let content = "\
---
title: Heading
---

Here is a fenced code block:

```shell
# First invocation of my_command
$ my_command

# Second invocation of my_command
$ my_command
```
";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();
    assert_eq!(result.len(), 2, "Both commands should be flagged, got: {result:?}");
    assert_eq!(result[0].line, 9, "First violation on line 9");
    assert_eq!(result[1].line, 12, "Second violation on line 12");
}

#[test]
fn test_multiple_violations_fix_produces_valid_output() {
    use rumdl_lib::utils::fix_utils::apply_warning_fixes;

    let rule = MD014CommandsShowOutput::new();
    let content = "```bash\n$ echo one\n$ echo two\n$ echo three\n```";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let warnings = rule.check(&ctx).unwrap();
    assert_eq!(warnings.len(), 3, "All three commands should be flagged");

    // Applying all fixes should produce valid output (deduplication handles identical fixes)
    let fixed = apply_warning_fixes(content, &warnings).unwrap();
    assert_eq!(fixed, "```bash\necho one\necho two\necho three\n```");

    // Verify no warnings remain after fix
    let ctx2 = LintContext::new(&fixed, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result2 = rule.check(&ctx2).unwrap();
    assert!(
        result2.is_empty(),
        "No warnings should remain after fix, got: {result2:?}"
    );
}

/// Roundtrip safety: fix() output must match apply_warning_fixes(check()) output
fn assert_fix_roundtrip(content: &str) {
    use rumdl_lib::utils::fix_utils::apply_warning_fixes;

    let rule = MD014CommandsShowOutput::new();
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);

    let fix_output = rule.fix(&ctx).unwrap();
    let warnings = rule.check(&ctx).unwrap();

    if warnings.is_empty() {
        assert_eq!(fix_output, content, "fix() should be identity when no warnings");
        return;
    }

    let check_fix_output = apply_warning_fixes(content, &warnings).unwrap();
    assert_eq!(
        fix_output, check_fix_output,
        "fix() and apply_warning_fixes(check()) must produce identical output"
    );

    // Verify idempotence: fixing the fixed output should produce no warnings
    let ctx2 = LintContext::new(&fix_output, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let warnings2 = rule.check(&ctx2).unwrap();
    assert!(
        warnings2.is_empty(),
        "fix output should have no warnings, got: {warnings2:?}"
    );
}

#[test]
fn test_roundtrip_single_command() {
    assert_fix_roundtrip("```bash\n$ echo test\n```");
}

#[test]
fn test_roundtrip_multiple_commands() {
    assert_fix_roundtrip("```bash\n$ echo one\n$ echo two\n$ echo three\n```");
}

#[test]
fn test_roundtrip_mixed_silent_and_output_commands() {
    assert_fix_roundtrip("```bash\n$ cd /home\n$ ls -la\n```");
}

#[test]
fn test_roundtrip_with_surrounding_content() {
    assert_fix_roundtrip("# Header\n\nSome text\n\n```bash\n$ echo test\n```\n\nMore text\n");
}

#[test]
fn test_roundtrip_multiple_blocks() {
    assert_fix_roundtrip("```bash\n$ echo one\n```\n\nText\n\n```shell\n$ pwd\n```\n");
}

#[test]
fn test_roundtrip_no_warnings() {
    assert_fix_roundtrip("```bash\n$ echo test\ntest\n```");
}

#[test]
fn test_roundtrip_indented_commands() {
    assert_fix_roundtrip("```bash\n    $ echo test\n    $ pwd\n```");
}

#[test]
fn test_roundtrip_trailing_newline_preserved() {
    assert_fix_roundtrip("```bash\n$ echo test\n```\n");
}

#[test]
fn test_roundtrip_no_trailing_newline() {
    assert_fix_roundtrip("```bash\n$ echo test\n```");
}

#[test]
fn test_roundtrip_commands_with_comments() {
    assert_fix_roundtrip("```shell\n# First invocation\n$ my_command\n\n# Second invocation\n$ my_command\n```\n");
}
