use super::*;
use std::cell::Cell;

fn header(t: Target) -> Vec<u8> {
    let mut b = vec![0; 256];
    match t {
        Target::WindowsAmd64 => {
            b[..2].copy_from_slice(b"MZ");
            b[0x3c..0x40].copy_from_slice(&64u32.to_le_bytes());
            b[64..68].copy_from_slice(b"PE\0\0");
            b[68..70].copy_from_slice(&0x8664u16.to_le_bytes());
            b[86..88].copy_from_slice(&2u16.to_le_bytes());
            b[88..90].copy_from_slice(&0x20bu16.to_le_bytes());
            b[156..158].copy_from_slice(&3u16.to_le_bytes());
        }
        Target::MacosArm64 => {
            b[..4].copy_from_slice(&[0xcf, 0xfa, 0xed, 0xfe]);
            b[4..8].copy_from_slice(&0x0100000cu32.to_le_bytes());
            b[12..16].copy_from_slice(&2u32.to_le_bytes());
        }
        Target::LinuxAmd64 | Target::LinuxArm64 => {
            b[..7].copy_from_slice(b"\x7fELF\x02\x01\x01");
            b[16..18].copy_from_slice(&2u16.to_le_bytes());
            let cpu: u16 = if t == Target::LinuxAmd64 { 62 } else { 183 };
            b[18..20].copy_from_slice(&cpu.to_le_bytes());
        }
    }
    b.extend_from_slice(&runtime_marker(t));
    b
}
struct Dir(PathBuf);
impl Dir {
    fn new() -> Self {
        let p = std::env::temp_dir().join(format!(
            "tb-export-test-{}-{}",
            std::process::id(),
            NEXT.fetch_add(1, Ordering::Relaxed)
        ));
        fs::create_dir(&p).unwrap();
        Self(p)
    }
}
impl Drop for Dir {
    fn drop(&mut self) {
        fs::remove_dir_all(&self.0).unwrap();
    }
}

#[test]
fn targets_versions_integrity_and_payload_bounds_are_enforced() {
    let d = Dir::new();
    for t in Target::ALL {
        let b = header(t);
        assert_eq!(native_target(&b).unwrap(), t);
        let path = d.0.join(t.runtime_name());
        fs::write(&path, &b).unwrap();
        prepare_template(&path, t).unwrap();
        assert_eq!(template(&path, t).unwrap(), b);
        let other = if t == Target::LinuxAmd64 {
            Target::LinuxArm64
        } else {
            Target::LinuxAmd64
        };
        assert!(template(&path, other)
            .unwrap_err()
            .to_string()
            .contains("Architektur"));
        let meta = manifest(&b, t);
        for wrong in [
            meta.replace("runtime=1", "runtime=2"),
            meta.replace(
                &format!("package={}", env!("CARGO_PKG_VERSION")),
                "package=9",
            ),
            meta.replace("tbc=4", "tbc=99"),
            meta.replace("checksum=", "checksum=0"),
        ] {
            fs::write(manifest_path(&path), wrong).unwrap();
            assert!(template(&path, t).is_err());
        }
        fs::remove_file(manifest_path(&path)).unwrap();
        assert!(template(&path, t).is_err());
        fs::remove_file(&path).unwrap();
        assert!(template(&path, t)
            .unwrap_err()
            .to_string()
            .contains("fehlt"));
        for len in 0..64 {
            assert!(native_target(&b[..len]).is_err());
        }
    }
    assert!(Target::parse("aarch64-pc-windows-msvc").is_err());
    assert!(Target::parse("x86_64-apple-darwin").is_err());
    let mut foreign_abi = header(Target::LinuxAmd64);
    foreign_abi[7] = 9;
    assert!(native_target(&foreign_abi).is_err());
    let module = tb_vm::compile_source("TEST", "PRINT 42\nEND\n").unwrap();
    for t in [Target::WindowsAmd64, Target::LinuxAmd64, Target::LinuxArm64] {
        let mut b = header(t);
        if t == Target::WindowsAmd64 {
            let mut signed = b.clone();
            signed[196..200].copy_from_slice(&5u32.to_le_bytes());
            signed[232] = 1;
            assert!(append_payload(&mut signed, &module, t)
                .unwrap_err()
                .to_string()
                .contains("Signierte PE"));
            b[152..156].copy_from_slice(&123u32.to_le_bytes());
        }
        append_payload(&mut b, &module, t).unwrap();
        if t == Target::WindowsAmd64 {
            assert_eq!(&b[152..156], &[0; 4]);
        }
        assert_eq!(embedded(&b, t).unwrap().to_tbc(), module.to_tbc());
        let at = b.len() - FOOTER;
        for field in [8, 12, 16, 20, 24, 32, 40] {
            let mut bad = b.clone();
            bad[at + field] ^= 0xff;
            assert!(embedded(&bad, t).is_err(), "{field}");
        }
        for cut in 1..=FOOTER + 1 {
            assert!(embedded(&b[..b.len() - cut], t).is_err());
        }
        let mut bad = b.clone();
        bad[300] ^= 1;
        assert!(embedded(&bad, t).is_err());
    }
}

#[test]
fn publication_preserves_originals_on_conflict_failure_race_and_cancel() {
    let d = Dir::new();
    let out = d.0.join("result");
    let source = d.0.join("source.bas");
    fs::write(&source, b"source").unwrap();
    fs::write(&out, b"old").unwrap();
    assert!(publish(&out, false, &[], &|| false, |_| panic!(
        "darf nicht schreiben"
    ))
    .is_err());
    assert!(publish(
        &source,
        true,
        std::slice::from_ref(&source),
        &|| false,
        |_| panic!("Projektschutz")
    )
    .is_err());
    assert!(publish(&out, true, &[], &|| false, |p| {
        fs::write(p, b"new")?;
        bail!("Finalisierungsfehler")
    })
    .is_err());
    assert_eq!(fs::read(&out).unwrap(), b"old");
    let cancel = Cell::new(false);
    assert!(publish(&out, true, &[], &|| cancel.get(), |p| {
        fs::write(p, b"new")?;
        cancel.set(true);
        Ok(())
    })
    .is_err());
    assert_eq!(fs::read(&out).unwrap(), b"old");
    assert!(publish(&out, true, &[], &|| true, |_| panic!("abgebrochen")).is_err());
    assert!(publish(&out, true, &[], &|| false, |p| {
        fs::write(p, b"new")?;
        fs::write(&out, b"external")?;
        Ok(())
    })
    .is_err());
    assert_eq!(fs::read(&out).unwrap(), b"external");
    fs::remove_file(&out).unwrap();
    assert!(publish(&out, true, &[], &|| false, |p| {
        fs::write(p, b"new")?;
        fs::write(&out, b"racer")?;
        Ok(())
    })
    .is_err());
    assert_eq!(fs::read(&out).unwrap(), b"racer");
    assert!(
        publish(&d.0.join("missing/result"), false, &[], &|| false, |_| Ok(
            ()
        ))
        .is_err()
    );
    publish(&out, true, &[], &|| false, |p| {
        fs::write(p, b"approved")?;
        Ok(())
    })
    .unwrap();
    assert_eq!(fs::read(&out).unwrap(), b"approved");
    assert_eq!(fs::read(&source).unwrap(), b"source");
    assert_eq!(fs::read_dir(&d.0).unwrap().count(), 2);
    #[cfg(unix)]
    {
        std::os::unix::fs::symlink(&source, d.0.join("alias")).unwrap();
        assert!(
            publish(&d.0.join("alias"), true, &[], &|| false, |_| panic!(
                "Symlink"
            ))
            .is_err()
        );
    }
}

#[test]
fn fehlgeschlagene_native_finalisierung_erhaelt_das_ziel() {
    let dir = Dir::new();
    let runtime = dir.0.join("tbrt");
    let out = dir.0.join("program");
    fs::write(&runtime, header(Target::MacosArm64)).unwrap();
    prepare_template(&runtime, Target::MacosArm64).unwrap();
    fs::write(&out, b"original").unwrap();
    let module = tb_vm::compile_source("TEST", "END\n").unwrap();
    let error = export(
        Export {
            module: &module,
            target: Target::MacosArm64,
            template: &runtime,
            output: &out,
            overwrite: true,
            protected: &[],
        },
        &|| false,
    )
    .unwrap_err();
    assert!(error.to_string().contains("Finalisierung"), "{error:#}");
    assert_eq!(fs::read(&out).unwrap(), b"original");
    assert_eq!(fs::read_dir(&dir.0).unwrap().count(), 3);
}
