//! Tests for `decode_front_matter` — SPEC §"Front-matter-only decode".
//!
//! Contract:
//! - `Ok(None)` when the source has no front matter at all.
//! - `Ok(Some(Value::Table(empty)))` when FM is present but empty.
//! - `Ok(Some(Value::Table(...)))` when FM has user keys.
//! - `_dms_tier: 0` is consumed (does not appear in the returned map).
//! - `_dms_tier: N` for N >= 1 is an error (no tier >= 1 defined).
//! - Unknown reserved (`_`-prefixed) keys are an error.
//! - Unterminated FM is an error.
//! - Body errors (e.g. unterminated heredoc below the FM closer) are
//!   NOT surfaced — body bytes aren't tokenized.

use dms::{decode_document, decode_front_matter, Value};

fn unwrap_table(v: Value) -> dms::DmsMap<Value> {
    match v {
        Value::Table(t) => t,
        other => panic!("expected Value::Table, got {other:?}"),
    }
}

#[test]
fn no_front_matter_returns_none() {
    let src = "key: 1\n";
    let got = decode_front_matter(src).unwrap();
    assert!(got.is_none(), "expected None, got {got:?}");
}

#[test]
fn no_front_matter_with_only_trivia_returns_none() {
    // Leading blank lines + comments but no `+++` → still None.
    let src = "\n\n# leading line comment\n\nkey: 1\n";
    let got = decode_front_matter(src).unwrap();
    assert!(got.is_none(), "expected None, got {got:?}");
}

#[test]
fn empty_document_returns_none() {
    // Blank document — no FM marker → None (distinguishable from
    // `+++\n+++\n` which would return `Some(empty table)`).
    let got = decode_front_matter("").unwrap();
    assert!(got.is_none());
    let got = decode_front_matter("\n\n\n").unwrap();
    assert!(got.is_none());
}

#[test]
fn empty_front_matter_returns_some_empty_table() {
    let src = "+++\n+++\n";
    let got = decode_front_matter(src).expect("empty FM should decode");
    let table = unwrap_table(got.expect("FM was present"));
    assert!(table.is_empty(), "expected empty table, got {table:?}");
}

#[test]
fn front_matter_with_user_keys_round_trips_values() {
    let src = "\
+++
title: \"My Doc\"
version: 3
draft: true
+++
body_key: 1
";
    let table = unwrap_table(decode_front_matter(src).unwrap().unwrap());
    assert_eq!(table.len(), 3);
    assert_eq!(table.get("title"), Some(&Value::String("My Doc".to_string())));
    assert_eq!(table.get("version"), Some(&Value::Integer(3)));
    assert_eq!(table.get("draft"), Some(&Value::Bool(true)));
}

#[test]
fn dms_tier_zero_is_consumed_not_returned() {
    // Reserved `_dms_*` keys are validated and consumed by the parser;
    // they MUST NOT appear in the returned meta table.
    let src = "\
+++
_dms_tier: 0
title: \"hello\"
+++
";
    let table = unwrap_table(decode_front_matter(src).unwrap().unwrap());
    assert!(
        !table.contains_key("_dms_tier"),
        "reserved key leaked into meta: {table:?}"
    );
    assert_eq!(table.get("title"), Some(&Value::String("hello".to_string())));
    assert_eq!(table.len(), 1);
}

#[test]
fn dms_tier_one_is_rejected() {
    let src = "\
+++
_dms_tier: 1
+++
";
    let err = decode_front_matter(src).expect_err("tier >= 1 must error");
    assert!(
        err.message.contains("_dms_tier"),
        "expected _dms_tier in message, got: {}",
        err.message
    );
}

#[test]
fn unknown_reserved_key_is_rejected() {
    let src = "\
+++
_foo: 1
+++
";
    let err = decode_front_matter(src).expect_err("unknown reserved key must error");
    assert!(
        err.message.contains("_foo") || err.message.contains("reserved"),
        "expected reserved-key diagnostic, got: {}",
        err.message
    );
}

#[test]
fn unterminated_front_matter_is_rejected() {
    let src = "\
+++
key: 1
key2: 2
";
    let err = decode_front_matter(src).expect_err("missing `+++` closer must error");
    assert!(
        err.message.contains("unterminated") || err.message.contains("front matter"),
        "expected unterminated-FM diagnostic, got: {}",
        err.message
    );
}

#[test]
fn body_errors_are_not_surfaced() {
    // Valid FM, then a body with an unterminated heredoc 50+ lines below
    // the closer. `decode_front_matter` must NOT tokenize the body, so
    // it returns Ok with the FM table. `decode_document` over the same
    // source MUST error — this lets the test assert "the body really
    // is broken; we just don't see it".
    let mut src = String::from("+++\ntitle: \"ok\"\n+++\n");
    src.push_str("doc: \"\"\"\n"); // open a basic-triple heredoc, never close it
    for i in 0..60 {
        src.push_str(&format!("line {i}\n"));
    }

    // Front-matter-only path: clean.
    let table = unwrap_table(decode_front_matter(&src).unwrap().unwrap());
    assert_eq!(table.get("title"), Some(&Value::String("ok".to_string())));

    // Full decode: must error (sanity — proves the body is genuinely
    // malformed, so the previous Ok wasn't accidental).
    assert!(
        decode_document(&src).is_err(),
        "expected full decode to surface the body error"
    );
}

#[test]
fn bom_at_start_is_rejected() {
    let src = "\u{FEFF}+++\n+++\n";
    let err = decode_front_matter(src).expect_err("leading BOM must error");
    assert_eq!(err.line, 1);
    assert_eq!(err.column, 1);
    assert!(err.message.contains("BOM"));
}

#[test]
fn nul_byte_in_source_is_rejected() {
    // NUL anywhere in the source → diagnostic, even before we'd reach
    // the FM block. (Matches `decode_document_with_options`.)
    let src = "+++\nkey: \"x\0y\"\n+++\n";
    let err = decode_front_matter(src).expect_err("embedded NUL must error");
    assert!(err.message.contains("NUL"), "got: {}", err.message);
}

#[test]
fn front_matter_after_leading_trivia_is_accepted() {
    // SPEC: leading blank lines + comments are scanned before the
    // opening `+++`.
    let src = "\
\n
# leading comment

+++
k: 1
+++
body: 2
";
    let table = unwrap_table(decode_front_matter(src).unwrap().unwrap());
    assert_eq!(table.get("k"), Some(&Value::Integer(1)));
}
