//! Integration test for `ft4::subtract::*` (PR #20).
//!
//! The PR's inline tests exercise self-cancellation (power drop on
//! subtract of the same signal). This test exercises the *intended
//! production use case*: two FT4 signals at different carriers in the
//! same slot, where the strong one masks the weak one in coarse-sync
//! score. Subtracting the strong signal must reveal the weak one to
//! the second decode pass.
//!
//! Mirrors the FT8 inline test
//! `ft8::subtract::tests::subtract_reveals_hidden_signal` but at the
//! `tests/` level so any `ft4::subtract` API regression that doesn't
//! break self-cancellation still gets caught.

use mfsk_core::core::{FrameLayout, MessageCodec, MessageFields, ModulationParams};
use mfsk_core::ft4::decode::decode_frame_with_options;
use mfsk_core::ft4::decode::{DecodeDepth, DecodeStrictness, decode_frame};
use mfsk_core::ft4::subtract::{refine_signal_freq, subtract_signal_lpf};
use mfsk_core::ft4::{Ft4, encode};
use mfsk_core::msg::Wsjt77Message;

const NSPS: usize = <Ft4 as ModulationParams>::NSPS as usize;
const NN: usize = <Ft4 as FrameLayout>::N_SYMBOLS as usize;
const SLOT_SAMPLES: usize = 90_000;

fn pack(call1: &str, call2: &str, grid: &str) -> [u8; 77] {
    let bits = Wsjt77Message
        .pack(&MessageFields {
            call1: Some(call1.into()),
            call2: Some(call2.into()),
            grid: Some(grid.into()),
            ..MessageFields::default()
        })
        .expect("pack succeeds");
    let mut out = [0u8; 77];
    out.copy_from_slice(&bits);
    out
}

fn lay_signal(audio: &mut [i16], msg77: &[u8; 77], freq_hz: f32, peak_i16: i16) {
    let itone = encode::message_to_tones(msg77);
    assert_eq!(itone.len(), NN);
    let pcm = encode::tones_to_i16(&itone, freq_hz, peak_i16);
    assert_eq!(pcm.len(), NN * NSPS);
    let pad = (<Ft4 as FrameLayout>::TX_START_OFFSET_S * 12_000.0) as usize;
    for (i, &s) in pcm.iter().enumerate() {
        let idx = pad + i;
        if idx >= audio.len() {
            break;
        }
        // saturating mix
        let v = audio[idx] as i32 + s as i32;
        audio[idx] = v.clamp(i16::MIN as i32, i16::MAX as i32) as i16;
    }
}

#[test]
fn subtract_reveals_hidden_ft4_signal() {
    let strong = pack("CQ", "K1ABC", "FN42");
    let weak = pack("CQ", "JA1XYZ", "PM95");

    let mut audio = vec![0i16; SLOT_SAMPLES];
    // Strong + weak at the same dt but well-separated frequencies (≥
    // 100 Hz apart so the strong signal's coarse-sync sidelobes don't
    // mask the weak carrier outright).
    lay_signal(&mut audio, &strong, 1500.0, 22_000);
    lay_signal(&mut audio, &weak, 1900.0, 7_000);

    // Pass 1: decode whatever's loudest. Should pick up the strong
    // signal; whether it also catches the weak one depends on how
    // much the strong one's leakage masks it. We don't assert on
    // weak-in-pass-1 either way.
    let pass1 = decode_frame(&audio, 100.0, 3000.0, 0.6, 5);
    let strong_hit = pass1
        .iter()
        .find(|r| r.message77() == strong)
        .expect("strong signal must decode in pass 1");

    // Refine the strong-signal carrier (real-WAV best practice per
    // the PR doc) and SIC it out.
    let mut residual = audio.clone();
    let refined_freq = refine_signal_freq(&residual, strong_hit);
    let mut refined = strong_hit.clone();
    refined.freq_hz = refined_freq;
    subtract_signal_lpf(&mut residual, &refined);

    // Pass 2 on the residual: must surface the weak signal.
    let pass2 = decode_frame_with_options(
        &residual,
        100.0,
        3000.0,
        0.5,
        None,
        DecodeDepth::BpAllOsd,
        DecodeStrictness::Deep,
        5,
    );
    let saw_weak = pass2.iter().any(|r| r.message77() == weak);
    let pass1_saw_weak = pass1.iter().any(|r| r.message77() == weak);
    assert!(
        saw_weak || pass1_saw_weak,
        "weak signal never surfaced — pass1 results: {:?}, pass2 results: {:?}",
        pass1
            .iter()
            .map(|r| r.message77().to_vec())
            .collect::<Vec<_>>(),
        pass2
            .iter()
            .map(|r| r.message77().to_vec())
            .collect::<Vec<_>>()
    );
}
