import asyncio
import json
import logging
import pytest
from unittest.mock import Mock, AsyncMock, patch
from pyprland import ipc
from pyprland.models import PyprError, VersionInfo
from pyprland.adapters.hyprland import HyprlandBackend


@pytest.fixture
def test_log():
    """Provide a silent logger for tests."""
    logger = logging.getLogger("test_ipc")
    logger.handlers.clear()
    logger.addHandler(logging.NullHandler())
    logger.propagate = False
    return logger


@pytest.fixture
def mock_open_connection(mocker):
    reader = AsyncMock()
    # StreamWriter methods write and close are synchronous, drain and wait_closed are async
    writer = Mock()
    writer.drain = AsyncMock()
    writer.wait_closed = AsyncMock()

    mock_connect = mocker.patch("asyncio.open_unix_connection", return_value=(reader, writer))
    return mock_connect, reader, writer


@pytest.mark.asyncio
async def test_hyprctl_connection_context_manager(mock_open_connection):
    mock_connect, reader, writer = mock_open_connection
    logger = Mock()

    async with ipc.hyprctl_connection(logger) as (r, w):
        assert r == reader
        assert w == writer

    writer.close.assert_called_once()
    writer.wait_closed.assert_awaited_once()


@pytest.mark.asyncio
async def test_hyprctl_connection_error(mocker):
    mocker.patch("asyncio.open_unix_connection", side_effect=FileNotFoundError)
    logger = Mock()

    with pytest.raises(PyprError):
        async with ipc.hyprctl_connection(logger):
            pass

    logger.critical.assert_called_with("hyprctl socket not found! is it running ?")


@pytest.mark.asyncio
async def test_get_response(mock_open_connection):
    mock_connect, reader, writer = mock_open_connection
    logger = Mock()
    reader.read.return_value = b'{"status": "ok"}'

    result = await ipc.get_response(b"command", logger)

    assert result == {"status": "ok"}
    writer.write.assert_called_with(b"command")
    writer.drain.assert_awaited_once()


@pytest.mark.asyncio
async def test_hyprland_backend_execute_success(mock_open_connection, test_log):
    mock_connect, reader, writer = mock_open_connection
    reader.read.return_value = b"ok"

    state = Mock()
    state.hyprland_version = VersionInfo(0, 40, 0)
    state.lua_mode = False
    backend = HyprlandBackend(state)

    result = await backend.execute("some_command", log=test_log)

    assert result is True
    writer.write.assert_called_with(b"/dispatch some_command")


@pytest.mark.asyncio
async def test_hyprland_backend_execute_failure(mock_open_connection, test_log):
    mock_connect, reader, writer = mock_open_connection
    reader.read.return_value = b"err"

    state = Mock()
    state.hyprland_version = VersionInfo(0, 40, 0)
    state.lua_mode = False
    backend = HyprlandBackend(state)

    result = await backend.execute("some_command", log=test_log, weak=True)

    assert result is False
    # Check if logged error? Need to spy on logger or check side effects if implemented


@pytest.mark.asyncio
async def test_hyprland_backend_execute_batch(mock_open_connection, test_log):
    mock_connect, reader, writer = mock_open_connection
    reader.read.return_value = b"okok"

    state = Mock()
    state.hyprland_version = VersionInfo(0, 40, 0)
    state.lua_mode = False
    backend = HyprlandBackend(state)

    cmds = ["cmd1", "cmd2"]
    result = await backend.execute(cmds, log=test_log)

    assert result is True
    # Verify the batch format string
    call_args = writer.write.call_args[0][0]
    assert b"[[BATCH]]" in call_args
    assert b"dispatch cmd1" in call_args
    assert b"dispatch cmd2" in call_args


@pytest.mark.asyncio
async def test_backend_get_client_props(mock_open_connection, test_log):
    # Mock execute_json on the backend instead of ipc.hyprctl_json
    state = Mock()
    backend = HyprlandBackend(state)

    clients = [{"address": "0x123", "class": "Term"}, {"address": "0x456", "class": "Browser"}]

    # We mock execute_json on the instance
    backend.execute_json = AsyncMock(return_value=clients)

    # By address
    client = await backend.get_client_props(addr="0x123", log=test_log)
    assert client == clients[0]

    # By class
    client = await backend.get_client_props(cls="Browser", log=test_log)
    assert client == clients[1]

    # By custom prop
    client = await backend.get_client_props(title="Something", log=test_log)  # Not found
    assert client is None
