use crate::suggest::Suggestion;
use crate::suggest::provider::providers;
use std::path::Path;

/// The result of a `suggest` run: the structured suggestions, the rendered assertions-file
/// body ready to write, and any per-provider warnings. The orchestrator returns data and the
/// binary owns the I/O, mirroring how `engine::executor::execute_all` returns a report and
/// `main.rs` does the writing.
pub struct SuggestResult {
    pub suggestions: Vec<Suggestion>,
    pub rendered: String,
    pub warnings: Vec<String>,
}

/// Inspects `path` and produces suggested assertions from every provider that handles it.
///
/// Providers run in registry order (broadest first). A provider that does not handle the file
/// is skipped silently; a provider that handles it but whose property read fails records a
/// warning and is skipped, so other providers still contribute (a partial suggestion is still
/// useful). [`FileProvider`] handles any path and always emits at least `file.exists`, so the
/// result is never empty.
///
/// [`FileProvider`]: crate::suggest::providers::FileProvider
pub fn suggest(path: &Path) -> SuggestResult {
    let mut suggestions = Vec::new();
    let mut warnings = Vec::new();

    for provider in providers() {
        if !provider.handles(path) {
            continue;
        }
        match provider.suggest(path) {
            Ok(mut produced) => suggestions.append(&mut produced),
            Err(e) => warnings.push(format!("{}: {}", provider.name(), e)),
        }
    }

    let rendered = render(path, &suggestions);
    SuggestResult {
        suggestions,
        rendered,
        warnings,
    }
}

/// Renders the suggestions into an assertions-file body: a two-line provenance banner, then one
/// `# <prefix>` group per metric family (in first-seen order, separated by a blank line), then
/// each suggestion line.
fn render(path: &Path, suggestions: &[Suggestion]) -> String {
    let mut out = format!(
        "# Generated by bioassert suggest from {}\n# These are guesses from the file's current state; review and adjust before use.\n",
        path.display()
    );

    let mut current_prefix: Option<&str> = None;
    for suggestion in suggestions {
        let prefix = suggestion.metric.split('.').next().unwrap_or("");
        if current_prefix != Some(prefix) {
            // A blank line precedes every group, including the first (separating it from the
            // banner), matching the documented output format.
            out.push('\n');
            out.push_str("# ");
            out.push_str(prefix);
            out.push('\n');
            current_prefix = Some(prefix);
        }
        out.push_str(&suggestion.render());
        out.push('\n');
    }

    out
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn tsv_file_group_precedes_tsv_group_with_headers() {
        let result = suggest(Path::new("tests/data/junctions.tsv"));
        let rendered = &result.rendered;

        assert!(rendered.starts_with(
            "# Generated by bioassert suggest from tests/data/junctions.tsv\n# These are guesses from the file's current state; review and adjust before use.\n"
        ));
        assert!(rendered.contains("\n# file\n"));
        assert!(rendered.contains("\n# tsv\n"));
        let file_at = rendered.find("# file").unwrap();
        let tsv_at = rendered.find("# tsv").unwrap();
        assert!(file_at < tsv_at, "file group must precede tsv group");
        // Five assertions: exists, size, columns, rows-lower, rows-upper.
        assert_eq!(result.suggestions.len(), 5);
        assert!(result.warnings.is_empty());
    }

    #[test]
    fn unknown_extension_yields_only_the_file_group() {
        let result = suggest(Path::new("tests/data/empty_file.txt"));
        assert!(result.rendered.contains("# file"));
        assert!(!result.rendered.contains("# tsv"));
        assert!(!result.rendered.contains("# bam"));
        assert!(!result.rendered.contains("# fasta"));
        assert!(result.warnings.is_empty());
    }

    #[test]
    fn missing_file_warns_but_still_suggests_exists() {
        let result = suggest(Path::new("tests/data/does_not_exist.tsv"));
        // FileProvider still emits the exists check; DelimitedProvider matches the extension
        // but fails to read the file, producing a warning.
        assert!(
            result
                .rendered
                .contains("does_not_exist.tsv file.exists eq true")
        );
        assert!(!result.warnings.is_empty());
        assert!(result.warnings.iter().any(|w| w.starts_with("delimited:")));
    }
}
