from pathlib import Path

import pytest
import yaml

from yamlium import ParsingError, parse, parse_full


def comp(s: str, /, validate_yaml: bool = True, expected_result: str = "") -> None:
    """Quick parse comparison."""
    s = s.strip() + "\n"
    # Make sure yaml is valid using pyyaml
    if validate_yaml:
        yaml.safe_load(s)
    expected_result = expected_result.strip() + "\n" if expected_result else s
    assert parse_full(input=s).to_yaml() == expected_result


def test_check_everything_yaml_is_parsable():
    yaml.safe_load(Path("tests/test_everything.yaml").read_text())


def test_simple_key_value():
    comp("""
name: bob

hey: yo
""")


def test_nested_mappings():
    comp("""
name:

  hey: yo
""")


def test_simple_inline_comment():
    comp("""
name: bob #  a common name
""")


def test_varying_comments():
    comp("""
# More comments
#   With varying spacing
# And varying tabbing
key2: alice

key1: bob #  a common name
""")


def test_null_values():
    comp("""
key1:
  key2:
  key3: ~
  key4: null
""")


def test_complex_sequence():
    comp("""
hey:
  - yo
  - but:

      # hey
      - asdf: ey
""")


def test_complex_sequence2():
    comp("""
hey:
  - yo
  - first: mapping
    what: hey
  - second: mapping
""")


def test_dangling_key():
    with pytest.raises(ParsingError) as e:
        comp("""
hey:
  yo:
""")
    assert "Found unexpected token EOF" in str(e)


def test_empty_anchor():
    comp("""
hey:
  yo: &anchor asdf
""")


def test_simple_anchor_alias():
    comp("""
hey: &my_anchor up # Anchored scalar

what: *my_anchor #   Applied with simple alias
""")


def test_anchor_missing_key():
    with pytest.raises(expected_exception=ParsingError) as e:
        comp("""
hey: asdf &faulty_anchor
""")
    assert "Found unexpected token ANCHOR" in str(e)


def test_alias_missing_anchor():
    with pytest.raises(expected_exception=ParsingError) as e:
        comp(
            """
hey: *missing_anchor
""",
            validate_yaml=False,
        )
    assert "No anchor found for alias" in str(e)


def test_scalar_types():
    """Test all scalar types: string, integer, float, boolean, null"""
    comp("""
string: hello world
integer: 42
float: 3.14
boolean_true: true
boolean_false: false
null_value: null
""")


def test_multiline_strings():
    """Test multiline string handling"""
    comp("""
multiline: |
  This is a
  multiline string
  with multiple lines

folded: >
  This is a folded
  string that will
  be joined with spaces
""")


def test_nested_anchors_and_aliases():
    """Test complex anchor and alias usage"""
    comp("""
base: &base
  name: default
  value: 42

derived1:
  <<: *base
  extra: value1

derived2:
  <<: *base
  extra: value2
""")


def test_flow_style():
    """Test flow style (inline) collections"""
    comp("""
flow_map: { a: 1, b: 2, c: 3 }
flow_seq: [1, 2, 3, 4]
mixed: { a: [1, 2], b: { x: 1, y: 2 } }
""")


def test_empty_structures():
    """Test empty mappings and sequences"""
    comp("""
empty_map: {}
empty_seq: []
nested_empty:
  map: {}
  seq: []
""")


def test_complex_nesting():
    """Test deeply nested structures"""
    comp("""
level1:
  level2:
    level3:
      level4:
        level5:
          final: value
        other: value
      another: value
    more: value
  extra: value
""")


def test_sequence_of_mappings():
    """Test sequences containing mappings"""
    comp("""
items:
  - name: item1
    value: 1
  - name: item2
    value: 2
  - name: item3
    value: 3
""")


def test_mixed_comments():
    """Test various comment placements"""
    comp("""
# Document level comment
key1: value1 # Inline comment

# Block comment
# With multiple lines
key2: value2

key3: # Comment after key
  value3 # Comment after value
""")


def test_special_characters():
    """Test handling of special characters in keys and values"""
    comp("""
special_chars: "!@#$%^&*()_+-=[]{}|;:,.<>?/~`"
unicode: "你好世界"
""")


def test_indentation_variations():
    """Test different indentation styles"""
    comp(
        """
normal:
  key: value
  nested:
    key: value

compact:
 key: value
 nested:
  key: value

spaced:
    key: value
    nested:
        key: value
""",
        expected_result="""
normal:
  key: value
  nested:
    key: value

compact:
  key: value
  nested:
    key: value

spaced:
  key: value
  nested:
    key: value
""",
    )


def test_invalid_yaml():
    """Test various invalid YAML constructs"""
    invalid_cases = [
        "key: [unclosed sequence",
        "key: {unclosed mapping",
        "  - improperly indented",
        "key: *undefined_anchor",
    ]

    for case in invalid_cases:
        with pytest.raises((ParsingError)):
            comp(case, validate_yaml=False)


def test_preserve_comments():
    """Test that comments are preserved in the output"""
    input_yaml = """
# Top level comment
key1: value1 # Inline comment

# Block comment
key2: value2
"""
    result = parse_full(input=input_yaml)._to_yaml()
    assert "# Top level comment" in result
    assert "# Inline comment" in result
    assert "# Block comment" in result


def test_anchor_reuse():
    """Test reusing anchors in different contexts"""
    comp("""
base: &base
  name: default
  value: 42

derived1: *base
derived2: *base
derived3:
  <<: *base
  extra: value
""")


def test_everything_yaml():
    comp(Path("tests/test_everything.yaml").read_text())


def test_complex_comment_preservation():
    """Test comment preservation in complex structures"""
    comp("""
# Document level comment
key1: value1 # Inline comment

# Block comment
# With multiple lines
key2: # Comment after key
  nested: # Nested comment
    value2 # Value comment
  # Standalone comment
  another: value3
""")


def test_complex_anchor_chains():
    """Test complex anchor and alias relationships"""
    comp("""
base: &base
  name: default
  value: 42

derived1: &derived1
  <<: *base
  extra: value1

derived2: &derived2
  <<: *derived1
  more: value2

final:
  <<: *derived2
  final: value3
""")


def test_complex_indentation():
    """Test various indentation patterns and edge cases"""
    comp("""
level1:
  level2:
    level3:
      - item1
      - item2
    level3_alt:
      key: value
  level2_alt:
    - nested:
        key: value
      extra: field
    - simple: value
""")


def test_complex_flow_collections():
    """Test nested flow collections and edge cases"""
    comp("""
flow_map: { a: 1, b: [2, 3], c: { x: 4, y: 5 } }
flow_seq: [1, { a: 2 }, [3, 4], 5]
mixed: { a: [1, 2], b: { x: 1, y: 2 }, c: [3, { z: 4 }] }
""")


def test_complex_merge_keys():
    """Test YAML merge keys with multiple sources"""
    comp("""
base: &base
  name: default
  value: 42
  nested:
    key: value

override: &override
  value: 100
  extra: field

merged:
  <<: [*base, *override]
  additional: value
""")


def test_quoted_strings():
    """Test YAML merge keys with multiple sources"""
    comp("""
key1: 'string1'
key2: "string2"
key3: string3
""")


def test_irregular_multiline_indentation():
    comp("""
key1: line1
  line2
    line3

key2: |
  line2
  line3
""")


def test_scalar_starting_after_key():
    comp("""
key1:
  scalar start

  scalar_continue
""")


def test_deeply_nested_multiline():
    comp("""
key1:
  key2:
    key3: |
      some text
      on multiple lines
    key4: normal scalar
""")


def test_multiline_in_sequence():
    comp("""
key1:
  - name: item1
    meta: |
      some multiline
      text
  - name: item2
    meta: some single line info
""")


def test_multiline_folded_with_indentation():
    """Test folded scalar (>) with indented content."""
    comp("""
key: >
  - text for scalar
    - indented bullet
  - more text
""")


def test_multiline_literal_with_indentation():
    """Test literal scalar (|) with indented content."""
    comp("""
key: |
  line 1
    indented line
  line 3
""")


def test_multiline_with_multiple_indentation_levels():
    """Test multiline scalar with multiple levels of indentation."""
    comp("""
key: >
  text
    level 1
      level 2
    back to 1
  back to base
""")


def test_multiline_literal_with_empty_lines_and_indentation():
    """Test literal scalar with empty lines and indentation."""
    comp("""
key: >
  line 1

    indented line

  back to base
""")
    # Input has blank lines without indentation
    comp(
        "key: |\n"
        "  line 1\n"
        "\n"  # blank line without indentation
        "    indented line\n"
        "\n"  # blank line without indentation
        "  back to base\n"
    )


def test_multiline_literal_preserves_indented_blank_lines():
    """Test that blank lines in multiline literals preserve their indentation."""
    # Use explicit string to ensure the blank line has indentation (4 spaces)
    yaml_input = (
        "config:\n"
        "  summary: |\n"
        "    First paragraph here.\n"
        "\n"
        "    Second paragraph here.\n"
    )
    comp(yaml_input)
    """Test folded scalar with code-like indented content."""
    comp("""
description: >
  This function does:
    - step 1
    - step 2
      - nested step
    - step 3
  End of description
""")


def test_comments_in_sequences():
    comp("""
key1:
  # Comment on sequence item start
  - key: val
    # Comment on mapping object
    key2: val
""")


def test_empty_yaml():
    assert parse("").to_yaml() == ""
    assert parse_full("").to_yaml() == ""


def test_single_quoted_multiline():
    """Test that single-quoted strings can span multiple lines and preserve formatting."""
    comp("""
key: 'foo
  bar'
""")


def test_double_quoted_multiline():
    """Test that double-quoted strings can span multiple lines and preserve formatting."""
    comp("""
key: "foo
  bar"
""")


def test_sequence_with_empty_line_and_comment():
    """Test sequence with empty lines containing spaces, followed by comments.

    Note: Comments at column 0 are attached to the next sequence item and
    output at the sequence indentation level. This is the expected behavior.
    """
    comp(
        """
test:
  - first_item
  - second_item

  - third_item
# a comment
  - fourth_item
""",
        expected_result="""
test:
  - first_item
  - second_item

  - third_item
  # a comment
  - fourth_item
""",
    )


def test_comment_at_column_zero_in_sequence():
    """Test that comments at column 0 don't break sequence parsing.

    Note: Comments at column 0 are attached to the next sequence item and
    output at the sequence indentation level. This is the expected behavior.
    """
    comp(
        """
items:
  - item1
# comment at column 0
  - item2
  - item3
""",
        expected_result="""
items:
  - item1
  # comment at column 0
  - item2
  - item3
""",
    )


# =============================================================================
# Chomping indicator tests
# =============================================================================


def test_literal_strip():
    """Test |- strips all trailing newlines."""
    yaml = "key: |-\n  value\n"
    result = parse(yaml)
    # Strip chomping removes all trailing newlines
    assert result["key"]._value == "value"
    assert not result["key"]._value.endswith("\n")


def test_literal_keep():
    """Test |+ keeps all trailing newlines."""
    yaml = "key: |+\n  value\n\n\n"
    result = parse(yaml)
    # Keep chomping preserves all trailing newlines
    assert result["key"]._value == "value\n\n\n"


def test_literal_clip():
    """Test | (clip) adds single trailing newline."""
    yaml = "key: |\n  value\n\n\n"
    result = parse(yaml)
    # Clip chomping (default) adds exactly one trailing newline
    assert result["key"]._value == "value\n"


def test_folded_strip():
    """Test >- strips all trailing newlines."""
    yaml = "key: >-\n  value\n"
    result = parse(yaml)
    # Strip chomping removes all trailing newlines
    assert result["key"]._value == "value"
    assert not result["key"]._value.endswith("\n")


def test_folded_keep():
    """Test >+ keeps all trailing newlines."""
    yaml = "key: >+\n  value\n\n\n"
    result = parse(yaml)
    # Keep chomping preserves all trailing newlines
    assert result["key"]._value == "value\n\n\n"


def test_folded_clip():
    """Test > (clip) adds single trailing newline."""
    yaml = "key: >\n  value\n\n\n"
    result = parse(yaml)
    # Clip chomping (default) adds exactly one trailing newline
    assert result["key"]._value == "value\n"


def test_chomping_roundtrip_strip():
    """Test |- is preserved in round-trip."""
    yaml = """key: |-
  no trailing newline
"""
    result = parse(yaml)
    output = result.to_yaml()
    assert "|-" in output


def test_chomping_roundtrip_keep():
    """Test |+ is preserved in round-trip."""
    yaml = """key: |+
  keep trailing newlines
"""
    result = parse(yaml)
    output = result.to_yaml()
    assert "|+" in output


def test_chomping_roundtrip_folded_strip():
    """Test >- is preserved in round-trip."""
    yaml = """key: >-
  no trailing newline
"""
    result = parse(yaml)
    output = result.to_yaml()
    assert ">-" in output


def test_chomping_roundtrip_folded_keep():
    """Test >+ is preserved in round-trip."""
    yaml = """key: >+
  keep trailing newlines
"""
    result = parse(yaml)
    output = result.to_yaml()
    assert ">+" in output


def test_chomping_multiline_content():
    """Test chomping with multiple lines of content."""
    yaml = """key: |-
  line1
  line2
  line3
"""
    result = parse(yaml)
    assert result["key"]._value == "line1\nline2\nline3"
    assert not result["key"]._value.endswith("\n")


# =============================================================================
# Head/Line/Foot comment semantics tests
# =============================================================================


def test_comments_head_line_foot():
    """Test the plan's example YAML with head/line/foot semantics."""
    comp("""
app:
  name: 'Demo'

  # head
  version: '1.0.24' # line
  # foot

  # env docs
  env: local
""")


def test_comment_ownership_semantics():
    """Test that comments are correctly classified as head/line/foot."""
    yaml_str = """
app:
  # head for version
  version: '1.0.24'
  # foot for version

  # head for env
  env: local
"""
    doc = parse(yaml_str)
    app = doc["app"]

    # Get the keys to access comments
    keys = list(app.keys())
    version_key = keys[0]
    env_key = keys[1]

    # Version key should have head comment
    assert version_key.comments.head == ["# head for version"]

    # Version value should have foot comment (comment followed by blank line)
    version_value = app["version"]
    assert version_value.comments.foot == ["# foot for version"]

    # Env key should have head comment (comment after blank line)
    assert env_key.comments.head == ["# head for env"]


def test_inline_comment_on_value():
    """Test that inline comments are attached to the correct node."""
    yaml_str = """key: value # inline comment
"""
    doc = parse(yaml_str)
    assert doc["key"].comments.line == "# inline comment"
    assert doc["key"].comments.head == []
    assert doc["key"].comments.foot == []


def test_multiple_head_comments():
    """Test multiple consecutive head comments."""
    yaml_str = """
# comment 1
# comment 2
# comment 3
key: value
"""
    doc = parse(yaml_str)
    key = list(doc.keys())[0]
    assert key.comments.head == ["# comment 1", "# comment 2", "# comment 3"]


def test_multiple_foot_comments():
    """Test multiple consecutive foot comments followed by blank line."""
    yaml_str = """
key1: value1
# foot 1
# foot 2

key2: value2
"""
    doc = parse(yaml_str)
    assert doc["key1"].comments.foot == ["# foot 1", "# foot 2"]


def test_comments_without_blank_line_become_head():
    """Test that comments without a following blank line become head of next node."""
    yaml_str = """
key1: value1
# this becomes head of key2
key2: value2
"""
    doc = parse(yaml_str)
    key2 = list(doc.keys())[1]
    assert key2.comments.head == ["# this becomes head of key2"]
    assert doc["key1"].comments.foot == []


def test_head_and_foot_comments_together():
    """Test a node with both head and foot comments."""
    yaml_str = """
# head comment
key: value
# foot comment

next: value2
"""
    doc = parse(yaml_str)
    key = list(doc.keys())[0]
    assert key.comments.head == ["# head comment"]
    assert doc["key"].comments.foot == ["# foot comment"]


def test_comments_dataclass_is_empty():
    """Test the is_empty method on Comments dataclass."""
    from yamlium.nodes import Comments

    empty = Comments()
    assert empty.is_empty() is True

    with_head = Comments(head=["# comment"])
    assert with_head.is_empty() is False

    with_line = Comments(line="# inline")
    assert with_line.is_empty() is False

    with_foot = Comments(foot=["# foot"])
    assert with_foot.is_empty() is False


def test_backward_compat_stand_alone_comments():
    """Test backward compatibility via stand_alone_comments property (deprecated)."""
    import warnings

    yaml_str = """
# head comment
key: value
"""
    doc = parse(yaml_str)
    key = list(doc.keys())[0]
    # Old API should still work but emit deprecation warning
    with warnings.catch_warnings(record=True) as w:
        warnings.simplefilter("always")
        assert key.stand_alone_comments == ["# head comment"]
        assert len(w) == 1
        assert issubclass(w[0].category, DeprecationWarning)
        assert "comments.head" in str(w[0].message)
    # Should be same as new API (no warning)
    assert key.comments.head == ["# head comment"]


def test_backward_compat_inline_comments():
    """Test backward compatibility via inline_comments property (deprecated)."""
    import warnings

    yaml_str = """key: value # inline
"""
    doc = parse(yaml_str)
    # Old API should still work but emit deprecation warning
    with warnings.catch_warnings(record=True) as w:
        warnings.simplefilter("always")
        assert doc["key"].inline_comments == "# inline"
        assert len(w) == 1
        assert issubclass(w[0].category, DeprecationWarning)
        assert "comments.line" in str(w[0].message)
    # Should be same as new API (no warning)
    assert doc["key"].comments.line == "# inline"


def test_foot_comments_roundtrip():
    """Test that foot comments are preserved in round-trip."""
    yaml_str = """key1: value1
# foot comment

key2: value2
"""
    doc = parse(yaml_str)
    assert doc["key1"].comments.foot == ["# foot comment"]
    # Round-trip should preserve foot comments
    result = doc.to_yaml()
    assert "# foot comment" in result


def test_nested_structure_comments():
    """Test comments in nested structures."""
    yaml_str = """
outer:
  # inner head
  inner: value # inner line
  # inner foot

  # next head
  next: value2
"""
    doc = parse(yaml_str)
    outer = doc["outer"]
    inner_keys = list(outer.keys())

    # Inner key has head comment
    assert inner_keys[0].comments.head == ["# inner head"]
    # Inner value has line comment
    assert outer["inner"].comments.line == "# inner line"
    # Inner value has foot comment
    assert outer["inner"].comments.foot == ["# inner foot"]
    # Next key has head comment
    assert inner_keys[1].comments.head == ["# next head"]


def test_sequence_item_comments():
    """Test comments on sequence items."""
    yaml_str = """
items:
  # head for item1
  - item1
  # foot for item1

  # head for item2
  - item2
"""
    doc = parse(yaml_str)
    seq = doc["items"]

    # First item has head comment
    assert seq[0].comments.head == ["# head for item1"]
    # First item has foot comment
    assert seq[0].comments.foot == ["# foot for item1"]
    # Second item has head comment
    assert seq[1].comments.head == ["# head for item2"]


# =============================================================================
# Escaped quote tests (round-trip)
# =============================================================================


def test_single_quote_escape_roundtrip():
    """Test that escaped single quotes are preserved in round-trip."""
    comp("""
key: 'it''s a test'
""")


def test_single_quote_escape_multiple_roundtrip():
    """Test multiple escaped single quotes are preserved."""
    comp("""
key: 'a''b''c'
""")


def test_double_quote_escape_roundtrip():
    """Test that escaped double quotes are preserved in round-trip."""
    comp("""
key: "hello\\"world"
""")


def test_double_quote_escape_backslash_roundtrip():
    """Test that escaped backslashes are preserved in round-trip."""
    comp("""
key: "path\\\\to\\\\file"
""")
