#![allow(
    clippy::doc_markdown,
    clippy::cast_possible_truncation,
    clippy::cast_possible_wrap,
    clippy::cast_sign_loss,
    clippy::similar_names,
    clippy::too_many_lines
)]

use crate::error::Error;
use crate::scd::decode::msadpcm::decode_to_pcm16 as msadpcm_decode;
use crate::scd::decode::ogg_vorbis::{OGG_XOR_TABLE, deobfuscate};
use crate::scd::track::{AdpcmWaveFormat, OggVorbisSeekTableHeader};

fn make_format(
    channels: i16,
    block_align: i16,
    samples_per_block: u16,
    coefficients: [i16; 14],
) -> AdpcmWaveFormat {
    AdpcmWaveFormat {
        format_tag: 2,
        channels,
        samples_per_sec: 44_100,
        avg_bytes_per_sec: 22_050,
        block_align,
        bits_per_sample: 4,
        size: 32,
        samples_per_block,
        num_coef: 7,
        coefficients,
    }
}

const STANDARD_COEFS: [i16; 14] = [
    256, 0, 512, -256, 0, 0, 192, 64, 240, 0, 460, -208, 392, -232,
];

#[test]
fn msadpcm_mono_single_block_decodes_preamble_then_nibbles() {
    let fmt = make_format(1, 8, 4, STANDARD_COEFS);

    let mut block = Vec::with_capacity(8);
    block.push(0u8);
    block.extend_from_slice(&16i16.to_le_bytes());
    block.extend_from_slice(&100i16.to_le_bytes());
    block.extend_from_slice(&200i16.to_le_bytes());
    block.push(0x00);

    assert_eq!(block.len(), 8);
    let pcm = msadpcm_decode(&fmt, &block).expect("mono decode");
    assert_eq!(pcm, vec![200, 100, 100, 100]);
}

#[test]
fn msadpcm_mono_multi_block_decodes_each_block_independently() {
    let fmt = make_format(1, 8, 4, STANDARD_COEFS);

    let mut block = Vec::with_capacity(8);
    block.push(0u8);
    block.extend_from_slice(&16i16.to_le_bytes());
    block.extend_from_slice(&100i16.to_le_bytes());
    block.extend_from_slice(&200i16.to_le_bytes());
    block.push(0x00);

    let mut payload = Vec::new();
    payload.extend_from_slice(&block);
    payload.extend_from_slice(&block);

    let pcm = msadpcm_decode(&fmt, &payload).expect("multi block");
    assert_eq!(pcm.len(), 8);
    assert_eq!(&pcm[..4], &[200, 100, 100, 100]);
    assert_eq!(&pcm[4..], &[200, 100, 100, 100]);
}

#[test]
fn msadpcm_zero_payload_returns_empty_pcm() {
    let fmt = make_format(1, 8, 4, STANDARD_COEFS);
    let pcm = msadpcm_decode(&fmt, &[]).expect("empty payload");
    assert!(pcm.is_empty());
}

#[test]
fn msadpcm_stereo_single_block_interleaves_l_r() {
    let fmt = make_format(2, 16, 4, STANDARD_COEFS);

    let mut block = Vec::with_capacity(16);
    block.push(0u8);
    block.push(0u8);
    block.extend_from_slice(&16i16.to_le_bytes());
    block.extend_from_slice(&16i16.to_le_bytes());
    block.extend_from_slice(&100i16.to_le_bytes());
    block.extend_from_slice(&500i16.to_le_bytes());
    block.extend_from_slice(&200i16.to_le_bytes());
    block.extend_from_slice(&600i16.to_le_bytes());
    block.push(0x00);
    block.push(0x00);

    assert_eq!(block.len(), 16);
    let pcm = msadpcm_decode(&fmt, &block).expect("stereo decode");
    assert_eq!(pcm, vec![200, 600, 100, 500, 100, 500, 100, 500]);
}

#[test]
fn msadpcm_stereo_signed_nibbles_apply_delta_correctly() {
    let fmt = make_format(2, 16, 4, STANDARD_COEFS);

    let mut block = Vec::with_capacity(16);
    block.push(0u8);
    block.push(0u8);
    block.extend_from_slice(&16i16.to_le_bytes());
    block.extend_from_slice(&16i16.to_le_bytes());
    block.extend_from_slice(&0i16.to_le_bytes());
    block.extend_from_slice(&0i16.to_le_bytes());
    block.extend_from_slice(&0i16.to_le_bytes());
    block.extend_from_slice(&0i16.to_le_bytes());
    block.push(0x1F);
    block.push(0x00);

    let pcm = msadpcm_decode(&fmt, &block).expect("stereo signed decode");
    assert_eq!(pcm, vec![0, 0, 0, 0, 16, -16, 16, -16]);
}

#[test]
fn msadpcm_invalid_channel_count_errors() {
    let fmt = make_format(3, 16, 4, STANDARD_COEFS);
    let err = msadpcm_decode(&fmt, &[]).expect_err("invalid channels");
    assert!(matches!(err, Error::InvalidMsAdpcmBlock { .. }));
}

#[test]
fn msadpcm_payload_not_multiple_of_block_align_errors() {
    let fmt = make_format(1, 8, 4, STANDARD_COEFS);
    let payload = vec![0u8; 11];
    let err = msadpcm_decode(&fmt, &payload).expect_err("unaligned payload");
    assert!(matches!(err, Error::InvalidMsAdpcmBlock { .. }));
}

#[test]
fn msadpcm_predictor_idx_past_coef_table_errors() {
    let fmt = make_format(1, 8, 4, STANDARD_COEFS);
    let mut block = vec![9u8];
    block.extend_from_slice(&16i16.to_le_bytes());
    block.extend_from_slice(&0i16.to_le_bytes());
    block.extend_from_slice(&0i16.to_le_bytes());
    block.push(0x00);
    let err = msadpcm_decode(&fmt, &block).expect_err("bad predictor idx");
    assert!(matches!(err, Error::InvalidMsAdpcmBlock { .. }));
}

#[test]
fn msadpcm_samples_per_block_too_small_errors() {
    let fmt = make_format(1, 8, 1, STANDARD_COEFS);
    let err = msadpcm_decode(&fmt, &[]).expect_err("samples_per_block too small");
    assert!(matches!(err, Error::InvalidMsAdpcmBlock { .. }));
}

#[test]
fn msadpcm_block_align_with_trailing_padding_decodes_cleanly() {
    let fmt = make_format(1, 10, 4, STANDARD_COEFS);

    let mut block = Vec::with_capacity(10);
    block.push(0u8);
    block.extend_from_slice(&16i16.to_le_bytes());
    block.extend_from_slice(&100i16.to_le_bytes());
    block.extend_from_slice(&200i16.to_le_bytes());
    block.push(0x00);
    block.push(0xFF);
    block.push(0xFF);

    assert_eq!(block.len(), 10);
    let pcm = msadpcm_decode(&fmt, &block).expect("decode with padding");
    assert_eq!(pcm, vec![200, 100, 100, 100]);
    assert_eq!(
        pcm.len(),
        4,
        "trailing padding must not produce extra samples"
    );
}

#[test]
fn msadpcm_num_coef_zero_with_nonempty_payload_errors() {
    let fmt = AdpcmWaveFormat {
        format_tag: 2,
        channels: 1,
        samples_per_sec: 44_100,
        avg_bytes_per_sec: 22_050,
        block_align: 8,
        bits_per_sample: 4,
        size: 32,
        samples_per_block: 4,
        num_coef: 0,
        coefficients: [0i16; 14],
    };

    let mut block = vec![0u8; 8];
    block[1..3].copy_from_slice(&16i16.to_le_bytes());
    block[3..5].copy_from_slice(&0i16.to_le_bytes());
    block[5..7].copy_from_slice(&0i16.to_le_bytes());
    block[7] = 0x00;

    let err = msadpcm_decode(&fmt, &block).expect_err("num_coef zero should error");
    assert!(
        matches!(err, Error::InvalidMsAdpcmBlock { .. }),
        "expected InvalidMsAdpcmBlock, got {err:?}",
    );
}

#[test]
fn msadpcm_stereo_even_samples_per_block_no_odd_tail() {
    let fmt = make_format(2, 18, 6, STANDARD_COEFS);

    let mut block = Vec::with_capacity(18);
    block.push(0u8);
    block.push(0u8);
    block.extend_from_slice(&16i16.to_le_bytes());
    block.extend_from_slice(&16i16.to_le_bytes());
    block.extend_from_slice(&0i16.to_le_bytes());
    block.extend_from_slice(&0i16.to_le_bytes());
    block.extend_from_slice(&0i16.to_le_bytes());
    block.extend_from_slice(&0i16.to_le_bytes());
    block.push(0x00);
    block.push(0x00);
    block.push(0x00);
    block.push(0x00);

    assert_eq!(block.len(), 18);
    let pcm = msadpcm_decode(&fmt, &block).expect("stereo even spb");
    assert_eq!(
        pcm.len(),
        12,
        "stereo even samples_per_block must produce 12 samples"
    );
}

fn make_ogg_header(version: u8, xor_byte: u8, ogg_header_size: u32) -> OggVorbisSeekTableHeader {
    OggVorbisSeekTableHeader {
        version,
        struct_size: 32,
        xor_byte,
        unknown: [0; 9],
        step: 1.0,
        seek_table_size: 0,
        ogg_header_size,
        padding: [0; 2],
    }
}

#[test]
fn ogg_v2_xor_header_only_round_trips() {
    let xor_byte = 0xA7u8;
    let ogg_header_size = 8;
    let payload: Vec<u8> = (0u8..16).collect();

    let header = make_ogg_header(2, xor_byte, ogg_header_size as u32);
    let deob = deobfuscate(&header, &payload).expect("v2 deobfuscate");

    for i in 0..ogg_header_size {
        assert_eq!(deob[i], payload[i] ^ xor_byte, "header byte {i}");
    }
    for i in ogg_header_size..payload.len() {
        assert_eq!(deob[i], payload[i], "body byte {i}");
    }

    let reob = deobfuscate(&header, &deob).expect("v2 re-deobfuscate");
    assert_eq!(reob, payload);
}

#[test]
fn ogg_v3_xor_full_round_trips() {
    let ogg_header_size = 4u32;
    let body_size = 20u32;
    let total = ogg_header_size + body_size;
    let payload: Vec<u8> = (0u8..total as u8).collect();

    let header = make_ogg_header(3, 0, ogg_header_size);
    let deob = deobfuscate(&header, &payload).expect("v3 deobfuscate");

    let byte1 = (body_size as usize & 0x7F) as u8;
    let byte2 = byte1 & 0x3F;
    for (j, &orig) in payload.iter().enumerate() {
        let idx = byte2.wrapping_add((j & 0xFF) as u8);
        let expected = OGG_XOR_TABLE[idx as usize] ^ orig ^ byte1;
        assert_eq!(deob[j], expected, "byte {j}");
    }

    let reob = deobfuscate(&header, &deob).expect("v3 re-deobfuscate");
    assert_eq!(reob, payload);
}

#[test]
fn ogg_unknown_version_passes_through() {
    let payload: Vec<u8> = (0u8..16).collect();
    let header = make_ogg_header(0, 0xFF, 4);
    let deob = deobfuscate(&header, &payload).expect("v0 passthrough");
    assert_eq!(deob, payload);

    let header = make_ogg_header(99, 0xFF, 4);
    let deob = deobfuscate(&header, &payload).expect("v99 passthrough");
    assert_eq!(deob, payload);
}

#[test]
fn ogg_v2_clean_oggs_magic_skips_xor() {
    let mut payload: Vec<u8> = Vec::new();
    payload.extend_from_slice(b"OggS");
    payload.extend_from_slice(&[0xAB; 12]);

    let header = make_ogg_header(2, 0x2C, 16);
    let deob = deobfuscate(&header, &payload).expect("modern OGG passthrough");
    assert_eq!(
        deob, payload,
        "clean OggS-prefixed bytes must pass through untouched"
    );
}

#[test]
fn ogg_header_size_exceeds_payload_errors() {
    let payload: Vec<u8> = vec![0u8; 8];
    let header = make_ogg_header(2, 0xAA, 32);
    let err = deobfuscate(&header, &payload).expect_err("oversized header");
    assert!(matches!(err, Error::InvalidOggHeader { .. }));
}

#[test]
fn ogg_v2_with_oggs_prefix_skips_xor_passthrough() {
    let xor_byte = 0xCC;
    let mut payload = Vec::new();
    payload.extend_from_slice(b"OggS");
    payload.extend_from_slice(&[0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]);

    let header = make_ogg_header(2, xor_byte, 12);
    let out = deobfuscate(&header, &payload).expect("v2 OggS passthrough");
    assert_eq!(
        out, payload,
        "v2 OggS-prefixed bytes must pass through untouched (XOR must NOT fire)"
    );
}

#[test]
fn ogg_v2_without_oggs_prefix_applies_xor() {
    let xor_byte = 0xA7;
    let ogg_header_size = 8usize;
    let payload: Vec<u8> = (0u8..16).collect();
    assert_ne!(&payload[..4], b"OggS", "test precondition: no OggS prefix");

    let header = make_ogg_header(2, xor_byte, ogg_header_size as u32);
    let out = deobfuscate(&header, &payload).expect("v2 XOR apply");

    for i in 0..ogg_header_size {
        assert_eq!(
            out[i],
            payload[i] ^ xor_byte,
            "header byte {i} must be XORed"
        );
    }
    for i in ogg_header_size..payload.len() {
        assert_eq!(out[i], payload[i], "body byte {i} must be untouched");
    }
    assert_ne!(out, payload, "XOR output must differ from input");
}

#[test]
fn ogg_v3_with_oggs_prefix_still_applies_table_xor() {
    let ogg_header_size = 4u32;
    let mut payload = Vec::new();
    payload.extend_from_slice(b"OggS");
    payload.extend_from_slice(&[0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88]);
    let total = payload.len();

    let header = make_ogg_header(3, 0, ogg_header_size);
    let out = deobfuscate(&header, &payload).expect("v3 table xor");

    let body_size = total - ogg_header_size as usize;
    #[allow(clippy::cast_possible_truncation)]
    let body_low = (body_size & 0x7F) as u8;
    let seed_lo = body_low & 0x3F;
    for (j, &orig) in payload.iter().enumerate() {
        #[allow(clippy::cast_possible_truncation)]
        let j_low = (j & 0xFF) as u8;
        let idx = seed_lo.wrapping_add(j_low);
        let expected = OGG_XOR_TABLE[idx as usize] ^ orig ^ body_low;
        assert_eq!(
            out[j], expected,
            "v3 XOR byte {j} must not skip for OggS prefix"
        );
    }

    assert_ne!(
        &out[..4],
        b"OggS",
        "v3 XOR must transform bytes — OggS prefix must NOT survive for non-trivial XOR",
    );
}

#[test]
fn ogg_v3_without_oggs_prefix_applies_table_xor() {
    let ogg_header_size = 4u32;
    let payload: Vec<u8> = (0u8..12).collect();
    assert_ne!(&payload[..4], b"OggS", "test precondition: no OggS prefix");

    let header = make_ogg_header(3, 0, ogg_header_size);
    let out = deobfuscate(&header, &payload).expect("v3 no-OggS xor");

    let roundtrip = deobfuscate(&header, &out).expect("v3 roundtrip");
    assert_eq!(roundtrip, payload, "v3 XOR must be its own inverse");

    assert_ne!(out, payload, "v3 XOR output must differ from input");
}

mod dispatch {
    use crate::error::Error;
    use crate::scd::Scd;

    fn build_min_scd(format_raw: i32, payload: &[u8]) -> Vec<u8> {
        use crate::scd::header::{BINARY_HEADER_SIZE, SCD_HEADER_SIZE};

        let mut buf = Vec::new();

        buf.extend_from_slice(b"SEDB");
        buf.extend_from_slice(b"SSCF");
        buf.extend_from_slice(&4u32.to_le_bytes());
        buf.push(0);
        buf.push(4);
        buf.extend_from_slice(&(BINARY_HEADER_SIZE as u16).to_le_bytes());
        let size_pos = buf.len();
        buf.extend_from_slice(&0u64.to_le_bytes());
        buf.extend_from_slice(&0u64.to_le_bytes());
        for _ in 0..4 {
            buf.extend_from_slice(&0u32.to_le_bytes());
        }
        assert_eq!(buf.len(), BINARY_HEADER_SIZE);

        let audio_offsets_start = u32::try_from(BINARY_HEADER_SIZE + SCD_HEADER_SIZE).unwrap();
        let audio_offsets_size = 4u32;
        let after_offsets = audio_offsets_start + audio_offsets_size;
        let track_offset = after_offsets;
        let audio_offset = audio_offsets_start;
        buf.extend_from_slice(&0u16.to_le_bytes());
        buf.extend_from_slice(&0u16.to_le_bytes());
        buf.extend_from_slice(&1u16.to_le_bytes());
        buf.extend_from_slice(&0u16.to_le_bytes());
        buf.extend_from_slice(&track_offset.to_le_bytes());
        buf.extend_from_slice(&audio_offset.to_le_bytes());
        buf.extend_from_slice(&0u32.to_le_bytes());
        buf.extend_from_slice(&0u32.to_le_bytes());
        buf.extend_from_slice(&0u32.to_le_bytes());
        buf.extend_from_slice(&0u16.to_le_bytes());
        buf.extend_from_slice(&0u16.to_le_bytes());
        assert_eq!(buf.len(), BINARY_HEADER_SIZE + SCD_HEADER_SIZE);

        let offsets_pos = buf.len();
        buf.resize(buf.len() + 4, 0);

        let desc_pos = u32::try_from(buf.len()).unwrap();
        buf[offsets_pos..offsets_pos + 4].copy_from_slice(&desc_pos.to_le_bytes());

        let payload_size = u32::try_from(payload.len()).unwrap();
        buf.extend_from_slice(&payload_size.to_le_bytes());
        buf.extend_from_slice(&1u32.to_le_bytes());
        buf.extend_from_slice(&44_100u32.to_le_bytes());
        buf.extend_from_slice(&format_raw.to_le_bytes());
        buf.extend_from_slice(&0u32.to_le_bytes());
        buf.extend_from_slice(&0u32.to_le_bytes());
        buf.extend_from_slice(&0u32.to_le_bytes());
        buf.extend_from_slice(&0u32.to_le_bytes());
        buf.extend_from_slice(payload);

        let total = u64::try_from(buf.len()).unwrap();
        buf[size_pos..size_pos + 8].copy_from_slice(&total.to_le_bytes());

        buf
    }

    #[test]
    fn empty_track_decodes_to_empty_pcm() {
        let bytes = build_min_scd(-1, &[]);
        let scd = Scd::parse(&bytes).expect("empty parse");
        let pcm = scd.tracks()[0].decode_to_pcm16().expect("empty decode");
        assert!(pcm.is_empty());
    }

    #[test]
    fn mp3_track_returns_mp3_unsupported() {
        let bytes = build_min_scd(7, b"\x00\x00\x00\x00");
        let scd = Scd::parse(&bytes).expect("mp3 parse");
        let err = scd.tracks()[0]
            .decode_to_pcm16()
            .expect_err("mp3 unsupported");
        assert!(matches!(err, Error::Mp3DecodingUnsupported));
    }

    #[test]
    fn atrac9_track_returns_atrac9_unsupported() {
        let bytes = build_min_scd(22, b"\x00\x00\x00\x00");
        let scd = Scd::parse(&bytes).expect("atrac9 parse");
        let err = scd.tracks()[0]
            .decode_to_pcm16()
            .expect_err("atrac9 unsupported");
        assert!(matches!(err, Error::Atrac9DecodingUnsupported));
    }

    #[test]
    fn unknown_codec_returns_unknown_unsupported_with_raw() {
        let bytes = build_min_scd(99, b"\x00\x00\x00\x00");
        let scd = Scd::parse(&bytes).expect("unknown parse");
        let err = scd.tracks()[0]
            .decode_to_pcm16()
            .expect_err("unknown unsupported");
        match err {
            Error::UnknownCodecDecodingUnsupported { raw } => {
                assert_eq!(raw, 99);
            }
            other => panic!("expected UnknownCodecDecodingUnsupported, got {other:?}"),
        }
    }

    #[test]
    fn ogg_vorbis_track_decode_to_pcm16_returns_ogg_unsupported() {
        let bytes = build_min_scd(6, b"\x00\x00\x00\x00");
        let scd = Scd::parse(&bytes).expect("ogg vorbis parse");
        let err = scd.tracks()[0]
            .decode_to_pcm16()
            .expect_err("ogg vorbis unsupported");
        assert!(
            matches!(err, Error::OggVorbisDecodingUnsupported),
            "expected OggVorbisDecodingUnsupported, got {err:?}",
        );
    }

    #[test]
    fn to_ogg_bytes_on_non_ogg_track_returns_not_applicable() {
        let bytes = build_min_scd(-1, &[]);
        let scd = Scd::parse(&bytes).expect("empty track parse");
        let err = scd.tracks()[0]
            .to_ogg_bytes()
            .expect_err("non-ogg to_ogg_bytes");
        assert!(
            matches!(err, Error::OggBytesNotApplicable),
            "expected OggBytesNotApplicable, got {err:?}",
        );
    }

    #[test]
    fn to_ogg_bytes_on_mp3_track_returns_not_applicable() {
        let bytes = build_min_scd(7, b"\x00\x00\x00\x00");
        let scd = Scd::parse(&bytes).expect("mp3 track parse");
        let err = scd.tracks()[0]
            .to_ogg_bytes()
            .expect_err("mp3 to_ogg_bytes");
        assert!(
            matches!(err, Error::OggBytesNotApplicable),
            "expected OggBytesNotApplicable, got {err:?}",
        );
    }
}
