"""Tests for the Bluetooth management module (src.system.bluetooth).

Uses mocking to avoid requiring actual Bluetooth hardware.

Run with:
    pytest tests/test_bt.py -v
"""

from __future__ import annotations

import subprocess
import sys
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

# Ensure src is on sys.path
SRC = Path(__file__).resolve().parent.parent / "src"
if str(SRC) not in sys.path:
    sys.path.insert(0, str(SRC))

from src.system.bluetooth import (
    bt_status,
    bt_power,
    bt_discoverable,
    bt_scan,
    bt_paired_devices,
    bt_pair,
    bt_unpair,
    bt_connect,
    bt_disconnect,
    bt_midi_status,
    bt_midi_enable,
    bt_midi_disable,
    bt_midi_connected_devices,
)


# ── Fixtures ─────────────────────────────────────────────────────────────────


@pytest.fixture
def mock_subprocess():
    """Patch subprocess.run globally for all BT tests."""
    with patch("src.system.bluetooth.subprocess.run") as mock:
        yield mock


# ── bt_status() tests ─────────────────────────────────────────────────────────


def test_bt_status_powered_on(mock_subprocess):
    """bt_status returns powered-on state."""
    mock_subprocess.return_value = MagicMock(
        stdout="Controller AA:BB:CC:DD:EE:FF\n"
               "\tName: pi-pedal\n"
               "\tPowered: yes\n"
               "\tDiscoverable: yes\n"
               "\tPairable: yes\n",
        returncode=0,
    )
    status = bt_status()
    assert status["available"] is True
    assert status["powered"] is True
    assert status["name"] == "pi-pedal"
    assert status["discoverable"] is True
    assert status["pairable"] is True
    assert status["address"] == "AA:BB:CC:DD:EE:FF"


def test_bt_status_powered_off(mock_subprocess):
    """bt_status returns powered-off state."""
    mock_subprocess.return_value = MagicMock(
        stdout="Controller AA:BB:CC:DD:EE:FF\n"
               "\tName: pi-pedal\n"
               "\tPowered: no\n"
               "\tDiscoverable: no\n"
               "\tPairable: no\n",
        returncode=0,
    )
    status = bt_status()
    assert status["powered"] is False
    assert status["discoverable"] is False


def test_bt_status_not_available(mock_subprocess):
    """bt_status returns not available when bluetoothctl fails."""
    mock_subprocess.side_effect = FileNotFoundError("no bluetoothctl")
    status = bt_status()
    assert status["available"] is False
    assert status["powered"] is False


# ── bt_power() tests ──────────────────────────────────────────────────────────


def test_bt_power_on(mock_subprocess):
    """bt_power(on=True) powers Bluetooth on."""
    mock_subprocess.side_effect = [
        MagicMock(stdout="", returncode=0),  # bluetoothctl power on
        MagicMock(  # bt_status call
            stdout="Controller A\n\tPowered: yes\n",
            returncode=0,
        ),
    ]
    result = bt_power(on=True)
    assert result["ok"] is True


def test_bt_power_off(mock_subprocess):
    """bt_power(on=False) powers Bluetooth off."""
    mock_subprocess.side_effect = [
        MagicMock(stdout="", returncode=0),  # bluetoothctl power off
        MagicMock(  # bt_status call
            stdout="Controller A\n\tPowered: no\n",
            returncode=0,
        ),
    ]
    result = bt_power(on=False)
    assert result["ok"] is True


# ── bt_discoverable() tests ────────────────────────────────────────────────────


def test_bt_discoverable_on(mock_subprocess):
    """bt_discoverable(on=True) makes adapter discoverable."""
    mock_subprocess.side_effect = [
        MagicMock(stdout="", returncode=0),  # discoverable on
        MagicMock(  # bt_status
            stdout="Controller A\n\tDiscoverable: yes\n",
            returncode=0,
        ),
    ]
    result = bt_discoverable(on=True)
    assert result["ok"] is True


def test_bt_discoverable_off(mock_subprocess):
    """bt_discoverable(on=False) hides adapter."""
    mock_subprocess.side_effect = [
        MagicMock(stdout="", returncode=0),
        MagicMock(
            stdout="Controller A\n\tDiscoverable: no\n",
            returncode=0,
        ),
    ]
    result = bt_discoverable(on=False)
    assert result["ok"] is True


# ── bt_scan() tests ───────────────────────────────────────────────────────────


def test_bt_scan_finds_devices(mock_subprocess):
    """bt_scan returns discovered devices."""
    # Simulate the three bluetoothctl calls
    mock_subprocess.side_effect = [
        MagicMock(stdout="", returncode=0),  # scan on
        MagicMock(stdout="", returncode=0),  # scan off
        MagicMock(  # devices
            stdout="Device AA:BB:CC:DD:EE:FF My Guitar\n"
                   "Device 11:22:33:44:55:66 Expression Pedal\n",
            returncode=0,
        ),
    ]
    # bt_scan uses time.sleep which we don't want to actually wait
    with patch("src.system.bluetooth.time.sleep", return_value=None):
        result = bt_scan(timeout=1)
    assert result["ok"] is True
    assert len(result["devices"]) == 2
    assert result["devices"][0]["name"] == "My Guitar"
    assert result["devices"][0]["address"] == "AA:BB:CC:DD:EE:FF"
    assert result["devices"][1]["name"] == "Expression Pedal"


def test_bt_scan_no_devices(mock_subprocess):
    """bt_scan returns empty list when no devices found."""
    mock_subprocess.side_effect = [
        MagicMock(stdout="", returncode=0),  # scan on
        MagicMock(stdout="", returncode=0),  # scan off
        MagicMock(stdout="", returncode=0),  # devices - empty
    ]
    with patch("src.system.bluetooth.time.sleep", return_value=None):
        result = bt_scan(timeout=1)
    assert result["ok"] is True
    assert result["devices"] == []


# ── bt_paired_devices() tests ──────────────────────────────────────────────────


def test_bt_paired_devices(mock_subprocess):
    """bt_paired_devices returns paired device list."""
    mock_subprocess.side_effect = [
        MagicMock(  # paired-devices
            stdout="Device AA:BB:CC:DD:EE:FF MIDI Controller\n"
                   "Device 11:22:33:44:55:66 Footswitch\n",
            returncode=0,
        ),
        MagicMock(  # info
            stdout="",
            returncode=0,
        ),
    ]
    devices = bt_paired_devices()
    assert len(devices) == 2
    assert devices[0]["name"] == "MIDI Controller"
    assert devices[0]["address"] == "AA:BB:CC:DD:EE:FF"


def test_bt_paired_devices_empty(mock_subprocess):
    """bt_paired_devices returns empty list."""
    mock_subprocess.side_effect = [
        MagicMock(stdout="", returncode=0),  # paired-devices - empty
        MagicMock(stdout="", returncode=0),
    ]
    devices = bt_paired_devices()
    assert devices == []


# ── bt_pair() tests ───────────────────────────────────────────────────────────


def test_bt_pair_success(mock_subprocess):
    """bt_pair successfully pairs with a device."""
    mock_subprocess.side_effect = [
        MagicMock(stdout="", returncode=0),  # trust
        MagicMock(stdout="Pairing successful\n", returncode=0),  # pair
        MagicMock(stdout="", returncode=0),  # connect
    ]
    with patch("src.system.bluetooth.time.sleep", return_value=None):
        result = bt_pair("AA:BB:CC:DD:EE:FF")
    assert result["ok"] is True


def test_bt_pair_invalid_address():
    """bt_pair returns error for invalid MAC address."""
    result = bt_pair("invalid")
    assert result["ok"] is False
    assert "Invalid" in result["error"]


def test_bt_pair_failure(mock_subprocess):
    """bt_pair returns error when pairing fails."""
    mock_subprocess.side_effect = [
        MagicMock(stdout="", returncode=0),  # trust
        MagicMock(stdout="Failed to pair\n", returncode=0),  # pair
    ]
    with patch("src.system.bluetooth.time.sleep", return_value=None):
        result = bt_pair("AA:BB:CC:DD:EE:FF")
    assert result["ok"] is False
    assert "failed" in result["error"].lower()


# ── bt_unpair() tests ──────────────────────────────────────────────────────────


def test_bt_unpair_success(mock_subprocess):
    """bt_unpair removes a device."""
    mock_subprocess.side_effect = [
        MagicMock(stdout="", returncode=0),  # disconnect
        MagicMock(stdout="Device has been removed\n", returncode=0),  # remove
    ]
    with patch("src.system.bluetooth.time.sleep", return_value=None):
        result = bt_unpair("AA:BB:CC:DD:EE:FF")
    assert result["ok"] is True


def test_bt_unpair_invalid():
    """bt_unpair returns error for invalid address."""
    result = bt_unpair("invalid")
    assert result["ok"] is False


# ── bt_connect() / bt_disconnect() tests ───────────────────────────────────────


def test_bt_connect_success(mock_subprocess):
    """bt_connect connects to a paired device."""
    mock_subprocess.return_value = MagicMock(
        stdout="Connection successful\n",
        returncode=0,
    )
    result = bt_connect("AA:BB:CC:DD:EE:FF")
    assert result["ok"] is True


def test_bt_connect_invalid():
    """bt_connect returns error for invalid address."""
    result = bt_connect("bad")
    assert result["ok"] is False


def test_bt_disconnect_success(mock_subprocess):
    """bt_disconnect disconnects a device."""
    mock_subprocess.return_value = MagicMock(stdout="", returncode=0)
    result = bt_disconnect("AA:BB:CC:DD:EE:FF")
    assert result["ok"] is True


def test_bt_disconnect_invalid():
    """bt_disconnect returns error for invalid address."""
    result = bt_disconnect("bad")
    assert result["ok"] is False


# ── bt_midi_status() tests ─────────────────────────────────────────────────────


def test_bt_midi_status_enabled_running(mock_subprocess):
    """bt_midi_status shows enabled and running."""
    mock_subprocess.side_effect = [
        MagicMock(stdout="enabled\n", returncode=0),  # is-enabled
        MagicMock(stdout="active\n", returncode=0),   # is-active
    ]
    status = bt_midi_status()
    assert status["enabled"] is True
    assert status["running"] is True


def test_bt_midi_status_disabled(mock_subprocess):
    """bt_midi_status shows disabled."""
    mock_subprocess.side_effect = [
        MagicMock(stdout="disabled\n", returncode=0),
        MagicMock(stdout="inactive\n", returncode=0),
    ]
    status = bt_midi_status()
    assert status["enabled"] is False
    assert status["running"] is False


# ── bt_midi_enable() / bt_midi_disable() tests ─────────────────────────────────


def test_bt_midi_enable_success(mock_subprocess):
    """bt_midi_enable starts the MIDI service."""
    mock_subprocess.return_value = MagicMock(returncode=0)
    result = bt_midi_enable()
    assert result["ok"] is True


def test_bt_midi_enable_failure(mock_subprocess):
    """bt_midi_enable returns error on failure."""
    mock_subprocess.side_effect = subprocess.CalledProcessError(1, "systemctl")
    result = bt_midi_enable()
    assert result["ok"] is False


def test_bt_midi_disable_success(mock_subprocess):
    """bt_midi_disable stops the MIDI service."""
    mock_subprocess.return_value = MagicMock(returncode=0)
    result = bt_midi_disable()
    assert result["ok"] is True


# ── bt_midi_connected_devices() tests ──────────────────────────────────────────


def test_bt_midi_connected_devices(mock_subprocess):
    """bt_midi_connected_devices returns connected devices."""
    mock_subprocess.side_effect = [
        MagicMock(  # paired-devices
            stdout="Device AA:BB:CC:DD:EE:FF MIDI Keys\n"
                   "Device 11:22:33:44:55:66 Pedal\n",
            returncode=0,
        ),
        MagicMock(stdout="", returncode=0),  # info - none connected
    ]
    devices = bt_midi_connected_devices()
    assert devices == []  # none connected


def test_bt_midi_connected_devices_with_connected(mock_subprocess):
    """bt_midi_connected_devices returns only connected devices."""
    with patch("src.system.bluetooth.bt_paired_devices", return_value=[
        {"address": "AA:BB:CC:DD:EE:FF", "name": "MIDI Keys", "connected": True},
        {"address": "11:22:33:44:55:66", "name": "Pedal", "connected": False},
    ]):
        devices = bt_midi_connected_devices()
        assert len(devices) == 1
        assert devices[0]["name"] == "MIDI Keys"