//! CLI integration tests for wasot
//!
//! Tests are organized to match reports/cli_tests.md
//!
//! To test a specific binary instead of cargo build:
//!   WASOT_TEST_BINARY=/path/to/wasot cargo test --test cli_tests

use assert_cmd::Command;
use predicates::prelude::*;
use std::fs;
use tempfile::TempDir;

/// Get a Command for the wasot binary.
/// Uses WASOT_TEST_BINARY env var if set, otherwise uses cargo_bin.
fn wasot() -> Command {
    if let Ok(binary_path) = std::env::var("WASOT_TEST_BINARY") {
        Command::new(binary_path)
    } else {
        #[allow(deprecated)]
        Command::cargo_bin("wasot").unwrap()
    }
}

/// Create a temp directory with test files
fn setup_test_dir() -> TempDir {
    let dir = TempDir::new().unwrap();

    // Create a valid wasot file
    let valid_wasot = r#".document
Hello world

.paragraph
This is a test paragraph.

..nested
Nested content here.

.footer
End of document.
"#;
    fs::write(dir.path().join("valid.wasot"), valid_wasot).unwrap();

    // Create a simple wasot file
    let simple = ".block\ntest content\n";
    fs::write(dir.path().join("simple.wasot"), simple).unwrap();

    // Create an empty file
    fs::write(dir.path().join("empty.wasot"), "").unwrap();

    // Create a whitespace-only file
    fs::write(dir.path().join("whitespace.wasot"), "   \n\t\n  ").unwrap();

    // Create a second valid file for multi-file tests
    fs::write(dir.path().join("second.wasot"), ".section\nAnother file\n").unwrap();

    dir
}

// =============================================================================
// 1. Basic Operations
// =============================================================================

mod basic_operations {
    use super::*;

    #[test]
    fn normalize_file_to_stdout() {
        // wasot file.wasot — normalize wasot file to stdout
        let dir = setup_test_dir();
        wasot()
            .arg(dir.path().join("valid.wasot"))
            .assert()
            .success()
            .stdout(predicate::str::contains(".document"));
    }

    #[test]
    fn normalize_file_to_output() {
        // wasot file.wasot -o out.wasot — normalize to file
        let dir = setup_test_dir();
        let output = dir.path().join("out.wasot");

        wasot()
            .arg(dir.path().join("valid.wasot"))
            .arg("-o")
            .arg(&output)
            .assert()
            .success();

        assert!(output.exists());
        let content = fs::read_to_string(&output).unwrap();
        assert!(content.contains(".document"));
    }

    #[test]
    fn normalize_stdin_to_stdout() {
        // wasot — read from stdin, normalize to stdout
        wasot()
            .write_stdin(".block\ntest\n")
            .assert()
            .success()
            .stdout(predicate::str::contains(".block"));
    }

    #[test]
    fn convert_to_xml() {
        // wasot file.wasot -t xml — convert to XML
        let dir = setup_test_dir();
        wasot()
            .arg(dir.path().join("simple.wasot"))
            .arg("-t")
            .arg("xml")
            .assert()
            .success()
            .stdout(predicate::str::contains("<block"));
    }

    #[test]
    fn convert_to_jsonl() {
        // wasot file.wasot -t jsonl — convert to JSON Lines
        let dir = setup_test_dir();
        wasot()
            .arg(dir.path().join("simple.wasot"))
            .arg("-t")
            .arg("jsonl")
            .assert()
            .success()
            .stdout(predicate::str::contains("block_type"));
    }

    #[test]
    fn convert_to_binary() {
        // wasot file.wasot -t binary — convert to binary protobuf
        let dir = setup_test_dir();
        let output = dir.path().join("out.wasotb");

        wasot()
            .arg(dir.path().join("simple.wasot"))
            .arg("-t")
            .arg("binary")
            .arg("-o")
            .arg(&output)
            .assert()
            .success();

        assert!(output.exists());
        let content = fs::read(&output).unwrap();
        assert!(!content.is_empty());
    }

    #[test]
    fn explicit_wasot_format() {
        // wasot file.wasot -t wasot — explicit normalize
        let dir = setup_test_dir();
        wasot()
            .arg(dir.path().join("simple.wasot"))
            .arg("-t")
            .arg("wasot")
            .assert()
            .success()
            .stdout(predicate::str::contains(".block"));
    }

    #[test]
    fn output_format_from_xml_extension() {
        // wasot file.wasot -t xml — XML output to stdout (note: -o not supported for XML yet)
        let dir = setup_test_dir();

        wasot()
            .arg(dir.path().join("simple.wasot"))
            .arg("-t")
            .arg("xml")
            .assert()
            .success()
            .stdout(predicate::str::contains("<"));
    }

    #[test]
    fn output_format_from_jsonl_extension() {
        // wasot file.wasot -o out.jsonl — format inferred from extension
        let dir = setup_test_dir();
        let output = dir.path().join("out.jsonl");

        wasot()
            .arg(dir.path().join("simple.wasot"))
            .arg("-o")
            .arg(&output)
            .assert()
            .success();

        let content = fs::read_to_string(&output).unwrap();
        assert!(content.contains("block_type"));
    }

    #[test]
    fn output_format_from_wasotb_extension() {
        // wasot file.wasot -o out.wasotb — format inferred from extension
        let dir = setup_test_dir();
        let output = dir.path().join("out.wasotb");

        wasot()
            .arg(dir.path().join("simple.wasot"))
            .arg("-o")
            .arg(&output)
            .assert()
            .success();

        let content = fs::read(&output).unwrap();
        assert!(!content.is_empty());
    }

    #[test]
    fn explicit_format_overrides_extension() {
        // wasot file.wasot -t jsonl -o out.dat — explicit format overrides extension
        // Note: Using jsonl instead of xml since xml doesn't support -o yet
        let dir = setup_test_dir();
        let output = dir.path().join("out.dat");

        wasot()
            .arg(dir.path().join("simple.wasot"))
            .arg("-t")
            .arg("jsonl")
            .arg("-o")
            .arg(&output)
            .assert()
            .success();

        let content = fs::read_to_string(&output).unwrap();
        assert!(content.contains("block_type"));
    }

    #[test]
    fn explicit_input_format() {
        // wasot -f wasot file.wasot — explicit wasot input
        let dir = setup_test_dir();
        wasot()
            .arg("-f")
            .arg("wasot")
            .arg(dir.path().join("simple.wasot"))
            .assert()
            .success()
            .stdout(predicate::str::contains(".block"));
    }
}

// =============================================================================
// 2. Stdin/Stdout
// =============================================================================

mod stdin_stdout {
    use super::*;

    #[test]
    fn pipe_input() {
        // cat file.wasot | wasot — pipe input
        wasot()
            .write_stdin(".document\nHello\n")
            .assert()
            .success()
            .stdout(predicate::str::contains(".document"));
    }

    #[test]
    fn inline_input() {
        // echo ".block" | wasot — inline input
        wasot()
            .write_stdin(".block\n")
            .assert()
            .success()
            .stdout(predicate::str::contains(".block"));
    }

    #[test]
    fn stdin_to_xml() {
        // cat file.wasot | wasot -t xml — full pipeline
        wasot()
            .write_stdin(".block\ntest\n")
            .arg("-t")
            .arg("xml")
            .assert()
            .success()
            .stdout(predicate::str::contains("<block"));
    }

    #[test]
    fn stdin_to_jsonl() {
        // stdin to jsonl
        wasot()
            .write_stdin(".block\ntest\n")
            .arg("-t")
            .arg("jsonl")
            .assert()
            .success()
            .stdout(predicate::str::contains("block_type"));
    }
}

// =============================================================================
// 3. Format Conversion Matrix
// =============================================================================

mod format_conversion {
    use super::*;

    #[test]
    fn wasot_to_wasot() {
        // wasot file.wasot -t wasot — normalize
        let dir = setup_test_dir();
        wasot()
            .arg(dir.path().join("simple.wasot"))
            .arg("-t")
            .arg("wasot")
            .assert()
            .success()
            .stdout(predicate::str::contains(".block"));
    }

    #[test]
    fn wasot_to_xml() {
        let dir = setup_test_dir();
        wasot()
            .arg(dir.path().join("simple.wasot"))
            .arg("-t")
            .arg("xml")
            .assert()
            .success()
            .stdout(predicate::str::contains("<block"));
    }

    #[test]
    fn wasot_to_jsonl() {
        let dir = setup_test_dir();
        wasot()
            .arg(dir.path().join("simple.wasot"))
            .arg("-t")
            .arg("jsonl")
            .assert()
            .success()
            .stdout(predicate::str::contains("block_type"));
    }

    #[test]
    fn wasot_to_binary_and_back() {
        // Roundtrip: wasot → binary → wasot
        let dir = setup_test_dir();
        let binary_file = dir.path().join("test.wasotb");

        // Convert to binary
        wasot()
            .arg(dir.path().join("simple.wasot"))
            .arg("-t")
            .arg("binary")
            .arg("-o")
            .arg(&binary_file)
            .assert()
            .success();

        // Convert back to wasot
        wasot()
            .arg(&binary_file)
            .assert()
            .success()
            .stdout(predicate::str::contains(".block"));
    }

    #[test]
    fn binary_to_jsonl() {
        // binary → jsonl
        let dir = setup_test_dir();
        let binary_file = dir.path().join("test.wasotb");

        // First create binary
        wasot()
            .arg(dir.path().join("simple.wasot"))
            .arg("-t")
            .arg("binary")
            .arg("-o")
            .arg(&binary_file)
            .assert()
            .success();

        // Then convert to jsonl
        wasot()
            .arg("-f")
            .arg("binary")
            .arg(&binary_file)
            .arg("-t")
            .arg("jsonl")
            .assert()
            .success()
            .stdout(predicate::str::contains("block_type"));
    }
}

// =============================================================================
// 4. Format Auto-Detection
// =============================================================================

mod auto_detection {
    use super::*;

    #[test]
    fn detect_wasot_from_extension() {
        let dir = setup_test_dir();
        wasot()
            .arg(dir.path().join("simple.wasot"))
            .assert()
            .success()
            .stdout(predicate::str::contains(".block"));
    }

    #[test]
    fn detect_binary_from_extension() {
        let dir = setup_test_dir();
        let binary_file = dir.path().join("test.wasotb");

        // Create binary file
        wasot()
            .arg(dir.path().join("simple.wasot"))
            .arg("-t")
            .arg("binary")
            .arg("-o")
            .arg(&binary_file)
            .assert()
            .success();

        // Auto-detect from .wasotb extension
        wasot()
            .arg(&binary_file)
            .assert()
            .success()
            .stdout(predicate::str::contains(".block"));
    }

    #[test]
    fn detect_wasot_content_dot() {
        // Content starting with . is wasot
        wasot()
            .write_stdin(".block\ntest\n")
            .assert()
            .success()
            .stdout(predicate::str::contains(".block"));
    }

    #[test]
    fn detect_wasot_content_hash() {
        // Content starting with # is wasot
        wasot().write_stdin("# heading\n").assert().success();
    }

    #[test]
    fn output_xml_via_explicit_flag() {
        // XML output is to stdout only (no -o support), so test with -t xml
        let dir = setup_test_dir();

        wasot()
            .arg(dir.path().join("simple.wasot"))
            .arg("-t")
            .arg("xml")
            .assert()
            .success()
            .stdout(predicate::str::contains("<block"));
    }

    #[test]
    fn output_jsonl_from_extension() {
        let dir = setup_test_dir();
        let output = dir.path().join("out.jsonl");

        wasot()
            .arg(dir.path().join("simple.wasot"))
            .arg("-o")
            .arg(&output)
            .assert()
            .success();

        let content = fs::read_to_string(&output).unwrap();
        assert!(content.contains("{"));
    }
}

// =============================================================================
// 5. Check Subcommand
// =============================================================================

mod check_subcommand {
    use super::*;

    #[test]
    fn check_single_valid_file() {
        let dir = setup_test_dir();
        wasot()
            .arg("tools")
            .arg("check")
            .arg(dir.path().join("valid.wasot"))
            .assert()
            .success();
    }

    #[test]
    fn check_multiple_files() {
        let dir = setup_test_dir();
        wasot()
            .arg("tools")
            .arg("check")
            .arg(dir.path().join("valid.wasot"))
            .arg(dir.path().join("second.wasot"))
            .assert()
            .success()
            .stderr(predicate::str::contains("2 files validated"));
    }

    #[test]
    fn check_from_stdin() {
        wasot()
            .arg("tools")
            .arg("check")
            .write_stdin(".block\ntest\n")
            .assert()
            .success();
    }

    #[test]
    fn check_missing_file_exit_code() {
        wasot()
            .arg("tools")
            .arg("check")
            .arg("nonexistent.wasot")
            .assert()
            .failure()
            .stderr(
                predicate::str::contains("not found")
                    .or(predicate::str::contains("file not found")),
            );
    }

    #[test]
    fn check_empty_file() {
        let dir = setup_test_dir();
        wasot()
            .arg("tools")
            .arg("check")
            .arg(dir.path().join("empty.wasot"))
            .assert()
            .success();
    }

    #[test]
    fn check_valid_exit_zero() {
        let dir = setup_test_dir();
        wasot()
            .arg("tools")
            .arg("check")
            .arg(dir.path().join("valid.wasot"))
            .assert()
            .code(0);
    }

    #[test]
    fn check_missing_exit_nonzero() {
        wasot()
            .arg("tools")
            .arg("check")
            .arg("missing.wasot")
            .assert()
            .code(predicate::ne(0));
    }
}

// =============================================================================
// 6. Info Subcommand
// =============================================================================

mod info_subcommand {
    use super::*;

    #[test]
    fn info_file() {
        let dir = setup_test_dir();
        wasot()
            .arg("tools")
            .arg("info")
            .arg(dir.path().join("valid.wasot"))
            .assert()
            .success()
            .stdout(predicate::str::contains("blocks:"))
            .stdout(predicate::str::contains("depth:"))
            .stdout(predicate::str::contains("lines:"))
            .stdout(predicate::str::contains("bytes:"));
    }

    #[test]
    fn info_from_stdin() {
        wasot()
            .arg("tools")
            .arg("info")
            .write_stdin(".block\ntest\n.another\nmore\n")
            .assert()
            .success()
            .stdout(predicate::str::contains("blocks: 2"));
    }

    #[test]
    fn info_shows_block_types() {
        let dir = setup_test_dir();
        wasot()
            .arg("tools")
            .arg("info")
            .arg(dir.path().join("valid.wasot"))
            .assert()
            .success()
            .stdout(predicate::str::contains("document:"));
    }

    #[test]
    fn info_missing_file() {
        wasot()
            .arg("tools")
            .arg("info")
            .arg("nonexistent.wasot")
            .assert()
            .failure()
            .stderr(
                predicate::str::contains("not found")
                    .or(predicate::str::contains("File not found")),
            );
    }
}

// =============================================================================
// 7. Selector (--select)
// =============================================================================

mod selector {
    use super::*;

    #[test]
    fn select_by_type() {
        let dir = setup_test_dir();
        wasot()
            .arg("--select=document")
            .arg(dir.path().join("valid.wasot"))
            .assert()
            .success()
            .stdout(predicate::str::contains(".document"));
    }

    #[test]
    fn select_paragraph() {
        let dir = setup_test_dir();
        wasot()
            .arg("--select=paragraph")
            .arg(dir.path().join("valid.wasot"))
            .assert()
            .success()
            .stdout(predicate::str::contains(".paragraph"));
    }

    #[test]
    fn select_footer() {
        let dir = setup_test_dir();
        wasot()
            .arg("--select=footer")
            .arg(dir.path().join("valid.wasot"))
            .assert()
            .success()
            .stdout(predicate::str::contains(".footer"));
    }

    #[test]
    fn select_to_jsonl() {
        let dir = setup_test_dir();
        wasot()
            .arg("--select=document")
            .arg(dir.path().join("valid.wasot"))
            .arg("-t")
            .arg("jsonl")
            .assert()
            .success()
            .stdout(predicate::str::contains("document"));
    }

    #[test]
    fn select_to_xml() {
        let dir = setup_test_dir();
        wasot()
            .arg("--select=document")
            .arg(dir.path().join("valid.wasot"))
            .arg("-t")
            .arg("xml")
            .assert()
            .success()
            .stdout(predicate::str::contains("<block"));
    }

    #[test]
    fn select_to_binary() {
        let dir = setup_test_dir();
        let output = dir.path().join("selected.wasotb");

        wasot()
            .arg("--select=document")
            .arg(dir.path().join("valid.wasot"))
            .arg("-t")
            .arg("binary")
            .arg("-o")
            .arg(&output)
            .assert()
            .success();

        assert!(output.exists());
    }

    #[test]
    fn select_with_output_file() {
        let dir = setup_test_dir();
        let output = dir.path().join("selected.wasot");

        wasot()
            .arg("--select=footer")
            .arg(dir.path().join("valid.wasot"))
            .arg("-o")
            .arg(&output)
            .assert()
            .success();

        let content = fs::read_to_string(&output).unwrap();
        assert!(content.contains(".footer"));
    }

    #[test]
    fn select_from_stdin() {
        wasot()
            .arg("--select=block")
            .write_stdin(".block\ntest\n.other\ndata\n")
            .assert()
            .success()
            .stdout(predicate::str::contains(".block"));
    }
}

// =============================================================================
// 8. Exit Codes
// =============================================================================

mod exit_codes {
    use super::*;

    #[test]
    fn success_exit_zero() {
        let dir = setup_test_dir();
        wasot()
            .arg(dir.path().join("simple.wasot"))
            .assert()
            .code(0);
    }

    #[test]
    fn check_valid_exit_zero() {
        let dir = setup_test_dir();
        wasot()
            .arg("tools")
            .arg("check")
            .arg(dir.path().join("valid.wasot"))
            .assert()
            .code(0);
    }

    #[test]
    fn info_success_exit_zero() {
        let dir = setup_test_dir();
        wasot()
            .arg("tools")
            .arg("info")
            .arg(dir.path().join("valid.wasot"))
            .assert()
            .code(0);
    }

    #[test]
    fn missing_file_exit_two() {
        wasot().arg("missing_file.wasot").assert().code(2);
    }

    #[test]
    fn check_missing_file_nonzero() {
        wasot()
            .arg("tools")
            .arg("check")
            .arg("missing.wasot")
            .assert()
            .code(predicate::ne(0));
    }

    #[test]
    fn unknown_format_exit_three() {
        let dir = setup_test_dir();
        wasot()
            .arg(dir.path().join("simple.wasot"))
            .arg("-t")
            .arg("invalid_format")
            .assert()
            .failure();
    }
}

// =============================================================================
// 9. Edge Cases
// =============================================================================

mod edge_cases {
    use super::*;

    #[test]
    fn empty_file() {
        let dir = setup_test_dir();
        wasot()
            .arg(dir.path().join("empty.wasot"))
            .assert()
            .success();
    }

    #[test]
    fn whitespace_only_file() {
        let dir = setup_test_dir();
        wasot()
            .arg(dir.path().join("whitespace.wasot"))
            .assert()
            .success();
    }

    #[test]
    fn empty_stdin() {
        wasot().write_stdin("").assert().success();
    }

    #[test]
    fn whitespace_only_stdin() {
        wasot().write_stdin("   \n\t\n  ").assert().success();
    }

    #[test]
    fn check_empty_file() {
        let dir = setup_test_dir();
        wasot()
            .arg("tools")
            .arg("check")
            .arg(dir.path().join("empty.wasot"))
            .assert()
            .success();
    }

    #[test]
    fn info_empty_file() {
        let dir = setup_test_dir();
        wasot()
            .arg("tools")
            .arg("info")
            .arg(dir.path().join("empty.wasot"))
            .assert()
            .success()
            .stdout(predicate::str::contains("blocks: 0"));
    }

    #[test]
    fn relative_path() {
        let dir = setup_test_dir();
        wasot()
            .current_dir(dir.path())
            .arg("simple.wasot")
            .assert()
            .success();
    }

    #[test]
    fn file_with_many_blocks() {
        let dir = setup_test_dir();
        let mut content = String::new();
        for i in 0..100 {
            content.push_str(&format!(".block{}\ncontent {}\n", i, i));
        }
        fs::write(dir.path().join("many.wasot"), &content).unwrap();

        wasot()
            .arg(dir.path().join("many.wasot"))
            .assert()
            .success();
    }

    #[test]
    fn deeply_nested_blocks() {
        let content = ".level1\n..level2\n...level3\n....level4\n.....level5\ndeep content\n";
        let dir = setup_test_dir();
        fs::write(dir.path().join("nested.wasot"), content).unwrap();

        wasot()
            .arg(dir.path().join("nested.wasot"))
            .assert()
            .success()
            .stdout(predicate::str::contains(".....level5"));
    }
}

// =============================================================================
// 10. Help and Version
// =============================================================================

mod help_version {
    use super::*;

    #[test]
    fn help_flag() {
        wasot()
            .arg("--help")
            .assert()
            .success()
            .stdout(predicate::str::contains("Usage:"))
            .stdout(predicate::str::contains("Commands:"));
    }

    #[test]
    fn short_help_flag() {
        wasot()
            .arg("-h")
            .assert()
            .success()
            .stdout(predicate::str::contains("Usage:"));
    }

    #[test]
    fn version_flag() {
        wasot()
            .arg("--version")
            .assert()
            .success()
            .stdout(predicate::str::contains("wasot"));
    }

    #[test]
    fn short_version_flag() {
        wasot().arg("-V").assert().success();
    }

    #[test]
    fn check_help() {
        wasot()
            .arg("tools")
            .arg("check")
            .arg("--help")
            .assert()
            .success()
            .stdout(predicate::str::contains("Validate"));
    }

    #[test]
    fn info_help() {
        wasot()
            .arg("tools")
            .arg("info")
            .arg("--help")
            .assert()
            .success()
            .stdout(predicate::str::contains("statistics"));
    }
}

// =============================================================================
// 11. Completions
// =============================================================================

mod completions {
    use super::*;

    #[test]
    fn bash_completions() {
        wasot()
            .arg("tools")
            .arg("completions")
            .arg("bash")
            .assert()
            .success()
            .stdout(predicate::str::contains("_wasot"));
    }

    #[test]
    fn zsh_completions() {
        wasot()
            .arg("tools")
            .arg("completions")
            .arg("zsh")
            .assert()
            .success()
            .stdout(predicate::str::contains("wasot"));
    }

    #[test]
    fn fish_completions() {
        wasot()
            .arg("tools")
            .arg("completions")
            .arg("fish")
            .assert()
            .success()
            .stdout(predicate::str::contains("wasot"));
    }
}

// =============================================================================
// 11b. Pretty Command
// =============================================================================

mod pretty {
    use super::*;

    #[test]
    fn pretty_help() {
        wasot()
            .arg("tools")
            .arg("pretty")
            .arg("--help")
            .assert()
            .success()
            .stdout(predicate::str::contains("Pretty-print wasot files"))
            .stdout(predicate::str::contains("--yes"))
            .stdout(predicate::str::contains("--stdout"));
    }

    #[test]
    fn pretty_single_file_to_stdout() {
        let dir = setup_test_dir();
        wasot()
            .arg("tools")
            .arg("pretty")
            .arg(dir.path().join("simple.wasot"))
            .arg("--stdout")
            .assert()
            .success()
            .stdout(predicate::str::contains("./id ")) // Should have auto-generated ID
            .stdout(predicate::str::contains(".block"));
    }

    #[test]
    fn pretty_directory_to_stdout() {
        let dir = setup_test_dir();
        wasot()
            .arg("tools")
            .arg("pretty")
            .arg(dir.path())
            .arg("--stdout")
            .assert()
            .success()
            .stdout(predicate::str::contains(".block")); // Content from simple.wasot
    }

    #[test]
    fn pretty_with_yes_flag() {
        let dir = setup_test_dir();
        // Create a second file so confirmation would be triggered
        fs::write(dir.path().join("another.wasot"), ".block2\ncontent2\n").unwrap();

        wasot()
            .arg("tools")
            .arg("pretty")
            .arg(dir.path())
            .arg("--yes")
            .assert()
            .success();

        // Verify files were modified (should have ./id added)
        let content = fs::read_to_string(dir.path().join("simple.wasot")).unwrap();
        assert!(
            content.starts_with("./id "),
            "Should have auto-generated ID"
        );
    }

    #[test]
    fn pretty_modifies_file_in_place() {
        let dir = setup_test_dir();
        let file_path = dir.path().join("single.wasot");
        fs::write(&file_path, ".block\noriginal content\n").unwrap();

        // Single file doesn't need confirmation
        wasot()
            .arg("tools")
            .arg("pretty")
            .arg(&file_path)
            .assert()
            .success();

        let content = fs::read_to_string(&file_path).unwrap();
        assert!(
            content.starts_with("./id "),
            "Should have auto-generated ID"
        );
        assert!(
            content.contains("original content"),
            "Should preserve content"
        );
    }

    #[test]
    fn pretty_nonexistent_path_error() {
        wasot()
            .arg("tools")
            .arg("pretty")
            .arg("/nonexistent/path")
            .assert()
            .failure()
            .stderr(predicate::str::contains("Path not found"));
    }

    #[test]
    fn pretty_respects_gitignore() {
        let dir = setup_test_dir();

        // Initialize git repo
        std::process::Command::new("git")
            .args(["init", "-q"])
            .current_dir(dir.path())
            .status()
            .expect("git init failed");

        // Create .gitignore
        fs::write(dir.path().join(".gitignore"), "ignored/\n").unwrap();

        // Create ignored and non-ignored directories
        fs::create_dir_all(dir.path().join("ignored")).unwrap();
        fs::create_dir_all(dir.path().join("included")).unwrap();

        fs::write(
            dir.path().join("ignored/test.wasot"),
            ".ignored\nshould be skipped\n",
        )
        .unwrap();
        fs::write(
            dir.path().join("included/test.wasot"),
            ".included\nshould be processed\n",
        )
        .unwrap();

        // Run pretty with stdout to see what's processed
        let output = wasot()
            .arg("tools")
            .arg("pretty")
            .arg(dir.path())
            .arg("--stdout")
            .assert()
            .success();

        let stdout = String::from_utf8_lossy(&output.get_output().stdout);

        // Should include the non-ignored file
        assert!(
            stdout.contains(".included"),
            "Should process non-ignored files"
        );
        // Should NOT include the ignored file
        assert!(!stdout.contains(".ignored"), "Should skip gitignored files");
    }

    #[test]
    fn pretty_verbose_shows_progress() {
        let dir = setup_test_dir();
        wasot()
            .arg("tools")
            .arg("pretty")
            .arg(dir.path().join("simple.wasot"))
            .arg("--stdout")
            .arg("--verbose")
            .assert()
            .success()
            .stderr(predicate::str::contains("Processing:"));
    }
}

// =============================================================================
// 12. Error Handling
// =============================================================================

mod error_handling {
    use super::*;

    #[test]
    fn missing_file_error_message() {
        wasot()
            .arg("nonexistent.wasot")
            .assert()
            .failure()
            .stderr(predicate::str::contains("not found").or(predicate::str::contains("wasot:")));
    }

    #[test]
    fn check_missing_file_error() {
        wasot()
            .arg("tools")
            .arg("check")
            .arg("missing.wasot")
            .assert()
            .failure()
            .stderr(predicate::str::contains("not found").or(predicate::str::contains("missing")));
    }

    #[test]
    fn info_missing_file_error() {
        wasot()
            .arg("tools")
            .arg("info")
            .arg("missing.wasot")
            .assert()
            .failure()
            .stderr(
                predicate::str::contains("not found")
                    .or(predicate::str::contains("File not found")),
            );
    }

    #[test]
    fn errors_go_to_stderr() {
        wasot()
            .arg("missing.wasot")
            .assert()
            .failure()
            .stderr(predicate::str::is_empty().not())
            .stdout(predicate::str::is_empty());
    }

    #[test]
    fn success_output_to_stdout() {
        let dir = setup_test_dir();
        wasot()
            .arg(dir.path().join("simple.wasot"))
            .assert()
            .success()
            .stdout(predicate::str::is_empty().not());
    }
}

// =============================================================================
// 13. Idempotency
// =============================================================================

mod idempotency {
    use super::*;

    #[test]
    fn normalize_is_idempotent() {
        let dir = setup_test_dir();

        // First normalize
        let first = wasot()
            .arg(dir.path().join("valid.wasot"))
            .assert()
            .success()
            .get_output()
            .stdout
            .clone();

        // Write first output
        let intermediate = dir.path().join("intermediate.wasot");
        fs::write(&intermediate, &first).unwrap();

        // Second normalize
        let second = wasot()
            .arg(&intermediate)
            .assert()
            .success()
            .get_output()
            .stdout
            .clone();

        assert_eq!(first, second, "Normalizing twice should give same result");
    }

    #[test]
    fn binary_roundtrip_preserves_content() {
        let dir = setup_test_dir();
        let binary_file = dir.path().join("roundtrip.wasotb");

        // Helper to strip auto-generated document ID line for comparison
        // Document IDs are generated fresh on each reconstruction, so we compare content only
        fn strip_document_id(output: &[u8]) -> Vec<u8> {
            let s = String::from_utf8_lossy(output);
            let lines: Vec<&str> = s.lines().collect();
            if lines.first().is_some_and(|l| l.starts_with("./id ")) {
                // Skip the ID line and the blank line after it
                let skip = if lines.get(1).is_some_and(|l| l.is_empty()) {
                    2
                } else {
                    1
                };
                lines[skip..].join("\n").into_bytes()
            } else {
                output.to_vec()
            }
        }

        // Get original normalized output
        let original = wasot()
            .arg(dir.path().join("simple.wasot"))
            .assert()
            .success()
            .get_output()
            .stdout
            .clone();

        // Convert to binary
        wasot()
            .arg(dir.path().join("simple.wasot"))
            .arg("-t")
            .arg("binary")
            .arg("-o")
            .arg(&binary_file)
            .assert()
            .success();

        // Convert back
        let roundtrip = wasot()
            .arg(&binary_file)
            .assert()
            .success()
            .get_output()
            .stdout
            .clone();

        // Compare content without auto-generated document IDs
        assert_eq!(
            strip_document_id(&original),
            strip_document_id(&roundtrip),
            "Binary roundtrip should preserve content"
        );
    }
}

// =============================================================================
// 14. Combination Tests
// =============================================================================

// =============================================================================
// 15. Tools Subcommand
// =============================================================================

mod tools_document_id {
    use super::*;

    /// Create a test file with a document ID
    fn create_file_with_id(dir: &TempDir, filename: &str, id: &str) -> std::path::PathBuf {
        let content = format!("./id {}\n.paragraph\nSome content.\n", id);
        let path = dir.path().join(filename);
        fs::write(&path, content).unwrap();
        path
    }

    /// Create a test file without a document ID
    fn create_file_without_id(dir: &TempDir, filename: &str) -> std::path::PathBuf {
        let content = ".paragraph\nSome content without ID.\n.section\nMore content.\n";
        let path = dir.path().join(filename);
        fs::write(&path, content).unwrap();
        path
    }

    #[test]
    fn document_id_basic() {
        // Extract document ID from a file - output format: "<id> <path>"
        let dir = TempDir::new().unwrap();
        let file = create_file_with_id(&dir, "doc.wasot", "my-document-123");

        wasot()
            .arg("tools")
            .arg("document-ids")
            .arg("scan")
            .arg(&file)
            .assert()
            .success()
            .stdout(predicate::str::contains("my-document-123"))
            .stdout(predicate::str::contains("doc.wasot"));
    }

    #[test]
    fn document_id_with_spaces() {
        // ID with spaces (quoted in context line)
        let dir = TempDir::new().unwrap();
        let content = "./id \"document with spaces\"\n.paragraph\nContent.\n";
        let file = dir.path().join("spaced.wasot");
        fs::write(&file, content).unwrap();

        wasot()
            .arg("tools")
            .arg("document-ids")
            .arg("scan")
            .arg(&file)
            .assert()
            .success()
            .stdout(predicate::str::contains("document with spaces"));
    }

    #[test]
    fn document_id_uuid_format() {
        // UUID-style document ID
        let dir = TempDir::new().unwrap();
        let file = create_file_with_id(&dir, "uuid.wasot", "550e8400-e29b-41d4-a716-446655440000");

        wasot()
            .arg("tools")
            .arg("document-ids")
            .arg("scan")
            .arg(&file)
            .assert()
            .success()
            .stdout(predicate::str::contains(
                "550e8400-e29b-41d4-a716-446655440000",
            ));
    }

    #[test]
    fn document_id_base36_format() {
        // Base36-style document ID (common in wasot)
        let dir = TempDir::new().unwrap();
        let file = create_file_with_id(&dir, "base36.wasot", "abc123xyz");

        wasot()
            .arg("tools")
            .arg("document-ids")
            .arg("scan")
            .arg(&file)
            .assert()
            .success()
            .stdout(predicate::str::contains("abc123xyz"));
    }

    #[test]
    fn document_id_no_id_produces_empty_output() {
        // File without document ID should produce empty output (not an error)
        let dir = TempDir::new().unwrap();
        let file = create_file_without_id(&dir, "no_id.wasot");

        wasot()
            .arg("tools")
            .arg("document-ids")
            .arg("scan")
            .arg(&file)
            .assert()
            .success()
            .stdout(predicate::str::is_empty());
    }

    #[test]
    fn document_id_empty_file_produces_empty_output() {
        // Empty file should produce empty output
        let dir = TempDir::new().unwrap();
        let file = dir.path().join("empty.wasot");
        fs::write(&file, "").unwrap();

        wasot()
            .arg("tools")
            .arg("document-ids")
            .arg("scan")
            .arg(&file)
            .assert()
            .success()
            .stdout(predicate::str::is_empty());
    }

    #[test]
    fn document_id_path_not_found() {
        // Non-existent path should fail with appropriate error
        wasot()
            .arg("tools")
            .arg("document-ids")
            .arg("scan")
            .arg("nonexistent.wasot")
            .assert()
            .failure()
            .stderr(
                predicate::str::contains("not found")
                    .or(predicate::str::contains("Path not found")),
            );
    }

    #[test]
    fn document_id_exit_code_success() {
        // Successful extraction should return exit code 0
        let dir = TempDir::new().unwrap();
        let file = create_file_with_id(&dir, "success.wasot", "test-id");

        wasot()
            .arg("tools")
            .arg("document-ids")
            .arg("scan")
            .arg(&file)
            .assert()
            .code(0);
    }

    #[test]
    fn document_id_exit_code_no_id_still_success() {
        // No ID found should still return exit code 0 (not an error)
        let dir = TempDir::new().unwrap();
        let file = create_file_without_id(&dir, "no_id.wasot");

        wasot()
            .arg("tools")
            .arg("document-ids")
            .arg("scan")
            .arg(&file)
            .assert()
            .code(0);
    }

    #[test]
    fn document_id_exit_code_path_not_found() {
        // Path not found should return exit code 2 (IO error)
        wasot()
            .arg("tools")
            .arg("document-ids")
            .arg("scan")
            .arg("missing.wasot")
            .assert()
            .code(2);
    }

    #[test]
    fn document_id_first_id_wins() {
        // If multiple ID context lines exist, first one should be returned
        let dir = TempDir::new().unwrap();
        let content = "./id first-id\n.paragraph\nContent.\n./id second-id\n";
        let file = dir.path().join("multi_id.wasot");
        fs::write(&file, content).unwrap();

        wasot()
            .arg("tools")
            .arg("document-ids")
            .arg("scan")
            .arg(&file)
            .assert()
            .success()
            .stdout(predicate::str::contains("first-id"))
            .stdout(predicate::str::contains("second-id").not());
    }

    #[test]
    fn document_id_other_context_lines_ignored() {
        // Other context line types should be ignored
        let dir = TempDir::new().unwrap();
        let content = "./author John Doe\n./date 2024-01-15\n./id my-doc-id\n./version 1.0\n.paragraph\nContent.\n";
        let file = dir.path().join("mixed_context.wasot");
        fs::write(&file, content).unwrap();

        wasot()
            .arg("tools")
            .arg("document-ids")
            .arg("scan")
            .arg(&file)
            .assert()
            .success()
            .stdout(predicate::str::contains("my-doc-id"));
    }

    #[test]
    fn document_id_output_format() {
        // Output should be "<id> <path>" format
        let dir = TempDir::new().unwrap();
        let file = create_file_with_id(&dir, "format.wasot", "test-id-123");

        let output = wasot()
            .arg("tools")
            .arg("document-ids")
            .arg("scan")
            .arg(&file)
            .assert()
            .success()
            .get_output()
            .stdout
            .clone();

        let output_str = String::from_utf8(output).unwrap();
        let line = output_str.trim();
        assert!(
            line.starts_with("test-id-123 "),
            "Output should start with ID followed by space"
        );
        assert!(
            line.ends_with("format.wasot"),
            "Output should end with filename"
        );
    }

    #[test]
    fn document_id_id_at_end_of_file() {
        // ID at the end of file should still be found
        let dir = TempDir::new().unwrap();
        let content =
            ".paragraph\nSome content first.\n.section\nMore content.\n./id end-of-file-id\n";
        let file = dir.path().join("end_id.wasot");
        fs::write(&file, content).unwrap();

        wasot()
            .arg("tools")
            .arg("document-ids")
            .arg("scan")
            .arg(&file)
            .assert()
            .success()
            .stdout(predicate::str::contains("end-of-file-id"));
    }

    #[test]
    fn document_id_nested_content() {
        // ID should be found even with nested blocks
        let dir = TempDir::new().unwrap();
        let content = "./id nested-doc\n.document\n..section\n...paragraph\nDeeply nested.\n";
        let file = dir.path().join("nested.wasot");
        fs::write(&file, content).unwrap();

        wasot()
            .arg("tools")
            .arg("document-ids")
            .arg("scan")
            .arg(&file)
            .assert()
            .success()
            .stdout(predicate::str::contains("nested-doc"));
    }

    #[test]
    fn document_id_directory_input() {
        // Should recursively process directories
        let dir = TempDir::new().unwrap();
        create_file_with_id(&dir, "one.wasot", "id-one");
        create_file_with_id(&dir, "two.wasot", "id-two");
        create_file_without_id(&dir, "no_id.wasot");

        // Create subdirectory with file
        let subdir = dir.path().join("subdir");
        fs::create_dir(&subdir).unwrap();
        fs::write(
            subdir.join("nested.wasot"),
            "./id id-nested\n.block\nContent.\n",
        )
        .unwrap();

        let output = wasot()
            .arg("tools")
            .arg("document-ids")
            .arg("scan")
            .arg(dir.path())
            .assert()
            .success()
            .get_output()
            .stdout
            .clone();

        let output_str = String::from_utf8(output).unwrap();

        // Should find all three files with IDs
        assert!(output_str.contains("id-one"), "Should find id-one");
        assert!(output_str.contains("id-two"), "Should find id-two");
        assert!(
            output_str.contains("id-nested"),
            "Should find id-nested in subdir"
        );

        // Should NOT include the file without ID
        assert!(
            !output_str.contains("no_id.wasot"),
            "Should not include file without ID"
        );

        // Should have 3 lines (one per file with ID)
        assert_eq!(output_str.lines().count(), 3, "Should have exactly 3 lines");
    }

    #[test]
    fn document_id_multiple_paths() {
        // Should accept multiple paths
        let dir = TempDir::new().unwrap();
        let file1 = create_file_with_id(&dir, "first.wasot", "id-first");
        let file2 = create_file_with_id(&dir, "second.wasot", "id-second");

        let output = wasot()
            .arg("tools")
            .arg("document-ids")
            .arg("scan")
            .arg(&file1)
            .arg(&file2)
            .assert()
            .success()
            .get_output()
            .stdout
            .clone();

        let output_str = String::from_utf8(output).unwrap();
        assert!(output_str.contains("id-first"));
        assert!(output_str.contains("id-second"));
        assert_eq!(output_str.lines().count(), 2);
    }

    #[test]
    fn document_id_empty_directory() {
        // Empty directory should produce empty output
        let dir = TempDir::new().unwrap();
        let empty_dir = dir.path().join("empty");
        fs::create_dir(&empty_dir).unwrap();

        wasot()
            .arg("tools")
            .arg("document-ids")
            .arg("scan")
            .arg(&empty_dir)
            .assert()
            .success()
            .stdout(predicate::str::is_empty());
    }

    #[test]
    fn document_id_only_wasot_files() {
        // Should only process .wasot files, ignoring others
        let dir = TempDir::new().unwrap();
        create_file_with_id(&dir, "valid.wasot", "wasot-id");
        fs::write(dir.path().join("ignored.txt"), "./id txt-id\n.block\n").unwrap();
        fs::write(dir.path().join("ignored.md"), "./id md-id\n.block\n").unwrap();

        let output = wasot()
            .arg("tools")
            .arg("document-ids")
            .arg("scan")
            .arg(dir.path())
            .assert()
            .success()
            .get_output()
            .stdout
            .clone();

        let output_str = String::from_utf8(output).unwrap();
        assert!(output_str.contains("wasot-id"));
        assert!(!output_str.contains("txt-id"));
        assert!(!output_str.contains("md-id"));
        assert_eq!(output_str.lines().count(), 1);
    }

    #[test]
    fn tools_help() {
        // Tools help should show document-ids subcommand
        wasot()
            .arg("tools")
            .arg("--help")
            .assert()
            .success()
            .stdout(predicate::str::contains("document-ids"))
            .stdout(predicate::str::contains("Document processing tools"));
    }

    #[test]
    fn document_ids_help() {
        // document-ids help should show subcommands
        wasot()
            .arg("tools")
            .arg("document-ids")
            .arg("--help")
            .assert()
            .success()
            .stdout(predicate::str::contains("scan"))
            .stdout(predicate::str::contains("update"))
            .stdout(predicate::str::contains("get"));
    }

    #[test]
    fn document_ids_scan_help() {
        // document-ids scan help should show usage
        wasot()
            .arg("tools")
            .arg("document-ids")
            .arg("scan")
            .arg("--help")
            .assert()
            .success()
            .stdout(predicate::str::contains("Scan"))
            .stdout(predicate::str::contains("document ID"))
            .stdout(predicate::str::contains("PATHS"));
    }
}

mod combinations {
    use super::*;

    #[test]
    fn select_and_convert_to_jsonl() {
        let dir = setup_test_dir();
        wasot()
            .arg("--select=document")
            .arg(dir.path().join("valid.wasot"))
            .arg("-t")
            .arg("jsonl")
            .assert()
            .success()
            .stdout(predicate::str::contains("document"));
    }

    #[test]
    fn select_and_output_file() {
        let dir = setup_test_dir();
        let output = dir.path().join("combined.jsonl");

        wasot()
            .arg("--select=paragraph")
            .arg(dir.path().join("valid.wasot"))
            .arg("-t")
            .arg("jsonl")
            .arg("-o")
            .arg(&output)
            .assert()
            .success();

        let content = fs::read_to_string(&output).unwrap();
        assert!(content.contains("paragraph"));
    }

    #[test]
    fn stdin_select_to_xml() {
        wasot()
            .arg("--select=block")
            .arg("-t")
            .arg("xml")
            .write_stdin(".block\ntest\n.other\ndata\n")
            .assert()
            .success()
            .stdout(predicate::str::contains("<block"));
    }

    #[test]
    fn convert_chain_wasot_binary_wasot() {
        let dir = setup_test_dir();
        let binary = dir.path().join("chain.wasotb");
        let final_wasot = dir.path().join("chain.wasot");

        // wasot -> binary
        wasot()
            .arg(dir.path().join("simple.wasot"))
            .arg("-t")
            .arg("binary")
            .arg("-o")
            .arg(&binary)
            .assert()
            .success();

        // binary -> wasot
        wasot()
            .arg(&binary)
            .arg("-o")
            .arg(&final_wasot)
            .assert()
            .success();

        let content = fs::read_to_string(&final_wasot).unwrap();
        assert!(content.contains(".block"));
    }
}

// =============================================================================
// HTML Format Tests
// =============================================================================

mod html_format {
    use super::*;

    /// Create a temp directory with HTML test files
    fn setup_html_test_dir() -> TempDir {
        let dir = TempDir::new().unwrap();

        // Create a simple HTML file
        let simple_html = r#"<!DOCTYPE html>
<html>
<head><title>Test</title></head>
<body>
<article>
<h1>Welcome</h1>
<p>Hello world</p>
</article>
</body>
</html>"#;
        fs::write(dir.path().join("simple.html"), simple_html).unwrap();

        // Create HTML with void elements
        let void_html = r#"<div>
<hr>
<img src="photo.jpg" alt="A photo">
<br>
<p>Text after void elements</p>
</div>"#;
        fs::write(dir.path().join("void.html"), void_html).unwrap();

        // Create HTML with attributes
        let attrs_html = r#"<div id="main" class="container" data-value="42">
<p class="intro">Introduction</p>
</div>"#;
        fs::write(dir.path().join("attrs.html"), attrs_html).unwrap();

        // Create a wasot file for HTML output tests
        let wasot_content = r#".article :myart
..h1 :hdr
Welcome
..p :para
Hello world
"#;
        fs::write(dir.path().join("test.wasot"), wasot_content).unwrap();

        dir
    }

    // -------------------------------------------------------------------------
    // Wasot to HTML tests
    // -------------------------------------------------------------------------

    #[test]
    fn wasot_to_html_stdout() {
        // wasot file.wasot -t html — convert to HTML
        let dir = setup_html_test_dir();
        wasot()
            .arg(dir.path().join("test.wasot"))
            .arg("-t")
            .arg("html")
            .assert()
            .success()
            .stdout(predicate::str::contains("<article"))
            .stdout(predicate::str::contains("<h1"))
            .stdout(predicate::str::contains("<p"))
            .stdout(predicate::str::contains("</article>"));
    }

    #[test]
    fn wasot_to_html_preserves_ids() {
        // IDs should be preserved in HTML output
        let dir = setup_html_test_dir();
        wasot()
            .arg(dir.path().join("test.wasot"))
            .arg("-t")
            .arg("html")
            .assert()
            .success()
            .stdout(predicate::str::contains("id=\"myart\""))
            .stdout(predicate::str::contains("id=\"hdr\""))
            .stdout(predicate::str::contains("id=\"para\""));
    }

    #[test]
    fn wasot_to_html_stdin() {
        // cat file.wasot | wasot -t html
        wasot()
            .arg("-t")
            .arg("html")
            .write_stdin(".p\nHello World\n")
            .assert()
            .success()
            .stdout(predicate::str::contains("<p"))
            .stdout(predicate::str::contains("Hello World"))
            .stdout(predicate::str::contains("</p>"));
    }

    #[test]
    fn wasot_to_html_void_elements() {
        // Void elements should be rendered correctly
        wasot()
            .arg("-t")
            .arg("html")
            .write_stdin(".div\n..hr:\n..br:\n")
            .assert()
            .success()
            .stdout(predicate::str::contains("<hr"))
            .stdout(predicate::str::contains("<br"));
    }

    #[test]
    fn wasot_to_html_with_select() {
        // wasot file.wasot --select p -t html
        let dir = setup_html_test_dir();
        wasot()
            .arg(dir.path().join("test.wasot"))
            .arg("--select")
            .arg("p")
            .arg("-t")
            .arg("html")
            .assert()
            .success()
            .stdout(predicate::str::contains("<p"))
            .stdout(predicate::str::contains("Hello world"));
    }
}

// =============================================================================
// Markdown Format Tests
// =============================================================================

mod markdown_format {
    use super::*;

    /// Create a temp directory with Markdown test files
    fn setup_markdown_test_dir() -> TempDir {
        let dir = TempDir::new().unwrap();

        // Create a simple markdown file
        let simple_md = r#"# Hello World

This is a paragraph.

## Section Two

More text here.
"#;
        fs::write(dir.path().join("simple.md"), simple_md).unwrap();

        // Create markdown with code blocks
        let code_md = r#"# Code Example

```rust
fn main() {
    println!("Hello");
}
```

Regular text.
"#;
        fs::write(dir.path().join("code.md"), code_md).unwrap();

        // Create markdown with quotes
        let quote_md = r#"# Quote Test

> This is a quote
> spanning multiple lines

After the quote.
"#;
        fs::write(dir.path().join("quote.md"), quote_md).unwrap();

        // Create file with .markdown extension
        fs::write(dir.path().join("test.markdown"), "# Test Heading\n").unwrap();

        dir
    }

    // -------------------------------------------------------------------------
    // Markdown to Wasot tests
    // -------------------------------------------------------------------------

    #[test]
    fn markdown_to_wasot_by_extension() {
        // wasot file.md — auto-detect markdown by extension
        let dir = setup_markdown_test_dir();
        wasot()
            .arg(dir.path().join("simple.md"))
            .assert()
            .success()
            .stdout(predicate::str::contains(".# Hello World"))
            .stdout(predicate::str::contains(".## Section Two"));
    }

    #[test]
    fn markdown_to_wasot_explicit_format() {
        // wasot -f markdown file.md — explicit format flag
        let dir = setup_markdown_test_dir();
        wasot()
            .arg("-f")
            .arg("markdown")
            .arg(dir.path().join("simple.md"))
            .assert()
            .success()
            .stdout(predicate::str::contains(".# Hello World"));
    }

    #[test]
    fn markdown_extension_variant() {
        // .markdown extension should also work
        let dir = setup_markdown_test_dir();
        wasot()
            .arg(dir.path().join("test.markdown"))
            .assert()
            .success()
            .stdout(predicate::str::contains(".# Test Heading"));
    }

    #[test]
    fn markdown_stdin_with_format_flag() {
        // echo "# Hello" | wasot -f markdown
        wasot()
            .arg("-f")
            .arg("markdown")
            .write_stdin("# Hello\n\nSome text\n")
            .assert()
            .success()
            .stdout(predicate::str::contains(".# Hello"))
            .stdout(predicate::str::contains("Some text"));
    }

    #[test]
    fn markdown_to_wasot_output_file() {
        // wasot file.md -o output.wasot
        let dir = setup_markdown_test_dir();
        let output = dir.path().join("output.wasot");

        wasot()
            .arg(dir.path().join("simple.md"))
            .arg("-o")
            .arg(&output)
            .assert()
            .success();

        assert!(output.exists());
        let content = fs::read_to_string(&output).unwrap();
        assert!(content.contains(".# Hello World"));
    }

    #[test]
    fn markdown_with_code_blocks() {
        // Code blocks should be converted to .code blocks
        let dir = setup_markdown_test_dir();
        wasot()
            .arg(dir.path().join("code.md"))
            .assert()
            .success()
            .stdout(predicate::str::contains(".# Code Example"))
            .stdout(predicate::str::contains(".code"));
    }

    #[test]
    fn markdown_with_quotes() {
        // Block quotes should be converted to .quote blocks
        let dir = setup_markdown_test_dir();
        wasot()
            .arg(dir.path().join("quote.md"))
            .assert()
            .success()
            .stdout(predicate::str::contains(".# Quote Test"))
            .stdout(predicate::str::contains(".quote"));
    }

    // -------------------------------------------------------------------------
    // Markdown to other formats tests
    // -------------------------------------------------------------------------

    #[test]
    fn markdown_to_xml() {
        // wasot file.md -t xml
        let dir = setup_markdown_test_dir();
        wasot()
            .arg(dir.path().join("simple.md"))
            .arg("-t")
            .arg("xml")
            .assert()
            .success()
            .stdout(predicate::str::contains("<block type=\"#\""))
            .stdout(predicate::str::contains(
                "<inline-text>Hello World</inline-text>",
            ));
    }

    #[test]
    fn markdown_to_jsonl() {
        // wasot file.md -t jsonl
        let dir = setup_markdown_test_dir();
        wasot()
            .arg(dir.path().join("simple.md"))
            .arg("-t")
            .arg("jsonl")
            .assert()
            .success()
            .stdout(predicate::str::contains("\"block_type\":\"#\""))
            .stdout(predicate::str::contains("Hello World"));
    }

    #[test]
    #[cfg(feature = "html")]
    fn markdown_to_html() {
        // wasot file.md -t html
        // Only runs when html feature is enabled
        let dir = setup_markdown_test_dir();
        wasot()
            .arg(dir.path().join("simple.md"))
            .arg("-t")
            .arg("html")
            .assert()
            .success()
            .stdout(predicate::str::contains("Hello World"));
    }

    #[test]
    #[cfg(not(feature = "html"))]
    fn markdown_to_html_feature_disabled() {
        // wasot file.md -t html — returns error when html feature disabled
        let dir = setup_markdown_test_dir();
        wasot()
            .arg(dir.path().join("simple.md"))
            .arg("-t")
            .arg("html")
            .assert()
            .failure()
            .stderr(predicate::str::contains("html").or(predicate::str::contains("HTML")));
    }

    // -------------------------------------------------------------------------
    // Error cases
    // -------------------------------------------------------------------------

    #[test]
    fn markdown_as_output_format_fails() {
        // wasot file.wasot -t markdown — should fail (markdown is input only)
        let dir = setup_test_dir();
        wasot()
            .arg(dir.path().join("simple.wasot"))
            .arg("-t")
            .arg("markdown")
            .assert()
            .failure()
            .stderr(
                predicate::str::contains("not yet implemented")
                    .or(predicate::str::contains("input format")),
            );
    }

    #[test]
    fn markdown_file_not_found() {
        // wasot nonexistent.md — file not found
        wasot().arg("/nonexistent/path/file.md").assert().failure();
    }
}

// =============================================================================
// S-Expression Format Tests
// =============================================================================

#[cfg(feature = "sexp")]
mod sexp_format {
    use super::*;

    /// Create a temp directory with sexp test files
    fn setup_sexp_test_dir() -> TempDir {
        let dir = TempDir::new().unwrap();

        // Create a simple wasot file
        let simple_wasot = r#".block
test content
"#;
        fs::write(dir.path().join("simple.wasot"), simple_wasot).unwrap();

        // Create a wasot file with nested blocks
        let nested_wasot = r#".section
..paragraph
Nested content
...span
Deeply nested
"#;
        fs::write(dir.path().join("nested.wasot"), nested_wasot).unwrap();

        // Create a wasot file with links
        let links_wasot = r#".paragraph
Check out ((our site :https://example.com))
"#;
        fs::write(dir.path().join("links.wasot"), links_wasot).unwrap();

        // Create a wasot file with kwargs
        let kwargs_wasot = r#".code language=rust filename="test.rs"
fn main() {}
"#;
        fs::write(dir.path().join("kwargs.wasot"), kwargs_wasot).unwrap();

        // Create a wasot file with ID
        let id_wasot = r#".block :myid
content
"#;
        fs::write(dir.path().join("with_id.wasot"), id_wasot).unwrap();

        // Create a wasot file with void elements
        let void_wasot = r#".document
..hr:
..br:
"#;
        fs::write(dir.path().join("void.wasot"), void_wasot).unwrap();

        // Create a wasot file with context lines
        let context_wasot = r#"./id my-doc-123
./author John
.paragraph
Content
"#;
        fs::write(dir.path().join("context.wasot"), context_wasot).unwrap();

        dir
    }

    // -------------------------------------------------------------------------
    // Basic conversion tests
    // -------------------------------------------------------------------------

    #[test]
    fn wasot_to_sexp_stdout() {
        // wasot file.wasot -t sexp — convert to S-expression
        let dir = setup_sexp_test_dir();
        wasot()
            .arg(dir.path().join("simple.wasot"))
            .arg("-t")
            .arg("sexp")
            .assert()
            .success()
            .stdout(predicate::str::contains("(block type: \"block\""))
            .stdout(predicate::str::contains("level: 1"));
    }

    #[test]
    fn wasot_to_sexp_stdin() {
        // echo ".block\ntest" | wasot -t sexp
        wasot()
            .arg("-t")
            .arg("sexp")
            .write_stdin(".block\ntest content\n")
            .assert()
            .success()
            .stdout(predicate::str::contains("(block type: \"block\""))
            .stdout(predicate::str::contains("(text"));
    }

    #[test]
    fn wasot_to_sexp_output_file() {
        // wasot file.wasot -t sexp -o output.sexp
        let dir = setup_sexp_test_dir();
        let output = dir.path().join("output.sexp");

        wasot()
            .arg(dir.path().join("simple.wasot"))
            .arg("-t")
            .arg("sexp")
            .arg("-o")
            .arg(&output)
            .assert()
            .success();

        assert!(output.exists());
        let content = fs::read_to_string(&output).unwrap();
        assert!(content.contains("(block type: \"block\""));
    }

    #[test]
    fn sexp_format_from_extension() {
        // wasot file.wasot -o output.sexp — infer format from extension
        let dir = setup_sexp_test_dir();
        let output = dir.path().join("output.sexp");

        wasot()
            .arg(dir.path().join("simple.wasot"))
            .arg("-o")
            .arg(&output)
            .assert()
            .success();

        let content = fs::read_to_string(&output).unwrap();
        assert!(content.contains("(block"));
    }

    // -------------------------------------------------------------------------
    // S-expression content tests
    // -------------------------------------------------------------------------

    #[test]
    fn sexp_includes_block_type() {
        let dir = setup_sexp_test_dir();
        wasot()
            .arg(dir.path().join("simple.wasot"))
            .arg("-t")
            .arg("sexp")
            .assert()
            .success()
            .stdout(predicate::str::contains("type: \"block\""));
    }

    #[test]
    fn sexp_includes_level() {
        let dir = setup_sexp_test_dir();
        wasot()
            .arg(dir.path().join("nested.wasot"))
            .arg("-t")
            .arg("sexp")
            .assert()
            .success()
            .stdout(predicate::str::contains("level: 1"))
            .stdout(predicate::str::contains("level: 2"))
            .stdout(predicate::str::contains("level: 3"));
    }

    #[test]
    fn sexp_includes_id() {
        let dir = setup_sexp_test_dir();
        wasot()
            .arg(dir.path().join("with_id.wasot"))
            .arg("-t")
            .arg("sexp")
            .assert()
            .success()
            .stdout(predicate::str::contains("id: \"myid\""));
    }

    #[test]
    fn sexp_includes_text_content() {
        let dir = setup_sexp_test_dir();
        wasot()
            .arg(dir.path().join("simple.wasot"))
            .arg("-t")
            .arg("sexp")
            .assert()
            .success()
            .stdout(predicate::str::contains("(text"))
            .stdout(predicate::str::contains("(line index:"))
            .stdout(predicate::str::contains("test content"));
    }

    #[test]
    fn sexp_includes_links() {
        // Links in wasot text get converted to footnote references
        let dir = setup_sexp_test_dir();
        wasot()
            .arg(dir.path().join("links.wasot"))
            .arg("-t")
            .arg("sexp")
            .assert()
            .success()
            .stdout(predicate::str::contains("(footnote"))
            .stdout(predicate::str::contains("https://example.com"));
    }

    #[test]
    fn sexp_includes_kwargs() {
        // Test that kwargs in inline text are captured
        let dir = setup_sexp_test_dir();
        wasot()
            .arg(dir.path().join("kwargs.wasot"))
            .arg("-t")
            .arg("sexp")
            .assert()
            .success()
            .stdout(predicate::str::contains("(inline-text"))
            .stdout(predicate::str::contains("language"))
            .stdout(predicate::str::contains("rust"));
    }

    #[test]
    fn sexp_handles_void_elements() {
        let dir = setup_sexp_test_dir();
        wasot()
            .arg(dir.path().join("void.wasot"))
            .arg("-t")
            .arg("sexp")
            .assert()
            .success()
            .stdout(predicate::str::contains("(void type: \"hr\""))
            .stdout(predicate::str::contains("(void type: \"br\""));
    }

    #[test]
    fn sexp_handles_context_lines() {
        // Context line types are stored without the ./ prefix
        let dir = setup_sexp_test_dir();
        wasot()
            .arg(dir.path().join("context.wasot"))
            .arg("-t")
            .arg("sexp")
            .assert()
            .success()
            .stdout(predicate::str::contains("(context-line type: \"id\""))
            .stdout(predicate::str::contains("(context-line type: \"author\""));
    }

    // -------------------------------------------------------------------------
    // S-expression with --select tests
    // -------------------------------------------------------------------------

    #[test]
    fn sexp_with_select() {
        let dir = setup_sexp_test_dir();
        wasot()
            .arg("--select=paragraph")
            .arg(dir.path().join("nested.wasot"))
            .arg("-t")
            .arg("sexp")
            .assert()
            .success()
            .stdout(predicate::str::contains("(block type: \"paragraph\""));
    }

    #[test]
    fn sexp_with_select_to_file() {
        let dir = setup_sexp_test_dir();
        let output = dir.path().join("selected.sexp");

        wasot()
            .arg("--select=paragraph")
            .arg(dir.path().join("nested.wasot"))
            .arg("-t")
            .arg("sexp")
            .arg("-o")
            .arg(&output)
            .assert()
            .success();

        let content = fs::read_to_string(&output).unwrap();
        assert!(content.contains("(block type: \"paragraph\""));
    }

    // -------------------------------------------------------------------------
    // Nested structures tests
    // -------------------------------------------------------------------------

    #[test]
    fn sexp_nested_blocks_proper_nesting() {
        let dir = setup_sexp_test_dir();
        let output = wasot()
            .arg(dir.path().join("nested.wasot"))
            .arg("-t")
            .arg("sexp")
            .assert()
            .success()
            .get_output()
            .stdout
            .clone();

        let content = String::from_utf8(output).unwrap();

        // Count opening and closing parens to verify proper nesting
        let open_count = content.matches('(').count();
        let close_count = content.matches(')').count();
        assert_eq!(open_count, close_count, "Parentheses should be balanced");

        // Verify nesting structure
        assert!(content.contains("(block type: \"section\""));
        assert!(content.contains("(block type: \"paragraph\""));
        assert!(content.contains("(block type: \"span\""));
    }

    // -------------------------------------------------------------------------
    // Edge cases
    // -------------------------------------------------------------------------

    #[test]
    fn sexp_empty_file() {
        let dir = setup_sexp_test_dir();
        fs::write(dir.path().join("empty.wasot"), "").unwrap();

        wasot()
            .arg(dir.path().join("empty.wasot"))
            .arg("-t")
            .arg("sexp")
            .assert()
            .success();
    }

    #[test]
    fn sexp_whitespace_only_file() {
        let dir = setup_sexp_test_dir();
        fs::write(dir.path().join("whitespace.wasot"), "   \n\t\n  ").unwrap();

        wasot()
            .arg(dir.path().join("whitespace.wasot"))
            .arg("-t")
            .arg("sexp")
            .assert()
            .success();
    }

    #[test]
    fn sexp_special_characters_escaped() {
        // Test that special characters in content are properly escaped
        let dir = setup_sexp_test_dir();
        let content = ".block\nContent with \"quotes\" and \\backslashes\n";
        fs::write(dir.path().join("special.wasot"), content).unwrap();

        wasot()
            .arg(dir.path().join("special.wasot"))
            .arg("-t")
            .arg("sexp")
            .assert()
            .success()
            .stdout(predicate::str::contains("\\\"quotes\\\""))
            .stdout(predicate::str::contains("\\\\backslashes"));
    }

    #[test]
    fn sexp_newlines_escaped() {
        let dir = setup_sexp_test_dir();
        // Test multiline content
        wasot()
            .arg(dir.path().join("simple.wasot"))
            .arg("-t")
            .arg("sexp")
            .assert()
            .success()
            // Each line should be a separate (line ...) node
            .stdout(predicate::str::contains("(line index:"));
    }

    // -------------------------------------------------------------------------
    // Integration with other formats
    // -------------------------------------------------------------------------

    #[test]
    fn binary_to_sexp() {
        // binary → sexp (roundtrip test)
        let dir = setup_sexp_test_dir();
        let binary_file = dir.path().join("test.wasotb");

        // First create binary
        wasot()
            .arg(dir.path().join("simple.wasot"))
            .arg("-t")
            .arg("binary")
            .arg("-o")
            .arg(&binary_file)
            .assert()
            .success();

        // Then convert to sexp
        wasot()
            .arg("-f")
            .arg("binary")
            .arg(&binary_file)
            .arg("-t")
            .arg("sexp")
            .assert()
            .success()
            .stdout(predicate::str::contains("(block"));
    }
}
