ryo-app 0.1.0

[preview] Application layer for RYO - Project management, Intent handling, API
Documentation
//! Pattern Integration Tests
//!
//! Tests design pattern transformations.

use std::path::PathBuf;

use rstest::{fixture, rstest};
use ryo_app::{AnalysisContext, BlueprintExecutor, Goal, ParallelBlueprint, Planner};
use ryo_test::harness::{source_diff, sources_equal};

// =============================================================================
// Fixtures
// =============================================================================

#[fixture]
fn pattern_source() -> &'static str {
    include_str!("fixtures/patterns.rs")
}

#[fixture]
fn pattern_ctx(pattern_source: &'static str) -> PatternTestContext {
    PatternTestContext::new(pattern_source)
}

// =============================================================================
// Test Context
// =============================================================================

pub struct PatternTestContext {
    _temp_dir: tempfile::TempDir,
    workspace_root: PathBuf,
    source: String,
}

impl PatternTestContext {
    pub fn new(source: &str) -> Self {
        let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
        let src_dir = temp_dir.path().join("src");
        std::fs::create_dir_all(&src_dir).expect("Failed to create src dir");

        let lib_rs = src_dir.join("lib.rs");
        std::fs::write(&lib_rs, source).expect("Failed to write lib.rs");

        let cargo_toml = temp_dir.path().join("Cargo.toml");
        std::fs::write(
            &cargo_toml,
            r#"[package]
name = "test_crate"
version = "0.1.0"
edition = "2021"
"#,
        )
        .expect("Failed to write Cargo.toml");

        Self {
            workspace_root: temp_dir.path().to_path_buf(),
            _temp_dir: temp_dir,
            source: source.to_string(),
        }
    }

    pub fn execute(&self, goal: &Goal) -> Result<String, String> {
        // Create AnalysisContext first to get registry for planning
        let mut ctx = AnalysisContext::from_workspace_root(&self.workspace_root)
            .map_err(|e| format!("Context creation error: {e}"))?;

        let specs = Planner::plan(goal, Some(&ctx.registry))
            .map_err(|e| format!("Planning failed: {}", e))?;

        if specs.is_empty() {
            return Ok(self.source.clone());
        }

        let blueprint = ParallelBlueprint::from_mutations(specs);
        if blueprint.needs_escalation() {
            return Err("Blueprint has conflicts".to_string());
        }

        let executor = BlueprintExecutor::default();
        let result = executor.execute_v2(&blueprint, &mut ctx);

        if !result.success {
            return Err(result.error.unwrap_or_else(|| "Unknown error".to_string()));
        }

        // Sync files after execute_v2 (required for ctx.files() to see changes)
        BlueprintExecutor::sync_files_and_rebuild(&result, &mut ctx).map_err(|e| e.to_string())?;

        ctx.files()
            .iter()
            .next()
            .map(|(_, file)| file.to_source().map_err(|e| e.to_string()))
            .ok_or_else(|| "No files".to_string())?
    }

    pub fn assert_output(&self, goal: &Goal, expected: &str) {
        let result = self.execute(goal).expect("Transformation failed");
        if !sources_equal(expected, &result) {
            let diff = source_diff(expected, &result);
            eprintln!("=== ACTUAL OUTPUT ===\n{}\n=== END ===", result);
            panic!("Mismatch!\n\nDiff:\n{}", diff);
        }
    }
}

// =============================================================================
// Tests
// =============================================================================

#[rstest]
fn test_add_default_derive(pattern_ctx: PatternTestContext) {
    let goal: Goal = serde_json::from_str(
        r#"{
        "intents": [{
            "type": "AddDerive",
            "target_name": "Config",
            "derives": ["Default"]
        }]
    }"#,
    )
    .unwrap();
    let expected = include_str!("snapshots/add-default-derive.snap");
    pattern_ctx.assert_output(&goal, expected);
}

#[rstest]
fn test_add_eq_derive(pattern_ctx: PatternTestContext) {
    let goal: Goal = serde_json::from_str(
        r#"{
        "intents": [{
            "type": "AddDerive",
            "target_name": "Token",
            "derives": ["PartialEq", "Eq"]
        }]
    }"#,
    )
    .unwrap();
    let expected = include_str!("snapshots/add-eq-derive.snap");
    pattern_ctx.assert_output(&goal, expected);
}

#[rstest]
fn test_add_hash_derive(pattern_ctx: PatternTestContext) {
    let goal: Goal = serde_json::from_str(
        r#"{
        "intents": [{
            "type": "AddDerive",
            "target_name": "CacheKey",
            "derives": ["Hash"]
        }]
    }"#,
    )
    .unwrap();
    let expected = include_str!("snapshots/add-hash-derive.snap");
    pattern_ctx.assert_output(&goal, expected);
}

#[rstest]
fn test_add_constructor(pattern_ctx: PatternTestContext) {
    let goal: Goal = serde_json::from_str(
        r#"{
        "intents": [{
            "type": "AddMethod",
            "target_type": "Counter",
            "method_name": "new",
            "params": [["max", "i32"]],
            "return_type": "Self",
            "body": "Self { value: 0, max }",
            "is_pub": true
        }]
    }"#,
    )
    .unwrap();
    // Note: add-constructor.snap not found, using expected pattern
    let result = pattern_ctx.execute(&goal).expect("Transformation failed");
    assert!(result.contains("pub fn new"));
}

#[rstest]
fn test_add_from_impl(pattern_ctx: PatternTestContext) {
    let goal: Goal = serde_json::from_str(
        r#"{
        "intents": [{
            "type": "AddItem",
            "symbol_path": "test_crate",
            "item_kind": "Impl",
            "content": "impl From<u64> for UserId { fn from(id: u64) -> Self { UserId(id) } }"
        }]
    }"#,
    )
    .unwrap();
    let expected = include_str!("snapshots/add-from-impl.snap");
    pattern_ctx.assert_output(&goal, expected);
}