"""Tests for brightness_controller module."""

from __future__ import annotations

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

import pytest

from python_pkg.brightness_controller import brightness_controller

# ── _find_als_device ─────────────────────────────────────────────────────


class TestFindAlsDevice:
    """Tests for _find_als_device."""

    @patch.object(
        Path,
        "glob",
        return_value=[Path("/sys/bus/iio/devices/iio0/in_illuminance_raw")],
    )
    def test_found(self, mock_glob: MagicMock) -> None:
        result = brightness_controller._find_als_device()
        assert result == Path("/sys/bus/iio/devices/iio0")

    @patch.object(Path, "glob", return_value=[])
    def test_not_found(self, mock_glob: MagicMock) -> None:
        assert brightness_controller._find_als_device() is None


# ── _read_lux ────────────────────────────────────────────────────────────


class TestReadLux:
    """Tests for _read_lux."""

    def test_all_files_present(self, tmp_path: Path) -> None:
        (tmp_path / "in_illuminance_raw").write_text("100\n")
        (tmp_path / "in_illuminance_scale").write_text("2.0\n")
        (tmp_path / "in_illuminance_offset").write_text("5.0\n")
        assert brightness_controller._read_lux(tmp_path) == pytest.approx(210.0)

    def test_missing_scale(self, tmp_path: Path) -> None:
        (tmp_path / "in_illuminance_raw").write_text("50\n")
        (tmp_path / "in_illuminance_offset").write_text("0\n")
        assert brightness_controller._read_lux(tmp_path) == pytest.approx(50.0)

    def test_missing_offset(self, tmp_path: Path) -> None:
        (tmp_path / "in_illuminance_raw").write_text("50\n")
        (tmp_path / "in_illuminance_scale").write_text("1.0\n")
        assert brightness_controller._read_lux(tmp_path) == pytest.approx(50.0)

    def test_invalid_scale(self, tmp_path: Path) -> None:
        (tmp_path / "in_illuminance_raw").write_text("50\n")
        (tmp_path / "in_illuminance_scale").write_text("bad\n")
        (tmp_path / "in_illuminance_offset").write_text("0\n")
        assert brightness_controller._read_lux(tmp_path) == pytest.approx(50.0)

    def test_invalid_offset(self, tmp_path: Path) -> None:
        (tmp_path / "in_illuminance_raw").write_text("50\n")
        (tmp_path / "in_illuminance_scale").write_text("1.0\n")
        (tmp_path / "in_illuminance_offset").write_text("bad\n")
        assert brightness_controller._read_lux(tmp_path) == pytest.approx(50.0)


# ── _lux_to_brightness ──────────────────────────────────────────────────


class TestLuxToBrightness:
    """Tests for _lux_to_brightness."""

    def test_below_minimum(self) -> None:
        assert brightness_controller._lux_to_brightness(-1.0) == 10

    def test_at_minimum(self) -> None:
        assert brightness_controller._lux_to_brightness(0.0) == 10

    def test_above_maximum(self) -> None:
        assert brightness_controller._lux_to_brightness(10000.0) == 100

    def test_at_maximum(self) -> None:
        assert brightness_controller._lux_to_brightness(5000.0) == 100

    def test_interpolation(self) -> None:
        # Between (5.0, 40) and (50.0, 75), at lux=27.5
        assert brightness_controller._lux_to_brightness(27.5) == 57

    def test_fallback_return(self) -> None:
        """Exercise the post-loop fallback (unreachable with monotonic curves)."""
        nan = float("nan")
        with patch.object(
            brightness_controller,
            "LUX_CURVE",
            [(nan, 10), (nan, 99)],
        ):
            assert brightness_controller._lux_to_brightness(50.0) == 99


# ── _run_brightnessctl ───────────────────────────────────────────────────


class TestRunBrightnessctl:
    """Tests for _run_brightnessctl."""

    @patch("python_pkg.brightness_controller.brightness_controller.subprocess.run")
    def test_captures_stdout(self, mock_run: MagicMock) -> None:
        mock_run.return_value = MagicMock(stdout="  some output  ")
        result = brightness_controller._run_brightnessctl("-l", "-m")
        assert result == "some output"
        mock_run.assert_called_once_with(
            [brightness_controller._BRIGHTNESSCTL, "-l", "-m"],
            capture_output=True,
            text=True,
            check=False,
        )


# ── _get_devices ─────────────────────────────────────────────────────────


class TestGetDevices:
    """Tests for _get_devices."""

    @patch("python_pkg.brightness_controller.brightness_controller._run_brightnessctl")
    def test_returns_backlight_devices(self, mock_run: MagicMock) -> None:
        mock_run.return_value = (
            "intel_backlight,backlight,50,42%,120000\nkbd_backlight,leds,0,0%,3"
        )
        devices = brightness_controller._get_devices()
        assert len(devices) == 1
        assert devices[0].name == "intel_backlight"
        assert devices[0].device_class == "backlight"
        assert devices[0].current == 42
        assert devices[0].percent == "42%"
        assert devices[0].max_brightness == 120000

    @patch("python_pkg.brightness_controller.brightness_controller._run_brightnessctl")
    def test_empty_output(self, mock_run: MagicMock) -> None:
        mock_run.return_value = ""
        assert brightness_controller._get_devices() == []

    @patch("python_pkg.brightness_controller.brightness_controller._run_brightnessctl")
    def test_too_few_fields(self, mock_run: MagicMock) -> None:
        mock_run.return_value = "a,b,c"
        assert brightness_controller._get_devices() == []


# ── _get_brightness ──────────────────────────────────────────────────────


class TestGetBrightness:
    """Tests for _get_brightness."""

    @patch("python_pkg.brightness_controller.brightness_controller._run_brightnessctl")
    def test_valid(self, mock_run: MagicMock) -> None:
        mock_run.side_effect = ["123", "intel_backlight,backlight,50,42%,120000"]
        assert brightness_controller._get_brightness("intel_backlight") == 42

    @patch("python_pkg.brightness_controller.brightness_controller._run_brightnessctl")
    def test_empty_get_output(self, mock_run: MagicMock) -> None:
        mock_run.return_value = ""
        assert brightness_controller._get_brightness("intel_backlight") == -1

    @patch("python_pkg.brightness_controller.brightness_controller._run_brightnessctl")
    def test_info_no_valid_fields(self, mock_run: MagicMock) -> None:
        mock_run.side_effect = ["123", "a,b,c"]
        assert brightness_controller._get_brightness("intel_backlight") == -1


# ── _set_brightness ──────────────────────────────────────────────────────


class TestSetBrightness:
    """Tests for _set_brightness."""

    @patch("python_pkg.brightness_controller.brightness_controller._run_brightnessctl")
    def test_calls_brightnessctl(self, mock_run: MagicMock) -> None:
        brightness_controller._set_brightness("intel_backlight", 75)
        mock_run.assert_called_once_with("-d", "intel_backlight", "set", "75%")


# ── Device NamedTuple ────────────────────────────────────────────────────


class TestDevice:
    """Tests for Device NamedTuple."""

    def test_create(self) -> None:
        d = brightness_controller.Device("test", "backlight", 50, "50%", 1000)
        assert d.name == "test"
        assert d.max_brightness == 1000


# ── BrightnessController ────────────────────────────────────────────────


def _make_controller(
    devices: list[brightness_controller.Device] | None = None,
    als_path: Path | None = None,
    *,
    daemon_state: bool = False,
) -> brightness_controller.BrightnessController:
    """Create a BrightnessController with all Tk operations mocked."""
    if devices is None:
        devices = [
            brightness_controller.Device(
                "intel_backlight", "backlight", 50, "50%", 120000
            )
        ]

    with (
        patch(
            "python_pkg.brightness_controller.brightness_controller._get_devices",
            return_value=devices,
        ),
        patch(
            "python_pkg.brightness_controller.brightness_controller._find_als_device",
            return_value=als_path,
        ),
        patch.object(
            brightness_controller.BrightnessController,
            "_read_daemon_state",
            return_value=daemon_state,
        ),
        patch(
            "python_pkg.brightness_controller.brightness_controller.tk.Tk"
        ) as mock_tk,
        patch(
            "python_pkg.brightness_controller.brightness_controller.tk.StringVar"
        ) as mock_str_var,
        patch(
            "python_pkg.brightness_controller.brightness_controller.tk.IntVar"
        ) as mock_int_var,
        patch("python_pkg.brightness_controller.brightness_controller.ttk"),
        patch(
            "python_pkg.brightness_controller.brightness_controller._get_brightness",
            return_value=50,
        ),
    ):
        mock_root = MagicMock()
        mock_tk.return_value = mock_root
        mock_root.after = MagicMock()
        mock_str_var.return_value = MagicMock()
        mock_int_var.return_value = MagicMock()

        return brightness_controller.BrightnessController()


class TestBrightnessControllerInit:
    """Tests for BrightnessController.__init__."""

    def test_single_device(self) -> None:
        ctrl = _make_controller()
        assert ctrl.current_device == "intel_backlight"

    def test_no_devices(self) -> None:
        ctrl = _make_controller(devices=[])
        assert ctrl.current_device == ""

    def test_multiple_devices(self) -> None:
        devices = [
            brightness_controller.Device("led0", "leds", 0, "0%", 3),
            brightness_controller.Device("intel_bl", "backlight", 50, "50%", 120000),
        ]
        ctrl = _make_controller(devices=devices)
        # Should prefer backlight device
        assert ctrl.current_device == "intel_bl"

    def test_with_als(self, tmp_path: Path) -> None:
        ctrl = _make_controller(als_path=tmp_path)
        assert ctrl.als_path == tmp_path

    def test_auto_mode_enabled(self) -> None:
        ctrl = _make_controller(daemon_state=True)
        assert ctrl.auto_mode is True


class TestSelectDefaultDevice:
    """Tests for _select_default_device."""

    def test_no_devices_sets_message(self) -> None:
        ctrl = _make_controller(devices=[])
        ctrl.pct_var = MagicMock()
        ctrl._select_default_device()
        ctrl.pct_var.set.assert_called_with("No devices")

    def test_prefers_backlight(self) -> None:
        devices = [
            brightness_controller.Device("led0", "leds", 0, "0%", 3),
            brightness_controller.Device("bl", "backlight", 50, "50%", 120000),
        ]
        ctrl = _make_controller(devices=devices)
        ctrl._refresh_brightness = MagicMock()
        ctrl._select_default_device()
        assert ctrl.current_device == "bl"

    def test_no_backlight_device(self) -> None:
        """When no backlight device exists, uses the first device."""
        devices = [
            brightness_controller.Device("led0", "leds", 0, "0%", 3),
            brightness_controller.Device("led1", "leds", 0, "0%", 5),
        ]
        ctrl = _make_controller(devices=devices)
        ctrl._refresh_brightness = MagicMock()
        ctrl._select_default_device()
        assert ctrl.current_device == "led0"


class TestOnDeviceChange:
    """Tests for _on_device_change."""

    def test_updates_current_device(self) -> None:
        ctrl = _make_controller()
        ctrl.device_var = MagicMock()
        ctrl.device_var.get.return_value = "new_device"
        ctrl._refresh_brightness = MagicMock()
        ctrl._on_device_change(MagicMock())
        assert ctrl.current_device == "new_device"
        ctrl._refresh_brightness.assert_called_once()


class TestRefreshBrightness:
    """Tests for _refresh_brightness."""

    @patch(
        "python_pkg.brightness_controller.brightness_controller._get_brightness",
        return_value=75,
    )
    def test_updates_ui(self, mock_get: MagicMock) -> None:
        ctrl = _make_controller()
        ctrl.pct_var = MagicMock()
        ctrl.slider_var = MagicMock()
        ctrl._refresh_brightness()
        ctrl.pct_var.set.assert_called_with("75%")
        ctrl.slider_var.set.assert_called_with(75)

    @patch(
        "python_pkg.brightness_controller.brightness_controller._get_brightness",
        return_value=-1,
    )
    def test_error(self, mock_get: MagicMock) -> None:
        ctrl = _make_controller()
        ctrl.pct_var = MagicMock()
        ctrl._refresh_brightness()
        ctrl.pct_var.set.assert_called_with("Error")

    def test_no_current_device(self) -> None:
        ctrl = _make_controller(devices=[])
        ctrl.pct_var = MagicMock()
        ctrl._refresh_brightness()
        ctrl.pct_var.set.assert_not_called()


class TestOnSliderMove:
    """Tests for _on_slider_move."""

    @patch("python_pkg.brightness_controller.brightness_controller._set_brightness")
    def test_sets_brightness(self, mock_set: MagicMock) -> None:
        ctrl = _make_controller()
        ctrl.pct_var = MagicMock()
        ctrl._updating_slider = False
        ctrl._on_slider_move("75.0")
        mock_set.assert_called_once_with("intel_backlight", 75)
        ctrl.pct_var.set.assert_called_with("75%")

    def test_skips_during_update(self) -> None:
        ctrl = _make_controller()
        ctrl._updating_slider = True
        ctrl.pct_var = MagicMock()
        ctrl._on_slider_move("75.0")
        ctrl.pct_var.set.assert_not_called()

    def test_no_device(self) -> None:
        ctrl = _make_controller(devices=[])
        ctrl.pct_var = MagicMock()
        ctrl._on_slider_move("75.0")
        ctrl.pct_var.set.assert_not_called()

    @patch("python_pkg.brightness_controller.brightness_controller._set_brightness")
    def test_disables_auto_mode(self, mock_set: MagicMock) -> None:
        ctrl = _make_controller(daemon_state=True)
        ctrl.auto_mode = True
        ctrl.pct_var = MagicMock()
        ctrl._set_auto = MagicMock()
        ctrl._updating_slider = False
        ctrl._on_slider_move("50.0")
        ctrl._set_auto.assert_called_once_with(enabled=False)


class TestSetPct:
    """Tests for _set_pct."""

    @patch("python_pkg.brightness_controller.brightness_controller._set_brightness")
    @patch(
        "python_pkg.brightness_controller.brightness_controller._get_brightness",
        return_value=25,
    )
    def test_sets_brightness(self, mock_get: MagicMock, mock_set: MagicMock) -> None:
        ctrl = _make_controller()
        ctrl.pct_var = MagicMock()
        ctrl.slider_var = MagicMock()
        ctrl._set_pct(25)
        mock_set.assert_called_once_with("intel_backlight", 25)

    def test_no_device(self) -> None:
        ctrl = _make_controller(devices=[])
        # Should not raise
        ctrl._set_pct(50)


class TestDecrease:
    """Tests for _decrease."""

    @patch("python_pkg.brightness_controller.brightness_controller._set_brightness")
    @patch(
        "python_pkg.brightness_controller.brightness_controller._get_brightness",
        return_value=50,
    )
    def test_decrease(self, mock_get: MagicMock, mock_set: MagicMock) -> None:
        ctrl = _make_controller()
        ctrl.pct_var = MagicMock()
        ctrl.slider_var = MagicMock()
        ctrl._decrease()
        mock_set.assert_called_once_with("intel_backlight", 45)

    @patch("python_pkg.brightness_controller.brightness_controller._set_brightness")
    @patch(
        "python_pkg.brightness_controller.brightness_controller._get_brightness",
        return_value=2,
    )
    def test_clamps_to_zero(self, mock_get: MagicMock, mock_set: MagicMock) -> None:
        ctrl = _make_controller()
        ctrl.pct_var = MagicMock()
        ctrl.slider_var = MagicMock()
        ctrl._decrease()
        mock_set.assert_called_once_with("intel_backlight", 0)

    def test_no_device(self) -> None:
        ctrl = _make_controller(devices=[])
        ctrl._decrease()


class TestIncrease:
    """Tests for _increase."""

    @patch("python_pkg.brightness_controller.brightness_controller._set_brightness")
    @patch(
        "python_pkg.brightness_controller.brightness_controller._get_brightness",
        return_value=50,
    )
    def test_increase(self, mock_get: MagicMock, mock_set: MagicMock) -> None:
        ctrl = _make_controller()
        ctrl.pct_var = MagicMock()
        ctrl.slider_var = MagicMock()
        ctrl._increase()
        mock_set.assert_called_once_with("intel_backlight", 55)

    @patch("python_pkg.brightness_controller.brightness_controller._set_brightness")
    @patch(
        "python_pkg.brightness_controller.brightness_controller._get_brightness",
        return_value=98,
    )
    def test_clamps_to_100(self, mock_get: MagicMock, mock_set: MagicMock) -> None:
        ctrl = _make_controller()
        ctrl.pct_var = MagicMock()
        ctrl.slider_var = MagicMock()
        ctrl._increase()
        mock_set.assert_called_once_with("intel_backlight", 100)

    def test_no_device(self) -> None:
        ctrl = _make_controller(devices=[])
        ctrl._increase()
