"""Tests for SGR state tracking and propagation."""

from __future__ import annotations

# std imports
import re

# local
from wcwidth import clip, wrap
from wcwidth.sgr_state import (_SGR_STATE_DEFAULT,
                               _SGRState,
                               propagate_sgr,
                               _parse_sgr_params,
                               _sgr_state_update,
                               _sgr_state_is_active,
                               _sgr_state_to_sequence)


def test_wrap_propagates_sgr():
    """Wrap() propagates SGR codes across lines by default."""
    assert wrap('\x1b[1;34mHello world\x1b[0m', width=6) == [
        '\x1b[1;34mHello\x1b[0m', '\x1b[1;34mworld\x1b[0m']
    assert wrap('\x1b[1mHello world\x1b[0m', width=6) == [
        '\x1b[1mHello\x1b[0m', '\x1b[1mworld\x1b[0m']
    assert wrap('\x1b[31mred text here\x1b[0m', width=5) == [
        '\x1b[31mred\x1b[0m', '\x1b[31mtext\x1b[0m', '\x1b[31mhere\x1b[0m']
    assert wrap('\x1b[38;5;208morange text\x1b[0m', width=7) == [
        '\x1b[38;5;208morange\x1b[0m', '\x1b[38;5;208mtext\x1b[0m']
    assert wrap('\x1b[38;2;255;0;0mred text\x1b[0m', width=5) == [
        '\x1b[38;2;255;0;0mred\x1b[0m', '\x1b[38;2;255;0;0mtext\x1b[0m']
    # multiple attributes combined
    result = wrap('\x1b[1;3;34mbold italic blue\x1b[0m', width=5)
    assert result[0] == '\x1b[1;3;34mbold\x1b[0m'
    assert result[1].startswith('\x1b[') and result[1].endswith('\x1b[0m')


def test_wrap_reset_and_no_sgr():
    """Wrap() handles reset and plain text."""
    assert wrap('\x1b[31mred\x1b[0m plain text', width=6) == ['\x1b[31mred\x1b[0m', 'plain', 'text']
    assert wrap('hello world', width=6) == ['hello', 'world']


def test_wrap_propagate_sgr_disabled():
    """Wrap() with propagate_sgr=False returns old behavior."""
    assert wrap('\x1b[31mhello world\x1b[0m', width=6, propagate_sgr=False) == [
        '\x1b[31mhello', 'world\x1b[0m']


def test_wrap_preserves_non_sgr_sequences():
    """Wrap() preserves non-SGR sequences (OSC hyperlinks)."""
    result = wrap('\x1b]8;;url\x07long link text\x1b]8;;\x07', width=5)
    # Hyperlinks get IDs added for identity preservation across lines
    osc_pattern = re.compile(r'\x1b]8;[^;]*;url\x07')
    assert all(osc_pattern.search(line) for line in result)


def test_clip_propagates_sgr():
    """Clip() restores SGR state at start and reset at end."""
    assert clip('\x1b[1;34mHello world\x1b[0m', 6, 11) == '\x1b[1;34mworld\x1b[0m'
    assert clip('\x1b[1mHello world\x1b[0m', 6, 11) == '\x1b[1mworld\x1b[0m'
    assert clip('\x1b[31mHello world\x1b[0m', 6, 11) == '\x1b[31mworld\x1b[0m'
    assert clip('\x1b[31mHello\x1b[0m', 0, 5) == '\x1b[31mHello\x1b[0m'


def test_clip_no_active_style_and_plain():
    """Clip() handles reset and plain text."""
    assert clip('\x1b[31mred\x1b[0m plain', 4, 9) == 'plain'
    assert clip('Hello world', 6, 11) == 'world'


def test_clip_propagate_sgr_disabled():
    """Clip() with propagate_sgr=False returns old behavior."""
    assert clip('\x1b[1;34mHello world\x1b[0m', 6, 11, propagate_sgr=False) == '\x1b[1;34mworld\x1b[0m'


def test_clip_preserves_non_sgr_sequences():
    """Clip() preserves non-SGR sequences (OSC hyperlinks)."""
    result = clip('\x1b]8;;url\x07link\x1b]8;;\x07', 0, 4)
    assert '\x1b]8;;url\x07' in result and '\x1b]8;;\x07' in result


def test_clip_sgr_only_no_visible_content():
    """Clip() returns empty when only SGR sequences exist."""
    assert clip('\x1b[31m\x1b[0m', 0, 10) == ''


def test_sgr_state_parse_boolean_attributes_on():
    """_sgr_state_update parses all boolean attribute on codes."""
    assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[1m').bold is True
    assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[2m').dim is True
    assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[3m').italic is True
    assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[4m').underline is True
    assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[5m').blink is True
    assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[6m').rapid_blink is True
    assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[7m').inverse is True
    assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[8m').hidden is True
    assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[9m').strikethrough is True
    assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[21m').double_underline is True


def test_sgr_state_parse_boolean_attributes_off():
    """_sgr_state_update parses all boolean attribute off codes."""
    assert _sgr_state_update(_SGRState(italic=True), '\x1b[23m').italic is False
    assert _sgr_state_update(_SGRState(inverse=True), '\x1b[27m').inverse is False
    assert _sgr_state_update(_SGRState(hidden=True), '\x1b[28m').hidden is False
    assert _sgr_state_update(_SGRState(strikethrough=True), '\x1b[29m').strikethrough is False
    # code 22 resets both bold and dim
    state = _sgr_state_update(_SGRState(bold=True, dim=True), '\x1b[22m')
    assert state.bold is False and state.dim is False
    # code 24 resets both underline and double_underline
    state = _sgr_state_update(_SGRState(underline=True, double_underline=True), '\x1b[24m')
    assert state.underline is False and state.double_underline is False
    # code 25 resets both blink and rapid_blink
    state = _sgr_state_update(_SGRState(blink=True, rapid_blink=True), '\x1b[25m')
    assert state.blink is False and state.rapid_blink is False


def test_sgr_state_parse_colors():
    """_sgr_state_update parses all color formats."""
    # basic foreground/background
    assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[31m').foreground == (31,)
    assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[41m').background == (41,)
    # bright foreground/background
    assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[91m').foreground == (91,)
    assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[101m').background == (101,)
    # 256-color (semicolon format)
    assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[38;5;208m').foreground == (38, 5, 208)
    assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[48;5;208m').background == (48, 5, 208)
    # RGB (semicolon format)
    assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[38;2;255;128;0m').foreground == (38, 2, 255, 128, 0)
    assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[48;2;255;128;0m').background == (48, 2, 255, 128, 0)


def test_sgr_state_parse_colors_colon_format():
    """_sgr_state_update parses ITU T.416 colon-separated color format."""
    # 256-color (colon format)
    assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[38:5:208m').foreground == (38, 5, 208)
    assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[48:5:208m').background == (48, 5, 208)
    # RGB with empty colorspace (colon format): 38:2::R:G:B
    assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[38:2::255:128:0m').foreground == (38, 2, 0, 255, 128, 0)
    assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[48:2::255:128:0m').background == (48, 2, 0, 255, 128, 0)
    # RGB with colorspace (colon format): 38:2:colorspace:R:G:B
    assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[38:2:1:255:128:0m').foreground == (38, 2, 1, 255, 128, 0)
    # Mixed: colon color with semicolon attributes
    state = _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[1;38:2::255:0:0;4m')
    assert state.bold is True
    assert state.underline is True
    assert state.foreground == (38, 2, 0, 255, 0, 0)


def test_sgr_state_color_override():
    """Newer color replaces older regardless of format."""
    state = _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[38;5;208m')
    assert state.foreground == (38, 5, 208)
    state = _sgr_state_update(state, '\x1b[31m')
    assert state.foreground == (31,)
    state = _sgr_state_update(state, '\x1b[38;2;0;255;0m')
    assert state.foreground == (38, 2, 0, 255, 0)
    state = _sgr_state_update(state, '\x1b[38;5;99m')
    assert state.foreground == (38, 5, 99)


def test_sgr_state_parse_default_colors():
    """_sgr_state_update parses default color codes (39, 49)."""
    assert _sgr_state_update(_SGRState(foreground=(31,)), '\x1b[39m').foreground is None
    assert _sgr_state_update(_SGRState(background=(41,)), '\x1b[49m').background is None


def test_sgr_state_parse_compound_and_reset():
    """_sgr_state_update handles compound sequences and reset."""
    state = _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[1;34;3m')
    assert state.bold is True and state.italic is True and state.foreground == (34,)
    # reset with 0
    state = _sgr_state_update(_SGRState(bold=True, foreground=(31,)), '\x1b[0m')
    assert state == _SGR_STATE_DEFAULT
    # empty is reset
    state = _sgr_state_update(_SGRState(bold=True), '\x1b[m')
    assert state == _SGR_STATE_DEFAULT
    # empty param in compound treated as 0
    state = _sgr_state_update(_SGRState(bold=True), '\x1b[;1m')
    assert state.bold is True


def test_sgr_state_to_sequence():
    """_sgr_state_to_sequence generates correct sequences."""
    assert _sgr_state_to_sequence(_SGR_STATE_DEFAULT) == ''
    assert _sgr_state_to_sequence(_SGRState(bold=True)) == '\x1b[1m'
    assert _sgr_state_to_sequence(_SGRState(rapid_blink=True)) == '\x1b[6m'
    assert _sgr_state_to_sequence(_SGRState(double_underline=True)) == '\x1b[21m'
    assert _sgr_state_to_sequence(_SGRState(foreground=(31,))) == '\x1b[31m'
    assert _sgr_state_to_sequence(_SGRState(foreground=(38, 5, 208))) == '\x1b[38;5;208m'
    assert _sgr_state_to_sequence(_SGRState(foreground=(38, 2, 255, 128, 0))) == '\x1b[38;2;255;128;0m'
    assert _sgr_state_to_sequence(_SGRState(bold=True, italic=True, foreground=(34,))) == '\x1b[1;3;34m'


def test_sgr_state_to_sequence_all_attributes():
    """_sgr_state_to_sequence handles all attributes."""
    state = _SGRState(bold=True, italic=True, underline=True, inverse=True,
                      foreground=(31,), background=(44,))
    seq = _sgr_state_to_sequence(state)
    assert '\x1b[' in seq and 'm' in seq
    assert all(code in seq for code in ('1', '3', '4', '7', '31', '44'))


def test_sgr_state_is_active():
    """_sgr_state_is_active detects active state."""
    assert _sgr_state_is_active(_SGR_STATE_DEFAULT) is False
    assert _sgr_state_is_active(_SGRState(bold=True)) is True
    assert _sgr_state_is_active(_SGRState(foreground=(31,))) is True


def test_propagate_sgr():
    """propagate_sgr handles various input cases."""
    assert len(propagate_sgr([])) == 0
    assert propagate_sgr(['hello', 'world']) == ['hello', 'world']
    assert propagate_sgr(['\x1b[31mhello\x1b[0m']) == ['\x1b[31mhello\x1b[0m']
    assert propagate_sgr(['\x1b[31mhello', 'world\x1b[0m']) == [
        '\x1b[31mhello\x1b[0m', '\x1b[31mworld\x1b[0m']
    assert propagate_sgr(['\x1b[31mred\x1b[0m', 'plain']) == ['\x1b[31mred\x1b[0m', 'plain']


def test_propagate_sgr_empty_lines():
    """propagate_sgr handles empty lines."""
    result = propagate_sgr(['\x1b[31mred', '', 'text\x1b[0m'])
    assert result[0] == '\x1b[31mred\x1b[0m'
    assert result[1] == '\x1b[31m\x1b[0m'
    assert result[2] == '\x1b[31mtext\x1b[0m'


def test_sgr_malformed_sequences():
    """Malformed SGR sequences are ignored."""
    assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[38;5m').foreground is None
    assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[38;2;255m').foreground is None
    assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[999m') == _SGR_STATE_DEFAULT
    # malformed background extended colors
    assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[48;5m').background is None
    assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[48;2;255m').background is None
    # invalid mode (not 2 or 5) for extended color
    assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[38;3;128m').foreground is None
    assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[48;3;128m').background is None


def test_parse_sgr_params_invalid():
    """_parse_sgr_params returns empty list for invalid sequences."""
    assert not _parse_sgr_params('invalid')


def test_extended_color_mixed_format_edge_cases():
    """Extended color parsing handles mixed semicolon/colon format edge cases."""
    # 38=FG_EXTENDED, colon tuple (48, 2, 255, 0, 0) consumed as mode - returns None
    assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[38;48:2:255:0:0m').foreground is None
    # 38=FG_EXTENDED, 5=256-color mode, colon tuple consumed as index - returns None
    assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[38;5;48:5:99m').foreground is None
    # 38=FG_EXTENDED, 2=RGB mode, r=255, g=128, colon tuple consumed as b - returns None
    assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[38;2;255;128;48:2:0:0:0m').foreground is None
    # colon tuple with invalid base (99) is ignored
    assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[99:2:255:0:0m') == _SGR_STATE_DEFAULT
