cooklang-import 0.7.1

A tool for importing recipes into Cooklang format
Documentation
use cooklang_import::{
    convert_text_to_cooklang, extract_recipe_from_url, import_from_url, ImportResult,
    RecipeImporter,
};

/// Test Use Case 1: URL → Cooklang with builder API
/// This test is ignored by default since it requires network access
#[tokio::test]
#[ignore]
async fn test_builder_url_to_cooklang() {
    let result = RecipeImporter::builder()
        .url("https://www.bbcgoodfood.com/recipes/classic-cottage-pie")
        .build()
        .await;

    assert!(result.is_ok());
    match result.unwrap() {
        ImportResult::Cooklang(cooklang) => {
            assert!(!cooklang.is_empty());
            assert!(cooklang.contains(">>"));
        }
        ImportResult::Recipe(_) => panic!("Expected Cooklang result"),
    }
}

/// Test Use Case 2: URL → Recipe with builder API (extract only)
/// This test is ignored by default since it requires network access
#[tokio::test]
#[ignore]
async fn test_builder_url_to_recipe() {
    let result = RecipeImporter::builder()
        .url("https://www.bbcgoodfood.com/recipes/classic-cottage-pie")
        .extract_only()
        .build()
        .await;

    assert!(result.is_ok());
    match result.unwrap() {
        ImportResult::Recipe(recipe) => {
            assert!(!recipe.content.is_empty());
        }
        ImportResult::Cooklang(_) => panic!("Expected Recipe result"),
    }
}

/// Test Use Case 3: Content → Cooklang with builder API
/// This test is ignored by default since it requires OpenAI API key
#[tokio::test]
#[ignore]
async fn test_builder_content_to_cooklang() {
    let content = "2 eggs\n1 cup flour\n1/2 cup milk\n\nMix all ingredients together. Bake at 350°F for 30 minutes.";

    let result = RecipeImporter::builder().text(content).build().await;

    assert!(result.is_ok());
    match result.unwrap() {
        ImportResult::Cooklang(cooklang) => {
            assert!(!cooklang.is_empty());
            assert!(cooklang.contains(">>"));
        }
        ImportResult::Recipe(_) => panic!("Expected Cooklang result"),
    }
}

/// Test convenience function: import_from_url
/// This test is ignored by default since it requires network access
#[tokio::test]
#[ignore]
async fn test_convenience_import_from_url() {
    let result = import_from_url("https://www.bbcgoodfood.com/recipes/classic-cottage-pie").await;

    assert!(result.is_ok());
    let cooklang = result.unwrap();
    assert!(!cooklang.is_empty());
    assert!(cooklang.contains(">>"));
}

/// Test convenience function: extract_recipe_from_url
/// This test is ignored by default since it requires network access
#[tokio::test]
#[ignore]
async fn test_convenience_extract_recipe_from_url() {
    let result =
        extract_recipe_from_url("https://www.bbcgoodfood.com/recipes/classic-cottage-pie").await;

    assert!(result.is_ok());
    let recipe = result.unwrap();
    assert!(!recipe.content.is_empty());
}

/// Test convenience function: convert_text_to_cooklang with structured content
/// This test is ignored by default since it requires OpenAI API key
#[tokio::test]
#[ignore]
async fn test_convenience_convert_text_to_cooklang_with_content() {
    let content = "2 eggs\n1 cup flour\n1/2 cup milk\n\nMix all ingredients together. Bake at 350°F for 30 minutes.";

    let result = convert_text_to_cooklang(content).await;

    assert!(result.is_ok());
    let cooklang = result.unwrap();
    assert!(!cooklang.is_empty());
    assert!(cooklang.contains(">>"));
}

/// Test Use Case 4: Text → Cooklang with builder API
/// This test is ignored by default since it requires OpenAI API key
#[tokio::test]
#[ignore]
async fn test_builder_text_to_cooklang() {
    let recipe_text =
        "Take 2 eggs and 1 cup of flour. Mix them together and bake at 350°F for 30 minutes.";

    let result = RecipeImporter::builder().text(recipe_text).build().await;

    assert!(result.is_ok());
    match result.unwrap() {
        ImportResult::Cooklang(cooklang) => {
            assert!(!cooklang.is_empty());
            assert!(cooklang.contains(">>"));
        }
        ImportResult::Recipe(_) => panic!("Expected Cooklang result"),
    }
}

/// Test convenience function: convert_text_to_cooklang
/// This test is ignored by default since it requires OpenAI API key
#[tokio::test]
#[ignore]
async fn test_convenience_convert_text_to_cooklang() {
    use cooklang_import::convert_text_to_cooklang;

    let recipe_text =
        "Take 2 eggs and 1 cup of flour. Mix them together and bake at 350°F for 30 minutes.";

    let result = convert_text_to_cooklang(recipe_text).await;

    assert!(result.is_ok());
    let cooklang = result.unwrap();
    assert!(!cooklang.is_empty());
    assert!(cooklang.contains(">>"));
}

/// Test builder validation: no source specified
#[tokio::test]
async fn test_builder_no_source_error() {
    let result = RecipeImporter::builder().build().await;

    assert!(result.is_err());
    let err = result.unwrap_err();
    assert!(err.to_string().contains("No input source specified"));
}

/// Test builder validation: text + extract_only is invalid
#[tokio::test]
async fn test_builder_text_extract_only_error() {
    let result = RecipeImporter::builder()
        .text("content")
        .extract_only()
        .build()
        .await;

    assert!(result.is_err());
    let err = result.unwrap_err();
    assert!(err
        .to_string()
        .contains("Cannot use extract_only() with text input"));
}

/// Test builder validation: empty text
#[tokio::test]
async fn test_builder_empty_text_error_duplicate() {
    let result = RecipeImporter::builder().text("").build().await;

    assert!(result.is_err());
    let err = result.unwrap_err();
    assert!(err.to_string().contains("Recipe text cannot be empty"));
}

/// Test builder validation: original empty text test
#[tokio::test]
async fn test_builder_empty_text_error() {
    let result = RecipeImporter::builder().text("").build().await;

    assert!(result.is_err());
    let err = result.unwrap_err();
    assert!(err.to_string().contains("Recipe text cannot be empty"));
}

/// Test builder method chaining
#[tokio::test]
async fn test_builder_method_chaining() {
    use cooklang_import::LlmProvider;
    use std::time::Duration;

    // Just test that method chaining compiles and builds correctly
    // The actual execution would require network access
    let builder = RecipeImporter::builder()
        .url("https://example.com/recipe")
        .provider(LlmProvider::OpenAI)
        .timeout(Duration::from_secs(30));

    // We can't actually execute this without network access,
    // but we can verify the builder is constructed correctly
    assert!(std::mem::size_of_val(&builder) > 0);
}

/// Test builder with timeout that expires (this should fail fast)
#[tokio::test]
#[ignore] // Requires network
async fn test_builder_with_short_timeout() {
    use std::time::Duration;

    // Set a very short timeout (1ms) to ensure it fails
    let result = RecipeImporter::builder()
        .url("https://www.bbcgoodfood.com/recipes/classic-cottage-pie")
        .timeout(Duration::from_millis(1))
        .build()
        .await;

    // Should fail due to timeout
    assert!(result.is_err());
}

/// Test builder with custom provider (Anthropic)
#[tokio::test]
#[ignore] // Requires ANTHROPIC_API_KEY and config
async fn test_builder_with_anthropic_provider() {
    use cooklang_import::LlmProvider;

    let result = RecipeImporter::builder()
        .url("https://www.bbcgoodfood.com/recipes/classic-cottage-pie")
        .provider(LlmProvider::Anthropic)
        .build()
        .await;

    assert!(result.is_ok());
    match result.unwrap() {
        ImportResult::Cooklang(cooklang) => {
            assert!(!cooklang.is_empty());
            assert!(cooklang.contains(">>"));
        }
        ImportResult::Recipe(_) => panic!("Expected Cooklang result"),
    }
}

/// Test direct fetch_recipe_with_timeout function
#[tokio::test]
#[ignore] // Requires network
async fn test_fetch_recipe_with_timeout() {
    use std::time::Duration;

    let result = cooklang_import::fetch_recipe_with_timeout(
        "https://www.bbcgoodfood.com/recipes/classic-cottage-pie",
        Some(Duration::from_secs(30)),
    )
    .await;

    assert!(result.is_ok());
    let recipe = result.unwrap();
    assert!(!recipe.content.is_empty());
}

/// Test direct convert_recipe_with_provider function
#[tokio::test]
#[ignore] // Requires API key
async fn test_convert_recipe_with_provider() {
    use cooklang_import::{convert_recipe_with_provider, Recipe};

    let recipe = Recipe {
        name: "Test Recipe".to_string(),
        content: "2 eggs\n1 cup flour\n\nMix and bake".to_string(),
        ..Default::default()
    };

    // Test with default (OpenAI via env vars)
    let result = convert_recipe_with_provider(&recipe, None).await;
    assert!(result.is_ok());

    // Test with explicit OpenAI provider
    let result = convert_recipe_with_provider(&recipe, Some("openai")).await;
    assert!(result.is_ok());
}