/*
 * Copyright (c) 2010-2026 Contributors to the openHAB project
 *
 * See the NOTICE file(s) distributed with this work for additional
 * information.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0
 *
 * SPDX-License-Identifier: EPL-2.0
 */
package org.openhab.binding.bluelink.internal.handler;

import static com.github.tomakehurst.wiremock.client.WireMock.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.when;
import static org.openhab.binding.bluelink.internal.MockApiData.DEVICE_REGISTRATION_RESPONSE_EU;
import static org.openhab.binding.bluelink.internal.MockApiData.ENROLLMENT_RESPONSE_US;
import static org.openhab.binding.bluelink.internal.MockApiData.TOKEN_RESPONSE_EU;
import static org.openhab.binding.bluelink.internal.MockApiData.TOKEN_RESPONSE_US;
import static org.openhab.binding.bluelink.internal.MockApiData.VEHICLES_RESPONSE_EU;

import java.time.ZoneId;
import java.util.Locale;
import java.util.Map;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.HttpClient;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import org.openhab.binding.bluelink.internal.MockApiData;
import org.openhab.binding.bluelink.internal.api.BluelinkApiException;
import org.openhab.binding.bluelink.internal.api.Region;
import org.openhab.binding.bluelink.internal.dto.eu.Vehicle;
import org.openhab.binding.bluelink.internal.model.Brand;
import org.openhab.binding.bluelink.internal.model.IVehicle;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.test.java.JavaTest;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.ThingHandlerCallback;

import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.client.WireMock;
import com.github.tomakehurst.wiremock.core.WireMockConfiguration;

/**
 * Integration tests for the Bluelink binding using WireMock to mock the API.
 *
 * @author Marcus Better - Initial contribution
 * @author Florian Hotze - Added tests for EU account
 */
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
@NonNullByDefault
class BluelinkAccountHandlerTest extends JavaTest {

    private static final WireMockServer WIREMOCK_SERVER = new WireMockServer(
            WireMockConfiguration.options().dynamicPort());
    private static final HttpClient HTTP_CLIENT = new HttpClient();

    private @Mock @NonNullByDefault({}) Bridge bridge;
    private @Mock @NonNullByDefault({}) ThingHandlerCallback callback;

    private @NonNullByDefault({}) BluelinkAccountHandler handler;

    @BeforeAll
    static void setUpHttpServer() throws Exception {
        WIREMOCK_SERVER.start();
        HTTP_CLIENT.start();
    }

    @AfterEach
    void tearDown() {
        handler.dispose();
    }

    @AfterAll
    static void tearDownHttpServer() throws Exception {
        WIREMOCK_SERVER.stop();
        HTTP_CLIENT.stop();
    }

    @Nested
    class US {
        private final TimeZoneProvider timeZoneProvider = () -> ZoneId.of("America/New_York");
        private final LocaleProvider localeProvider = () -> Locale.US;

        @BeforeAll
        static void setUpStubs() {
            WireMock.configureFor("localhost", WIREMOCK_SERVER.port());
            WireMock.reset();
            stubFor(post(urlEqualTo("/v2/ac/oauth/token")).willReturn(aResponse().withStatus(200)
                    .withHeader("Content-Type", "application/json").withBody(TOKEN_RESPONSE_US)));
            stubFor(get(urlPathMatching("/ac/v2/enrollment/details/.*")).willReturn(aResponse().withStatus(200)
                    .withHeader("Content-Type", "application/json").withBody(ENROLLMENT_RESPONSE_US)));
        }

        @BeforeEach
        void setUp() {
            final Configuration config = new Configuration(Map.of("username", MockApiData.TEST_USERNAME, "password",
                    MockApiData.TEST_PASSWORD, "apiBaseUrl", "http://localhost:" + WIREMOCK_SERVER.port()));
            when(bridge.getConfiguration()).thenReturn(config);
            when(bridge.getUID()).thenReturn(new ThingUID("bluelink:account:testaccount"));

            handler = new BluelinkAccountHandler(bridge, HTTP_CLIENT, timeZoneProvider, localeProvider);
            handler.setCallback(callback);
            handler.initialize();
        }

        @Test
        void testLoginAndGetVehicles() throws BluelinkApiException {
            waitForAssert(() -> {
                try {
                    assertThat(handler.getVehicles(), is(not(empty())));
                } catch (final BluelinkApiException e) {
                    throw new IllegalStateException(e);
                }
            });
            final var vehicle = handler.getVehicles().getFirst();
            assertNotNull(vehicle);
            assertEquals("IONIQ 6", vehicle.model());
            assertEquals(IVehicle.EngineType.EV, vehicle.engineType());
            assertTrue(vehicle.isElectric());
        }
    }

    @Nested
    class EU {
        private final TimeZoneProvider timeZoneProvider = () -> ZoneId.of("Europe/Berlin");
        private final LocaleProvider localeProvider = () -> Locale.GERMAN;

        @BeforeAll
        static void setUpStubs() {
            WireMock.configureFor("localhost", WIREMOCK_SERVER.port());
            WireMock.reset();
            stubFor(post(urlEqualTo("/auth/api/v2/user/oauth2/token")).willReturn(aResponse().withStatus(200)
                    .withHeader("Content-Type", "application/json").withBody(TOKEN_RESPONSE_EU)));
            stubFor(post(urlEqualTo("/api/v1/spa/notifications/register")).willReturn(aResponse().withStatus(200)
                    .withHeader("Content-Type", "application/json").withBody(DEVICE_REGISTRATION_RESPONSE_EU)));
            stubFor(get(urlPathMatching("/api/v1/spa/vehicles")).willReturn(aResponse().withStatus(200)
                    .withHeader("Content-Type", "application/json").withBody(VEHICLES_RESPONSE_EU)));
        }

        @BeforeEach
        void setUp() {
            final Configuration config = new Configuration(
                    Map.of("password", MockApiData.TEST_REFRESH_TOKEN, "region", Region.EU.name(), "brand",
                            Brand.HYUNDAI.name(), "apiBaseUrl", "http://localhost:" + WIREMOCK_SERVER.port()));
            when(bridge.getConfiguration()).thenReturn(config);
            when(bridge.getUID()).thenReturn(new ThingUID("bluelink:account:testaccount"));

            handler = new BluelinkAccountHandler(bridge, HTTP_CLIENT, timeZoneProvider, localeProvider);
            handler.setCallback(callback);
            handler.initialize();
        }

        @Test
        void testLoginAndGetVehicles() throws BluelinkApiException {
            final var vehicles = handler.getVehicles();

            assertEquals(2, vehicles.size());
            final var first = vehicles.getFirst();
            assertNotNull(first);
            assertInstanceOf(Vehicle.class, first);
            assertEquals("aa6c0ca6-48eb-430c-9eea-902bb6cd281c", first.id());
            assertEquals("VIN1234", first.vin());
            assertEquals("IONIQ 5", first.model());
            assertEquals(2022, first.modelYear());
            assertEquals(IVehicle.EngineType.EV, first.engineType());
            assertEquals("My Car", first.getDisplayName());
            assertTrue(first.isElectric());
            assertInstanceOf(Vehicle.class, first);
            final var firstEu = (Vehicle) first;
            assertFalse(firstEu.ccs2ProtocolSupport());

            final var second = vehicles.getLast();
            assertNotNull(second);
            assertInstanceOf(Vehicle.class, second);
            assertEquals("60392c08-c747-40fe-9de9-7c36e7e24da5", second.id());
            assertEquals("VIN5678", second.vin());
            assertEquals("EV9", second.model());
            assertEquals(2024, second.modelYear());
            assertEquals(IVehicle.EngineType.EV, second.engineType());
            assertEquals("My Other Car", second.getDisplayName());
            assertTrue(second.isElectric());
            final var secondEu = (Vehicle) second;
            assertTrue(secondEu.ccs2ProtocolSupport());
        }

        @Test
        void testSetChargeLimitDC() throws BluelinkApiException {
            final var vehicleId = "aa6c0ca6-48eb-430c-9eea-902bb6cd281c";
            stubFor(post(urlEqualTo("/api/v1/spa/vehicles/" + vehicleId + "/charge/target"))
                    .withRequestBody(equalToJson("{\"targetSOClist\":[{\"plugType\":0,\"targetSOClevel\":80}]}"))
                    .willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json")
                            .withBody("{\"retCode\":\"S\"}")));

            final var vehicle = handler.getVehicles().getFirst();
            assertTrue(handler.setChargeLimitDC(vehicle, 80));
        }

        @Test
        void testSetChargeLimitAC() throws BluelinkApiException {
            final var vehicleId = "aa6c0ca6-48eb-430c-9eea-902bb6cd281c";
            stubFor(post(urlEqualTo("/api/v1/spa/vehicles/" + vehicleId + "/charge/target"))
                    .withRequestBody(equalToJson("{\"targetSOClist\":[{\"plugType\":1,\"targetSOClevel\":90}]}"))
                    .willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json")
                            .withBody("{\"retCode\":\"S\"}")));

            final var vehicle = handler.getVehicles().getFirst();
            assertTrue(handler.setChargeLimitAC(vehicle, 90));
        }
    }
}
