pasta_lua 0.2.3

Pasta Lua - Lua integration for Pasta DSL
Documentation
//! Integration tests for PastaLoader startup sequence, config, and error handling.

use crate::common;

use common::{copy_dir_recursive, copy_fixture_to_temp, loader_fixtures_path, value_as_str};
use pasta_lua::loader::{LoaderError, PastaConfig, PastaLoader};
use std::path::PathBuf;
use tempfile::TempDir;

// ============================================================================
// Startup Sequence Tests
// ============================================================================

#[test]
fn test_load_minimal() {
    let temp = copy_fixture_to_temp("minimal");
    let runtime = PastaLoader::load(temp.path()).unwrap();

    // Verify runtime is usable
    let result = runtime.exec("return 1 + 1").unwrap();
    assert_eq!(result.as_i64(), Some(2));
}

#[test]
fn test_load_with_config() {
    let temp = copy_fixture_to_temp("with_config");
    let runtime = PastaLoader::load(temp.path()).unwrap();

    // Verify runtime is usable
    let result = runtime.exec("return 'hello'").unwrap();
    assert_eq!(value_as_str(&result).as_deref(), Some("hello"));
}

#[test]
fn test_load_with_custom_config() {
    let temp = copy_fixture_to_temp("with_custom_config");
    let runtime = PastaLoader::load(temp.path()).unwrap();

    // Verify @pasta_config module is accessible
    let result = runtime
        .exec(
            r#"
        local config = require("@pasta_config")
        return config.ghost_name
    "#,
        )
        .unwrap();
    assert_eq!(value_as_str(&result).as_deref(), Some("TestGhost"));
}

#[test]
fn test_pasta_config_nested_table() {
    let temp = copy_fixture_to_temp("with_custom_config");
    let runtime = PastaLoader::load(temp.path()).unwrap();

    // Verify nested table access
    let result = runtime
        .exec(
            r#"
        local config = require("@pasta_config")
        return config.user_data.key2
    "#,
        )
        .unwrap();
    assert_eq!(result.as_i64(), Some(42));
}

#[test]
fn test_pasta_config_deeply_nested() {
    let temp = copy_fixture_to_temp("with_custom_config");
    let runtime = PastaLoader::load(temp.path()).unwrap();

    // Verify deeply nested table access
    let result = runtime
        .exec(
            r#"
        local config = require("@pasta_config")
        return config.user_data.nested.inner
    "#,
        )
        .unwrap();
    assert_eq!(value_as_str(&result).as_deref(), Some("data"));
}

#[test]
fn test_pasta_config_excludes_loader() {
    let temp = copy_fixture_to_temp("with_custom_config");
    let runtime = PastaLoader::load(temp.path()).unwrap();

    // Verify [loader] section is NOT in @pasta_config
    let result = runtime
        .exec(
            r#"
        local config = require("@pasta_config")
        return config.loader
    "#,
        )
        .unwrap();
    assert!(result.is_nil());
}

#[test]
fn test_pasta_config_ghost_section() {
    let temp = copy_fixture_to_temp("with_ghost_config");
    let runtime = PastaLoader::load(temp.path()).unwrap();

    // Verify [ghost] section is accessible via pasta.config
    let result = runtime
        .exec(
            r#"
        local config = require("pasta.config")
        return config.get("ghost", "spot_newlines", 1.5)
    "#,
        )
        .unwrap();
    // with_ghost_config/pasta.toml has spot_newlines = 2.0
    let numeric_result = result
        .as_f64()
        .or_else(|| result.as_i64().map(|v| v as f64));
    assert_eq!(numeric_result, Some(2.0));
}

#[test]
fn test_pasta_config_returns_default_for_missing_section() {
    let temp = copy_fixture_to_temp("minimal");
    let runtime = PastaLoader::load(temp.path()).unwrap();

    // [ghost] section doesn't exist in minimal fixture
    let result = runtime
        .exec(
            r#"
        local config = require("pasta.config")
        return config.get("ghost", "spot_newlines", 1.5)
    "#,
        )
        .unwrap();
    // Should return default value 1.5
    assert_eq!(result.as_f64(), Some(1.5));
}

#[test]
fn test_shiori_act_uses_config_spot_newlines() {
    let temp = copy_fixture_to_temp("with_ghost_config");
    let runtime = PastaLoader::load(temp.path()).unwrap();

    // Verify SHIORI_ACT uses spot_newlines from config
    let result = runtime
        .exec(
            r#"
        local SHIORI_ACT = require("pasta.shiori.act")
        local actors = {
            sakura = { name = "さくら" },
            kero = { name = "うにゅう" },
        }
        local act = SHIORI_ACT.new(actors)
        -- 新アーキテクチャ: set_spot()でスポット位置を明示的に設定
        act:set_spot("sakura", 0)
        act:set_spot("kero", 1)
        act:talk(actors.sakura, "Hello")
        act:talk(actors.kero, "Hi")
        return act:build()
    "#,
        )
        .unwrap();

    let script = value_as_str(&result).unwrap();
    // spot_newlines = 2.0 → \n[200]
    assert!(
        script.contains("\\n[200]"),
        "Expected \\n[200] but got: {}",
        script
    );
}

// ============================================================================
// Package Path Tests
// ============================================================================

#[test]
fn test_package_path_set() {
    let temp = copy_fixture_to_temp("minimal");
    let runtime = PastaLoader::load(temp.path()).unwrap();

    // Verify package.path is set
    let result = runtime.exec("return package.path").unwrap();
    let path = value_as_str(&result).unwrap();

    // Should contain all search paths
    assert!(path.contains("profile/pasta/save/lua") || path.contains("profile\\pasta\\save\\lua"));
    assert!(path.contains("scripts"));
    assert!(
        path.contains("profile/pasta/cache/lua") || path.contains("profile\\pasta\\cache\\lua")
    );
    assert!(path.contains("scriptlibs"));
}

// ============================================================================
// Error Handling Tests
// ============================================================================

#[test]
fn test_load_nonexistent_directory() {
    // Use a path that definitely doesn't exist on any OS
    let temp = TempDir::new().unwrap();
    let nonexistent = temp.path().join("definitely_nonexistent_subdir");

    let result = PastaLoader::load(&nonexistent);

    assert!(result.is_err());
    match result {
        Err(LoaderError::DirectoryNotFound(path)) => {
            assert!(path.to_string_lossy().contains("definitely_nonexistent"));
        }
        _ => panic!("Expected DirectoryNotFound error"),
    }
}

#[test]
fn test_load_empty_dic() {
    // Create a temporary directory with no .pasta files
    let temp = TempDir::new().unwrap();
    let base_dir = temp.path();

    std::fs::create_dir_all(base_dir.join("dic/empty")).unwrap();

    // Create minimal pasta.toml
    std::fs::write(base_dir.join("pasta.toml"), "[loader]\ndebug_mode = true\n").unwrap();

    // Copy pasta_scripts directory for pasta module
    let crate_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    let scripts_src = crate_root.join("pasta_scripts");
    let scripts_dst = base_dir.join("pasta_scripts");
    if scripts_src.exists() {
        std::fs::create_dir_all(&scripts_dst).unwrap();
        copy_dir_recursive(&scripts_src, &scripts_dst).unwrap();
    }

    // Copy scriptlibs directory
    let scriptlibs_src = crate_root.join("scriptlibs");
    let scriptlibs_dst = base_dir.join("scriptlibs");
    if scriptlibs_src.exists() {
        std::fs::create_dir_all(&scriptlibs_dst).unwrap();
        copy_dir_recursive(&scriptlibs_src, &scriptlibs_dst).unwrap();
    }

    // Should succeed but with warning (no files found)
    let runtime = PastaLoader::load(base_dir).unwrap();

    // Runtime should still be usable
    let result = runtime.exec("return 42").unwrap();
    assert_eq!(result.as_i64(), Some(42));
}

#[test]
fn test_load_missing_pasta_toml() {
    // Base dir exists but has no pasta.toml -> ConfigNotFound surfaced via load()
    let temp = TempDir::new().unwrap();

    let result = PastaLoader::load(temp.path());

    assert!(result.is_err());
    match result {
        Err(LoaderError::ConfigNotFound(path)) => {
            assert_eq!(path, temp.path().join("pasta.toml"));
        }
        Err(other) => panic!("Expected ConfigNotFound error, got: {}", other),
        Ok(_) => panic!("Expected ConfigNotFound error, got Ok"),
    }
}

#[test]
fn test_load_custom_pasta_patterns() {
    // Custom [loader] pasta_patterns are honored, and the derived .lua pattern
    // (".pasta" -> ".lua") discovers passthrough .lua files in the same dirs.
    let temp = TempDir::new().unwrap();
    let base_dir = temp.path();

    std::fs::write(
        base_dir.join("pasta.toml"),
        "[loader]\ndebug_mode = true\npasta_patterns = [\"dic/*/*.pasta\", \"extra/*.pasta\"]\n",
    )
    .unwrap();

    std::fs::create_dir_all(base_dir.join("dic/talk")).unwrap();
    std::fs::write(
        base_dir.join("dic/talk/hello.pasta"),
        "*テスト\n  ゴースト:「こんにちは」\n",
    )
    .unwrap();

    std::fs::create_dir_all(base_dir.join("extra")).unwrap();
    std::fs::write(
        base_dir.join("extra/foo.pasta"),
        "*エクストラ\n  ゴースト:「extra」\n",
    )
    .unwrap();
    std::fs::write(base_dir.join("extra/bar.lua"), "return { bar = true }\n").unwrap();

    // Copy runtime deps (pasta_scripts, scriptlibs)
    let crate_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    for dir_name in &["pasta_scripts", "scriptlibs"] {
        let src = crate_root.join(dir_name);
        let dst = base_dir.join(dir_name);
        if src.exists() {
            std::fs::create_dir_all(&dst).unwrap();
            copy_dir_recursive(&src, &dst).unwrap();
        }
    }

    let _runtime = PastaLoader::load(base_dir).unwrap();

    // All three modules must be registered in scene_dic.lua
    let scene_dic_path = base_dir.join("profile/pasta/cache/lua/pasta/scene_dic.lua");
    let scene_dic = std::fs::read_to_string(&scene_dic_path).unwrap();
    assert!(
        scene_dic.contains("pasta.scene.talk.hello"),
        "default-pattern module missing: {}",
        scene_dic
    );
    assert!(
        scene_dic.contains("pasta.scene.extra.foo"),
        "custom-pattern .pasta module missing: {}",
        scene_dic
    );
    assert!(
        scene_dic.contains("pasta.scene.extra.bar"),
        "derived .lua pattern module missing: {}",
        scene_dic
    );
}

// ============================================================================
// Config Loading Tests
// ============================================================================

#[test]
fn test_config_load_not_found() {
    let temp = TempDir::new().unwrap();
    let result = PastaConfig::load(temp.path());

    assert!(result.is_err());
    match result.unwrap_err() {
        LoaderError::ConfigNotFound(path) => {
            assert_eq!(path, temp.path().join("pasta.toml"));
        }
        _ => panic!("Expected ConfigNotFound error"),
    }
}

#[test]
fn test_config_load_with_file() {
    let base_dir = loader_fixtures_path("with_custom_config");
    let config = PastaConfig::load(&base_dir).unwrap();

    assert!(config.loader.debug_mode);
    assert_eq!(
        config.custom_fields.get("ghost_name"),
        Some(&toml::Value::String("TestGhost".to_string()))
    );
}