// SPDX-License-Identifier: AGPL-3.0-or-later

#[path = "../src/arm64_reloc.rs"]
mod arm64_reloc;

use arm64_reloc::*;

fn adr(rd: u32, imm21: i32) -> u32 {
    let raw = (imm21 as u32) & 0x1F_FFFF;
    let immlo = (raw & 0x3) << 29;
    let immhi = ((raw >> 2) & 0x7FFFF) << 5;
    0x1000_0000 | immlo | immhi | (rd & 0x1F)
}
fn adrp(rd: u32, imm21: i32) -> u32 {
    let raw = (imm21 as u32) & 0x1F_FFFF;
    let immlo = (raw & 0x3) << 29;
    let immhi = ((raw >> 2) & 0x7FFFF) << 5;
    0x9000_0000 | immlo | immhi | (rd & 0x1F)
}
fn b(off_words: i32) -> u32 {
    0x1400_0000 | ((off_words as u32) & 0x03FF_FFFF)
}
fn bl(off_words: i32) -> u32 {
    0x9400_0000 | ((off_words as u32) & 0x03FF_FFFF)
}
fn bcond(cond: u32, off_words: i32) -> u32 {
    0x5400_0000 | (((off_words as u32) & 0x7FFFF) << 5) | (cond & 0xF)
}
fn cbz(rt: u32, off_words: i32) -> u32 {
    0xB400_0000 | (((off_words as u32) & 0x7FFFF) << 5) | (rt & 0x1F)
}
fn tbz(rt: u32, bit: u32, off_words: i32) -> u32 {
    let b5 = (bit & 0x20) << (31 - 5);
    let b40 = (bit & 0x1F) << 19;
    0x3600_0000 | b5 | b40 | (((off_words as u32) & 0x3FFF) << 5) | (rt & 0x1F)
}
fn ldr_lit(rt: u32, off_words: i32) -> u32 {
    0x5800_0000 | (((off_words as u32) & 0x7FFFF) << 5) | (rt & 0x1F)
}

fn adr_target(insn: u32, pc: u64, page: bool) -> u64 {
    let immlo = ((insn >> 29) & 0x3) as i64;
    let immhi = ((insn >> 5) & 0x7FFFF) as i64;
    let raw = (immhi << 2) | immlo;
    let imm21 = (raw << 43) >> 43;
    if page {
        ((pc & !0xFFF) as i64 + imm21 * 4096) as u64
    } else {
        (pc as i64 + imm21) as u64
    }
}
fn imm19_target(insn: u32, pc: u64) -> u64 {
    let imm19 = ((insn >> 5) & 0x7FFFF) as i64;
    let off = ((imm19 << 45) >> 45) * 4;
    (pc as i64 + off) as u64
}
fn imm14_target(insn: u32, pc: u64) -> u64 {
    let imm14 = ((insn >> 5) & 0x3FFF) as i64;
    let off = ((imm14 << 50) >> 50) * 4;
    (pc as i64 + off) as u64
}

#[test]
fn non_pc_relative_copied_verbatim() {
    let stp = 0xA9BF_7BFD;
    assert_eq!(relocate_instruction(stp, 0x1000, 0x9000), Some(stp));
    let mov = 0xAA01_03E0;
    assert_eq!(relocate_instruction(mov, 0x1000, 0x9000), Some(mov));
    let sub = 0xD100_83FF;
    assert_eq!(relocate_instruction(sub, 0x1000, 0x9000), Some(sub));
    let mov_fp_sp = 0x9100_03FD;
    assert_eq!(
        relocate_instruction(mov_fp_sp, 0x1000, 0x9000),
        Some(mov_fp_sp)
    );
}

#[test]
fn adr_relocates_to_same_target() {
    let src_pc = 0x140_0010_0000u64;
    let dst_pc = 0x140_0010_8000u64;
    let insn = adr(0, 0x4000);
    let original = adr_target(insn, src_pc, false);
    let reloc = relocate_instruction(insn, src_pc, dst_pc).expect("in range");
    assert_eq!(adr_target(reloc, dst_pc, false), original);
    assert_eq!(reloc & 0x1F, 0);
}

#[test]
fn adr_negative_offset() {
    let src_pc = 0x140_0010_0000u64;
    let dst_pc = 0x140_0010_0010u64;
    let insn = adr(5, -0x100);
    let original = adr_target(insn, src_pc, false);
    let reloc = relocate_instruction(insn, src_pc, dst_pc).expect("in range");
    assert_eq!(adr_target(reloc, dst_pc, false), original);
    assert_eq!(reloc & 0x1F, 5);
}

#[test]
fn adr_out_of_range_refused() {
    let src_pc = 0x0000_0000_0000u64;
    let dst_pc = 0x0000_0080_0000u64;
    let insn = adr(0, 0x1000);
    assert_eq!(relocate_instruction(insn, src_pc, dst_pc), None);
}

#[test]
fn adrp_relocates_to_same_page() {
    let src_pc = 0x140_0010_0000u64;
    let dst_pc = 0x140_0030_0000u64;
    let insn = adrp(9, 0x10);
    let original = adr_target(insn, src_pc, true);
    let reloc = relocate_instruction(insn, src_pc, dst_pc).expect("in range");
    assert_eq!(adr_target(reloc, dst_pc, true), original);
    assert_eq!(reloc & 0x1F, 9);
    assert_eq!(reloc & 0x9F00_0000, 0x9000_0000);
}

#[test]
fn adrp_negative() {
    let src_pc = 0x140_0090_0000u64;
    let dst_pc = 0x140_0050_0000u64;
    let insn = adrp(1, -0x20);
    let original = adr_target(insn, src_pc, true);
    let reloc = relocate_instruction(insn, src_pc, dst_pc).expect("in range");
    assert_eq!(adr_target(reloc, dst_pc, true), original);
}

#[test]
fn bcond_relocates() {
    let src_pc = 0x10_0000u64;
    let dst_pc = 0x12_0000u64;
    let insn = bcond(0x0, 0x40);
    let original = imm19_target(insn, src_pc);
    let reloc = relocate_instruction(insn, src_pc, dst_pc).expect("in range");
    assert_eq!(imm19_target(reloc, dst_pc), original);
    assert_eq!(reloc & 0xF, 0x0);
}

#[test]
fn bcond_out_of_range_refused() {
    let src_pc = 0x0u64;
    let dst_pc = 0x20_0000u64;
    let insn = bcond(0x1, 0x10);
    assert_eq!(relocate_instruction(insn, src_pc, dst_pc), None);
}

#[test]
fn cbz_relocates() {
    let src_pc = 0x10_0000u64;
    let dst_pc = 0x10_8000u64;
    let insn = cbz(3, -0x20);
    let original = imm19_target(insn, src_pc);
    let reloc = relocate_instruction(insn, src_pc, dst_pc).expect("in range");
    assert_eq!(imm19_target(reloc, dst_pc), original);
    assert_eq!(reloc & 0x1F, 3);
    assert_eq!(reloc & 0x8000_0000, 0x8000_0000);
}

#[test]
fn tbz_relocates() {
    let src_pc = 0x10_0000u64;
    let dst_pc = 0x10_1000u64;
    let insn = tbz(7, 5, 0x10);
    let original = imm14_target(insn, src_pc);
    let reloc = relocate_instruction(insn, src_pc, dst_pc).expect("in range");
    assert_eq!(imm14_target(reloc, dst_pc), original);
    assert_eq!(reloc & 0x1F, 7);
}

#[test]
fn tbz_out_of_range_refused() {
    let src_pc = 0x0u64;
    let dst_pc = 0x1_0000u64;
    let insn = tbz(0, 1, 0x8);
    assert_eq!(relocate_instruction(insn, src_pc, dst_pc), None);
}

#[test]
fn ldr_literal_relocates() {
    let src_pc = 0x20_0000u64;
    let dst_pc = 0x20_4000u64;
    let insn = ldr_lit(2, 0x100);
    let original = imm19_target(insn, src_pc);
    let reloc = relocate_instruction(insn, src_pc, dst_pc).expect("in range");
    assert_eq!(imm19_target(reloc, dst_pc), original);
    assert_eq!(reloc & 0x1F, 2);
}

#[test]
fn direct_b_relocation_in_range() {
    let src_pc = 0x10_0000u64;
    let dst_pc = 0x14_0000u64;
    let insn = b(0x100);
    let target = branch_target(insn, src_pc).unwrap();
    let reloc = relocate_instruction(insn, src_pc, dst_pc).expect("in range");
    assert_eq!(branch_target(reloc, dst_pc).unwrap(), target);
}

#[test]
fn branch_target_decode() {
    let pc = 0x10_0000u64;
    assert_eq!(branch_target(b(4), pc), Some(pc + 16));
    assert_eq!(branch_target(b(-4), pc), Some(pc - 16));
    assert_eq!(branch_target(bl(1), pc), Some(pc + 4));
}

#[test]
fn abs_branch_encoding() {
    let mut bytes = Vec::new();
    append_abs_branch(&mut bytes, 0x1234_5678_9ABC_DEF0, false);
    assert_eq!(bytes.len(), 16);
    assert_eq!(
        u32::from_le_bytes(bytes[0..4].try_into().unwrap()),
        LDR_X16_PC8
    );
    assert_eq!(u32::from_le_bytes(bytes[4..8].try_into().unwrap()), BR_X16);
    assert_eq!(
        u64::from_le_bytes(bytes[8..16].try_into().unwrap()),
        0x1234_5678_9ABC_DEF0
    );
    let mut linked = Vec::new();
    append_abs_branch(&mut linked, 0xDEAD_BEEF, true);
    assert_eq!(
        u32::from_le_bytes(linked[4..8].try_into().unwrap()),
        BLR_X16
    );
}

#[test]
fn classify_branches() {
    assert!(is_b(b(1)));
    assert!(!is_bl(b(1)));
    assert!(is_bl(bl(1)));
    assert!(!is_b(bl(1)));
    assert!(needs_absolute_island(b(1)));
    assert!(needs_absolute_island(bl(1)));
    assert!(!needs_absolute_island(adr(0, 1)));
    assert!(!needs_absolute_island(NOP));
}

#[test]
fn assemble_trampoline_relocates_prologue() {
    let stp = 0xA9BF_7BFDu32;
    let mov = 0x9100_03FDu32;
    let adrp_insn = adrp(8, 0x20);
    let mut prologue = Vec::new();
    for insn in [stp, mov, adrp_insn, NOP] {
        prologue.extend_from_slice(&insn.to_le_bytes());
    }
    let src_base = 0x140_0010_0000u64;
    let dst_base = 0x140_0030_0000u64;
    let resume = src_base + STOLEN_BYTES as u64;
    let body = assemble_trampoline(&prologue, src_base, dst_base, resume).expect("relocatable");
    assert_eq!(body.len(), 32);
    assert_eq!(u32::from_le_bytes(body[0..4].try_into().unwrap()), stp);
    assert_eq!(u32::from_le_bytes(body[4..8].try_into().unwrap()), mov);
    assert_eq!(u32::from_le_bytes(body[12..16].try_into().unwrap()), NOP);
    let orig_target = adr_target(adrp_insn, src_base + 8, true);
    let reloc_adrp = u32::from_le_bytes(body[8..12].try_into().unwrap());
    assert_eq!(adr_target(reloc_adrp, dst_base + 8, true), orig_target);
    assert_eq!(
        u32::from_le_bytes(body[16..20].try_into().unwrap()),
        LDR_X16_PC8
    );
    assert_eq!(u32::from_le_bytes(body[20..24].try_into().unwrap()), BR_X16);
    assert_eq!(u64::from_le_bytes(body[24..32].try_into().unwrap()), resume);
}

#[test]
fn assemble_trampoline_promotes_leading_branch() {
    let lead_b = b(0x4000);
    let mut prologue = Vec::new();
    for insn in [lead_b, NOP, NOP, NOP] {
        prologue.extend_from_slice(&insn.to_le_bytes());
    }
    let src_base = 0x140_0010_0000u64;
    let dst_base = 0x0000_7000_0000u64;
    let resume = src_base + STOLEN_BYTES as u64;
    let body = assemble_trampoline(&prologue, src_base, dst_base, resume).expect("island path");
    assert_eq!(body.len(), 48);
    let first = u32::from_le_bytes(body[0..4].try_into().unwrap());
    assert!(is_b(first));
    let island_addr = branch_target(first, dst_base).unwrap();
    assert_eq!(island_addr, dst_base + 16 + 16);
    let original_b_target = branch_target(lead_b, src_base).unwrap();
    assert_eq!(
        u32::from_le_bytes(body[32..36].try_into().unwrap()),
        LDR_X16_PC8
    );
    assert_eq!(u32::from_le_bytes(body[36..40].try_into().unwrap()), BR_X16);
    assert_eq!(
        u64::from_le_bytes(body[40..48].try_into().unwrap()),
        original_b_target
    );
}

#[test]
fn assemble_trampoline_promotes_leading_bl_with_blr() {
    let lead_bl = bl(0x100);
    let mut prologue = Vec::new();
    for insn in [lead_bl, NOP, NOP, NOP] {
        prologue.extend_from_slice(&insn.to_le_bytes());
    }
    let src_base = 0x140_0010_0000u64;
    let dst_base = 0x0000_7000_0000u64;
    let resume = src_base + STOLEN_BYTES as u64;
    let body = assemble_trampoline(&prologue, src_base, dst_base, resume).expect("island path");
    assert_eq!(body.len(), 48);
    let first = u32::from_le_bytes(body[0..4].try_into().unwrap());
    assert!(is_bl(first));
    let original = branch_target(lead_bl, src_base).unwrap();
    assert_eq!(
        u32::from_le_bytes(body[36..40].try_into().unwrap()),
        BLR_X16
    );
    assert_eq!(
        u64::from_le_bytes(body[40..48].try_into().unwrap()),
        original
    );
}

#[test]
fn assemble_trampoline_two_islands() {
    let b0 = b(0x10);
    let bl1 = bl(0x20);
    let mut prologue = Vec::new();
    for insn in [b0, NOP, bl1, NOP] {
        prologue.extend_from_slice(&insn.to_le_bytes());
    }
    let src_base = 0x140_0010_0000u64;
    let dst_base = 0x0000_7000_0000u64;
    let resume = src_base + STOLEN_BYTES as u64;
    let body = assemble_trampoline(&prologue, src_base, dst_base, resume).expect("islands");
    assert_eq!(body.len(), 64);
    let first = u32::from_le_bytes(body[0..4].try_into().unwrap());
    let third = u32::from_le_bytes(body[8..12].try_into().unwrap());
    let island0 = branch_target(first, dst_base).unwrap();
    let island1 = branch_target(third, dst_base + 8).unwrap();
    assert_eq!(island0, dst_base + 32);
    assert_eq!(island1, dst_base + 48);
    assert_eq!(
        u64::from_le_bytes(body[40..48].try_into().unwrap()),
        branch_target(b0, src_base).unwrap()
    );
    assert_eq!(
        u64::from_le_bytes(body[56..64].try_into().unwrap()),
        branch_target(bl1, src_base + 8).unwrap()
    );
}

#[test]
fn assemble_trampoline_refuses_unrelocatable_narrow_branch() {
    let cond = bcond(0x2, 0x10);
    let mut prologue = Vec::new();
    for insn in [NOP, cond, NOP, NOP] {
        prologue.extend_from_slice(&insn.to_le_bytes());
    }
    let src_base = 0x0u64;
    let dst_base = 0x0000_0080_0000u64;
    let resume = src_base + STOLEN_BYTES as u64;
    assert_eq!(
        assemble_trampoline(&prologue, src_base, dst_base, resume),
        None
    );
}

#[test]
fn relocated_prologue_matches_assemble_prefix() {
    let stp = 0xA9BF_7BFDu32;
    let mut prologue = Vec::new();
    for insn in [stp, NOP, NOP, NOP] {
        prologue.extend_from_slice(&insn.to_le_bytes());
    }
    let src_base = 0x140_0010_0000u64;
    let dst_base = 0x140_0030_0000u64;
    let pro = relocated_prologue(&prologue, src_base, dst_base).expect("ok");
    assert_eq!(pro.len(), 16);
    assert_eq!(u32::from_le_bytes(pro[0..4].try_into().unwrap()), stp);
}

#[test]
fn island_holds_absolute_branch_to_original_target() {
    let lead_bl = bl(0x100);
    let src_base = 0x140_0010_0000u64;
    let original_target = branch_target(lead_bl, src_base).unwrap();
    let island = island_for_branch(lead_bl, src_base).expect("island");
    assert_eq!(island.len(), 16);
    assert_eq!(
        u32::from_le_bytes(island[4..8].try_into().unwrap()),
        BLR_X16
    );
    assert_eq!(
        u64::from_le_bytes(island[8..16].try_into().unwrap()),
        original_target
    );
}

#[test]
fn stolen_bytes_is_four_instructions() {
    assert_eq!(STOLEN_BYTES, 16);
    assert_eq!(STOLEN_BYTES % 4, 0);
}

#[test]
fn encode_imm26_rejects_unaligned_and_overflow() {
    assert_eq!(encode_imm26(3), None);
    assert_eq!(encode_imm26(4), Some(1));
    assert_eq!(encode_imm26(-4), Some((-1i32 as u32) & 0x03FF_FFFF));
    assert_eq!(encode_imm26(1 << 27), None);
}
