"""Test the Volvo config flow."""

from collections.abc import AsyncGenerator
from unittest.mock import AsyncMock, patch

import pytest
from volvocarsapi.api import VolvoCarsApi
from volvocarsapi.auth import AUTHORIZE_URL, TOKEN_URL
from volvocarsapi.models import VolvoApiException, VolvoCarsVehicle
from volvocarsapi.scopes import ALL_SCOPES
from yarl import URL

from homeassistant import config_entries
from homeassistant.components.volvo.const import CONF_VIN, DOMAIN
from homeassistant.config_entries import ConfigFlowResult
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_API_KEY, CONF_TOKEN
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers import config_entry_oauth2_flow

from . import async_load_fixture_as_json, configure_mock
from .const import (
    CLIENT_ID,
    DEFAULT_API_KEY,
    DEFAULT_MODEL,
    DEFAULT_VIN,
    REDIRECT_URI,
    SERVER_TOKEN_RESPONSE,
)

from tests.common import MockConfigEntry
from tests.test_util.aiohttp import AiohttpClientMocker
from tests.typing import ClientSessionGenerator


@pytest.mark.usefixtures("current_request_with_host")
async def test_full_flow(
    hass: HomeAssistant,
    config_flow: ConfigFlowResult,
    mock_setup_entry: AsyncMock,
    mock_config_flow_api: VolvoCarsApi,
) -> None:
    """Check full flow."""
    result = await _async_run_flow_to_completion(
        hass, config_flow, mock_config_flow_api
    )

    assert len(hass.config_entries.async_entries(DOMAIN)) == 1
    assert len(mock_setup_entry.mock_calls) == 1
    assert result["type"] is FlowResultType.CREATE_ENTRY
    assert result["data"][CONF_API_KEY] == DEFAULT_API_KEY
    assert result["data"][CONF_VIN] == DEFAULT_VIN
    assert result["context"]["unique_id"] == DEFAULT_VIN


@pytest.mark.usefixtures("current_request_with_host")
async def test_single_vin_flow(
    hass: HomeAssistant,
    config_flow: ConfigFlowResult,
    mock_setup_entry: AsyncMock,
    mock_config_flow_api: VolvoCarsApi,
) -> None:
    """Check flow where API returns a single VIN."""
    _configure_mock_vehicles_success(mock_config_flow_api, single_vin=True)

    # Since there is only one VIN, the api_key step is the only step
    result = await hass.config_entries.flow.async_configure(config_flow["flow_id"])
    assert result["step_id"] == "api_key"

    result = await hass.config_entries.flow.async_configure(
        config_flow["flow_id"], {CONF_API_KEY: "abcdef0123456879abcdef"}
    )

    assert len(hass.config_entries.async_entries(DOMAIN)) == 1
    assert len(mock_setup_entry.mock_calls) == 1
    assert result["type"] is FlowResultType.CREATE_ENTRY


@pytest.mark.parametrize(("api_key_failure"), [pytest.param(True), pytest.param(False)])
@pytest.mark.usefixtures("current_request_with_host")
async def test_reauth_flow(
    hass: HomeAssistant,
    mock_config_entry: MockConfigEntry,
    hass_client_no_auth: ClientSessionGenerator,
    mock_config_flow_api: VolvoCarsApi,
    api_key_failure: bool,
) -> None:
    """Test reauthentication flow."""
    result = await mock_config_entry.start_reauth_flow(hass)
    assert result["type"] is FlowResultType.FORM
    assert result["step_id"] == "reauth_confirm"

    result = await hass.config_entries.flow.async_configure(result["flow_id"], {})

    state = config_entry_oauth2_flow._encode_jwt(
        hass,
        {
            "flow_id": result["flow_id"],
            "redirect_uri": REDIRECT_URI,
        },
    )

    client = await hass_client_no_auth()
    resp = await client.get(f"/auth/external/callback?code=abcd&state={state}")
    assert resp.status == 200
    assert resp.headers["content-type"] == "text/html; charset=utf-8"

    result = await _async_run_flow_to_completion(
        hass,
        result,
        mock_config_flow_api,
        has_vin_step=False,
        is_reauth=True,
        api_key_failure=api_key_failure,
    )
    assert result["type"] is FlowResultType.ABORT
    assert result["reason"] == "reauth_successful"


@pytest.mark.usefixtures("current_request_with_host")
async def test_reauth_no_stale_data(
    hass: HomeAssistant,
    mock_config_entry: MockConfigEntry,
    hass_client_no_auth: ClientSessionGenerator,
    mock_config_flow_api: VolvoCarsApi,
) -> None:
    """Test if reauthentication flow does not use stale data."""
    old_access_token = mock_config_entry.data[CONF_TOKEN][CONF_ACCESS_TOKEN]

    with patch(
        "homeassistant.components.volvo.config_flow._create_volvo_cars_api",
        return_value=mock_config_flow_api,
    ) as mock_create_volvo_cars_api:
        result = await mock_config_entry.start_reauth_flow(hass)
        assert result["type"] is FlowResultType.FORM
        assert result["step_id"] == "reauth_confirm"

        result = await hass.config_entries.flow.async_configure(result["flow_id"], {})

        state = config_entry_oauth2_flow._encode_jwt(
            hass,
            {
                "flow_id": result["flow_id"],
                "redirect_uri": REDIRECT_URI,
            },
        )

        client = await hass_client_no_auth()
        resp = await client.get(f"/auth/external/callback?code=abcd&state={state}")
        assert resp.status == 200
        assert resp.headers["content-type"] == "text/html; charset=utf-8"

        result = await _async_run_flow_to_completion(
            hass,
            result,
            mock_config_flow_api,
            has_vin_step=False,
            is_reauth=True,
        )

        assert mock_create_volvo_cars_api.called
        call = mock_create_volvo_cars_api.call_args_list[0]
        access_token_arg = call.args[1]
        assert old_access_token != access_token_arg


async def test_reconfigure_flow(
    hass: HomeAssistant,
    mock_config_entry: MockConfigEntry,
    mock_config_flow_api: VolvoCarsApi,
) -> None:
    """Test reconfiguration flow."""
    result = await mock_config_entry.start_reconfigure_flow(hass)
    assert result["type"] is FlowResultType.FORM
    assert result["step_id"] == "api_key"

    result = await hass.config_entries.flow.async_configure(
        result["flow_id"], {CONF_API_KEY: "abcdef0123456879abcdef"}
    )
    assert result["type"] is FlowResultType.ABORT
    assert result["reason"] == "reconfigure_successful"


@pytest.mark.usefixtures("current_request_with_host", "mock_config_entry")
async def test_unique_id_flow(
    hass: HomeAssistant,
    config_flow: ConfigFlowResult,
    mock_config_flow_api: VolvoCarsApi,
) -> None:
    """Test unique ID flow."""
    assert len(hass.config_entries.async_entries(DOMAIN)) == 1

    result = await _async_run_flow_to_completion(
        hass, config_flow, mock_config_flow_api
    )

    assert result["type"] is FlowResultType.ABORT
    assert result["reason"] == "already_configured"
    assert len(hass.config_entries.async_entries(DOMAIN)) == 1


@pytest.mark.usefixtures("current_request_with_host")
async def test_api_failure_flow(
    hass: HomeAssistant,
    config_flow: ConfigFlowResult,
    mock_config_flow_api: VolvoCarsApi,
) -> None:
    """Check flow where API throws an exception."""
    _configure_mock_vehicles_failure(mock_config_flow_api)

    result = await hass.config_entries.flow.async_configure(config_flow["flow_id"])
    assert result["step_id"] == "api_key"

    result = await hass.config_entries.flow.async_configure(
        config_flow["flow_id"], {CONF_API_KEY: "abcdef0123456879abcdef"}
    )

    assert len(hass.config_entries.async_entries(DOMAIN)) == 0
    assert result["type"] is FlowResultType.FORM
    assert result["errors"]["base"] == "cannot_load_vehicles"
    assert result["step_id"] == "api_key"

    result = await _async_run_flow_to_completion(
        hass, result, mock_config_flow_api, configure=False
    )
    assert result["type"] is FlowResultType.CREATE_ENTRY


@pytest.fixture
async def config_flow(
    hass: HomeAssistant,
    hass_client_no_auth: ClientSessionGenerator,
) -> config_entries.ConfigFlowResult:
    """Initialize a new config flow."""
    result = await hass.config_entries.flow.async_init(
        DOMAIN, context={"source": config_entries.SOURCE_USER}
    )
    state = config_entry_oauth2_flow._encode_jwt(
        hass,
        {
            "flow_id": result["flow_id"],
            "redirect_uri": REDIRECT_URI,
        },
    )

    result_url = URL(result["url"])
    assert f"{result_url.origin()}{result_url.path}" == AUTHORIZE_URL
    assert result_url.query["response_type"] == "code"
    assert result_url.query["client_id"] == CLIENT_ID
    assert result_url.query["redirect_uri"] == REDIRECT_URI
    assert result_url.query["state"] == state
    assert result_url.query["code_challenge"]
    assert result_url.query["code_challenge_method"] == "S256"
    assert result_url.query["scope"] == " ".join(ALL_SCOPES)

    client = await hass_client_no_auth()
    resp = await client.get(f"/auth/external/callback?code=abcd&state={state}")
    assert resp.status == 200
    assert resp.headers["content-type"] == "text/html; charset=utf-8"

    return result


@pytest.fixture
async def mock_config_flow_api(hass: HomeAssistant) -> AsyncGenerator[AsyncMock]:
    """Mock API used in config flow."""
    with patch(
        "homeassistant.components.volvo.config_flow.VolvoCarsApi",
        autospec=True,
    ) as mock_api:
        api: VolvoCarsApi = mock_api.return_value

        _configure_mock_vehicles_success(api)

        vehicle_data = await async_load_fixture_as_json(hass, "vehicle", DEFAULT_MODEL)
        configure_mock(
            api.async_get_vehicle_details,
            return_value=VolvoCarsVehicle.from_dict(vehicle_data),
        )

        yield api


@pytest.fixture(autouse=True)
async def mock_auth_client(
    aioclient_mock: AiohttpClientMocker,
) -> AsyncGenerator[AsyncMock]:
    """Mock auth requests."""
    aioclient_mock.clear_requests()
    aioclient_mock.post(
        TOKEN_URL,
        json=SERVER_TOKEN_RESPONSE,
    )


async def _async_run_flow_to_completion(
    hass: HomeAssistant,
    config_flow: ConfigFlowResult,
    mock_config_flow_api: VolvoCarsApi,
    *,
    configure: bool = True,
    has_vin_step: bool = True,
    is_reauth: bool = False,
    api_key_failure: bool = False,
) -> ConfigFlowResult:
    if configure:
        if api_key_failure:
            _configure_mock_vehicles_failure(mock_config_flow_api)

        config_flow = await hass.config_entries.flow.async_configure(
            config_flow["flow_id"]
        )

    if is_reauth and not api_key_failure:
        return config_flow

    assert config_flow["type"] is FlowResultType.FORM
    assert config_flow["step_id"] == "api_key"

    _configure_mock_vehicles_success(mock_config_flow_api)
    config_flow = await hass.config_entries.flow.async_configure(
        config_flow["flow_id"], {CONF_API_KEY: "abcdef0123456879abcdef"}
    )

    if has_vin_step:
        assert config_flow["type"] is FlowResultType.FORM
        assert config_flow["step_id"] == "vin"

        config_flow = await hass.config_entries.flow.async_configure(
            config_flow["flow_id"], {CONF_VIN: DEFAULT_VIN}
        )

    return config_flow


def _configure_mock_vehicles_success(
    mock_config_flow_api: VolvoCarsApi, single_vin: bool = False
) -> None:
    vins = [{"vin": DEFAULT_VIN}]

    if not single_vin:
        vins.append({"vin": "YV10000000AAAAAAA"})

    configure_mock(mock_config_flow_api.async_get_vehicles, return_value=vins)


def _configure_mock_vehicles_failure(mock_config_flow_api: VolvoCarsApi) -> None:
    configure_mock(
        mock_config_flow_api.async_get_vehicles, side_effect=VolvoApiException()
    )
