"""The tests for the hassio component."""
import asyncio
from http import HTTPStatus
from unittest.mock import patch

from aiohttp import StreamReader
import pytest

from tests.test_util.aiohttp import AiohttpClientMocker


@pytest.fixture
def mock_not_onboarded():
    """Mock that we're not onboarded."""
    with patch(
        "homeassistant.components.hassio.http.async_is_onboarded", return_value=False
    ):
        yield


@pytest.fixture
def hassio_user_client(hassio_client, hass_admin_user):
    """Return a Hass.io HTTP client tied to a non-admin user."""
    hass_admin_user.groups = []
    return hassio_client


@pytest.mark.parametrize(
    "path",
    [
        "app/entrypoint.js",
        "addons/bl_b392/logo",
        "addons/bl_b392/icon",
    ],
)
async def test_forward_request_onboarded_user_get(
    hassio_user_client, aioclient_mock: AiohttpClientMocker, path: str
) -> None:
    """Test fetching normal path."""
    aioclient_mock.get(f"http://127.0.0.1/{path}", text="response")

    resp = await hassio_user_client.get(f"/api/hassio/{path}")

    # Check we got right response
    assert resp.status == HTTPStatus.OK
    body = await resp.text()
    assert body == "response"

    # Check we forwarded command
    assert len(aioclient_mock.mock_calls) == 1
    # We only expect a single header.
    assert aioclient_mock.mock_calls[0][3] == {"X-Hass-Source": "core.http"}


@pytest.mark.parametrize("method", ["POST", "PUT", "DELETE", "RANDOM"])
async def test_forward_request_onboarded_user_unallowed_methods(
    hassio_user_client, aioclient_mock: AiohttpClientMocker, method: str
) -> None:
    """Test fetching normal path."""
    resp = await hassio_user_client.post("/api/hassio/app/entrypoint.js")

    # Check we got right response
    assert resp.status == HTTPStatus.METHOD_NOT_ALLOWED

    # Check we did not forward command
    assert len(aioclient_mock.mock_calls) == 0


@pytest.mark.parametrize(
    ("bad_path", "expected_status"),
    [
        # Caught by bullshit filter
        ("app/%252E./entrypoint.js", HTTPStatus.BAD_REQUEST),
        # The .. is processed, making it an unauthenticated path
        ("app/../entrypoint.js", HTTPStatus.UNAUTHORIZED),
        ("app/%2E%2E/entrypoint.js", HTTPStatus.UNAUTHORIZED),
        # Unauthenticated path
        ("supervisor/info", HTTPStatus.UNAUTHORIZED),
        ("supervisor/logs", HTTPStatus.UNAUTHORIZED),
        ("addons/bl_b392/logs", HTTPStatus.UNAUTHORIZED),
    ],
)
async def test_forward_request_onboarded_user_unallowed_paths(
    hassio_user_client,
    aioclient_mock: AiohttpClientMocker,
    bad_path: str,
    expected_status: int,
) -> None:
    """Test fetching normal path."""
    resp = await hassio_user_client.get(f"/api/hassio/{bad_path}")

    # Check we got right response
    assert resp.status == expected_status
    # Check we didn't forward command
    assert len(aioclient_mock.mock_calls) == 0


@pytest.mark.parametrize(
    "path",
    [
        "app/entrypoint.js",
        "addons/bl_b392/logo",
        "addons/bl_b392/icon",
    ],
)
async def test_forward_request_onboarded_noauth_get(
    hassio_noauth_client, aioclient_mock: AiohttpClientMocker, path: str
) -> None:
    """Test fetching normal path."""
    aioclient_mock.get(f"http://127.0.0.1/{path}", text="response")

    resp = await hassio_noauth_client.get(f"/api/hassio/{path}")

    # Check we got right response
    assert resp.status == HTTPStatus.OK
    body = await resp.text()
    assert body == "response"

    # Check we forwarded command
    assert len(aioclient_mock.mock_calls) == 1
    # We only expect a single header.
    assert aioclient_mock.mock_calls[0][3] == {"X-Hass-Source": "core.http"}


@pytest.mark.parametrize("method", ["POST", "PUT", "DELETE", "RANDOM"])
async def test_forward_request_onboarded_noauth_unallowed_methods(
    hassio_noauth_client, aioclient_mock: AiohttpClientMocker, method: str
) -> None:
    """Test fetching normal path."""
    resp = await hassio_noauth_client.post("/api/hassio/app/entrypoint.js")

    # Check we got right response
    assert resp.status == HTTPStatus.METHOD_NOT_ALLOWED

    # Check we did not forward command
    assert len(aioclient_mock.mock_calls) == 0


@pytest.mark.parametrize(
    ("bad_path", "expected_status"),
    [
        # Caught by bullshit filter
        ("app/%252E./entrypoint.js", HTTPStatus.BAD_REQUEST),
        # The .. is processed, making it an unauthenticated path
        ("app/../entrypoint.js", HTTPStatus.UNAUTHORIZED),
        ("app/%2E%2E/entrypoint.js", HTTPStatus.UNAUTHORIZED),
        # Unauthenticated path
        ("supervisor/info", HTTPStatus.UNAUTHORIZED),
        ("supervisor/logs", HTTPStatus.UNAUTHORIZED),
        ("addons/bl_b392/logs", HTTPStatus.UNAUTHORIZED),
    ],
)
async def test_forward_request_onboarded_noauth_unallowed_paths(
    hassio_noauth_client,
    aioclient_mock: AiohttpClientMocker,
    bad_path: str,
    expected_status: int,
) -> None:
    """Test fetching normal path."""
    resp = await hassio_noauth_client.get(f"/api/hassio/{bad_path}")

    # Check we got right response
    assert resp.status == expected_status
    # Check we didn't forward command
    assert len(aioclient_mock.mock_calls) == 0


@pytest.mark.parametrize(
    ("path", "authenticated"),
    [
        ("app/entrypoint.js", False),
        ("addons/bl_b392/logo", False),
        ("addons/bl_b392/icon", False),
        ("backups/1234abcd/info", True),
    ],
)
async def test_forward_request_not_onboarded_get(
    hassio_noauth_client,
    aioclient_mock: AiohttpClientMocker,
    path: str,
    authenticated: bool,
    mock_not_onboarded,
) -> None:
    """Test fetching normal path."""
    aioclient_mock.get(f"http://127.0.0.1/{path}", text="response")

    resp = await hassio_noauth_client.get(f"/api/hassio/{path}")

    # Check we got right response
    assert resp.status == HTTPStatus.OK
    body = await resp.text()
    assert body == "response"

    # Check we forwarded command
    assert len(aioclient_mock.mock_calls) == 1
    expected_headers = {
        "X-Hass-Source": "core.http",
    }
    if authenticated:
        expected_headers["Authorization"] = "Bearer 123456"

    assert aioclient_mock.mock_calls[0][3] == expected_headers


@pytest.mark.parametrize(
    "path",
    [
        "backups/new/upload",
        "backups/1234abcd/restore/full",
        "backups/1234abcd/restore/partial",
    ],
)
async def test_forward_request_not_onboarded_post(
    hassio_noauth_client,
    aioclient_mock: AiohttpClientMocker,
    path: str,
    mock_not_onboarded,
) -> None:
    """Test fetching normal path."""
    aioclient_mock.get(f"http://127.0.0.1/{path}", text="response")

    resp = await hassio_noauth_client.get(f"/api/hassio/{path}")

    # Check we got right response
    assert resp.status == HTTPStatus.OK
    body = await resp.text()
    assert body == "response"

    # Check we forwarded command
    assert len(aioclient_mock.mock_calls) == 1
    # We only expect a single header.
    assert aioclient_mock.mock_calls[0][3] == {
        "X-Hass-Source": "core.http",
        "Authorization": "Bearer 123456",
    }


@pytest.mark.parametrize("method", ["POST", "PUT", "DELETE", "RANDOM"])
async def test_forward_request_not_onboarded_unallowed_methods(
    hassio_noauth_client, aioclient_mock: AiohttpClientMocker, method: str
) -> None:
    """Test fetching normal path."""
    resp = await hassio_noauth_client.post("/api/hassio/app/entrypoint.js")

    # Check we got right response
    assert resp.status == HTTPStatus.METHOD_NOT_ALLOWED

    # Check we did not forward command
    assert len(aioclient_mock.mock_calls) == 0


@pytest.mark.parametrize(
    ("bad_path", "expected_status"),
    [
        # Caught by bullshit filter
        ("app/%252E./entrypoint.js", HTTPStatus.BAD_REQUEST),
        # The .. is processed, making it an unauthenticated path
        ("app/../entrypoint.js", HTTPStatus.UNAUTHORIZED),
        ("app/%2E%2E/entrypoint.js", HTTPStatus.UNAUTHORIZED),
        # Unauthenticated path
        ("supervisor/info", HTTPStatus.UNAUTHORIZED),
        ("supervisor/logs", HTTPStatus.UNAUTHORIZED),
        ("addons/bl_b392/logs", HTTPStatus.UNAUTHORIZED),
    ],
)
async def test_forward_request_not_onboarded_unallowed_paths(
    hassio_noauth_client,
    aioclient_mock: AiohttpClientMocker,
    bad_path: str,
    expected_status: int,
    mock_not_onboarded,
) -> None:
    """Test fetching normal path."""
    resp = await hassio_noauth_client.get(f"/api/hassio/{bad_path}")

    # Check we got right response
    assert resp.status == expected_status
    # Check we didn't forward command
    assert len(aioclient_mock.mock_calls) == 0


@pytest.mark.parametrize(
    ("path", "authenticated"),
    [
        ("app/entrypoint.js", False),
        ("addons/bl_b392/logo", False),
        ("addons/bl_b392/icon", False),
        ("backups/1234abcd/info", True),
        ("supervisor/logs", True),
        ("addons/bl_b392/logs", True),
        ("addons/bl_b392/changelog", True),
        ("addons/bl_b392/documentation", True),
    ],
)
async def test_forward_request_admin_get(
    hassio_client,
    aioclient_mock: AiohttpClientMocker,
    path: str,
    authenticated: bool,
) -> None:
    """Test fetching normal path."""
    aioclient_mock.get(f"http://127.0.0.1/{path}", text="response")

    resp = await hassio_client.get(f"/api/hassio/{path}")

    # Check we got right response
    assert resp.status == HTTPStatus.OK
    body = await resp.text()
    assert body == "response"

    # Check we forwarded command
    assert len(aioclient_mock.mock_calls) == 1
    expected_headers = {
        "X-Hass-Source": "core.http",
    }
    if authenticated:
        expected_headers["Authorization"] = "Bearer 123456"

    assert aioclient_mock.mock_calls[0][3] == expected_headers


@pytest.mark.parametrize(
    "path",
    [
        "backups/new/upload",
        "backups/1234abcd/restore/full",
        "backups/1234abcd/restore/partial",
    ],
)
async def test_forward_request_admin_post(
    hassio_client,
    aioclient_mock: AiohttpClientMocker,
    path: str,
) -> None:
    """Test fetching normal path."""
    aioclient_mock.get(f"http://127.0.0.1/{path}", text="response")

    resp = await hassio_client.get(f"/api/hassio/{path}")

    # Check we got right response
    assert resp.status == HTTPStatus.OK
    body = await resp.text()
    assert body == "response"

    # Check we forwarded command
    assert len(aioclient_mock.mock_calls) == 1
    # We only expect a single header.
    assert aioclient_mock.mock_calls[0][3] == {
        "X-Hass-Source": "core.http",
        "Authorization": "Bearer 123456",
    }


@pytest.mark.parametrize("method", ["POST", "PUT", "DELETE", "RANDOM"])
async def test_forward_request_admin_unallowed_methods(
    hassio_client, aioclient_mock: AiohttpClientMocker, method: str
) -> None:
    """Test fetching normal path."""
    resp = await hassio_client.post("/api/hassio/app/entrypoint.js")

    # Check we got right response
    assert resp.status == HTTPStatus.METHOD_NOT_ALLOWED

    # Check we did not forward command
    assert len(aioclient_mock.mock_calls) == 0


@pytest.mark.parametrize(
    ("bad_path", "expected_status"),
    [
        # Caught by bullshit filter
        ("app/%252E./entrypoint.js", HTTPStatus.BAD_REQUEST),
        # The .. is processed, making it an unauthenticated path
        ("app/../entrypoint.js", HTTPStatus.UNAUTHORIZED),
        ("app/%2E%2E/entrypoint.js", HTTPStatus.UNAUTHORIZED),
        # Unauthenticated path
        ("supervisor/info", HTTPStatus.UNAUTHORIZED),
    ],
)
async def test_forward_request_admin_unallowed_paths(
    hassio_client,
    aioclient_mock: AiohttpClientMocker,
    bad_path: str,
    expected_status: int,
) -> None:
    """Test fetching normal path."""
    resp = await hassio_client.get(f"/api/hassio/{bad_path}")

    # Check we got right response
    assert resp.status == expected_status
    # Check we didn't forward command
    assert len(aioclient_mock.mock_calls) == 0


async def test_bad_gateway_when_cannot_find_supervisor(
    hassio_client, aioclient_mock: AiohttpClientMocker
) -> None:
    """Test we get a bad gateway error if we can't find supervisor."""
    aioclient_mock.get("http://127.0.0.1/app/entrypoint.js", exc=asyncio.TimeoutError)

    resp = await hassio_client.get("/api/hassio/app/entrypoint.js")
    assert resp.status == HTTPStatus.BAD_GATEWAY


async def test_backup_upload_headers(
    hassio_client,
    aioclient_mock: AiohttpClientMocker,
    caplog: pytest.LogCaptureFixture,
    mock_not_onboarded,
) -> None:
    """Test that we forward the full header for backup upload."""
    content_type = "multipart/form-data; boundary='--webkit'"
    aioclient_mock.post("http://127.0.0.1/backups/new/upload")

    resp = await hassio_client.post(
        "/api/hassio/backups/new/upload", headers={"Content-Type": content_type}
    )

    # Check we got right response
    assert resp.status == HTTPStatus.OK

    assert len(aioclient_mock.mock_calls) == 1

    req_headers = aioclient_mock.mock_calls[0][-1]
    assert req_headers["Content-Type"] == content_type


async def test_backup_download_headers(
    hassio_client, aioclient_mock: AiohttpClientMocker, mock_not_onboarded
) -> None:
    """Test that we forward the full header for backup download."""
    content_disposition = "attachment; filename=test.tar"
    aioclient_mock.get(
        "http://127.0.0.1/backups/1234abcd/download",
        headers={
            "Content-Length": "50000000",
            "Content-Disposition": content_disposition,
        },
    )

    resp = await hassio_client.get("/api/hassio/backups/1234abcd/download")

    # Check we got right response
    assert resp.status == HTTPStatus.OK

    assert len(aioclient_mock.mock_calls) == 1

    assert resp.headers["Content-Disposition"] == content_disposition


async def test_stream(hassio_client, aioclient_mock: AiohttpClientMocker) -> None:
    """Verify that the request is a stream."""
    content_type = "multipart/form-data; boundary='--webkit'"
    aioclient_mock.post("http://127.0.0.1/backups/new/upload")
    resp = await hassio_client.post(
        "/api/hassio/backups/new/upload", headers={"Content-Type": content_type}
    )
    # Check we got right response
    assert resp.status == HTTPStatus.OK
    assert isinstance(aioclient_mock.mock_calls[-1][2], StreamReader)


async def test_simple_get_no_stream(
    hassio_client, aioclient_mock: AiohttpClientMocker
) -> None:
    """Verify that a simple GET request is not a stream."""
    aioclient_mock.get("http://127.0.0.1/app/entrypoint.js")
    resp = await hassio_client.get("/api/hassio/app/entrypoint.js")
    assert resp.status == HTTPStatus.OK
    assert aioclient_mock.mock_calls[-1][2] is None


async def test_entrypoint_cache_control(
    hassio_client, aioclient_mock: AiohttpClientMocker
) -> None:
    """Test that we return cache control for requests to the entrypoint only."""
    aioclient_mock.get("http://127.0.0.1/app/entrypoint.js")
    aioclient_mock.get("http://127.0.0.1/app/entrypoint.fdhkusd8y43r.js")

    resp1 = await hassio_client.get("/api/hassio/app/entrypoint.js")
    resp2 = await hassio_client.get("/api/hassio/app/entrypoint.fdhkusd8y43r.js")

    # Check we got right response
    assert resp1.status == HTTPStatus.OK
    assert resp2.status == HTTPStatus.OK

    assert len(aioclient_mock.mock_calls) == 2
    assert resp1.headers["Cache-Control"] == "no-store, max-age=0"

    assert "Cache-Control" not in resp2.headers