//! Per-variant byte-range assertions for the `span` field on every
//! `ErrorKind` variant, plus `Span::slice` / `Span::line_col` semantics.
//!
//! These are the contract tests for the `0.1.6` Span plumbing: every
//! variant's `span` must point at the offending substring inside the
//! original input.

use ktav::{Error, ErrorKind, Span};

fn span_of(text: &str) -> (ErrorKind, String) {
    let err = ktav::parse(text).expect_err("expected parse failure");
    match err {
        Error::Structured(k) => {
            let slice = k
                .span()
                .slice(text)
                .map(|s| s.to_string())
                .unwrap_or_default();
            (k, slice)
        }
        other => panic!("expected Structured, got {other:?}"),
    }
}

// ---------------------------------------------------------------------------
// One fixture per variant — span byte range checked.
// ---------------------------------------------------------------------------

#[test]
fn span_missing_separator_space() {
    // `key:value` — body offset 4..9 ("value")
    let text = "key:value\n";
    let (k, slice) = span_of(text);
    assert!(matches!(k, ErrorKind::MissingSeparatorSpace { .. }));
    assert_eq!(slice, "value");
}

#[test]
fn span_invalid_typed_scalar() {
    // `port:i abc` — body span covers " abc" (or close to).
    let text = "port:i abc\n";
    let (k, slice) = span_of(text);
    assert!(matches!(k, ErrorKind::InvalidTypedScalar { .. }));
    // Body span starts immediately after `:i` and runs to end of trimmed
    // line. The raw body string is " abc" but slicing the input gives
    // exactly that range.
    assert_eq!(slice, " abc");
}

#[test]
fn span_duplicate_key() {
    let text = "port: 80\nport: 443\n";
    let (k, slice) = span_of(text);
    assert!(matches!(k, ErrorKind::DuplicateKey { .. }));
    // Span covers the duplicate `port` key on line 2.
    assert_eq!(slice, "port");
}

#[test]
fn span_key_path_conflict() {
    let text = "db: 1\ndb.x: 2\n";
    let (k, slice) = span_of(text);
    assert!(matches!(k, ErrorKind::KeyPathConflict { .. }));
    // Span covers the `db.x` key on line 2.
    assert_eq!(slice, "db.x");
}

#[test]
fn span_empty_key() {
    let text = ": value\n";
    let (k, slice) = span_of(text);
    assert!(matches!(k, ErrorKind::EmptyKey { .. }));
    // Span points at the leading `:` byte.
    assert_eq!(slice, ":");
}

#[test]
fn span_invalid_key() {
    let text = "a.: 1\n";
    let (k, slice) = span_of(text);
    assert!(matches!(k, ErrorKind::InvalidKey { .. }));
    // Span covers the offending key (`a.`).
    assert_eq!(slice, "a.");
}

#[test]
fn span_unclosed_compound_object() {
    let text = "obj: {\n  a: 1\n";
    let (k, slice) = span_of(text);
    assert!(matches!(k, ErrorKind::UnclosedCompound { .. }));
    // Span starts at the `{` opener and runs to EOF.
    assert!(slice.starts_with('{'), "got: {slice:?}");
}

#[test]
fn span_unbalanced_bracket_stray() {
    let text = "}\n";
    let (k, slice) = span_of(text);
    assert!(matches!(k, ErrorKind::UnbalancedBracket { .. }));
    assert_eq!(slice, "}");
}

#[test]
fn span_unbalanced_bracket_mismatch() {
    let text = "obj: {\n]\n";
    let (k, slice) = span_of(text);
    assert!(matches!(k, ErrorKind::UnbalancedBracket { .. }));
    assert_eq!(slice, "]");
}

#[test]
fn span_inline_nonempty_compound() {
    let text = "server: { host: 127.0.0.1 }\n";
    let (k, slice) = span_of(text);
    assert!(matches!(k, ErrorKind::InlineNonEmptyCompound { .. }));
    // The trimmed line, minus the leading `server: ` prefix, is the
    // inline compound body — but our parser spans the full trimmed line.
    assert_eq!(slice, "server: { host: 127.0.0.1 }");
}

#[test]
fn span_missing_separator() {
    let text = "just-some-text\n";
    let (k, slice) = span_of(text);
    assert!(matches!(k, ErrorKind::MissingSeparator { .. }));
    assert_eq!(slice, "just-some-text");
}

// ---------------------------------------------------------------------------
// `Span::slice` and `Span::line_col` semantics.
// ---------------------------------------------------------------------------

#[test]
fn span_slice_returns_substring() {
    let s = Span::new(2, 5);
    assert_eq!(s.slice("abcdef"), Some("cde"));
}

#[test]
fn span_slice_out_of_bounds_returns_none() {
    let s = Span::new(10, 20);
    assert_eq!(s.slice("abcdef"), None);
}

#[test]
fn span_slice_non_char_boundary_returns_none() {
    // 'ы' is 2 bytes in UTF-8. Slicing inside it yields None.
    let text = "ыz";
    let s = Span::new(1, 2);
    assert_eq!(s.slice(text), None);
}

#[test]
fn span_line_col_first_line() {
    let text = "abc\ndef\n";
    let s = Span::new(0, 1);
    assert_eq!(s.line_col(text), (1, 0));
    let s = Span::new(2, 3);
    assert_eq!(s.line_col(text), (1, 2));
}

#[test]
fn span_line_col_second_line() {
    let text = "abc\ndef\n";
    // 'd' is at byte 4.
    let s = Span::new(4, 5);
    assert_eq!(s.line_col(text), (2, 0));
    // 'f' is at byte 6.
    let s = Span::new(6, 7);
    assert_eq!(s.line_col(text), (2, 2));
}

#[test]
fn span_line_col_with_multibyte_utf8_returns_byte_column() {
    // 'й' is a 2-byte UTF-8 char (0xD0 0xB9). Column is 0-based BYTES,
    // not Unicode chars.
    let text = "йx\nz";
    // Byte 2 = 'x' (after the 2-byte 'й'); column should be 2.
    let s = Span::new(2, 3);
    assert_eq!(s.line_col(text), (1, 2));
    // Byte 3 = '\n', still on line 1, column 3.
    let s = Span::new(3, 4);
    assert_eq!(s.line_col(text), (1, 3));
    // Byte 4 = 'z' on line 2 column 0.
    let s = Span::new(4, 5);
    assert_eq!(s.line_col(text), (2, 0));
}

#[test]
fn span_line_col_with_emoji_returns_byte_column() {
    // 🦀 is 4 bytes in UTF-8.
    let text = "🦀X";
    let s = Span::new(4, 5);
    let (line, col) = s.line_col(text);
    assert_eq!((line, col), (1, 4));
}
