#![allow(
    clippy::unwrap_used,
    clippy::panic,
    reason = "unwraps and panics are allowed in tests"
)]

use std::borrow::Cow;

use std::assert_matches;

use super::{eq, from_raw, Warning};
use crate::{json, warning::CaveatDeferred};

fn parse_name(s: &str) -> CaveatDeferred<Cow<'_, str>, Warning> {
    let doc = json::parse(s.into()).unwrap();
    let elem = doc.root().find_field("name").unwrap().element();
    let s = elem.to_raw_str().unwrap().as_unescaped_str();
    from_raw(s)
}

// --- strings that require no decoding ---

#[test]
fn should_unescape_empty_str() {
    const INPUT: &str = r#"{ "name": "" }"#;

    let (string, warnings) = parse_name(INPUT).into_parts();

    assert_matches!(string, Cow::Borrowed(""));
    assert!(warnings.is_empty(), "{:#?}", warnings.id_map());
}

#[test]
fn should_unescape_str_without_escapes() {
    const INPUT: &str = r#"{ "name": "ab" }"#;

    let (string, warnings) = parse_name(INPUT).into_parts();

    assert_matches!(string, Cow::Borrowed(_));
    assert!(warnings.is_empty(), "{:#?}", warnings.id_map());
}

// --- Simple escape sequences that decode without warnings ---

#[test]
fn should_unescape_forward_slash_escape() {
    const INPUT: &str = r#"{ "name": "a\/b" }"#;

    let (string, warnings) = parse_name(INPUT).into_parts();

    let Cow::Owned(s) = string else {
        panic!("expected an owned Cow, got {string:?}");
    };
    assert_eq!(s, "a/b");
    assert!(warnings.is_empty(), "{:#?}", warnings.id_map());
}

#[test]
fn should_unescape_backslash_escape() {
    const INPUT: &str = r#"{ "name": "a\\b" }"#;

    let (string, warnings) = parse_name(INPUT).into_parts();

    let Cow::Owned(s) = string else {
        panic!("expected an owned Cow, got {string:?}");
    };
    assert_eq!(s, r"a\b");
    assert!(warnings.is_empty(), "{:#?}", warnings.id_map());
}

#[test]
fn should_unescape_quote_escape() {
    const INPUT: &str = r#"{ "name": "a\"b" }"#;

    let (string, warnings) = parse_name(INPUT).into_parts();

    let Cow::Owned(s) = string else {
        panic!("expected an owned Cow, got {string:?}");
    };
    assert_eq!(s, r#"a"b"#);
    assert!(warnings.is_empty(), "{:#?}", warnings.id_map());
}

#[test]
fn should_unescape_str_with_many_escapes() {
    const INPUT: &str = r#"{"name": "a\/\"b\"" }"#;

    let (string, warnings) = parse_name(INPUT).into_parts();

    let Cow::Owned(s) = string else {
        panic!("expected an owned Cow, got {string:?}");
    };
    assert_eq!(s, r#"a/"b""#);
    assert!(warnings.is_empty(), "{:#?}", warnings.id_map());
}

#[test]
fn should_fail_to_unescape_raw_str_with_rust_unicode_literal_control_char() {
    const INPUT: &str = r#"{ "name": "hello\u{0019}world" }"#;

    let (string, warnings) = parse_name(INPUT).into_parts();

    assert_matches!(string, Cow::Borrowed(_));
    assert_matches!(
        warnings.into_warnings().as_slice(),
        [Warning::InvalidEscape(10)]
    );
}

#[test]
fn should_fail_to_unescape_json_control_escape() {
    const INPUT: &str = r#"{ "name": "hello\u0019world" }"#;

    let (string, warnings) = parse_name(INPUT).into_parts();

    assert_matches!(string, Cow::Borrowed(_));
    assert_matches!(
        warnings.into_warnings().as_slice(),
        [Warning::ControlCharacter(10)]
    );
}

// --- `\uXXXX` escapes that decode without warnings ---

#[test]
fn should_unescape_unicode_literals() {
    //   = space (U+0020), ! = '!' (U+0021)
    const INPUT: &str = r#"{ "name": "hello\u0020world\u0021" }"#;

    let (string, warnings) = parse_name(INPUT).into_parts();

    let Cow::Owned(s) = string else {
        panic!("expected an owned Cow, got {string:?}");
    };
    assert_eq!(s, "hello world!");
    assert!(warnings.is_empty(), "{:#?}", warnings.id_map());
}

#[test]
fn should_unescape_utf_16_surrogate_pair() {
    // This test data is taken from the JSON RFC 8259 spec.
    //
    // * See: <https://datatracker.ietf.org/doc/html/rfc8259#section-7>
    const INPUT: &str = r#"{ "name": "hello\uD834\uDD1Eworld" }"#;

    let (string, warnings) = parse_name(INPUT).into_parts();

    let Cow::Owned(s) = string else {
        panic!("expected an owned Cow, got {string:?}");
    };
    assert_eq!(s, "hello\u{1D11E}world");
    assert!(warnings.is_empty(), "{:#?}", warnings.id_map());
}

#[test]
fn should_unescape_unicode_literal_followed_by_simple_escape() {
    //   = space, ! = '!', \/ = '/' - mix of `\uXXXX` and simple escapes
    const INPUT: &str = r#"{ "name": "hello \/world!" }"#;

    let (string, warnings) = parse_name(INPUT).into_parts();

    let Cow::Owned(s) = string else {
        panic!("expected an owned Cow, got {string:?}");
    };
    assert_eq!(s, "hello /world!");
    assert!(warnings.is_empty(), "{:#?}", warnings.id_map());
}

#[test]
fn should_unescape_two_consecutive_bmp_unicode_escapes() {
    // Regression: two consecutive `\uXXXX` escapes where neither is a surrogate
    // must both decode. The bug caused the second codepoint to be silently
    // dropped because is_next_escape was called unconditionally, and
    // push_surrogate_pair only took `.next()` from `char::decode_utf16`.
    // `A` = 'A' (U+0041), `B` = 'B' (U+0042)
    const INPUT: &str = "{ \"name\": \"\\u0041\\u0042\" }";

    let (string, warnings) = parse_name(INPUT).into_parts();

    let Cow::Owned(s) = string else {
        panic!("expected an owned Cow, got {string:?}");
    };
    assert_eq!(s, "AB");
    assert!(warnings.is_empty(), "{:#?}", warnings.id_map());
}

#[test]
fn should_unescape_bmp_unicode_escape_followed_by_simple_escape() {
    // Regression: a `\uXXXX` (BMP) followed by a simple escape like \/ must
    // decode both. The bug caused is_next_escape to consume the `\` of the
    // following escape, return `Ok(None)`, and permanently lose the backslash
    // so the subsequent char was treated as a plain character instead.
    // `A` = 'A' (U+0041), \/ = '/'
    const INPUT: &str = "{ \"name\": \"\\u0041\\/B\" }";

    let (string, warnings) = parse_name(INPUT).into_parts();

    let Cow::Owned(s) = string else {
        panic!("expected an owned Cow, got {string:?}");
    };
    assert_eq!(s, "A/B");
    assert!(warnings.is_empty(), "{:#?}", warnings.id_map());
}

// --- Simple escape sequences that produce `ControlCharacter` ---
//
// `\b`, `\f`, `\n`, `\r`, `\t` are valid JSON escape sequences but they decode
// to control characters (U+0008, U+000C, U+000A, U+000D, U+0009). The decode
// layer rejects control characters regardless of how they were encoded.

#[test]
fn should_warn_on_backspace_escape() {
    const INPUT: &str = r#"{ "name": "a\bb" }"#;

    let (string, warnings) = parse_name(INPUT).into_parts();

    assert_matches!(string, Cow::Borrowed(_));
    assert_matches!(
        warnings.into_warnings().as_slice(),
        [Warning::ControlCharacter(_)]
    );
}

#[test]
fn should_warn_on_form_feed_escape() {
    const INPUT: &str = r#"{ "name": "a\fb" }"#;

    let (string, warnings) = parse_name(INPUT).into_parts();

    assert_matches!(string, Cow::Borrowed(_));
    assert_matches!(
        warnings.into_warnings().as_slice(),
        [Warning::ControlCharacter(_)]
    );
}

#[test]
fn should_warn_on_newline_escape() {
    const INPUT: &str = r#"{ "name": "a\nb" }"#;

    let (string, warnings) = parse_name(INPUT).into_parts();

    assert_matches!(string, Cow::Borrowed(_));
    assert_matches!(
        warnings.into_warnings().as_slice(),
        [Warning::ControlCharacter(_)]
    );
}

#[test]
fn should_warn_on_carriage_return_escape() {
    const INPUT: &str = r#"{ "name": "a\rb" }"#;

    let (string, warnings) = parse_name(INPUT).into_parts();

    assert_matches!(string, Cow::Borrowed(_));
    assert_matches!(
        warnings.into_warnings().as_slice(),
        [Warning::ControlCharacter(_)]
    );
}

#[test]
fn should_warn_on_tab_escape() {
    const INPUT: &str = r#"{ "name": "a\tb" }"#;

    let (string, warnings) = parse_name(INPUT).into_parts();

    assert_matches!(string, Cow::Borrowed(_));
    assert_matches!(
        warnings.into_warnings().as_slice(),
        [Warning::ControlCharacter(_)]
    );
}

// --- bare (unescaped) control characters ---

#[test]
fn should_warn_on_bare_control_char() {
    // The parser accepts bare control chars; the decode layer flags them.
    const INPUT: &str = "{ \"name\": \"a\tb\" }"; // literal tab

    let (string, warnings) = parse_name(INPUT).into_parts();

    assert_matches!(string, Cow::Borrowed(_));
    assert_matches!(
        warnings.into_warnings().as_slice(),
        [Warning::ControlCharacter(_)]
    );
}

// --- `\uXXXX` that resolves to a control character ---

#[test]
fn should_warn_on_unicode_escape_to_control_char() {
    // `	` is a valid four-hex-digit escape but decodes to U+0009 (tab),
    // which is a control character.
    const INPUT: &str = r#"{ "name": "	" }"#;

    let (string, warnings) = parse_name(INPUT).into_parts();

    assert_matches!(string, Cow::Borrowed(_));
    assert_matches!(
        warnings.into_warnings().as_slice(),
        [Warning::ControlCharacter(_)]
    );
}

// --- Invalid escape sequences => `InvalidEscape` ---

#[test]
fn should_warn_on_invalid_escape() {
    // `\z` is not a recognized JSON escape character.
    const INPUT: &str = r#"{ "name": "a\zb" }"#;

    let (string, warnings) = parse_name(INPUT).into_parts();

    assert_matches!(string, Cow::Borrowed(_));
    assert_matches!(
        warnings.into_warnings().as_slice(),
        [Warning::InvalidEscape(_)]
    );
}

#[test]
fn should_warn_on_lone_high_surrogate() {
    // `\uD800` is a high surrogate; without a following low surrogate it
    // cannot be converted to a scalar Unicode value.
    const INPUT: &str = r#"{ "name": "\uD800" }"#;

    let (string, warnings) = parse_name(INPUT).into_parts();

    assert_matches!(string, Cow::Borrowed(_));
    assert_matches!(
        warnings.into_warnings().as_slice(),
        [Warning::InvalidEscape(_)]
    );
}

// --- incomplete `\uXXXX` escape => `UnexpectedEndOfString` ---

#[test]
fn should_warn_on_incomplete_unicode_escape() {
    // `\u00` has only two hex digits; the decoder needs four.
    const INPUT: &str = r#"{ "name": "\u00" }"#;

    let (string, warnings) = parse_name(INPUT).into_parts();

    assert_matches!(string, Cow::Borrowed(_));
    assert_matches!(
        warnings.into_warnings().as_slice(),
        [Warning::UnexpectedEndOfString(_)]
    );
}

// --- invalid UTF-16 surrogate pairs => `DecodeUtf16` ---

#[test]
fn should_warn_on_double_high_surrogate_pair() {
    // Two high surrogates (`\uD834\uD834`) do not form a valid pair.
    const INPUT: &str = r#"{ "name": "\uD834\uD834" }"#;

    let (string, warnings) = parse_name(INPUT).into_parts();

    assert_matches!(string, Cow::Borrowed(_));
    assert_matches!(
        warnings.into_warnings().as_slice(),
        [Warning::DecodeUtf16(_, _)]
    );
}

// --- `eq`: non-allocating comparison against a decoded key ---

#[test]
fn eq_matches_plain_string() {
    assert_eq!(eq("country_code", "country_code"), Ok(true));
}

#[test]
fn eq_reports_mismatch() {
    assert_eq!(eq("country_code", "currency"), Ok(false));
}

#[test]
fn eq_reports_length_mismatch() {
    // A prefix is not equal to the full string (raw shorter, then raw longer).
    assert_eq!(eq("country", "country_code"), Ok(false));
    assert_eq!(eq("country_code", "country"), Ok(false));
}

#[test]
fn eq_matches_through_unicode_escape() {
    // `\u0079` decodes to 'y', so the escaped key equals the plain target.
    assert_eq!(eq(r"countr\u0079_code", "country_code"), Ok(true));
}

#[test]
fn eq_matches_through_surrogate_pair() {
    // `\uD834\uDD1E` is the UTF-16 surrogate pair for U+1D11E.
    assert_eq!(eq(r"a\uD834\uDD1Eb", "a\u{1D11E}b"), Ok(true));
}

#[test]
fn eq_errors_on_invalid_escape() {
    assert_eq!(eq(r"a\zb", "anything"), Err(Warning::InvalidEscape(2)));
}

#[test]
fn eq_short_circuits_before_later_decode_error() {
    // The strings differ at index 0, so the later invalid escape is never reached.
    assert_eq!(eq(r"x\z", "y"), Ok(false));
}
