package vassistent.service;

import org.eclipse.paho.client.mqttv3.MqttClient;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import vassistent.model.AppState;

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

/**
 * Unit tests for {@link MqttClientService}.
 */
@ExtendWith(MockitoExtension.class)
class MqttClientServiceTest {

    @Mock
    private MqttClient mqttClient;

    private AppState appState;

    /**
     * Creates a fresh app state per test.
     */
    @BeforeEach
    void setUp() {
        appState = new AppState();
    }

    /**
     * Verifies constructor behavior when auto-connect is enabled.
     *
     * @throws Exception if mock interaction setup fails
     */
    @Test
    void constructor_withConnectOnStart_connectsAndSetsState() throws Exception {
        MqttClientService service = new MqttClientService(appState, mqttClient, true);

        verify(mqttClient).setCallback(service);
        verify(mqttClient).connect(any());
        assertTrue(appState.isMqttConnected());
    }

    /**
     * Verifies constructor handles connection failures without throwing.
     *
     * @throws Exception if mock interaction setup fails
     */
    @Test
    void constructor_connectFailure_keepsStateDisconnected() throws Exception {
        doThrow(new MqttException(0)).when(mqttClient).connect(any());

        MqttClientService service = new MqttClientService(appState, mqttClient, true);

        verify(mqttClient).setCallback(service);
        verify(mqttClient).connect(any());
        assertFalse(appState.isMqttConnected());
    }

    /**
     * Verifies subscribe registers the listener and routes arriving payloads.
     *
     * @throws Exception if callback invocation fails
     */
    @Test
    void subscribe_thenMessageArrived_routesPayloadToListener() throws Exception {
        MqttClientService service = new MqttClientService(appState, mqttClient, false);
        List<String> payloads = new ArrayList<>();

        service.subscribe("PREDICTION", payloads::add);
        service.messageArrived("PREDICTION", new MqttMessage("data".getBytes(StandardCharsets.UTF_8)));

        verify(mqttClient).subscribe("PREDICTION");
        assertEquals(List.of("data"), payloads);
    }

    /**
     * Verifies publish forwards a message with the configured QoS and payload.
     *
     * @throws Exception if mock interaction setup fails
     */
    @Test
    void publish_forwardsMessageWithQos() throws Exception {
        MqttClientService service = new MqttClientService(appState, mqttClient, false);
        ArgumentCaptor<MqttMessage> captor = ArgumentCaptor.forClass(MqttMessage.class);

        service.publish("PREDICTION", "hello", 1);

        verify(mqttClient).publish(org.mockito.ArgumentMatchers.eq("PREDICTION"), captor.capture());
        MqttMessage published = captor.getValue();
        assertEquals("hello", new String(published.getPayload(), StandardCharsets.UTF_8));
        assertEquals(1, published.getQos());
    }

    /**
     * Verifies disconnect only calls client disconnect when connection is active.
     *
     * @throws Exception if mock interaction setup fails
     */
    @Test
    void disconnect_onlyWhenConnected() throws Exception {
        MqttClientService service = new MqttClientService(appState, mqttClient, false);

        when(mqttClient.isConnected()).thenReturn(false);
        service.disconnect();
        verify(mqttClient, never()).disconnect();

        when(mqttClient.isConnected()).thenReturn(true);
        service.disconnect();
        verify(mqttClient).disconnect();
    }

    /**
     * Verifies connection loss updates app state to disconnected.
     */
    @Test
    void connectionLost_setsMqttStateFalse() {
        appState.setMqttConnected(true);
        MqttClientService service = new MqttClientService(appState, mqttClient, false);

        service.connectionLost(new RuntimeException("lost"));

        assertFalse(appState.isMqttConnected());
    }

    /**
     * Verifies unknown topics do not fail when a message arrives.
     *
     * @throws Exception if callback invocation fails
     */
    @Test
    void messageArrived_unknownTopic_doesNotThrow() throws Exception {
        MqttClientService service = new MqttClientService(appState, mqttClient, false);

        service.messageArrived("UNKNOWN", new MqttMessage("x".getBytes(StandardCharsets.UTF_8)));

        verify(mqttClient, never()).subscribe("UNKNOWN");
    }
}
