pasta_lua 0.2.2

Pasta Lua - Lua integration for Pasta DSL
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
//! E2E integration tests for the finalize_scene() function.
//!
//! These tests verify the complete flow:
//! 1. Transpile .pasta source to Lua
//! 2. Execute Lua code (registers scenes/words in Lua registries)
//! 3. Call finalize_scene() to build SearchContext from Lua registries
//! 4. Verify search operations work correctly
//!
//! # Requirements Coverage
//! - Req 1.1, 1.2, 1.4, 8.2, 8.4: Scene dictionary collection E2E
//! - Req 2.1, 2.2, 2.6, 2.7, 9.3, 9.5: Word dictionary collection E2E
//! - Req 1.3, 6.1, 6.2, 6.3: Error handling
//! - Req 5.2, 5.3, 5.4: Initialization timing control

use crate::common;
use common::e2e_helpers::{create_runtime_with_finalize, transpile};
use std::path::PathBuf;

// ============================================================================
// Task 8.1: Scene Dictionary Collection E2E Tests
// ============================================================================

/// Test 8.1.1: Basic scene collection and search using existing fixture
#[test]
fn test_scene_collection_basic() {
    let lua = create_runtime_with_finalize().unwrap();

    // Use sample.pasta fixture (known good format)
    let pasta_source = include_str!("../fixtures/sample.pasta");
    let lua_code = transpile(pasta_source);

    // Execute transpiled code
    lua.load(&lua_code).exec().unwrap();

    // Call finalize_scene
    lua.load("require('pasta').finalize_scene()")
        .exec()
        .unwrap();

    // Verify scene can be searched
    let result: (String, String) = lua
        .load(
            r#"
        local SEARCH = require "@pasta_search"
        return SEARCH:search_scene("メイン", nil)
    "#,
        )
        .eval()
        .unwrap();

    assert!(
        result.0.starts_with("メイン"),
        "Global name should start with 'メイン', got: {}",
        result.0
    );
    assert_eq!(result.1, "__start__", "Local name should be '__start__'");
}

/// Test 8.1.2: Multiple global scenes with same name (counter test)
#[test]
fn test_scene_collection_multiple_global_scenes() {
    let lua = create_runtime_with_finalize().unwrap();

    // Use sample.pasta which has multiple "会話分岐" scenes (global + local)
    let pasta_source = include_str!("../fixtures/sample.pasta");
    let lua_code = transpile(pasta_source);

    // Execute transpiled code
    lua.load(&lua_code).exec().unwrap();

    // Call finalize_scene
    lua.load("require('pasta').finalize_scene()")
        .exec()
        .unwrap();

    // Verify 会話分岐 scene is registered
    let result: (String, String) = lua
        .load(
            r#"
        local SEARCH = require "@pasta_search"
        return SEARCH:search_scene("会話分岐", nil)
    "#,
        )
        .eval()
        .unwrap();

    // Should find scene with name containing '会話分岐'
    assert!(
        result.0.contains("会話分岐"),
        "Should find scene with name containing '会話分岐', got: {}",
        result.0
    );
}

/// Test 8.1.3: Local scenes under global scene
#[test]
fn test_scene_collection_local_scenes() {
    let lua = create_runtime_with_finalize().unwrap();

    // Use sample.pasta which has local scenes like "グローバル単語呼び出し"
    let pasta_source = include_str!("../fixtures/sample.pasta");
    let lua_code = transpile(pasta_source);

    // Execute transpiled code
    lua.load(&lua_code).exec().unwrap();

    // Call finalize_scene
    lua.load("require('pasta').finalize_scene()")
        .exec()
        .unwrap();

    // Verify @pasta_search module is available
    let search_exists: bool = lua
        .load(
            r#"
        local ok, SEARCH = pcall(require, "@pasta_search")
        return ok and SEARCH ~= nil
    "#,
        )
        .eval()
        .unwrap();

    assert!(search_exists, "@pasta_search module should be available");
}

/// Test: Local scene search via finalize path (Req 4.2, 4.3)
///
/// Verifies that after transpile → execute → finalize_scene(),
/// local scenes can be resolved via search_scene with a parent scope.
#[test]
fn test_scene_collection_local_scene_search() {
    let lua = create_runtime_with_finalize().unwrap();

    let pasta_source = include_str!("../fixtures/sample.pasta");
    let lua_code = transpile(pasta_source);

    lua.load(&lua_code).exec().unwrap();
    lua.load("require('pasta').finalize_scene()")
        .exec()
        .unwrap();

    // Search for local scene "グローバル単語呼び出し" within parent "メイン"
    // Note: PASTA.create_scene("メイン") generates global_name "メイン1"
    let result: (String, String) = lua
        .load(
            r#"
        local SEARCH = require "@pasta_search"
        local SCENE = require "pasta.scene"
        local gn = SCENE.get_global_table("メイン1").__global_name__
        return SEARCH:search_scene("グローバル単語呼び出し", gn)
    "#,
        )
        .eval()
        .unwrap();

    assert!(
        result.0.contains("メイン"),
        "Global name should contain 'メイン', got: {}",
        result.0
    );
    assert!(
        result.1.contains("グローバル単語呼び出し"),
        "Local name should contain 'グローバル単語呼び出し', got: {}",
        result.1
    );
}

/// Test: Local scene prefix search via finalize path (Req 4.2)
///
/// Verifies that register_global_raw → SceneTable prefix matching
/// correctly resolves local scenes with a shared prefix.
#[test]
fn test_scene_collection_local_scene_prefix_search() {
    let lua = create_runtime_with_finalize().unwrap();

    let pasta_source = include_str!("../fixtures/sample.pasta");
    let lua_code = transpile(pasta_source);

    lua.load(&lua_code).exec().unwrap();
    lua.load("require('pasta').finalize_scene()")
        .exec()
        .unwrap();

    // Prefix search "会話分岐" should find one of the local scenes (会話分岐_1 or 会話分岐_2)
    // Note: PASTA.create_scene("メイン") generates global_name "メイン1"
    let result: (String, String) = lua
        .load(
            r#"
        local SEARCH = require "@pasta_search"
        local SCENE = require "pasta.scene"
        local gn = SCENE.get_global_table("メイン1").__global_name__
        return SEARCH:search_scene("会話分岐", gn)
    "#,
        )
        .eval()
        .unwrap();

    assert!(
        result.0.contains("メイン"),
        "Global name should contain 'メイン', got: {}",
        result.0
    );
    assert!(
        result.1.contains("会話分岐"),
        "Local name should contain '会話分岐', got: {}",
        result.1
    );
}

// ============================================================================
// Task 8.2: Word Dictionary Collection E2E Tests
// ============================================================================

/// Test 8.2.1: Global word collection
#[test]
fn test_word_collection_global() {
    let lua = create_runtime_with_finalize().unwrap();

    // Use sample.pasta which has global word "挨拶"
    let pasta_source = include_str!("../fixtures/sample.pasta");
    let lua_code = transpile(pasta_source);

    // Execute transpiled code
    lua.load(&lua_code).exec().unwrap();

    // Call finalize_scene
    lua.load("require('pasta').finalize_scene()")
        .exec()
        .unwrap();

    // Verify word can be searched
    let result: mlua::Value = lua
        .load(
            r#"
        local SEARCH = require "@pasta_search"
        return SEARCH:search_word("挨拶", nil)
    "#,
        )
        .eval()
        .unwrap();

    assert!(result.is_string(), "Word search should return a string");
    let word = result.as_string().map(|s| s.to_string_lossy()).unwrap();
    assert!(
        word == "こんにちは" || word == "やあ" || word == "ハロー",
        "Word should be one of the registered values, got: {}",
        word
    );
}

/// Test 8.2.2: Local word collection
#[test]
fn test_word_collection_local() {
    let lua = create_runtime_with_finalize().unwrap();

    // Use sample.pasta which has local word "場所"
    let pasta_source = include_str!("../fixtures/sample.pasta");
    let lua_code = transpile(pasta_source);

    // Execute transpiled code
    lua.load(&lua_code).exec().unwrap();

    // Call finalize_scene
    lua.load("require('pasta').finalize_scene()")
        .exec()
        .unwrap();

    // Verify @pasta_search is available after finalize
    let search_available: bool = lua
        .load(
            r#"
        local ok = pcall(require, "@pasta_search")
        return ok
    "#,
        )
        .eval()
        .unwrap();

    assert!(
        search_available,
        "@pasta_search should be available after finalize_scene"
    );
}

/// Test 8.2.3: Builder pattern API (method chaining)
#[test]
fn test_word_builder_pattern() {
    let lua = create_runtime_with_finalize().unwrap();

    // Test builder pattern directly in Lua
    let result: bool = lua
        .load(
            r#"
        local PASTA = require("pasta")
        
        -- Create global word using builder pattern
        PASTA.create_word("テスト"):entry("値1", "値2", "値3")
        
        -- Call finalize_scene
        PASTA.finalize_scene()
        
        -- Verify word is searchable
        local SEARCH = require "@pasta_search"
        local word = SEARCH:search_word("テスト", nil)
        return word == "値1" or word == "値2" or word == "値3"
    "#,
        )
        .eval()
        .unwrap();

    assert!(result, "Builder pattern should register word correctly");
}

// ============================================================================
// Task 8.3: Error Handling Tests
// ============================================================================

/// Test 8.3.1: Empty registry finalize (warning log + empty SearchContext)
#[test]
fn test_empty_registry_finalize() {
    let lua = create_runtime_with_finalize().unwrap();

    // Call finalize_scene without any scenes or words
    let result: bool = lua
        .load(
            r#"
        local PASTA = require("pasta")
        return PASTA.finalize_scene()
    "#,
        )
        .eval()
        .unwrap();

    assert!(
        result,
        "finalize_scene should return true even with empty registry"
    );

    // Verify @pasta_search is available
    let search_available: bool = lua
        .load(
            r#"
        local ok = pcall(require, "@pasta_search")
        return ok
    "#,
        )
        .eval()
        .unwrap();

    assert!(
        search_available,
        "@pasta_search should be available even with empty registry"
    );
}

/// Test 8.3.2: Scene not found returns nil
#[test]
fn test_scene_not_found() {
    let lua = create_runtime_with_finalize().unwrap();

    // Use sample.pasta then search for non-existent scene
    let pasta_source = include_str!("../fixtures/sample.pasta");
    let lua_code = transpile(pasta_source);

    lua.load(&lua_code).exec().unwrap();
    lua.load("require('pasta').finalize_scene()")
        .exec()
        .unwrap();

    let result: bool = lua
        .load(
            r#"
        local SEARCH = require "@pasta_search"
        local global_name = SEARCH:search_scene("存在しないシーン", nil)
        return global_name == nil
    "#,
        )
        .eval()
        .unwrap();

    assert!(result, "Non-existent scene search should return nil");
}

/// Test 8.3.3: Word not found returns nil
#[test]
fn test_word_not_found() {
    let lua = create_runtime_with_finalize().unwrap();

    // Finalize empty registry
    lua.load("require('pasta').finalize_scene()")
        .exec()
        .unwrap();

    let result: bool = lua
        .load(
            r#"
        local SEARCH = require "@pasta_search"
        local word = SEARCH:search_word("存在しない単語", nil)
        return word == nil
    "#,
        )
        .eval()
        .unwrap();

    assert!(result, "Non-existent word search should return nil");
}

// ============================================================================
// Task 8.4: Initialization Timing Control Tests (Optional)
// ============================================================================

/// Test 8.4.1: Multiple finalize_scene calls (SearchContext rebuild)
#[test]
fn test_multiple_finalize_calls() {
    let lua = create_runtime_with_finalize().unwrap();

    // First: create word via PASTA API and finalize
    lua.load(
        r#"
        local PASTA = require("pasta")
        PASTA.create_word("単語A"):entry("値A1", "値A2")
        PASTA.finalize_scene()
    "#,
    )
    .exec()
    .unwrap();

    // Verify word A is searchable
    let word_a_found: bool = lua
        .load(
            r#"
        local SEARCH = require "@pasta_search"
        local word = SEARCH:search_word("単語A", nil)
        return word ~= nil
    "#,
        )
        .eval()
        .unwrap();

    assert!(word_a_found, "単語A should be found after first finalize");

    // Second: create another word and finalize again
    lua.load(
        r#"
        local PASTA = require("pasta")
        PASTA.create_word("単語B"):entry("値B1", "値B2")
        PASTA.finalize_scene()
    "#,
    )
    .exec()
    .unwrap();

    // Verify both words are searchable after rebuild
    let both_found: bool = lua
        .load(
            r#"
        local SEARCH = require "@pasta_search"
        local a = SEARCH:search_word("単語A", nil)
        local b = SEARCH:search_word("単語B", nil)
        return a ~= nil and b ~= nil
    "#,
        )
        .eval()
        .unwrap();

    assert!(
        both_found,
        "Both words should be found after second finalize"
    );
}

/// Test 8.4.2: @pasta_search not available before finalize_scene
#[test]
fn test_search_unavailable_before_finalize() {
    use mlua::{Lua, StdLib};

    // Create fresh Lua without finalize_scene registration
    let lua = unsafe { Lua::unsafe_new_with(StdLib::ALL_SAFE, mlua::LuaOptions::default()) };

    // Configure package.path
    let scripts_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .join("pasta_scripts")
        .to_string_lossy()
        .replace('\\', "/");

    lua.load(&format!(
        r#"
        package.path = "{scripts_dir}/?.lua;{scripts_dir}/?/init.lua;" .. package.path
        "#
    ))
    .exec()
    .unwrap();

    // Try to require @pasta_search before any setup
    let result: bool = lua
        .load(
            r#"
        local ok = pcall(require, "@pasta_search")
        return ok
    "#,
        )
        .eval()
        .unwrap();

    assert!(
        !result,
        "@pasta_search should NOT be available before finalize_scene"
    );
}

/// Test 8.4.3: Full E2E flow with transpiled pasta
#[test]
fn test_full_e2e_flow() {
    let lua = create_runtime_with_finalize().unwrap();

    // Use sample.pasta for complete E2E test
    let pasta_source = include_str!("../fixtures/sample.pasta");
    let lua_code = transpile(pasta_source);

    // Execute transpiled code
    lua.load(&lua_code).exec().unwrap();

    // Call finalize_scene
    lua.load("require('pasta').finalize_scene()")
        .exec()
        .unwrap();

    // Verify everything is searchable
    let all_ok: bool = lua
        .load(
            r#"
        local SEARCH = require "@pasta_search"
        
        -- Check scenes
        local main_ok = SEARCH:search_scene("メイン", nil) ~= nil
        local talk_ok = SEARCH:search_scene("会話分岐", nil) ~= nil
        
        -- Check words
        local greet_ok = SEARCH:search_word("挨拶", nil) ~= nil
        
        return main_ok and talk_ok and greet_ok
    "#,
        )
        .eval()
        .unwrap();

    assert!(all_ok, "All scenes and words should be searchable");
}