from xulbux.console import Spinner, ProgressBar
from xulbux.console import ArgResult, Args
from xulbux.console import Console
from xulbux import console

from unittest.mock import MagicMock, patch, call
from collections import namedtuple
import builtins
import pytest
import sys
import io


@pytest.fixture
def mock_terminal_size(monkeypatch):
    TerminalSize = namedtuple("TerminalSize", ["columns", "lines"])
    mock_get_terminal_size = lambda: TerminalSize(columns=80, lines=24)
    monkeypatch.setattr(console._os, "get_terminal_size", mock_get_terminal_size)


@pytest.fixture
def mock_formatcodes_print(monkeypatch):
    mock = MagicMock()
    monkeypatch.setattr(console.FormatCodes, "print", mock)
    return mock


@pytest.fixture
def mock_builtin_input(monkeypatch):
    mock = MagicMock()
    monkeypatch.setattr(builtins, "input", mock)
    return mock


@pytest.fixture
def mock_prompt_toolkit(monkeypatch):
    mock = MagicMock(return_value="mocked multiline input")
    monkeypatch.setattr(console._pt, "prompt", mock)
    return mock


################################################## Console TESTS ##################################################


def test_console_user():
    user_output = Console.user
    assert isinstance(user_output, str)
    assert user_output != ""


def test_console_width(mock_terminal_size):
    width_output = Console.w
    assert isinstance(width_output, int)
    assert width_output == 80


def test_console_height(mock_terminal_size):
    height_output = Console.h
    assert isinstance(height_output, int)
    assert height_output == 24


def test_console_size(mock_terminal_size):
    size_output = Console.size
    assert isinstance(size_output, tuple)
    assert len(size_output) == 2
    assert size_output[0] == 80
    assert size_output[1] == 24


@pytest.mark.parametrize(
    # CASES WITHOUT SPACES (allow_spaces=False)
    "argv, find_args, expected_args_dict", [
        # NO ARGS PROVIDED
        (
            ["script.py"],
            {"file": {"-f"}, "debug": {"-d"}},
            {"file": {"exists": False, "value": None}, "debug": {"exists": False, "value": None}},
        ),
        # SIMPLE FLAG
        (
            ["script.py", "-d"],
            {"file": {"-f"}, "debug": {"-d"}},
            {"file": {"exists": False, "value": None}, "debug": {"exists": True, "value": None}},
        ),
        # FLAG WITH VALUE
        (
            ["script.py", "-f", "test.txt"],
            {"file": {"-f"}, "debug": {"-d"}},
            {"file": {"exists": True, "value": "test.txt"}, "debug": {"exists": False, "value": None}},
        ),
        # LONG FLAGS WITH VALUE AND FLAG
        (
            ["script.py", "--file", "path/to/file", "--debug"],
            {"file": {"-f", "--file"}, "debug": {"-d", "--debug"}},
            {"file": {"exists": True, "value": "path/to/file"}, "debug": {"exists": True, "value": None}},
        ),
        # VALUE WITH SPACES (ONLY FIRST PART DUE TO allow_spaces=False)
        (
            ["script.py", "-t", "text", "with", "spaces"],
            {"text": {"-t"}},
            {"text": {"exists": True, "value": "text"}},
        ),
        # UNKNOWN ARG
        (
            ["script.py", "-x"],
            {"file": {"-f"}},
            {"file": {"exists": False, "value": None}},
        ),
        # TWO FLAGS
        (
            ["script.py", "-f", "-d"],
            {"file": {"-f"}, "debug": {"-d"}},
            {"file": {"exists": True, "value": None}, "debug": {"exists": True, "value": None}},
        ),
        # CASE SENSITIVE FLAGS
        (
            ["script.py", "-i", "input.txt", "-I", "ignore"],
            {"input": {"-i"}, "ignore": {"-I"}, "help": {"-h"}},
            {
                "input": {"exists": True, "value": "input.txt"},
                "ignore": {"exists": True, "value": "ignore"},
                "help": {"exists": False, "value": None},
            },
        ),

        # --- CASES WITH DEFAULT VALUES ---
        # DEFAULT USED
        (
            ["script.py"],
            {"output": {"flags": {"-o"}, "default": "out.txt"}, "verbose": {"-v"}},
            {"output": {"exists": False, "value": "out.txt"}, "verbose": {"exists": False, "value": None}},
        ),
        # VALUE OVERRIDES DEFAULT
        (
            ["script.py", "-o", "my_file.log"],
            {"output": {"flags": {"-o"}, "default": "out.txt"}, "verbose": {"-v"}},
            {"output": {"exists": True, "value": "my_file.log"}, "verbose": {"exists": False, "value": None}},
        ),
        # FLAG PRESENCE OVERRIDES DEFAULT (string -> None)
        (
            ["script.py", "-o"],
            {"output": {"flags": {"-o"}, "default": "out.txt"}, "verbose": {"-v"}},
            {"output": {"exists": True, "value": None}, "verbose": {"exists": False, "value": None}},
        ),
        # FLAG PRESENCE OVERRIDES DEFAULT (False -> None)
        (
            ["script.py", "-v"],
            {"output": {"flags": {"-o"}, "default": "out.txt"}, "verbose": {"flags": {"-v"}, "default": "False"}},
            {"output": {"exists": False, "value": "out.txt"}, "verbose": {"exists": True, "value": None}},
        ),

        # --- MIXED list/tuple AND dict FORMATS (allow_spaces=False) ---
        # DICT VALUE PROVIDED, LIST NOT PROVIDED
        (
            ["script.py", "--config", "dev.cfg"],
            {"config": {"flags": {"-c", "--config"}, "default": "prod.cfg"}, "log": {"-l"}},
            {"config": {"exists": True, "value": "dev.cfg"}, "log": {"exists": False, "value": None}},
        ),
        # LIST FLAG PROVIDED, DICT NOT PROVIDED (USES DEFAULT)
        (
            ["script.py", "-l"],
            {"config": {"flags": {"-c", "--config"}, "default": "prod.cfg"}, "log": {"-l"}},
            {"config": {"exists": False, "value": "prod.cfg"}, "log": {"exists": True, "value": None}},
        ),

        # --- 'before' / 'after' SPECIAL CASES ---
        # 'before' SPECIAL CASE
        (
            ["script.py", "arg1", "arg2.1 arg2.2", "-f", "file.txt"],
            {"before": "before", "file": {"-f"}},
            {"before": {"exists": True, "values": ["arg1", "arg2.1 arg2.2"]}, "file": {"exists": True, "value": "file.txt"}},
        ),
        (
            ["script.py", "-f", "file.txt"],
            {"before": "before", "file": {"-f"}},
            {"before": {"exists": False, "values": []}, "file": {"exists": True, "value": "file.txt"}},
        ),
        # 'after' SPECIAL CASE
        (
            ["script.py", "-f", "file.txt", "arg1", "arg2.1 arg2.2"],
            {"after": "after", "file": {"-f"}},
            {"after": {"exists": True, "values": ["arg1", "arg2.1 arg2.2"]}, "file": {"exists": True, "value": "file.txt"}},
        ),
        (
            ["script.py", "-f", "file.txt"],
            {"after": "after", "file": {"-f"}},
            {"after": {"exists": False, "values": []}, "file": {"exists": True, "value": "file.txt"}},
        ),

        # --- CUSTOM PREFIX TESTS ---
        # COLON AND SLASH PREFIXES
        (
            ["script.py", ":config", "dev.json", "/output", "result.txt"],
            {"config": {":config"}, "output": {"/output"}},
            {"config": {"exists": True, "value": "dev.json"}, "output": {"exists": True, "value": "result.txt"}},
        ),
        # WORD FLAGS WITHOUT PREFIXES
        (
            ["script.py", "verbose", "help", "123"],
            {"verbose": {"verbose"}, "help": {"help"}, "number": {"-n"}},
            {
                "verbose": {"exists": True, "value": None},
                "help": {"exists": True, "value": "123"},
                "number": {"exists": False, "value": None},
            },
        ),
        # MIXED CUSTOM PREFIXES WITH DEFAULTS
        (
            ["script.py", "@user", "admin"],
            {"user": {"flags": {"@user"}, "default": "guest"}, "mode": {"flags": {"++mode"}, "default": "normal"}},
            {"user": {"exists": True, "value": "admin"}, "mode": {"exists": False, "value": "normal"}},
        ),
    ]
)
def test_get_args_no_spaces(monkeypatch, argv, find_args, expected_args_dict):
    monkeypatch.setattr(sys, "argv", argv)
    args_result = Console.get_args(allow_spaces=False, **find_args)
    assert isinstance(args_result, Args)
    assert args_result.dict() == expected_args_dict
    for key, expected in expected_args_dict.items():
        assert (key in args_result) is True
        assert isinstance(args_result[key], ArgResult)
        assert args_result[key].exists == expected["exists"]  # type: ignore[cannot-access-attr]
        # CHECK IF THIS IS A POSITIONAL ARG (HAS 'values') OR REGULAR ARG (HAS 'value')
        if "values" in expected:
            assert args_result[key].values == expected["values"]  # type: ignore[cannot-access-attr]
        else:
            assert args_result[key].value == expected["value"]  # type: ignore[cannot-access-attr]
        assert bool(args_result[key]) == expected["exists"]
    assert list(args_result.keys()) == list(expected_args_dict.keys())
    assert [v.exists for v in args_result.values()] == [d["exists"] for d in expected_args_dict.values()]
    assert len(args_result) == len(expected_args_dict)


@pytest.mark.parametrize(
    # CASES WITH SPACES (allow_spaces=True)
    "argv, find_args, expected_args_dict", [
        # SIMPLE VALUE WITH SPACES
        (
            ["script.py", "-f", "file with spaces", "-d"],
            {"file": {"-f"}, "debug": {"-d"}},
            {"file": {"exists": True, "value": "file with spaces"}, "debug": {"exists": True, "value": None}},
        ),
        # LONG VALUE WITH SPACES
        (
            ["script.py", "--message", "Hello", "world", "how", "are", "you"],
            {"message": {"--message"}},
            {"message": {"exists": True, "value": "Hello world how are you"}},
        ),
        # VALUE WITH SPACES FOLLOWED BY ANOTHER FLAG
        (
            ["script.py", "-m", "this is", "a message", "--flag"],
            {"message": {"-m"}, "flag": {"--flag"}},
            {"message": {"exists": True, "value": "this is a message"}, "flag": {"exists": True, "value": None}},
        ),
        # VALUE WITH SPACES AT THE END
        (
            ["script.py", "-m", "end", "of", "args"],
            {"message": {"-m"}},
            {"message": {"exists": True, "value": "end of args"}},
        ),
        # CASE SENSITIVE FLAGS WITH SPACES
        (
            ["script.py", "-t", "this is", "a test", "-T", "UPPERCASE"],
            {"text": {"-t"}, "title": {"-T"}},
            {"text": {"exists": True, "value": "this is a test"}, "title": {"exists": True, "value": "UPPERCASE"}},
        ),

        # --- CASES WITH DEFAULTS (dict FORMAT, allow_spaces=True) ---
        # VALUE WITH SPACE OVERRIDES DEFAULT
        (
            ["script.py", "--msg", "Default message"],
            {"msg": {"flags": {"--msg"}, "default": "No message"}, "other": {"-o"}},
            {"msg": {"exists": True, "value": "Default message"}, "other": {"exists": False, "value": None}},
        ),
        # DEFAULT USED WHEN OTHER FLAG PRESENT
        (
            ["script.py", "-o"],
            {"msg": {"flags": {"--msg"}, "default": "No message"}, "other": {"-o"}},
            {"msg": {"exists": False, "value": "No message"}, "other": {"exists": True, "value": None}},
        ),
        # FLAG PRESENCE OVERRIDES DEFAULT (str -> True)
        # FLAG WITH NO VALUE SHOULD HAVE None AS VALUE
        (
            ["script.py", "--msg"],
            {"msg": {"flags": {"--msg"}, "default": "No message"}, "other": {"-o"}},
            {"msg": {"exists": True, "value": None}, "other": {"exists": False, "value": None}},
        ),

        # --- MIXED FORMATS WITH SPACES (allow_spaces=True) ---
        # LIST VALUE WITH SPACES, dict VALUE PROVIDED
        (
            ["script.py", "-f", "input file name", "--mode", "test"],
            {"file": {"-f"}, "mode": {"flags": {"--mode"}, "default": "prod"}},
            {"file": {"exists": True, "value": "input file name"}, "mode": {"exists": True, "value": "test"}},
        ),
        # LIST VALUE WITH SPACES, dict NOT PROVIDED (USES DEFAULT)
        (
            ["script.py", "-f", "another file"],
            {"file": {"-f"}, "mode": {"flags": {"--mode"}, "default": "prod"}},
            {"file": {"exists": True, "value": "another file"}, "mode": {"exists": False, "value": "prod"}},
        ),

        # --- 'before' / 'after' SPECIAL CASES (ARE NOT AFFECTED BY allow_spaces) ---
        # 'before' SPECIAL CASE
        (
            ["script.py", "arg1", "arg2.1 arg2.2", "-f", "file.txt"],
            {"before": "before", "file": {"-f"}},
            {"before": {"exists": True, "values": ["arg1", "arg2.1 arg2.2"]}, "file": {"exists": True, "value": "file.txt"}},
        ),
        # 'after' SPECIAL CASE
        (
            ["script.py", "-f", "file.txt", "arg1", "arg2.1 arg2.2"],
            {"after": "after", "file": {"-f"}},
            {"after": {"exists": False, "values": []}, "file": {"exists": True, "value": "file.txt arg1 arg2.1 arg2.2"}},
        ),
        (
            ["script.py", "arg1", "arg2.1 arg2.2"],
            {"after": "after", "file": {"-f"}},
            {"after": {"exists": True, "values": ["arg1", "arg2.1 arg2.2"]}, "file": {"exists": False, "value": None}},
        ),

        # --- CUSTOM PREFIX TESTS WITH SPACES ---
        # QUESTION MARK AND DOUBLE PLUS PREFIXES WITH MULTIWORD VALUES
        (
            ["script.py", "?help", "show", "detailed", "info", "++mode", "test"],
            {"help": {"?help"}, "mode": {"++mode"}},
            {"help": {"exists": True, "value": "show detailed info"}, "mode": {"exists": True, "value": "test"}},
        ),
        # AT SYMBOL PREFIX WITH SPACES
        (
            ["script.py", "@message", "Hello", "World", "How", "are", "you"],
            {"message": {"@message"}},
            {"message": {"exists": True, "value": "Hello World How are you"}},
        ),
    ]
)
def test_get_args_with_spaces(monkeypatch, argv, find_args, expected_args_dict):
    monkeypatch.setattr(sys, "argv", argv)
    args_result = Console.get_args(allow_spaces=True, **find_args)
    assert isinstance(args_result, Args)
    assert args_result.dict() == expected_args_dict


def test_get_args_flag_without_value(monkeypatch):
    """Test that flags without values have None as their value, not True."""
    # TEST SINGLE FLAG WITHOUT VALUE AT END OF ARGS
    monkeypatch.setattr(sys, "argv", ["script.py", "--verbose"])
    args_result = Console.get_args(verbose={"--verbose"})
    assert args_result.verbose.exists is True
    assert args_result.verbose.value is None

    # TEST FLAG WITHOUT VALUE FOLLOWED BY ANOTHER FLAG
    monkeypatch.setattr(sys, "argv", ["script.py", "--verbose", "--debug"])
    args_result = Console.get_args(verbose={"--verbose"}, debug={"--debug"})
    assert args_result.verbose.exists is True
    assert args_result.verbose.value is None
    assert args_result.debug.exists is True
    assert args_result.debug.value is None

    # TEST FLAG WITH DEFAULT VALUE BUT NO PROVIDED VALUE
    monkeypatch.setattr(sys, "argv", ["script.py", "--mode"])
    args_result = Console.get_args(mode={"flags": {"--mode"}, "default": "production"})
    assert args_result.mode.exists is True
    assert args_result.mode.value is None


def test_get_args_duplicate_flag():
    with pytest.raises(ValueError, match="Duplicate flag '-f' found. It's assigned to both 'file1' and 'file2'."):
        Console.get_args(file1={"-f", "--file1"}, file2={"flags": {"-f", "--file2"}, "default": "..."})

    with pytest.raises(ValueError, match="Duplicate flag '--long' found. It's assigned to both 'arg1' and 'arg2'."):
        Console.get_args(arg1={"flags": {"--long"}, "default": "..."}, arg2={"-a", "--long"})


def test_get_args_dash_values_not_treated_as_flags(monkeypatch):
    """Test that values starting with dashes are not treated as flags unless explicitly defined"""
    monkeypatch.setattr(sys, "argv", ["script.py", "-v", "-42", "--input", "-3.14"])
    result = Console.get_args(verbose={"-v"}, input={"--input"})

    assert result.verbose.exists is True
    assert result.verbose.value == "-42"
    assert result.input.exists is True
    assert result.input.value == "-3.14"


def test_get_args_dash_strings_as_values(monkeypatch):
    """Test that dash-prefixed strings are treated as values when not defined as flags"""
    monkeypatch.setattr(sys, "argv", ["script.py", "-f", "--not-a-flag", "-t", "-another-value"])
    result = Console.get_args(file={"-f"}, text={"-t"})

    assert result.file.exists is True
    assert result.file.value == "--not-a-flag"
    assert result.text.exists is True
    assert result.text.value == "-another-value"


def test_get_args_positional_with_dashes_before(monkeypatch):
    """Test that positional 'before' arguments include dash-prefixed values"""
    monkeypatch.setattr(sys, "argv", ["script.py", "-123", "--some-file", "normal", "-v"])
    result = Console.get_args(before_args="before", verbose={"-v"})

    assert result.before_args.exists is True
    assert result.before_args.values == ["-123", "--some-file", "normal"]
    assert result.verbose.exists is True
    assert result.verbose.value is None


def test_get_args_positional_with_dashes_after(monkeypatch):
    """Test that positional 'after' arguments include dash-prefixed values"""
    monkeypatch.setattr(sys, "argv", ["script.py", "-v", "value", "-123", "--output-file", "-negative"])
    result = Console.get_args(verbose={"-v"}, after_args="after")

    assert result.verbose.exists is True
    assert result.verbose.value == "value"
    assert result.after_args.exists is True
    assert result.after_args.values == ["-123", "--output-file", "-negative"]


def test_get_args_multiword_with_dashes(monkeypatch):
    """Test multiword values with dashes when allow_spaces=True"""
    monkeypatch.setattr(sys, "argv", ["script.py", "-m", "start", "-middle", "--end", "-f", "other"])
    result = Console.get_args(allow_spaces=True, message={"-m"}, file={"-f"})

    assert result.message.exists is True
    assert result.message.value == "start -middle --end"
    assert result.file.exists is True
    assert result.file.value == "other"


def test_get_args_mixed_dash_scenarios(monkeypatch):
    """Test complex scenario mixing defined flags with dash-prefixed values"""
    monkeypatch.setattr(
        sys, "argv", [
            "script.py", "before1", "-not-flag", "before2", "-v", "--verbose-mode", "-d", "-42", "--file", "--my-file.txt",
            "after1", "-also-not-flag"
        ]
    )
    result = Console.get_args(
        before="before",
        verbose={"-v"},
        debug={"-d"},
        file={"--file"},
        after="after",
    )

    assert result.before.exists is True
    assert result.before.values == ["before1", "-not-flag", "before2"]

    assert result.verbose.exists is True
    assert result.verbose.value == "--verbose-mode"

    assert result.debug.exists is True
    assert result.debug.value == "-42"

    assert result.file.exists is True
    assert result.file.value == "--my-file.txt"

    assert result.after.exists is True
    assert result.after.values == ["after1", "-also-not-flag"]


def test_multiline_input(mock_prompt_toolkit, mock_formatcodes_print):
    expected_input = "mocked multiline input"
    result = Console.multiline_input("Enter text:", show_keybindings=True, default_color="#BCA")

    assert result == expected_input
    assert mock_formatcodes_print.call_count == 3
    prompt_call = mock_formatcodes_print.call_args_list[0]
    keybind_call = mock_formatcodes_print.call_args_list[1]
    reset_call = mock_formatcodes_print.call_args_list[2]

    assert prompt_call.args == ("Enter text:", )
    assert prompt_call.kwargs == {"default_color": "#BCA"}

    assert "[dim][[b](CTRL+D)[dim] : end of input][_dim]" in keybind_call.args[0]

    assert reset_call.args == ("[_]", )
    assert reset_call.kwargs == {"end": ""}

    mock_prompt_toolkit.assert_called_once()
    pt_args, pt_kwargs = mock_prompt_toolkit.call_args
    assert pt_args == (" ⮡ ", )
    assert pt_kwargs.get("multiline") is True
    assert pt_kwargs.get("wrap_lines") is True
    assert "key_bindings" in pt_kwargs


def test_multiline_input_no_bindings(mock_prompt_toolkit, mock_formatcodes_print):
    Console.multiline_input("Enter text:", show_keybindings=False, end="DONE")

    assert mock_formatcodes_print.call_count == 2
    prompt_call = mock_formatcodes_print.call_args_list[0]
    reset_call = mock_formatcodes_print.call_args_list[1]

    assert prompt_call.args == ("Enter text:", )
    assert reset_call.args == ("[_]", )
    assert reset_call.kwargs == {"end": "DONE"}

    mock_prompt_toolkit.assert_called_once()


def test_pause_exit_pause_only(monkeypatch):
    mock_keyboard = MagicMock()
    mock_formatcodes_print = MagicMock()
    monkeypatch.setattr(console._keyboard, "read_key", mock_keyboard)
    monkeypatch.setattr(console.FormatCodes, "print", mock_formatcodes_print)

    Console.pause_exit(pause=True, exit=False, prompt="Press any key...")

    mock_formatcodes_print.assert_called_once_with("Press any key...", end="", flush=True)
    mock_keyboard.assert_called_once_with(suppress=True)


def test_pause_exit_with_exit(monkeypatch):
    mock_keyboard = MagicMock()
    mock_formatcodes_print = MagicMock()
    mock_sys_exit = MagicMock()
    monkeypatch.setattr(console._keyboard, "read_key", mock_keyboard)
    monkeypatch.setattr(console.FormatCodes, "print", mock_formatcodes_print)
    monkeypatch.setattr(console._sys, "exit", mock_sys_exit)

    Console.pause_exit(pause=True, exit=True, prompt="Exiting...", exit_code=1)

    mock_formatcodes_print.assert_called_once_with("Exiting...", end="", flush=True)
    mock_keyboard.assert_called_once_with(suppress=True)
    mock_sys_exit.assert_called_once_with(1)


def test_pause_exit_reset_ansi(monkeypatch):
    mock_keyboard = MagicMock()
    mock_formatcodes_print = MagicMock()
    monkeypatch.setattr(console._keyboard, "read_key", mock_keyboard)
    monkeypatch.setattr(console.FormatCodes, "print", mock_formatcodes_print)

    Console.pause_exit(pause=True, exit=False, reset_ansi=True)

    assert mock_formatcodes_print.call_count == 2
    assert mock_formatcodes_print.call_args_list[1] == call("[_]", end="")


def test_cls(monkeypatch):
    mock_shutil = MagicMock()
    mock_os_system = MagicMock()
    mock_print = MagicMock()
    monkeypatch.setattr(console._shutil, "which", mock_shutil)
    monkeypatch.setattr(console._os, "system", mock_os_system)
    monkeypatch.setattr(builtins, "print", mock_print)

    mock_shutil.side_effect = lambda cmd: cmd == "cls"
    Console.cls()
    mock_os_system.assert_called_with("cls")
    mock_print.assert_called_with("\033[0m", end="", flush=True)

    mock_os_system.reset_mock()
    mock_print.reset_mock()

    mock_shutil.side_effect = lambda cmd: cmd == "clear"
    Console.cls()
    mock_os_system.assert_called_with("clear")
    mock_print.assert_called_with("\033[0m", end="", flush=True)


def test_log_basic(mock_formatcodes_print):
    Console.log("INFO", "Test message")

    mock_formatcodes_print.assert_called_once()
    args, kwargs = mock_formatcodes_print.call_args
    assert "INFO" in args[0]
    assert "Test message" in args[0]
    assert kwargs["end"] == "\n"


def test_log_no_title(mock_formatcodes_print):
    Console.log(title=None, prompt="Just a message")

    mock_formatcodes_print.assert_called_once()
    args, _ = mock_formatcodes_print.call_args
    assert "Just a message" in args[0]


def test_debug_active(mock_formatcodes_print):
    Console.debug("Debug message", active=True)

    assert mock_formatcodes_print.call_count == 3
    args, _ = mock_formatcodes_print.call_args_list[0]
    assert "DEBUG" in args[0]
    assert "Debug message" in args[0]


def test_debug_inactive(mock_formatcodes_print):
    Console.debug("Debug message", active=False)

    mock_formatcodes_print.assert_not_called()


def test_info(mock_formatcodes_print):
    Console.info("Info message")

    assert mock_formatcodes_print.call_count == 3
    args, _ = mock_formatcodes_print.call_args_list[0]
    assert "INFO" in args[0]
    assert "Info message" in args[0]


def test_done(mock_formatcodes_print):
    Console.done("Task completed")

    assert mock_formatcodes_print.call_count == 3
    args, _ = mock_formatcodes_print.call_args_list[0]
    assert "DONE" in args[0]
    assert "Task completed" in args[0]


def test_warn(mock_formatcodes_print):
    Console.warn("Warning message")

    assert mock_formatcodes_print.call_count == 3
    args, _ = mock_formatcodes_print.call_args_list[0]
    assert "WARN" in args[0]
    assert "Warning message" in args[0]


def test_fail(mock_formatcodes_print, monkeypatch):
    mock_sys_exit = MagicMock()
    monkeypatch.setattr(console._sys, "exit", mock_sys_exit)

    Console.fail("Error occurred")

    assert mock_formatcodes_print.call_count >= 2
    args, _ = mock_formatcodes_print.call_args_list[0]
    assert "FAIL" in args[0]
    assert "Error occurred" in args[0]

    mock_sys_exit.assert_called_once_with(1)


def test_exit_method(mock_formatcodes_print, monkeypatch):
    mock_sys_exit = MagicMock()
    monkeypatch.setattr(console._sys, "exit", mock_sys_exit)

    Console.exit("Program ending")

    assert mock_formatcodes_print.call_count >= 2
    args, _ = mock_formatcodes_print.call_args_list[0]
    assert "EXIT" in args[0]
    assert "Program ending" in args[0]

    mock_sys_exit.assert_called_once_with(0)


def test_log_box_filled(mock_formatcodes_print):
    Console.log_box_filled("Line 1", "Line 2", box_bg_color="green")

    mock_formatcodes_print.assert_called_once()
    args, _ = mock_formatcodes_print.call_args
    assert "Line 1" in args[0]
    assert "Line 2" in args[0]


def test_log_box_bordered(mock_formatcodes_print):
    Console.log_box_bordered("Content line", border_type="rounded")

    mock_formatcodes_print.assert_called_once()
    args, _ = mock_formatcodes_print.call_args
    assert "Content line" in args[0]


def test_confirm_yes(mock_builtin_input):
    mock_builtin_input.return_value = "y"
    result = Console.confirm("Continue?")
    assert result is True


def test_confirm_no(mock_builtin_input):
    mock_builtin_input.return_value = "n"
    result = Console.confirm("Continue?")
    assert result is False


def test_confirm_default_yes(mock_builtin_input):
    mock_builtin_input.return_value = ""
    result = Console.confirm("Continue?", default_is_yes=True)
    assert result is True


def test_confirm_default_no(mock_builtin_input):
    mock_builtin_input.return_value = ""
    result = Console.confirm("Continue?", default_is_yes=False)
    assert result is False


@pytest.fixture
def mock_prompt_session(monkeypatch):
    mock_session = MagicMock()
    mock_session_class = MagicMock(return_value=mock_session)
    mock_session.prompt.return_value = None
    monkeypatch.setattr(console._pt, "PromptSession", mock_session_class)
    return mock_session_class, mock_session


def test_input_creates_prompt_session(mock_prompt_session, mock_formatcodes_print):
    """Test that Console.input creates a PromptSession with correct parameters."""
    mock_session_class, mock_session = mock_prompt_session

    Console.input("Enter text: ")

    assert mock_session_class.called
    call_kwargs = mock_session_class.call_args[1]
    assert "message" in call_kwargs
    assert "validator" in call_kwargs
    assert "validate_while_typing" in call_kwargs
    assert "key_bindings" in call_kwargs
    assert "bottom_toolbar" in call_kwargs
    assert "style" in call_kwargs
    mock_session.prompt.assert_called_once()


def test_input_with_placeholder(mock_prompt_session, mock_formatcodes_print):
    """Test that placeholder is correctly passed to PromptSession."""
    mock_session_class, _ = mock_prompt_session

    Console.input("Enter text: ", placeholder="Type here...")

    assert mock_session_class.called
    call_kwargs = mock_session_class.call_args[1]
    assert "placeholder" in call_kwargs
    assert call_kwargs["placeholder"] != ""


def test_input_without_placeholder(mock_prompt_session, mock_formatcodes_print):
    """Test that placeholder is empty when not provided."""
    mock_session_class, _ = mock_prompt_session

    Console.input("Enter text: ")

    assert mock_session_class.called
    call_kwargs = mock_session_class.call_args[1]
    assert "placeholder" in call_kwargs
    assert call_kwargs["placeholder"] == ""


def test_input_with_validator_function(mock_prompt_session, mock_formatcodes_print):
    """Test that a custom validator function is properly handled."""
    mock_session_class, _ = mock_prompt_session

    def email_validator(text):
        if "@" not in text:
            return "Invalid email"
        return None

    Console.input("Enter email: ", validator=email_validator)

    assert mock_session_class.called
    call_kwargs = mock_session_class.call_args[1]
    assert "validator" in call_kwargs
    validator_instance = call_kwargs["validator"]
    assert hasattr(validator_instance, "validate")


def test_input_with_length_constraints(mock_prompt_session, mock_formatcodes_print):
    """Test that min_len and max_len are properly handled."""
    mock_session_class, _ = mock_prompt_session

    Console.input("Enter text: ", min_len=3, max_len=10)

    assert mock_session_class.called
    call_kwargs = mock_session_class.call_args[1]
    assert "validator" in call_kwargs
    validator_instance = call_kwargs["validator"]
    assert hasattr(validator_instance, "validate")


def test_input_with_allowed_chars(mock_prompt_session, mock_formatcodes_print):
    """Test that allowed_chars parameter is handled."""
    mock_session_class, _ = mock_prompt_session

    Console.input("Enter digits only: ", allowed_chars="0123456789")

    assert mock_session_class.called
    call_kwargs = mock_session_class.call_args[1]
    assert "key_bindings" in call_kwargs
    assert call_kwargs["key_bindings"] is not None


def test_input_disable_paste(mock_prompt_session, mock_formatcodes_print):
    """Test that allow_paste=False is handled."""
    mock_session_class, _ = mock_prompt_session

    Console.input("Enter text: ", allow_paste=False)

    assert mock_session_class.called
    call_kwargs = mock_session_class.call_args[1]
    assert "key_bindings" in call_kwargs
    assert call_kwargs["key_bindings"] is not None


def test_input_with_start_end_formatting(mock_prompt_session, mock_formatcodes_print):
    """Test that start and end parameters trigger FormatCodes.print calls."""
    mock_session_class, _ = mock_prompt_session

    Console.input("Enter text: ", start="[green]", end="[_c]")

    assert mock_session_class.called
    assert mock_formatcodes_print.call_count >= 2


def test_input_message_formatting(mock_prompt_session, mock_formatcodes_print):
    """Test that the prompt message is properly formatted."""
    mock_session_class, _ = mock_prompt_session

    Console.input("[b]Bold prompt:[_b] ", default_color="#ABC")

    assert mock_session_class.called
    call_kwargs = mock_session_class.call_args[1]
    assert "message" in call_kwargs
    assert call_kwargs["message"] is not None


def test_input_bottom_toolbar_function(mock_prompt_session, mock_formatcodes_print):
    """Test that bottom toolbar function is set up."""
    mock_session_class, _ = mock_prompt_session

    Console.input("Enter text: ")

    assert mock_session_class.called
    call_kwargs = mock_session_class.call_args[1]
    assert "bottom_toolbar" in call_kwargs
    toolbar_func = call_kwargs["bottom_toolbar"]
    assert callable(toolbar_func)

    try:
        result = toolbar_func()
        assert result is not None
    except Exception:
        pass


def test_input_style_configuration(mock_prompt_session, mock_formatcodes_print):
    """Test that custom style is applied."""
    mock_session_class, _ = mock_prompt_session

    Console.input("Enter text: ")

    assert mock_session_class.called
    call_kwargs = mock_session_class.call_args[1]
    assert "style" in call_kwargs
    assert call_kwargs["style"] is not None


def test_input_validate_while_typing_enabled(mock_prompt_session, mock_formatcodes_print):
    """Test that validate_while_typing is enabled."""
    mock_session_class, _ = mock_prompt_session

    Console.input("Enter text: ")

    assert mock_session_class.called
    call_kwargs = mock_session_class.call_args[1]
    assert "validate_while_typing" in call_kwargs
    assert call_kwargs["validate_while_typing"] is True


def test_input_validator_class_creation(mock_prompt_session, mock_formatcodes_print):
    """Test that InputValidator class is properly instantiated."""
    mock_session_class, _ = mock_prompt_session

    Console.input("Enter text: ", min_len=5)

    assert mock_session_class.called
    call_kwargs = mock_session_class.call_args[1]
    assert "validator" in call_kwargs
    validator_instance = call_kwargs["validator"]
    assert hasattr(validator_instance, "validate")
    assert callable(getattr(validator_instance, "validate", None))


def test_input_key_bindings_setup(mock_prompt_session, mock_formatcodes_print):
    """Test that key bindings are properly set up."""
    mock_session_class, _ = mock_prompt_session

    Console.input("Enter text: ")

    assert mock_session_class.called
    call_kwargs = mock_session_class.call_args[1]
    assert "key_bindings" in call_kwargs
    kb = call_kwargs["key_bindings"]
    assert kb is not None
    assert hasattr(kb, "bindings")


def test_input_mask_char_single_character(mock_prompt_session, mock_formatcodes_print):
    """Test that mask_char works with single characters."""
    mock_session_class, _ = mock_prompt_session

    Console.input("Enter password: ", mask_char="*")

    assert mock_session_class.called


def test_input_output_type_int(mock_prompt_session, mock_formatcodes_print):
    """Test that output_type parameter is handled for int conversion."""
    mock_session_class, _ = mock_prompt_session

    Console.input("Enter number: ", output_type=int, default_val=42)

    assert mock_session_class.called


def test_input_default_val_handling(mock_prompt_session, mock_formatcodes_print):
    """Test that default_val parameter is properly handled."""
    mock_session_class, _ = mock_prompt_session

    Console.input("Enter text: ", default_val="default_value")

    assert mock_session_class.called


def test_input_custom_style_object(mock_prompt_session, mock_formatcodes_print):
    """Test that a custom Style object is created."""
    mock_session_class, _ = mock_prompt_session

    Console.input("Enter text: ")

    assert mock_session_class.called
    call_kwargs = mock_session_class.call_args[1]
    assert "style" in call_kwargs
    style = call_kwargs["style"]
    assert style is not None
    assert hasattr(style, "style_rules") or hasattr(style, "_style")


################################################## ProgressBar TESTS ##################################################


def test_progressbar_init():
    pb = ProgressBar(min_width=5, max_width=30)
    assert pb.min_width == 5
    assert pb.max_width == 30
    assert pb.active is False
    assert len(pb.chars) == 9


def test_progressbar_set_width():
    pb = ProgressBar()
    pb.set_width(min_width=15, max_width=60)
    assert pb.min_width == 15
    assert pb.max_width == 60


def test_progressbar_set_width_invalid():
    pb = ProgressBar()
    with pytest.raises(TypeError):
        pb.set_width(min_width="not_int")  # type: ignore[arg-type]
    with pytest.raises(ValueError):
        pb.set_width(min_width=0)
    with pytest.raises(TypeError):
        pb.set_width(max_width="not_int")  # type: ignore[arg-type]
    with pytest.raises(ValueError):
        pb.set_width(max_width=0)


def test_progressbar_set_bar_format():
    pb = ProgressBar()
    pb.set_bar_format(bar_format=["{l}", "[{b}]", "{p}%"], limited_bar_format=["[{b}]"])
    assert pb.bar_format == ["{l}", "[{b}]", "{p}%"]
    assert pb.limited_bar_format == ["[{b}]"]


def test_progressbar_set_bar_format_invalid():
    pb = ProgressBar()
    with pytest.raises(ValueError, match="must contain the '{bar}' or '{b}' placeholder"):
        pb.set_bar_format(bar_format=["Progress: {p}%"])
    with pytest.raises(ValueError, match="must contain the '{bar}' or '{b}' placeholder"):
        pb.set_bar_format(limited_bar_format=["Progress: {p}%"])


def test_progressbar_set_chars():
    pb = ProgressBar()
    custom_chars = ("█", "▓", "▒", "░", " ")
    pb.set_chars(custom_chars)
    assert pb.chars == custom_chars


def test_progressbar_set_chars_invalid():
    pb = ProgressBar()
    with pytest.raises(ValueError, match="must contain at least two characters"):
        pb.set_chars(("█", ))
    with pytest.raises(ValueError, match="must be single-character strings"):
        pb.set_chars(("█", "▓▓", " "))


def test_progressbar_show_progress_invalid_total():
    pb = ProgressBar()
    with pytest.raises(ValueError, match="The 'total' parameter must be a positive integer."):
        pb.show_progress(10, 0)
    with pytest.raises(ValueError, match="The 'total' parameter must be a positive integer."):
        pb.show_progress(10, -5)


@patch("sys.stdout", new_callable=io.StringIO)
def test_progressbar_show_progress(mock_stdout):
    pb = ProgressBar()
    with patch.object(pb, "_original_stdout", mock_stdout):
        pb._original_stdout = mock_stdout
        pb.active = True
        pb._draw_progress_bar(50, 100, "Loading")
    output = mock_stdout.getvalue()
    assert len(output) > 0


def test_progressbar_hide_progress():
    pb = ProgressBar()
    pb.active = True
    pb._original_stdout = MagicMock()
    pb.hide_progress()
    assert pb.active is False
    assert pb._original_stdout is None


def test_progressbar_progress_context():
    pb = ProgressBar()
    with patch.object(pb, "show_progress") as mock_show, patch.object(pb, "hide_progress") as mock_hide:
        with pb.progress_context(100, "Testing") as update_progress:
            update_progress(25)
            update_progress(50)
        assert mock_show.call_count == 2
        mock_hide.assert_called_once()


def test_progressbar_progress_context_exception():
    pb = ProgressBar()
    with (patch.object(pb, "show_progress") as _, patch.object(pb, "hide_progress") as
          mock_hide, patch.object(pb, "_emergency_cleanup") as mock_cleanup):
        with pytest.raises(ValueError):
            with pb.progress_context(100, "Testing") as update_progress:
                update_progress(25)
                raise ValueError("Test exception")
        mock_cleanup.assert_called_once()
        mock_hide.assert_called_once()


def test_progressbar_create_bar():
    pb = ProgressBar()

    bar = pb._create_bar(50, 100, 10)
    assert len(bar) == 10
    assert bar[0] == pb.chars[0]
    assert bar[-1] == pb.chars[-1]

    bar = pb._create_bar(100, 100, 10)
    assert len(bar) == 10
    assert all(c == pb.chars[0] for c in bar)

    bar = pb._create_bar(0, 100, 10)
    assert len(bar) == 10
    assert all(c == pb.chars[-1] for c in bar)


def test_progressbar_intercepted_output():
    pb = ProgressBar()
    intercepted = console._InterceptedOutput(pb)
    result = intercepted.write("test content")
    assert result == len("test content")
    assert "test content" in pb._buffer
    intercepted.flush()


def test_progressbar_emergency_cleanup():
    pb = ProgressBar()
    pb.active = True
    original_stdout = MagicMock()
    pb._original_stdout = original_stdout
    pb._emergency_cleanup()
    assert pb.active is False


def test_progressbar_get_formatted_info_and_bar_width(mock_terminal_size):
    pb = ProgressBar()
    formatted, bar_width = pb._get_formatted_info_and_bar_width(["{l}", "|{b}|", "{c}/{t}", "({p}%)"], 50, 100, 50.0,
                                                                "Loading")
    assert "Loading" in formatted
    assert "50" in formatted
    assert "100" in formatted
    assert "50.0" in formatted
    assert isinstance(bar_width, int)
    assert bar_width > 0


def test_progressbar_start_stop_intercepting():
    pb = ProgressBar()
    original_stdout = sys.stdout

    pb._start_intercepting()
    assert pb.active is True
    assert pb._original_stdout == original_stdout
    assert isinstance(sys.stdout, console._InterceptedOutput)

    pb._stop_intercepting()
    assert pb.active is False
    assert pb._original_stdout is None
    assert sys.stdout == original_stdout


def test_progressbar_clear_progress_line():
    pb = ProgressBar()
    mock_stdout = MagicMock()
    pb._original_stdout = mock_stdout
    pb._last_line_len = 20
    pb._clear_progress_line()
    mock_stdout.write.assert_called_once()
    mock_stdout.flush.assert_called_once()


def test_progressbar_redraw_progress_bar():
    pb = ProgressBar()
    mock_stdout = MagicMock()
    pb._original_stdout = mock_stdout
    pb._current_progress_str = "\x1b[2K\rLoading |████████████| 50%"
    pb._redraw_display()
    mock_stdout.flush.assert_called_once()


################################################## Spinner TESTS ##################################################


def test_spinner_init_defaults():
    spinner = Spinner()
    assert spinner.label is None
    assert spinner.interval == 0.2
    assert spinner.active is False
    assert spinner.sep == " "
    assert len(spinner.frames) > 0


def test_spinner_init_custom():
    spinner = Spinner(label="Loading", interval=0.5, sep="-")
    assert spinner.label == "Loading"
    assert spinner.interval == 0.5
    assert spinner.sep == "-"


def test_spinner_set_format_valid():
    spinner = Spinner()
    spinner.set_format(["{l}", "{a}"])
    assert spinner.spinner_format == ["{l}", "{a}"]


def test_spinner_set_format_invalid():
    spinner = Spinner()
    with pytest.raises(ValueError):
        spinner.set_format(["{l}"])  # MISSING {a}


def test_spinner_set_frames_valid():
    spinner = Spinner()
    spinner.set_frames(("a", "b"))
    assert spinner.frames == ("a", "b")


def test_spinner_set_frames_invalid():
    spinner = Spinner()
    with pytest.raises(ValueError):
        spinner.set_frames(("a", ))  # LESS THAN 2 FRAMES


def test_spinner_set_interval_valid():
    spinner = Spinner()
    spinner.set_interval(1.0)
    assert spinner.interval == 1.0


def test_spinner_set_interval_invalid():
    spinner = Spinner()
    with pytest.raises(ValueError):
        spinner.set_interval(0)
    with pytest.raises(ValueError):
        spinner.set_interval(-1)


@patch("xulbux.console._threading.Thread")
@patch("xulbux.console._threading.Event")
@patch("sys.stdout", new_callable=MagicMock)
def test_spinner_start(mock_stdout, mock_event, mock_thread):
    spinner = Spinner()
    spinner.start("Test")

    assert spinner.active is True
    assert spinner.label == "Test"
    mock_event.assert_called_once()
    mock_thread.assert_called_once()

    # TEST CALLING START AGAIN DOESN'T DO ANYTHING
    spinner.start("Test2")
    assert mock_event.call_count == 1


@patch("xulbux.console._threading.Thread")
@patch("xulbux.console._threading.Event")
def test_spinner_stop(mock_event, mock_thread):
    spinner = Spinner()
    # MANUALLY SET ACTIVE TO SIMULATE RUNNING
    spinner.active = True
    mock_stop_event = MagicMock()
    spinner._stop_event = mock_stop_event
    mock_animation_thread = MagicMock()
    spinner._animation_thread = mock_animation_thread

    spinner.stop()

    assert spinner.active is False
    mock_stop_event.set.assert_called_once()
    mock_animation_thread.join.assert_called_once()


def test_spinner_update_label():
    spinner = Spinner()
    spinner.update_label("New Label")
    assert spinner.label == "New Label"


def test_spinner_context_manager():
    spinner = Spinner()
    with patch.object(spinner, "start") as mock_start, patch.object(spinner, "stop") as mock_stop:

        with spinner.context("Test") as update:
            mock_start.assert_called_with("Test")
            update("New Label")
            assert spinner.label == "New Label"

        mock_stop.assert_called_once()


def test_spinner_context_manager_exception():
    spinner = Spinner()
    with ( \
        patch.object(spinner, "start"),
        patch.object(spinner, "stop") as mock_stop,
        patch.object(spinner, "_emergency_cleanup") as mock_cleanup
    ):
        with pytest.raises(ValueError):
            with spinner.context("Test"):
                raise ValueError("Oops")

        mock_cleanup.assert_called_once()
        mock_stop.assert_called_once()
