//! 100% port of pdfminer.six test_pdfminer_psparser.py
//!
//! Tests for PostScript tokenizer and stack parser.

use bolivar_core::psparser::{Keyword, PSBaseParser, PSStackParser, PSToken};

/// Test data from pdfminer.six - raw PostScript content
const TESTDATA: &[u8] = br#"%!PS
begin end
 "  @ #
/a/BCD /Some_Name /foo#5f#xbaa
0 +1 -2 .5 1.234
(abc) () (abc ( def ) ghi)
(def\040\0\0404ghi) (bach\\slask) (foo\nbaa)
(this % is not a comment.)
(foo
baa)
(foo\
baa)
<> <20> < 40 4020 >
<abcd00
12345>
func/a/b{(c)do*}def
[ 1 (z) ! ]
<< /foo (bar) >>
"#;

/// Expected tokens from TESTDATA (position, token)
/// Format matches pdfminer.six TOKENS list
fn expected_tokens() -> Vec<(usize, PSToken)> {
    use crate::Keyword as Kw; // Disambiguate from PSToken::Keyword variant
    use PSToken::*;
    vec![
        (5, Keyword(Kw::Begin)),
        (11, Keyword(Kw::End)),
        (16, Keyword(Kw::DoubleQuote)),
        (19, Keyword(Kw::Unknown(b"@".to_vec()))),
        (21, Keyword(Kw::Unknown(b"#".to_vec()))),
        (23, Literal("a".to_string())),
        (25, Literal("BCD".to_string())),
        (30, Literal("Some_Name".to_string())),
        (41, Literal("foo_xbaa".to_string())), // #5f = '_', #xb is invalid so 'x' stays
        (54, Int(0)),
        (56, Int(1)),
        (59, Int(-2)),
        (62, Real(0.5)),
        (65, Real(1.234)),
        (71, String(b"abc".to_vec())),
        (77, String(b"".to_vec())),
        (80, String(b"abc ( def ) ghi".to_vec())),
        (98, String(b"def \x00 4ghi".to_vec())), // \040=space, \0=NUL, \040=space, 4=4
        (118, String(b"bach\\slask".to_vec())),  // \\ = backslash
        (132, String(b"foo\nbaa".to_vec())),     // \n = newline
        (143, String(b"this % is not a comment.".to_vec())),
        (170, String(b"foo\nbaa".to_vec())), // literal newline in string
        (180, String(b"foobaa".to_vec())),   // \<newline> = line continuation
        (191, String(b"".to_vec())),         // <>
        (194, String(b" ".to_vec())),        // <20> = space
        (199, String(b"@@ ".to_vec())),      // <404020> with spaces
        (211, String(b"\xab\xcd\x00\x12\x34\x05".to_vec())), // hex with odd digit
        (226, Keyword(Kw::Unknown(b"func".to_vec()))),
        (230, Literal("a".to_string())),
        (232, Literal("b".to_string())),
        (234, Keyword(Kw::BraceOpen)),
        (235, String(b"c".to_vec())),
        (238, Keyword(Kw::Unknown(b"do*".to_vec()))),
        (241, Keyword(Kw::BraceClose)),
        (242, Keyword(Kw::Def)),
        (246, Keyword(Kw::ArrayStart)),
        (248, Int(1)),
        (250, String(b"z".to_vec())),
        (254, Keyword(Kw::Unknown(b"!".to_vec()))),
        (256, Keyword(Kw::ArrayEnd)),
        (258, Keyword(Kw::DictStart)),
        (261, Literal("foo".to_string())),
        (266, String(b"bar".to_vec())),
        (272, Keyword(Kw::DictEnd)),
    ]
}

/// Test 1: Tokenization - verify all tokens match expected
#[test]
fn test_tokenization() {
    let mut parser = PSBaseParser::new(TESTDATA);
    let mut tokens: Vec<(usize, PSToken)> = Vec::new();

    while let Some(result) = parser.next_token() {
        match result {
            Ok((pos, token)) => tokens.push((pos, token)),
            Err(_) => break,
        }
    }

    let expected = expected_tokens();

    assert_eq!(
        tokens.len(),
        expected.len(),
        "Token count mismatch: got {}, expected {}",
        tokens.len(),
        expected.len()
    );

    for (i, ((pos, token), (exp_pos, exp_token))) in tokens.iter().zip(expected.iter()).enumerate()
    {
        assert_eq!(
            *pos, *exp_pos,
            "Token {} position mismatch: got {}, expected {}",
            i, pos, exp_pos
        );
        assert_eq!(
            token, exp_token,
            "Token {} value mismatch at pos {}: got {:?}, expected {:?}",
            i, pos, token, exp_token
        );
    }
}

/// Expected objects from TESTDATA (after stack parsing)
/// Matches Python's OBJS list exactly (27 objects)
fn expected_objects() -> Vec<(usize, PSToken)> {
    use PSToken::*;
    use std::collections::HashMap;
    vec![
        (23, Literal("a".to_string())),
        (25, Literal("BCD".to_string())),
        (30, Literal("Some_Name".to_string())),
        (41, Literal("foo_xbaa".to_string())),
        (54, Int(0)),
        (56, Int(1)),
        (59, Int(-2)),
        (62, Real(0.5)),
        (65, Real(1.234)),
        (71, String(b"abc".to_vec())),
        (77, String(b"".to_vec())),
        (80, String(b"abc ( def ) ghi".to_vec())),
        (98, String(b"def \x00 4ghi".to_vec())),
        (118, String(b"bach\\slask".to_vec())),
        (132, String(b"foo\nbaa".to_vec())),
        (143, String(b"this % is not a comment.".to_vec())),
        (170, String(b"foo\nbaa".to_vec())),
        (180, String(b"foobaa".to_vec())),
        (191, String(b"".to_vec())),
        (194, String(b" ".to_vec())),
        (199, String(b"@@ ".to_vec())),
        (211, String(b"\xab\xcd\x00\x12\x34\x05".to_vec())),
        (230, Literal("a".to_string())),
        (232, Literal("b".to_string())),
        // Proc {(c)do*} becomes Array([String(b"c")])
        (234, Array(vec![String(b"c".to_vec())])),
        // Array [ 1 (z) ! ] - ! is a keyword that's not pushed as object
        (246, Array(vec![Int(1), String(b"z".to_vec())])),
        // Dict << /foo (bar) >>
        (
            258,
            Dict({
                let mut d = HashMap::new();
                d.insert("foo".to_string(), String(b"bar".to_vec()));
                d
            }),
        ),
    ]
}

/// Test 2: Object parsing - verify stack-based object assembly
#[test]
fn test_object_parsing() {
    let mut parser = PSStackParser::new(TESTDATA);
    let mut objs: Vec<(usize, PSToken)> = Vec::new();

    while let Some(result) = parser.next_object() {
        match result {
            Ok((pos, obj)) => objs.push((pos, obj)),
            Err(_) => break,
        }
    }

    // Should have exactly 27 objects (matching Python OBJS list)
    let expected = expected_objects();
    assert_eq!(
        objs.len(),
        expected.len(),
        "Should parse exactly {} objects from TESTDATA (got {})",
        expected.len(),
        objs.len()
    );

    // Verify all objects match expected
    for (i, ((pos, obj), (exp_pos, exp_obj))) in objs.iter().zip(expected.iter()).enumerate() {
        assert_eq!(
            *pos, *exp_pos,
            "Object {} position mismatch: got {}, expected {}",
            i, pos, exp_pos
        );
        assert_eq!(
            obj, exp_obj,
            "Object {} value mismatch at pos {}: got {:?}, expected {:?}",
            i, pos, obj, exp_obj
        );
    }
}

/// Test 3: Regression test for streams ending with keyword (Issue #884)
#[test]
fn test_issue_884_keyword_at_stream_end() {
    let data = b"Do";
    let mut parser = PSBaseParser::new(data);

    let result = parser.next_token().expect("Should have token");
    let (pos, token) = result.expect("Should parse successfully");

    assert_eq!(pos, 0);
    assert!(
        matches!(token, PSToken::Keyword(Keyword::Do)),
        "Expected Keyword(Do), got {:?}",
        token
    );
}

/// BIGDATA - CMap content for buffer boundary test (Issue #1025)
const BIGDATA: &[u8] = include_bytes!("fixtures/cmap_bigdata.bin");

/// Test 4: Regression test for buffer boundary crossing (Issue #1025)
#[test]
fn test_issue_1025_buffer_boundary() {
    let mut parser = PSBaseParser::new(BIGDATA);
    let mut tokens: Vec<PSToken> = Vec::new();
    let mut beginbfchar_count = 0;

    while let Some(result) = parser.next_token() {
        match result {
            Ok((pos, token)) => {
                // Check that token at position 4093 is "beginbfchar"
                if pos == 4093 {
                    assert!(
                        matches!(&token, PSToken::Keyword(Keyword::BeginBfChar)),
                        "Token at pos 4093 should be 'beginbfchar', got {:?}",
                        token
                    );
                }
                if matches!(&token, PSToken::Keyword(Keyword::BeginBfChar)) {
                    beginbfchar_count += 1;
                }
                tokens.push(token);
            }
            Err(_) => break,
        }
    }

    // Should get "beginbfchar" 3 times
    assert_eq!(
        beginbfchar_count, 3,
        "Expected 3 'beginbfchar' tokens, got {}",
        beginbfchar_count
    );

    // Should get both "end" at the end
    let len = tokens.len();
    assert!(len >= 2, "Need at least 2 tokens");
    assert!(
        matches!(&tokens[len - 1], PSToken::Keyword(Keyword::End)),
        "Last token should be 'end'"
    );
    assert!(
        matches!(&tokens[len - 2], PSToken::Keyword(Keyword::End)),
        "Second to last token should be 'end'"
    );
}
