cridecoder 0.2.2

CRI codec library for ACB/AWB, HCA audio, and USM video extraction
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
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
//! Integration tests for cridecoder
//!
//! These tests require the test fixture files:
//! - se_0126_01.acb (ACB audio container)
//! - 0703.usm (USM video container)
//!
//! Tests are gated behind `#[ignore]` if the files are not present.

use std::fs;
use std::io::Cursor;
use std::path::Path;

/// Test that ACB extraction works with a real .acb file
#[test]
fn test_acb_extraction() {
    let acb_path = Path::new("se_0126_01.acb");
    if !acb_path.exists() {
        eprintln!("Skipping test: se_0126_01.acb not found");
        return;
    }

    let dir = tempfile::tempdir().unwrap();
    let result = cridecoder::extract_acb_from_file(acb_path, dir.path());

    let tracks = result.expect("ACB extraction should not error");
    let tracks = tracks.expect("Should find tracks in ACB");
    assert!(!tracks.is_empty(), "Should extract at least one track");

    // Verify extracted files exist and are HCA
    for track_path in &tracks {
        let p = Path::new(track_path);
        assert!(p.exists(), "Extracted file should exist: {}", track_path);
        let meta = fs::metadata(p).unwrap();
        assert!(
            meta.len() > 0,
            "Extracted file should not be empty: {}",
            track_path
        );

        // Verify HCA signature (first 4 bytes should match HCA magic masked)
        let data = fs::read(p).unwrap();
        assert!(data.len() >= 8, "HCA file too small: {}", track_path);
        // HCA files start with 'HCA\0' (0x48434100) masked by 0x7F7F7F7F
        let sig = u32::from_be_bytes([data[0], data[1], data[2], data[3]]) & 0x7F7F7F7F;
        assert_eq!(sig, 0x48434100, "Should be HCA file: {}", track_path);
    }
}

/// Test that ACB extraction reports the correct number of tracks
#[test]
fn test_acb_track_count() {
    let acb_path = Path::new("se_0126_01.acb");
    if !acb_path.exists() {
        eprintln!("Skipping test: se_0126_01.acb not found");
        return;
    }

    let dir = tempfile::tempdir().unwrap();
    let tracks = cridecoder::extract_acb_from_file(acb_path, dir.path())
        .unwrap()
        .unwrap();

    // se_0126_01.acb is known to have 4 tracks
    assert_eq!(tracks.len(), 4, "Expected 4 tracks from se_0126_01.acb");
}

/// Test that HCA files extracted from ACB can be decoded to WAV
#[test]
fn test_hca_decode_to_wav() {
    let acb_path = Path::new("se_0126_01.acb");
    if !acb_path.exists() {
        eprintln!("Skipping test: se_0126_01.acb not found");
        return;
    }

    let dir = tempfile::tempdir().unwrap();
    let tracks = cridecoder::extract_acb_from_file(acb_path, dir.path())
        .unwrap()
        .unwrap();

    // Decode the first HCA track
    let hca_path = &tracks[0];
    let mut decoder = cridecoder::HcaDecoder::from_file(hca_path).expect("Should open HCA file");

    let info = decoder.info();
    assert!(info.sampling_rate > 0, "Sample rate should be > 0");
    assert!(info.channel_count > 0, "Channel count should be > 0");
    assert!(info.block_count > 0, "Block count should be > 0");
    assert!(info.block_size > 0, "Block size should be > 0");
    assert_eq!(
        info.samples_per_block, 1024,
        "Samples per block should be 1024"
    );

    // Decode to WAV
    let mut wav_buf = Vec::new();
    decoder
        .decode_to_wav(&mut wav_buf)
        .expect("Should decode HCA to WAV");

    // Verify WAV header
    assert!(
        wav_buf.len() > 44,
        "WAV output should be > 44 bytes (header)"
    );
    assert_eq!(&wav_buf[0..4], b"RIFF", "Should start with RIFF magic");
    assert_eq!(&wav_buf[8..12], b"WAVE", "Should have WAVE marker");
    assert_eq!(&wav_buf[12..16], b"fmt ", "Should have fmt chunk");
    assert_eq!(&wav_buf[36..40], b"data", "Should have data chunk");
}

/// Test that direct PCM16 chunk decoding matches WAV payload bytes
#[test]
fn test_hca_decode_to_pcm16_chunks_matches_wav_data() {
    let acb_path = Path::new("se_0126_01.acb");
    if !acb_path.exists() {
        eprintln!("Skipping test: se_0126_01.acb not found");
        return;
    }

    let dir = tempfile::tempdir().unwrap();
    let tracks = cridecoder::extract_acb_from_file(acb_path, dir.path())
        .unwrap()
        .unwrap();
    let hca_path = &tracks[0];

    let mut wav_decoder =
        cridecoder::HcaDecoder::from_file(hca_path).expect("Should open HCA file");
    let mut wav_buf = Vec::new();
    wav_decoder
        .decode_to_wav(&mut wav_buf)
        .expect("Should decode HCA to WAV");

    let mut pcm_decoder =
        cridecoder::HcaDecoder::from_file(hca_path).expect("Should open HCA file");
    let mut pcm_bytes = Vec::new();
    pcm_decoder
        .decode_to_pcm16_chunks(|samples| {
            for sample in samples {
                pcm_bytes.extend_from_slice(&sample.to_le_bytes());
            }
            Ok(())
        })
        .expect("Should decode HCA to PCM chunks");

    assert_eq!(&wav_buf[44..], pcm_bytes.as_slice());
}

/// Test that ALL HCA tracks extracted from ACB can be decoded to valid WAV
#[test]
fn test_all_hca_export_to_wav() {
    let acb_path = Path::new("se_0126_01.acb");
    if !acb_path.exists() {
        eprintln!("Skipping test: se_0126_01.acb not found");
        return;
    }

    let dir = tempfile::tempdir().unwrap();
    let tracks = cridecoder::extract_acb_from_file(acb_path, dir.path())
        .unwrap()
        .unwrap();

    assert_eq!(tracks.len(), 4, "Should have 4 tracks");

    for (i, track_path) in tracks.iter().enumerate() {
        let p = Path::new(track_path);
        assert!(p.exists(), "Track {} should exist: {}", i, track_path);
        assert!(
            track_path.ends_with(".hca"),
            "Track {} should be .hca: {}",
            i,
            track_path
        );

        // Decode each HCA to WAV
        let mut decoder = cridecoder::HcaDecoder::from_file(track_path)
            .unwrap_or_else(|e| panic!("Track {} HCA open failed: {:?}", i, e));

        let info = decoder.info().clone();
        assert!(info.sampling_rate > 0, "Track {} sample rate > 0", i);
        assert!(info.channel_count > 0, "Track {} channels > 0", i);
        assert!(info.block_count > 0, "Track {} block count > 0", i);

        let mut wav_buf = Vec::new();
        decoder
            .decode_to_wav(&mut wav_buf)
            .unwrap_or_else(|e| panic!("Track {} WAV decode failed: {:?}", i, e));

        // Verify WAV output
        assert!(
            wav_buf.len() > 44,
            "Track {} WAV too small: {}",
            i,
            wav_buf.len()
        );
        assert_eq!(&wav_buf[0..4], b"RIFF", "Track {} RIFF magic", i);
        assert_eq!(&wav_buf[8..12], b"WAVE", "Track {} WAVE marker", i);

        // Verify WAV data size matches header
        let data_size = u32::from_le_bytes([wav_buf[40], wav_buf[41], wav_buf[42], wav_buf[43]]);
        assert_eq!(
            wav_buf.len() - 44,
            data_size as usize,
            "Track {} WAV data size mismatch",
            i
        );

        eprintln!(
            "Track {}: {} -> WAV {} bytes (rate={}, ch={}, blocks={})",
            i,
            p.file_name().unwrap().to_string_lossy(),
            wav_buf.len(),
            info.sampling_rate,
            info.channel_count,
            info.block_count
        );
    }
}

/// Test HCA decoder info metadata
#[test]
fn test_hca_decoder_info() {
    let acb_path = Path::new("se_0126_01.acb");
    if !acb_path.exists() {
        eprintln!("Skipping test: se_0126_01.acb not found");
        return;
    }

    let dir = tempfile::tempdir().unwrap();
    let tracks = cridecoder::extract_acb_from_file(acb_path, dir.path())
        .unwrap()
        .unwrap();

    let decoder = cridecoder::HcaDecoder::from_file(&tracks[0]).unwrap();
    let info = decoder.info();

    // Known values for se_0126_01.hca
    assert_eq!(info.sampling_rate, 44100);
    assert_eq!(info.channel_count, 2);
    assert_eq!(info.block_count, 4931);
    assert_eq!(info.block_size, 341);
    assert_eq!(info.encoder_delay, 128);
    assert_eq!(info.samples_per_block, 1024);
}

/// Test HCA decode_all returns expected sample count
#[test]
fn test_hca_decode_all_samples() {
    let acb_path = Path::new("se_0126_01.acb");
    if !acb_path.exists() {
        eprintln!("Skipping test: se_0126_01.acb not found");
        return;
    }

    let dir = tempfile::tempdir().unwrap();
    let tracks = cridecoder::extract_acb_from_file(acb_path, dir.path())
        .unwrap()
        .unwrap();

    let mut decoder = cridecoder::HcaDecoder::from_file(&tracks[0]).unwrap();
    let info = decoder.info().clone();
    let samples = decoder.decode_all().expect("Should decode all samples");

    // Total samples should be approximately (block_count * samples_per_block - encoder_delay) * channels
    let expected_total = ((info.block_count * info.samples_per_block as u32) - info.encoder_delay)
        as usize
        * info.channel_count as usize;
    assert_eq!(samples.len(), expected_total, "Sample count mismatch");

    // Verify that samples are in a reasonable range
    let max_val = samples.iter().copied().fold(0.0f32, f32::max);
    let min_val = samples.iter().copied().fold(0.0f32, f32::min);
    assert!(
        max_val <= 1.5,
        "Max sample value should be reasonable: {}",
        max_val
    );
    assert!(
        min_val >= -1.5,
        "Min sample value should be reasonable: {}",
        min_val
    );
}

/// Test USM extraction works with a real .usm file
#[test]
fn test_usm_extraction() {
    let usm_path = Path::new("0703.usm");
    if !usm_path.exists() {
        eprintln!("Skipping test: 0703.usm not found");
        return;
    }

    let dir = tempfile::tempdir().unwrap();
    let result = cridecoder::extract_usm_file(usm_path, dir.path(), None, false);

    let files = result.expect("USM extraction should not error");
    assert!(!files.is_empty(), "Should extract at least one file");

    // Verify extracted video file
    for file_path in &files {
        assert!(
            file_path.exists(),
            "Extracted file should exist: {:?}",
            file_path
        );
        let meta = fs::metadata(file_path).unwrap();
        assert!(
            meta.len() > 0,
            "Extracted file should not be empty: {:?}",
            file_path
        );
    }

    // The first file should be an .m2v video
    let video_path = &files[0];
    let ext = video_path
        .extension()
        .and_then(|e| e.to_str())
        .unwrap_or("");
    assert_eq!(ext, "m2v", "First extracted file should be .m2v video");
}

/// Test USM metadata extraction
#[test]
fn test_usm_metadata() {
    let usm_path = Path::new("0703.usm");
    if !usm_path.exists() {
        eprintln!("Skipping test: 0703.usm not found");
        return;
    }

    let result = cridecoder::usm::read_metadata_file(usm_path);
    let metadata = result.expect("USM metadata reading should not error");
    assert!(
        !metadata.sections.is_empty(),
        "Should have metadata sections"
    );
}

/// Test ACB extraction with an invalid file returns None
#[test]
fn test_acb_invalid_file() {
    let dir = tempfile::tempdir().unwrap();

    // Non-existent file
    let result = cridecoder::extract_acb_from_file(Path::new("nonexistent.acb"), dir.path());
    match result {
        Ok(None) => {} // expected
        Ok(Some(_)) => panic!("Should not find tracks in non-existent file"),
        Err(_) => {} // also acceptable
    }

    // File too small
    let tiny_file = dir.path().join("tiny.acb");
    fs::write(&tiny_file, b"too small").unwrap();
    let result = cridecoder::extract_acb_from_file(&tiny_file, dir.path());
    match result {
        Ok(None) => {} // expected
        Ok(Some(_)) => panic!("Should not find tracks in tiny file"),
        Err(_) => {} // also acceptable
    }
}

/// Test ACB extraction with a file that has wrong magic
#[test]
fn test_acb_wrong_magic() {
    let dir = tempfile::tempdir().unwrap();
    let bad_file = dir.path().join("bad_magic.acb");
    fs::write(&bad_file, vec![0u8; 64]).unwrap();
    let result = cridecoder::extract_acb_from_file(&bad_file, dir.path());
    match result {
        Ok(None) => {} // expected - wrong magic returns None
        Ok(Some(_)) => panic!("Should not extract from file with wrong magic"),
        Err(_) => {} // also acceptable
    }
}

/// Test HCA decoder rejects invalid data
#[test]
fn test_hca_invalid_data() {
    let bad_data = vec![0u8; 256];
    let result = cridecoder::HcaDecoder::from_reader(Cursor::new(bad_data));
    assert!(result.is_err(), "Should reject invalid HCA data");
}

/// Test HCA decoder from_reader with in-memory HCA data
#[test]
fn test_hca_from_reader() {
    let acb_path = Path::new("se_0126_01.acb");
    if !acb_path.exists() {
        eprintln!("Skipping test: se_0126_01.acb not found");
        return;
    }

    let dir = tempfile::tempdir().unwrap();
    let tracks = cridecoder::extract_acb_from_file(acb_path, dir.path())
        .unwrap()
        .unwrap();

    // Read HCA data into memory
    let hca_data = fs::read(&tracks[0]).unwrap();
    let decoder = cridecoder::HcaDecoder::from_reader(Cursor::new(hca_data))
        .expect("Should create decoder from reader");

    let info = decoder.info();
    assert_eq!(info.sampling_rate, 44100);
    assert_eq!(info.channel_count, 2);
}

/// Test extract_acb with in-memory data
#[test]
fn test_acb_from_memory() {
    let acb_path = Path::new("se_0126_01.acb");
    if !acb_path.exists() {
        eprintln!("Skipping test: se_0126_01.acb not found");
        return;
    }

    let data = fs::read(acb_path).unwrap();
    let dir = tempfile::tempdir().unwrap();
    let result = cridecoder::extract_acb(Cursor::new(data), dir.path(), None);
    let tracks = result.expect("Should extract from in-memory ACB data");
    assert_eq!(tracks.len(), 4, "Should extract 4 tracks from memory");
}

/// Test extract_usm with in-memory data
#[test]
fn test_usm_from_memory() {
    let usm_path = Path::new("0703.usm");
    if !usm_path.exists() {
        eprintln!("Skipping test: 0703.usm not found");
        return;
    }

    let data = fs::read(usm_path).unwrap();
    let dir = tempfile::tempdir().unwrap();
    let result =
        cridecoder::usm::extract_usm(Cursor::new(data), dir.path(), b"0703.usm", None, false);
    let files = result.expect("Should extract from in-memory USM data");
    assert!(!files.is_empty(), "Should extract at least one file");
}

// =============================================================================
// Encoder Tests
// =============================================================================

/// Test HCA encoder with synthetic sine wave
#[test]
fn test_hca_encoder_roundtrip() {
    use cridecoder::{HcaDecoder, HcaEncoder, HcaEncoderConfig};

    // Generate a 1-second stereo sine wave at 44.1kHz
    let sample_rate = 44100;
    let channels = 2u32;
    let duration_secs = 1.0;
    let sample_count = (sample_rate as f32 * duration_secs) as usize;
    let freq = 440.0; // A4 note

    let mut samples = Vec::with_capacity(sample_count * channels as usize);
    for i in 0..sample_count {
        let t = i as f32 / sample_rate as f32;
        let sample = (2.0 * std::f32::consts::PI * freq * t).sin() * 0.5;
        for _ in 0..channels {
            samples.push(sample);
        }
    }

    // Create encoder config
    let config = HcaEncoderConfig {
        channels,
        sample_rate,
        bitrate: 256_000,
        ..Default::default()
    };

    // Encode to HCA
    let mut encoder = HcaEncoder::new(config).expect("Should create encoder");
    let mut hca_data = Vec::new();
    encoder
        .encode(&samples, &mut Cursor::new(&mut hca_data))
        .expect("Should encode samples");

    // Verify HCA magic
    assert_eq!(&hca_data[0..4], b"HCA\x00", "Should have HCA magic");

    // Decode back
    let mut decoder = HcaDecoder::from_reader(Cursor::new(&hca_data))
        .expect("Should create decoder from encoded HCA");

    let info = decoder.info();
    assert_eq!(info.sampling_rate, sample_rate, "Sample rate should match");
    assert_eq!(info.channel_count, channels, "Channel count should match");

    let decoded = decoder.decode_all().expect("Should decode encoded HCA");
    assert!(!decoded.is_empty(), "Should decode some samples");

    // The decoded length won't exactly match due to HCA frame boundaries and encoder delay
    // but it should be reasonably close
    let expected_min = sample_count * channels as usize - 2048 * channels as usize;
    let expected_max = sample_count * channels as usize + 4096 * channels as usize;
    assert!(
        decoded.len() >= expected_min && decoded.len() <= expected_max,
        "Decoded sample count {} should be close to original {} (range {}-{})",
        decoded.len(),
        sample_count * channels as usize,
        expected_min,
        expected_max
    );

    let rms =
        (decoded.iter().map(|sample| sample * sample).sum::<f32>() / decoded.len() as f32).sqrt();
    let (min_sample, max_sample) = decoded
        .iter()
        .fold((f32::INFINITY, f32::NEG_INFINITY), |(min, max), sample| {
            (min.min(*sample), max.max(*sample))
        });
    assert!(
        rms > 0.01,
        "Decoded signal RMS should be non-trivial: {rms}"
    );
    assert!(
        max_sample - min_sample > 0.05,
        "Decoded signal should not be silent or DC-only: min={min_sample}, max={max_sample}"
    );
}

/// Test ACB builder creates valid container
#[test]
fn test_acb_builder_basic() {
    use cridecoder::{AcbBuilder, TrackInput};

    // Create a minimal HCA file (just header for testing structure)
    let dummy_hca = create_minimal_hca_header();

    let input = TrackInput::new("test_track", 0, dummy_hca);

    let mut builder = AcbBuilder::new();
    builder.add_track(input);

    let mut output = Vec::new();
    let result = builder.build(&mut Cursor::new(&mut output), None);
    assert!(
        result.is_ok(),
        "ACB build should succeed: {:?}",
        result.err()
    );
    assert!(output.len() > 64, "ACB should have content");

    // Verify UTF magic
    assert_eq!(&output[0..4], b"@UTF", "Should have UTF magic");

    let dir = tempfile::tempdir().unwrap();
    let extracted = cridecoder::extract_acb(Cursor::new(output), dir.path(), None)
        .expect("Built ACB should be extractable");
    assert_eq!(extracted.len(), 1, "Should extract one built track");

    let extracted_data = std::fs::read(&extracted[0]).expect("Should read extracted track");
    assert_eq!(&extracted_data[0..4], b"HCA\x00");
}

/// Test ACB builder keeps Waveform AWB ids aligned with non-zero cue ids
#[test]
fn test_acb_builder_nonzero_cue_id() {
    use cridecoder::{AcbBuilder, TrackInput};

    let dummy_hca = create_minimal_hca_header();
    let input = TrackInput::new("test_track_42", 42, dummy_hca);

    let mut builder = AcbBuilder::new();
    builder.add_track(input);

    let mut output = Vec::new();
    builder
        .build(&mut Cursor::new(&mut output), None)
        .expect("ACB build should succeed");

    let dir = tempfile::tempdir().unwrap();
    let extracted = cridecoder::extract_acb(Cursor::new(output), dir.path(), None)
        .expect("Built ACB with non-zero cue id should be extractable");
    assert_eq!(extracted.len(), 1, "Should extract one built track");

    let extracted_data = std::fs::read(&extracted[0]).expect("Should read extracted track");
    assert_eq!(&extracted_data[0..4], b"HCA\x00");
}

/// Test ACB builder rejects cue IDs that would make invalid Waveform AWB ids
#[test]
fn test_acb_builder_rejects_invalid_cue_ids() {
    use cridecoder::{AcbBuilder, BuilderError, TrackInput};

    let dummy_hca = create_minimal_hca_header();

    let mut too_large = AcbBuilder::new();
    too_large.add_track(TrackInput::new("too_large", 0x1_0000, dummy_hca.clone()));
    let err = too_large
        .build(&mut Cursor::new(Vec::new()), None)
        .expect_err("ACB build should reject cue ids above WaveformTable limits");
    assert!(matches!(err, BuilderError::CueIdTooLarge(0x1_0000)));

    let mut duplicate = AcbBuilder::new();
    duplicate.add_track(TrackInput::new("first", 7, dummy_hca.clone()));
    duplicate.add_track(TrackInput::new("second", 7, dummy_hca));
    let err = duplicate
        .build(&mut Cursor::new(Vec::new()), None)
        .expect_err("ACB build should reject duplicate cue ids");
    assert!(matches!(err, BuilderError::DuplicateCueId(7)));
}

/// Test music ACB builder emits sample-like tables
#[test]
fn test_music_acb_builder_structure() {
    use cridecoder::acb::UtfTable;
    use cridecoder::{AcbBuilder, TrackInput};

    let dummy_hca = create_minimal_hca_header();
    let input = TrackInput::new("3001_01", 0, dummy_hca);

    let mut builder = AcbBuilder::new().music_acb(
        0,
        Some("_alt".to_string()),
        0,
        1_024,
        1,
        1,
        vec![0; 16],
        vec![1; 16],
        "dummy".to_string(),
        1.0,
        0,
        0,
        "category".to_string(),
        0,
        vec!["bus".to_string()],
    );
    builder.add_track(input);

    let mut output = Vec::new();
    builder
        .build(&mut Cursor::new(&mut output), None)
        .expect("Music ACB build should succeed");

    let header = UtfTable::new(Cursor::new(&output)).expect("Header should parse");
    let row = &header.rows[0];
    assert_eq!(row["Name"].as_string(), Some("3001_01"));

    let cue_names = UtfTable::new(Cursor::new(row["CueNameTable"].as_bytes().unwrap()))
        .expect("CueNameTable should parse");
    let names: Vec<_> = cue_names
        .rows
        .iter()
        .map(|row| row["CueName"].as_string().unwrap())
        .collect();
    assert_eq!(names, vec!["3001_01", "3001_01_alt"]);

    let waveform = UtfTable::new(Cursor::new(row["WaveformTable"].as_bytes().unwrap()))
        .expect("WaveformTable should parse");
    assert_eq!(waveform.rows.len(), 1);
    assert_eq!(waveform.rows[0]["MemoryAwbId"].as_int(), Some(0));
    assert_eq!(waveform.rows[0]["EncodeType"].as_int(), Some(2));
    assert_eq!(waveform.rows[0]["NumChannels"].as_int(), Some(2));
    assert_eq!(waveform.rows[0]["SamplingRate"].as_int(), Some(44_100));

    let dir = tempfile::tempdir().unwrap();
    let extracted = cridecoder::extract_acb(Cursor::new(output), dir.path(), None)
        .expect("Music ACB should be extractable");
    assert_eq!(
        extracted.len(),
        2,
        "Music ACB should expose base and virtual cues"
    );
}

/// Test AFS2 archive builder
#[test]
fn test_afs_archive_builder() {
    use cridecoder::AfsArchiveBuilder;

    let file1 = vec![0x48, 0x43, 0x41, 0x00]; // HCA magic
    let file2 = vec![0x48, 0x43, 0x41, 0x00, 0x01, 0x02];

    let mut builder = AfsArchiveBuilder::new();
    builder.add_file(0, file1);
    builder.add_file(1, file2);

    let mut output = Cursor::new(Vec::new());
    builder
        .build(&mut output)
        .expect("AFS build should succeed");

    let data = output.into_inner();
    // Verify AFS2 magic
    assert_eq!(&data[0..4], b"AFS2", "Should have AFS2 magic");
}

/// Test USM builder writes the expected minimal container structure.
///
/// `UsmBuilder` is not a full muxer yet, so this intentionally does not assert
/// round-trip extraction through `extract_usm`.
#[test]
fn test_usm_builder_structure() {
    use cridecoder::UsmBuilder;

    // Create minimal M2V header (MPEG-2 sequence header)
    let video_data = vec![
        0x00, 0x00, 0x01, 0xB3, // sequence_header_code
        0x14, 0x00, 0xF0, 0x24, // picture size, aspect, frame rate
        0xFF, 0xFF, 0xE0, 0x00, // bit rate, vbv buffer
    ];

    let builder = UsmBuilder::new("test".to_string()).video(video_data);

    let mut output = Cursor::new(Vec::new());
    let result = builder.build(&mut output);
    assert!(
        result.is_ok(),
        "USM build should succeed: {:?}",
        result.err()
    );

    let data = output.into_inner();
    assert_eq!(&data[0..4], b"CRID", "Should have CRID magic");
    assert!(
        data.windows(4).any(|window| window == b"@SFV"),
        "Should include a video stream format chunk"
    );
    assert!(
        data.windows(4).any(|window| window == b"@SBV"),
        "Should include a video stream data chunk"
    );
    assert!(
        data.windows(4).any(|window| window == b"@END"),
        "Should include the current generic end marker"
    );
}

/// Helper to create a minimal HCA header for testing
fn create_minimal_hca_header() -> Vec<u8> {
    let mut data = Vec::new();

    // HCA signature
    data.extend_from_slice(b"HCA\x00");
    data.extend_from_slice(&[0x02, 0x00]); // version 2.0
    data.extend_from_slice(&[0x00, 0x60]); // header size 96

    // fmt chunk
    data.extend_from_slice(b"fmt\x00");
    data.extend_from_slice(&[2]); // 2 channels
    data.extend_from_slice(&[0x00, 0xAC, 0x44]); // 44100 Hz (BE)
    data.extend_from_slice(&[0x00, 0x00, 0x00, 0x10]); // 16 blocks
    data.extend_from_slice(&[0x00, 0x00]); // encoder delay
    data.extend_from_slice(&[0x00, 0x00]); // insert samples

    // comp chunk
    data.extend_from_slice(b"comp");
    data.extend_from_slice(&[0x01, 0x55]); // frame size 341
    data.extend_from_slice(&[0x00]); // min resolution
    data.extend_from_slice(&[0x0F]); // max resolution
    data.extend_from_slice(&[0x01]); // track count
    data.extend_from_slice(&[0x01]); // channel config
    data.extend_from_slice(&[0x80]); // total band count
    data.extend_from_slice(&[0x60]); // base band count
    data.extend_from_slice(&[0x00]); // stereo band count
    data.extend_from_slice(&[0x00]); // bands per hfr group
    data.extend_from_slice(&[0x00, 0x00]); // reserved

    // Pad to header size
    while data.len() < 96 {
        data.push(0);
    }

    data
}