//! Conformance corpus for the native sink (`live-conformance/native-sink/`,
//! extensions/native-sink.md §10).
//!
//!   wai_native_sink_vectors gen     <corpus-dir>   write vectors.json, from fixed seeds
//!   wai_native_sink_vectors verify  <corpus-dir>   vectors.json regenerates byte for byte,
//!                                                 and every call gets the answer it states
//!   wai_native_sink_vectors answers [--c-abi] <case.json>
//!                                                 the sink's answer to each call of one case
//!                                                 as `wai conform sink --suite native-sink`
//!                                                 gives it, its answers withheld: one JSON
//!                                                 text a line, RFC 8785; through the Rust
//!                                                 module, or with `--c-abi` through the C
//!                                                 interface a native integration links
//!
//! The generator and this verifier are one implementation. The corpus's second
//! implementation is `live-conformance/native_sink_verify.py`, written from the
//! extension's text and sharing no code with this binary.

use std::path::Path;
use wai::native_sink::corpus;

fn main() {
    let a: Vec<String> = std::env::args().collect();
    if a.get(1).map(String::as_str) == Some("answers") {
        let (abi, file) = match (a.get(2).map(String::as_str), a.get(3)) {
            (Some("--c-abi"), Some(f)) => (true, f),
            (Some(f), None) if f != "--c-abi" => (false, &a[2]),
            _ => {
                eprintln!("usage: wai_native_sink_vectors answers [--c-abi] <case.json>");
                std::process::exit(2);
            }
        };
        std::process::exit(answers(Path::new(file), abi));
    }
    match (a.get(1).map(String::as_str), a.get(2)) {
        (Some("gen"), Some(d)) => {
            let p = Path::new(d).join("vectors.json");
            if let Err(e) = std::fs::create_dir_all(d).and_then(|_| std::fs::write(&p, corpus::bytes(&corpus::generate()))) {
                eprintln!("write {}: {e}", p.display());
                std::process::exit(1);
            }
            println!("wrote {}", p.display());
        }
        (Some("verify"), Some(d)) => std::process::exit(verify(Path::new(d))),
        _ => {
            eprintln!("usage: wai_native_sink_vectors gen|verify <corpus-dir> | answers [--c-abi] <case.json>");
            std::process::exit(2);
        }
    }
}

fn verify(dir: &Path) -> i32 {
    let p = dir.join("vectors.json");
    let raw = match std::fs::read(&p) {
        Ok(b) => b,
        Err(e) => {
            eprintln!("read {}: {e}", p.display());
            return 1;
        }
    };
    let mut fail = 0;
    let same = raw == corpus::bytes(&corpus::generate());
    println!("{} vectors.json regenerates byte for byte from its seeds", if same { "PASS" } else { "FAIL" });
    fail += (!same) as i32;
    let r = serde_json::from_slice(&raw).map_err(|e| e.to_string()).and_then(|d| corpus::verify(&d));
    match &r {
        Ok(()) => println!("PASS every call gets the answer the corpus states"),
        Err(e) => println!("FAIL {e}"),
    }
    fail += r.is_err() as i32;
    (fail != 0) as i32
}

/// One case's answers, each on its own line in RFC 8785 form: the conformance runner hashes
/// what is written and compares it with the answers the corpus states.
fn answers(file: &Path, abi: bool) -> i32 {
    let case: serde_json::Value = match std::fs::read(file).map_err(|e| e.to_string()).and_then(|b| serde_json::from_slice(&b).map_err(|e| e.to_string())) {
        Ok(v) => v,
        Err(e) => {
            eprintln!("{}: {e}", file.display());
            return 1;
        }
    };
    let got = if abi { c_abi::answer_case(&case) } else { corpus::answer_case(&case) };
    match got.and_then(|v| v.iter().map(|a| wai::jcs::jcs(a).map_err(|e| e.to_string())).collect::<Result<Vec<_>, _>>()) {
        Ok(lines) => {
            for l in lines {
                println!("{l}");
            }
            0
        }
        Err(e) => {
            eprintln!("{}: {e}", file.display());
            1
        }
    }
}

/// The native sink through its C interface (`ffi_sink`, extensions/native-sink.md §9), as a
/// native integration links it: every call made through the C symbols, and a refusal read
/// from `wai_last_error`.
mod c_abi {
    use serde_json::{json, Value};
    use std::ffi::{CStr, CString};
    use wai::ffi::{wai_buffer_free, wai_last_error, WaiBuffer, WAI_OK};
    use wai::ffi_sink::*;

    /// A buffer's bytes, the buffer freed. An empty buffer may carry a null pointer, which
    /// is never read or freed.
    fn take(b: WaiBuffer) -> Vec<u8> {
        if b.data.is_null() {
            return Vec::new();
        }
        let v = if b.len == 0 { Vec::new() } else { unsafe { std::slice::from_raw_parts(b.data, b.len) }.to_vec() };
        unsafe { wai_buffer_free(b) };
        v
    }

    fn empty() -> WaiBuffer {
        WaiBuffer { data: std::ptr::null_mut(), len: 0, cap: 0 }
    }

    /// The reason of the last refusal: the code before the first colon.
    fn refused() -> Value {
        let s = unsafe { CStr::from_ptr(wai_last_error()) }.to_string_lossy().into_owned();
        json!({"error": s.split(':').next().unwrap_or_default()})
    }

    fn c(s: &str) -> Result<CString, String> {
        CString::new(s).map_err(|e| e.to_string())
    }

    fn parsed(b: WaiBuffer) -> Result<Value, String> {
        serde_json::from_slice(&take(b)).map_err(|e| e.to_string())
    }

    pub fn answer_case(case: &Value) -> Result<Vec<Value>, String> {
        let obj = |n: &Value| -> Result<Option<Vec<u8>>, String> {
            match n.as_str() {
                None => Ok(None),
                Some(n) => case["objects"][n].as_str().and_then(wai::stream_audit::unhex).map(Some).ok_or_else(|| format!("no object {n}")),
            }
        };
        let cfg = c(&case["config"].to_string())?;
        let seed = case["seed"].as_str().and_then(wai::stream_audit::unhex).filter(|s| s.len() == 32).ok_or("seed: 32 bytes, hexadecimal")?;
        let start = case["start_us"].as_u64().ok_or("start_us: an integer")?;
        let mut s: *mut WaiSink = std::ptr::null_mut();
        if unsafe { wai_sink_open(cfg.as_ptr(), seed.as_ptr(), 32, start, &mut s) } != WAI_OK {
            return Ok(vec![refused()]);
        }
        // The session is freed however the case ends.
        struct Open(*mut WaiSink);
        impl Drop for Open {
            fn drop(&mut self) {
                unsafe { wai_sink_free(self.0) };
            }
        }
        let open = Open(s);
        let s = open.0;
        let mut out = Vec::new();
        for call in case["calls"].as_array().ok_or("no calls")? {
            let d = &call["do"];
            let text = |k: &str| d[k].as_str().ok_or_else(|| format!("{k}: a string")).and_then(c);
            let mut buf = empty();
            let got = match d["call"].as_str().unwrap_or("") {
                "playlist" => {
                    let (url, b, r) = (text("url")?, obj(&d["body"])?.unwrap_or_default(), obj(&d["receipt"])?);
                    let rp = r.as_ref().map_or(std::ptr::null(), |r| r.as_ptr());
                    let st = unsafe { wai_sink_playlist(s, url.as_ptr(), b.as_ptr(), b.len(), rp, r.as_ref().map_or(0, Vec::len), &mut buf) };
                    if st == WAI_OK { parsed(buf)? } else { refused() }
                }
                "parent" => {
                    let (url, name, b) = (text("url")?, text("name")?, obj(&d["body"])?);
                    let bp = b.as_ref().map_or(std::ptr::null(), |b| b.as_ptr());
                    let st = unsafe { wai_sink_playlist_parent(s, url.as_ptr(), name.as_ptr(), bp, b.as_ref().map_or(0, Vec::len)) };
                    if st == WAI_OK { json!({"ok": true}) } else { refused() }
                }
                "object" => {
                    let (meta, b) = (c(&d["meta"].to_string())?, obj(&d["body"])?.unwrap_or_default());
                    let st = unsafe { wai_sink_object(s, meta.as_ptr(), b.as_ptr(), b.len(), &mut buf) };
                    if st == WAI_OK { parsed(buf)? } else { refused() }
                }
                "event" => {
                    let n = text("name")?;
                    let st = unsafe { wai_sink_event(s, n.as_ptr(), d["t_us"].as_u64().unwrap_or(0)) };
                    if st == WAI_OK { json!({"ok": true}) } else { refused() }
                }
                "observe" => {
                    let o = c(&d["obs"].to_string())?;
                    let st = unsafe { wai_sink_observe(s, o.as_ptr(), &mut buf) };
                    if st == WAI_OK { parsed(buf)? } else { refused() }
                }
                "energy" => {
                    let e = c(&d["entry"].to_string())?;
                    let st = unsafe { wai_sink_energy(s, e.as_ptr()) };
                    if st == WAI_OK { json!({"ok": true}) } else { refused() }
                }
                "attach" => {
                    let (n, b) = (text("name")?, obj(&d["body"])?.unwrap_or_default());
                    let st = unsafe { wai_sink_attach(s, n.as_ptr(), b.as_ptr(), b.len()) };
                    if st == WAI_OK { json!({"ok": true}) } else { refused() }
                }
                "cmcd" => {
                    let st = unsafe { wai_sink_take_cmcd(s, &mut buf) };
                    if st == WAI_OK { json!({"cmcd": String::from_utf8(take(buf)).map_err(|e| e.to_string())?}) } else { refused() }
                }
                "end" => {
                    let n = text("ended")?;
                    let st = unsafe { wai_sink_end(s, n.as_ptr(), d["t_us"].as_u64().unwrap_or(0), &mut buf) };
                    if st == WAI_OK { json!({"bundle": String::from_utf8(take(buf)).map_err(|e| e.to_string())?}) } else { refused() }
                }
                other => return Err(format!("an unknown call {other}")),
            };
            out.push(got);
        }
        drop(open);
        Ok(out)
    }
}
