from typing import Generator
from unittest.mock import MagicMock, patch

import pytest
from typer.testing import CliRunner

from hamqtt.config import Config, MqttConfig
from hamqtt.main import app

runner = CliRunner()


@pytest.fixture  # type: ignore[misc]
def mock_config() -> Config:
    return Config(mqtt=MqttConfig(host="localhost", port=1883, username="user", password="pass"))


@pytest.fixture  # type: ignore[misc]
def mock_load_config(mock_config: Config) -> Generator[MagicMock, None, None]:
    with patch("hamqtt.main.load_config", return_value=mock_config) as mock:
        yield mock


@pytest.fixture  # type: ignore[misc]
def mock_save_config() -> Generator[MagicMock, None, None]:
    with patch("hamqtt.main.save_config") as mock:
        yield mock


@pytest.fixture  # type: ignore[misc]
def mock_mqtt_client_ctx() -> Generator[MagicMock, None, None]:
    with patch("hamqtt.main.MqttClient") as mock:
        mock_instance = mock.return_value
        mock_instance.__enter__.return_value = MagicMock()
        yield mock_instance


def test_configure_interactive(mock_load_config: MagicMock, mock_save_config: MagicMock) -> None:
    """Test interactive configuration"""
    # Simulate user input
    input_str = "newhost\n1884\nnewuser\nnewpass\n"

    result = runner.invoke(app, ["configure"], input=input_str)

    assert result.exit_code == 0
    assert "Interactive Configuration" in result.stdout
    assert "Configuration saved successfully!" in result.stdout

    # Verify save_config called with new values
    mock_save_config.assert_called_once()
    saved_config = mock_save_config.call_args[0][0]
    assert saved_config.mqtt.host == "newhost"
    assert saved_config.mqtt.port == 1884
    assert saved_config.mqtt.username == "newuser"
    assert saved_config.mqtt.password == "newpass"


def test_configure_flags(mock_load_config: MagicMock, mock_save_config: MagicMock) -> None:
    """Test configuration via flags"""
    result = runner.invoke(app, ["configure", "--host", "flaghost", "--port", "9999"])

    assert result.exit_code == 0
    assert "Configuration saved successfully!" in result.stdout

    mock_save_config.assert_called_once()
    saved_config = mock_save_config.call_args[0][0]
    assert saved_config.mqtt.host == "flaghost"
    assert saved_config.mqtt.port == 9999
    # Should preserve existing values
    assert saved_config.mqtt.username == "user"


def test_register_command(mock_load_config: MagicMock, mock_mqtt_client_ctx: MagicMock) -> None:
    """Test register command"""
    result = runner.invoke(
        app,
        [
            "register",
            "--unique-id",
            "test_sensor",
            "--name",
            "Test Sensor",
            "--device-class",
            "temperature",
            "--unit",
            "C",
        ],
    )

    assert result.exit_code == 0
    assert "Registered entity Test Sensor" in result.stdout

    # Verify MQTT publish
    mock_client = mock_mqtt_client_ctx.__enter__.return_value
    mock_client.publish.assert_called_once()
    topic, payload = mock_client.publish.call_args[0][:2]

    assert "homeassistant/sensor/test_sensor/config" == topic
    assert '"name": "Test Sensor"' in payload
    assert '"device_class": "temperature"' in payload


def test_register_dry_run(mock_load_config: MagicMock, mock_mqtt_client_ctx: MagicMock) -> None:
    """Test register dry run"""
    result = runner.invoke(app, ["register", "--unique-id", "test_sensor", "--name", "Test Sensor", "--dry-run"])

    assert result.exit_code == 0
    assert "Topic:" in result.stdout

    # Should not publish
    mock_mqtt_client_ctx.__enter__.assert_not_called()


def test_register_failure_handling(mock_load_config: MagicMock, mock_mqtt_client_ctx: MagicMock) -> None:
    """Test register command failure handling"""
    mock_client = mock_mqtt_client_ctx.__enter__.return_value
    mock_client.publish.side_effect = Exception("MQTT Error")

    result = runner.invoke(app, ["register", "--unique-id", "test_sensor", "--name", "Test Sensor"])

    assert result.exit_code == 0
    assert "Failed to register" in result.stdout
    assert "MQTT Error" in result.stdout


def test_send_command(mock_load_config: MagicMock, mock_mqtt_client_ctx: MagicMock) -> None:
    """Test send command"""
    result = runner.invoke(app, ["send", "--unique-id", "test_sensor", "--state", "25.5"])

    assert result.exit_code == 0
    assert "Sent state '25.5'" in result.stdout

    mock_client = mock_mqtt_client_ctx.__enter__.return_value
    mock_client.publish.assert_called_with("ha-cli/test_sensor/state", "25.5")


def test_send_custom_topic(mock_load_config: MagicMock, mock_mqtt_client_ctx: MagicMock) -> None:
    """Test send command with custom topic"""
    result = runner.invoke(app, ["send", "--unique-id", "test_sensor", "--state", "ON", "--topic", "custom/topic"])

    assert result.exit_code == 0

    mock_client = mock_mqtt_client_ctx.__enter__.return_value
    mock_client.publish.assert_called_with("custom/topic", "ON")


def test_send_dry_run(mock_load_config: MagicMock, mock_mqtt_client_ctx: MagicMock) -> None:
    """Test send dry run"""
    result = runner.invoke(app, ["send", "--unique-id", "test_sensor", "--state", "ON", "--dry-run"])

    assert result.exit_code == 0
    assert "Payload: ON" in result.stdout

    mock_mqtt_client_ctx.__enter__.assert_not_called()


def test_send_failure_handling(mock_load_config: MagicMock, mock_mqtt_client_ctx: MagicMock) -> None:
    """Test send command failure handling"""
    mock_client = mock_mqtt_client_ctx.__enter__.return_value
    mock_client.publish.side_effect = Exception("Connection Lost")

    result = runner.invoke(app, ["send", "--unique-id", "test_sensor", "--state", "FAIL"])

    assert result.exit_code == 0
    assert "Failed to send state" in result.stdout
