//! Golden test cases for panache formatter.
//!
//! Each test case is a directory under `tests/cases/` containing:
//! - `input.*` - Source file (`.md`, `.qmd`, or `.Rmd`)
//! - `expected.*` - Expected formatted output (same extension as input)
//! - `cst.txt` - (Optional) Expected CST structure for parse regression testing
//! - `panache.toml` - (Optional) Config to test specific flavors/extensions
//!
//! Run with `UPDATE_EXPECTED=1 cargo test` to regenerate expected outputs.
//! Run with `UPDATE_CST=1 cargo test` to regenerate CST files.
//! Run with both flags to update both: `UPDATE_EXPECTED=1 UPDATE_CST=1 cargo test`.

use panache::{Config, format, parse};
use std::{
    fs,
    path::{Path, PathBuf},
};

/// Find a file with given base name and any supported extension.
fn find_file_with_extension(dir: &Path, base: &str) -> Option<PathBuf> {
    for ext in &["md", "qmd", "Rmd"] {
        let path = dir.join(format!("{}.{}", base, ext));
        if path.exists() {
            return Some(path);
        }
    }
    None
}

/// Load config from test case directory if it exists.
fn load_test_config(dir: &Path) -> Option<Config> {
    let config_path = dir.join("panache.toml");
    if config_path.exists() {
        let content = fs::read_to_string(config_path).ok()?;
        toml::from_str(&content).ok()
    } else {
        None
    }
}

/// Run a single golden test case.
fn run_golden_case(case_name: &str) {
    let dir = Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("tests")
        .join("cases")
        .join(case_name);

    let update_expected = std::env::var_os("UPDATE_EXPECTED").is_some();
    let update_cst = std::env::var_os("UPDATE_CST").is_some();

    // Find input file with any supported extension
    let input_path = find_file_with_extension(&dir, "input")
        .unwrap_or_else(|| panic!("No input file found in {}", case_name));

    // Determine expected path based on input extension
    let input_ext = input_path
        .extension()
        .and_then(|e| e.to_str())
        .unwrap_or("qmd");
    let expected_path = dir.join(format!("expected.{}", input_ext));

    let cst_path = dir.join("cst.txt");

    // Load optional config
    let config = load_test_config(&dir);

    // Read input file - preserve line endings exactly
    let input = fs::read_to_string(&input_path).unwrap();

    // Test losslessness: parser must preserve input byte-for-byte
    // This is critical for LSP, linting, and range formatting
    let ast = parse(&input, config.clone());
    let tree_text = ast.text().to_string();

    // Use similar_asserts for nice diff output showing exactly where bytes are lost
    similar_asserts::assert_eq!(
        input,
        tree_text,
        "losslessness check failed for {} (tree text does not match input, diff: {:+} bytes)",
        case_name,
        tree_text.len() as i64 - input.len() as i64
    );

    // Test formatting
    let output = format(&input, config.clone(), None);

    // Idempotency: formatting twice should equal once
    let output_twice = format(&output, config.clone(), None);
    similar_asserts::assert_eq!(output, output_twice, "idempotency: {}", case_name);

    // Test CST parsing (if cst.txt exists or we're updating CST)
    if cst_path.exists() || update_cst {
        let cst_output = format!("{:#?}\n", ast);

        if update_cst {
            fs::write(&cst_path, &cst_output).unwrap();
        } else {
            let expected_cst = fs::read_to_string(&cst_path)
                .unwrap_or_else(|_| panic!("Failed to read cst.txt in {}", case_name));
            similar_asserts::assert_eq!(expected_cst, cst_output, "CST mismatch: {}", case_name);
        }
    }

    if update_expected {
        fs::write(&expected_path, &output).unwrap();
        return;
    }

    let expected = fs::read_to_string(&expected_path).unwrap_or_else(|_| input.clone());

    similar_asserts::assert_eq!(expected, output, "case: {}", case_name);
}

/// Macro to generate individual test functions for each golden case.
///
/// Usage: `golden_test_cases!(case1, case2, case3);`
///
/// This generates separate test functions named `golden_case1`, `golden_case2`, etc.
/// Each test runs independently, so failures don't stop other tests from running.
macro_rules! golden_test_cases {
    ($($case:ident),+ $(,)?) => {
        $(
            #[test]
            fn $case() {
                run_golden_case(stringify!($case));
            }
        )+
    };
}

// Generate test functions for each case directory.
// To add a new test case:
// 1. Create a new directory under tests/cases/
// 2. Add the directory name to this list
golden_test_cases!(
    alerts,
    alerts_disabled,
    blankline_concatenation,
    blockquote_depth_change,
    blockquote_list_blanks,
    blockquote_list_blockquote,
    blockquotes,
    bracketed_spans,
    bookdown,
    chunk_options_complex,
    code_blocks_executable,
    code_blocks_raw,
    code_spans,
    crlf_basic,
    crlf_code_blocks,
    crlf_definition_lists,
    crlf_display_math,
    crlf_fenced_divs,
    crlf_headerless_table,
    crlf_horizontal_rules,
    crlf_line_endings,
    crlf_raw_blocks,
    crlf_yaml_metadata,
    citations,
    definition_list,
    definition_list_nesting,
    definition_list_wrapping,
    display_math,
    display_math_blank_line_termination,
    display_math_content_on_fence_line,
    display_math_escaped_dollar,
    display_math_trailing_text,
    double_backslash_math,
    emphasis,
    emphasis_complex,
    emphasis_nested_inlines,
    equation_attributes,
    equation_attributes_disabled,
    equation_attributes_single_line,
    escapes,
    fenced_code,
    fenced_code_quarto,
    fenced_divs,
    fenced_div_close_grid_table,
    footnote_continuation_idempotency,
    footnote_continuation_idempotency_reflow,
    footnote_def_paragraph,
    footnote_definition_list,
    headings,
    setext_headings,
    headerless_table,
    horizontal_rules,
    html_block,
    ignore_directives,
    images,
    indented_code,
    inline_code,
    inline_footnotes,
    inline_math,
    grid_table,
    grid_table_nordics,
    grid_table_planets,
    latex_environment,
    lazy_continuation_deep,
    leading_blanklines,
    line_blocks,
    line_ending_crlf,
    line_ending_lf,
    links,
    lists_bullet,
    lists_code,
    lists_example,
    lists_fancy,
    lists_nested,
    lists_ordered,
    lists_task,
    lists_wrapping_nested,
    lists_wrapping_simple,
    multiline_table_basic,
    multiline_table_caption,
    multiline_table_caption_after,
    multiline_table_headerless,
    multiline_table_inline_formatting,
    multiline_table_single_row,
    pandoc_title_block,
    paragraph_continuation,
    paragraph_plain_mixed,
    paragraph_wrapping,
    paragraphs,
    pipe_table,
    pipe_table_unicode,
    plain_continuation_edge_cases,
    quarto_code_blocks,
    quarto_hashpipe,
    quarto_shortcodes,
    raw_blocks,
    raw_tex_commands,
    reference_footnotes,
    reference_images,
    reference_links,
    rmarkdown_math,
    simple_table,
    standardize_bullets,
    sentence_wrap_basic,
    sentence_wrap_list_blockquote,
    sentence_wrap_lazy_continuation,
    sentence_wrap_links_figures,
    sentence_wrap_lists,
    sentence_wrap_ellipsis,
    table_with_caption,
    tab_handling,
    tab_preserve,
    trailing_blanklines,
    umlauts,
    unicode,
    writer_autolinks,
    writer_blockquote_not,
    writer_definition_lists_multiblock,
    writer_headers,
    writer_html_blocks,
    writer_paragraphs,
    writer_indented_code_escapes,
    yaml_metadata,
    yaml_metadata_dots_closer,
    yaml_metadata_normalization,
    yaml_metadata_opening_blank_not_metadata,
);
